Browse Source

Implement "Add Repository Page" function

master
EJ-Dannug 3 months ago
parent
commit
786374b874
  1. 80
      app/Http/Controllers/CategoryController.php
  2. 80
      app/Http/Controllers/FrameworkController.php
  3. 85
      app/Http/Controllers/InstructionController.php
  4. 86
      app/Http/Controllers/PageController.php
  5. 21
      app/Models/Category.php
  6. 21
      app/Models/Framework.php
  7. 43
      app/Models/Instruction.php
  8. 42
      app/Models/Page.php
  9. 1
      database/migrations/2024_06_09_184105_create_roles_table.php
  10. 29
      database/migrations/2024_06_27_124215_create_categories_table.php
  11. 29
      database/migrations/2024_06_27_202837_create_frameworks_table.php
  12. 32
      database/migrations/2024_06_29_044406_create_pages_table.php
  13. 33
      database/migrations/2024_06_29_215447_create_instructions_table.php
  14. 2024
      package-lock.json
  15. 12
      package.json
  16. 4
      resources/js/Components/DashboardComponents/ContentStatistics.tsx
  17. 3
      resources/js/Components/DashboardComponents/PagesOverview.tsx
  18. 17
      resources/js/Components/ErrorButton.tsx
  19. 2
      resources/js/Components/FormGroup.tsx
  20. 17
      resources/js/Components/SuccessButton.tsx
  21. 196
      resources/js/Pages/AppFramework/AddModal.tsx
  22. 2
      resources/js/Pages/Auth/Login.tsx
  23. 2
      resources/js/Pages/Auth/Register.tsx
  24. 285
      resources/js/Pages/RepositoryPage/Create.tsx
  25. 170
      resources/js/Pages/RepositoryPage/InstructionsForm.tsx
  26. 189
      resources/js/Pages/SubjectCategory/AddModal.tsx
  27. 25
      routes/auth.php

80
app/Http/Controllers/CategoryController.php

@ -0,0 +1,80 @@
<?php
namespace App\Http\Controllers;
use App\Models\Category;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
class CategoryController extends Controller
{
/**
* Display a listing of the resource.
*/
public function index(): JsonResponse
{
$categories = Category::all();
return response()->json($categories);
}
/**
* Show the form for creating a new resource.
*/
public function create()
{
//
}
/**
* Store a newly created resource in storage.
*/
public function store(Request $request): JsonResponse
{
$request->validate([
'subjectTitle' => 'required|string|max:255|unique:categories,subjectTitle',
'description' => 'nullable|string',
]);
$category = Category::create([
'subject_title' => $request->subjectTitle,
'description' => $request->description,
]);
return response()->json([
'message' => 'Subject category added!',
'category' => $category,
]);
}
/**
* Display the specified resource.
*/
public function show(Category $category): JsonResponse
{
return response()->json($category);
}
/**
* Show the form for editing the specified resource.
*/
public function edit(Category $category)
{
//
}
/**
* Update the specified resource in storage.
*/
public function update(Request $request, Category $category)
{
//
}
/**
* Remove the specified resource from storage.
*/
public function destroy(Category $category)
{
//
}
}

80
app/Http/Controllers/FrameworkController.php

@ -0,0 +1,80 @@
<?php
namespace App\Http\Controllers;
use App\Models\Framework;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
class FrameworkController extends Controller
{
/**
* Display a listing of the resource.
*/
public function index(): JsonResponse
{
$categories = Framework::all();
return response()->json($categories);
}
/**
* Show the form for creating a new resource.
*/
public function create()
{
//
}
/**
* Store a newly created resource in storage.
*/
public function store(Request $request): JsonResponse
{
$request->validate([
'frameworkName' => 'required|string|max:255',
'version' => 'nullable|string',
]);
$framework = Framework::create([
'framework_name' => $request->frameworkName,
'version' => $request->version,
]);
return response()->json([
'message' => 'Application framework added!',
'framework' => $framework,
]);
}
/**
* Display the specified resource.
*/
public function show(Framework $framework)
{
//
}
/**
* Show the form for editing the specified resource.
*/
public function edit(Framework $framework)
{
//
}
/**
* Update the specified resource in storage.
*/
public function update(Request $request, Framework $framework)
{
//
}
/**
* Remove the specified resource from storage.
*/
public function destroy(Framework $framework)
{
//
}
}

85
app/Http/Controllers/InstructionController.php

@ -0,0 +1,85 @@
<?php
namespace App\Http\Controllers;
use App\Models\Instruction;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
class InstructionController extends Controller
{
/**
* Display a listing of the resource.
*/
public function index(): JsonResponse
{
return response()->json();
}
/**
* Show the form for creating a new resource.
*/
public function create()
{
//
}
/**
* Store a newly created resource in storage.
*/
public function store(Request $request): JsonResponse
{
$request->validate([
'file' => 'required|file|mimes:json',
'frameworkID' => 'required|exists:frameworks,id',
'pageID' => 'required|exists:pages,id',
]);
$file = $request->file('file');
$path = $file->store('instructions', 'public');
$instruction = Instruction::create([
'page_id' => $request->pageID,
'framework_id' => $request->frameworkID,
'file_path' => $path,
]);
return response()->json([
'message' => 'Instruction saved successfully',
'instruction' => $instruction,
]);
}
/**
* Display the specified resource.
*/
public function show(Instruction $instruction)
{
//
}
/**
* Show the form for editing the specified resource.
*/
public function edit(Instruction $instruction)
{
//
}
/**
* Update the specified resource in storage.
*/
public function update(Request $request, Instruction $instruction)
{
//
}
/**
* Remove the specified resource from storage.
*/
public function destroy(Instruction $instruction)
{
//
}
}

86
app/Http/Controllers/PageController.php

@ -0,0 +1,86 @@
<?php
namespace App\Http\Controllers;
use App\Models\Page;
use Inertia\Inertia;
use Inertia\Response;
use Illuminate\Http\Request;
use Illuminate\Contracts\Auth\MustVerifyEmail;
class PageController extends Controller
{
/**
* Display a listing of the resource.
*/
public function index()
{
//
}
/**
* Show the form for creating a new resource.
*/
public function create(Request $request): Response
{
return Inertia::render('RepositoryPage/Create', [
'mustVerifyEmail' => $request->user() instanceof MustVerifyEmail,
'status' => session('status'),
]);
}
/**
* Store a newly created resource in storage.
*/
public function store(Request $request)
{
$request->validate([
'pageTitle' => 'required|string|max:255',
'category' => 'required|exists:categories,id',
'introduction' => 'required|string',
]);
$page = Page::create([
'page_title' => $request->pageTitle,
'category_id' => $request->category,
'introduction' => $request->introduction,
]);
return response()->json([
'message' => 'Page created successfully!',
'page' => $page,
]);
}
/**
* Display the specified resource.
*/
public function show(Page $page)
{
//
}
/**
* Show the form for editing the specified resource.
*/
public function edit(Page $page)
{
//
}
/**
* Update the specified resource in storage.
*/
public function update(Request $request, Page $page)
{
//
}
/**
* Remove the specified resource from storage.
*/
public function destroy(Page $page)
{
//
}
}

21
app/Models/Category.php

@ -0,0 +1,21 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Category extends Model
{
use HasFactory;
/**
* The attributes that are mass assignable.
*
* @var array<int, string>
*/
protected $fillable = [
'subjectTitle',
'description',
];
}

21
app/Models/Framework.php

@ -0,0 +1,21 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Framework extends Model
{
use HasFactory;
/**
* The attributes that are mass assignable.
*
* @var array<int, string>
*/
protected $fillable = [
'frameworkName',
'version',
];
}

43
app/Models/Instruction.php

@ -0,0 +1,43 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Instruction extends Model
{
use HasFactory;
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [
'page_id',
'framework_id',
'file_path',
];
/**
* Get the attributes that should be cast.
*
* @return array<string, string>
*/
protected function casts(): array
{
return [
'page_id' => 'integer',
'framework_id' => 'integer',
];
}
/**
* Get the framework associated with the instruction.
*/
public function framework()
{
return $this->belongsTo(Framework::class);
}
}

42
app/Models/Page.php

@ -0,0 +1,42 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Page extends Model
{
use HasFactory;
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [
'page_title',
'introduction',
'category_id',
];
/**
* Get the attributes that should be cast.
*
* @return array<string, string>
*/
protected function casts(): array
{
return [
'category_id' => 'integer',
];
}
/**
* Get the framework associated with the instruction.
*/
public function category()
{
return $this->belongsTo(Category::class);
}
}

1
database/migrations/2024_06_09_184105_create_roles_table.php

@ -14,7 +14,6 @@ return new class extends Migration
Schema::create('roles', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->timestamps();
});
}

29
database/migrations/2024_06_27_124215_create_categories_table.php

@ -0,0 +1,29 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('categories', function (Blueprint $table) {
$table->id();
$table->string('subject_title');
$table->text('description')->nullable();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('categories');
}
};

29
database/migrations/2024_06_27_202837_create_frameworks_table.php

@ -0,0 +1,29 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('frameworks', function (Blueprint $table) {
$table->id();
$table->string('framework_name');
$table->string('version');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('frameworks');
}
};

32
database/migrations/2024_06_29_044406_create_pages_table.php

@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('pages', function (Blueprint $table) {
$table->id();
$table->string('page_title');
$table->text('introduction');
$table->unsignedBigInteger('category_id')->nullable();
$table->timestamps();
$table->foreign('category_id')->references('id')->on('categories')->onDelete('set null');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('pages');
}
};

33
database/migrations/2024_06_29_215447_create_instructions_table.php

@ -0,0 +1,33 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('instructions', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('page_id');
$table->unsignedBigInteger('framework_id');
$table->string('file_path');
$table->timestamps();
$table->foreign('page_id')->references('id')->on('pages')->onDelete('cascade');
$table->foreign('framework_id')->references('id')->on('frameworks')->onDelete('cascade');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('instructions');
}
};

2024
package-lock.json
File diff suppressed because it is too large
View File

12
package.json

@ -26,7 +26,17 @@
"vite": "^5.0"
},
"dependencies": {
"@ckeditor/ckeditor5-react": "^7.0.0",
"@codemirror/lang-html": "^6.4.9",
"@codemirror/lang-javascript": "^6.2.2",
"@codemirror/lang-php": "^6.0.1",
"@uiw/react-codemirror": "^4.22.2",
"ckeditor5-build-classic": "^1.0.0",
"codemirror": "^5.65.16",
"dayjs": "^1.11.11",
"feather-icons-react": "^0.7.0",
"react-feather": "^2.0.10"
"react-codemirror2": "^8.0.0",
"react-feather": "^2.0.10",
"react-quill": "^2.0.0"
}
}

4
resources/js/Components/DashboardComponents/ContentStatistics.tsx

@ -24,9 +24,9 @@ export default function ContentStatistics () {
<div className="stat-figure">
<Hash className="stroke-[#FB5607]" size={40} />
</div>
<div className="stat-title font-semibold text-black">Programming Languages</div>
<div className="stat-title font-semibold text-black">Application Frameworks</div>
<div className="stat-value text-[#FB5607]">000</div>
<div className="stat-desc"><span className="btn btn-link p-0 text-xs text-black">View all languages</span></div>
<div className="stat-desc"><span className="btn btn-link p-0 text-xs text-black">View all frameworks</span></div>
</div>
<div className="stat">
<div className="stat-figure">

3
resources/js/Components/DashboardComponents/PagesOverview.tsx

@ -1,4 +1,5 @@
import { Edit3, File, FilePlus, Globe, Trash2 } from "react-feather";
import { Link } from '@inertiajs/react';
export default function PagesOverview() {
return (
@ -7,7 +8,7 @@ export default function PagesOverview() {
<p className="font-bold text-xl flex items-center"><Globe className="stroke-primary-main mr-2" />Page Activities</p>
<div>
<div className="btn btn-link text-success-main p-0 mr-8"><File size={20} />Manage pages</div>
<div className="btn btn-link text-primary-main p-0"><FilePlus size={20} />Add new page</div>
<Link href={route('page.create')}><div className="btn btn-link text-primary-main p-0"><FilePlus size={20} />Add new page</div></Link>
</div>
</div>

17
resources/js/Components/ErrorButton.tsx

@ -0,0 +1,17 @@
import { ButtonHTMLAttributes } from 'react';
export default function ErrorButton({ className = '', disabled, children, ...props }: ButtonHTMLAttributes<HTMLButtonElement>) {
return (
<button
{...props}
className={
`flex items-center btn bg-error-main outline-none text-white hover:bg-error-hover active:bg-error-pressed ${
disabled && 'opacity-25'
} ` + className
}
disabled={disabled}
>
{children}
</button>
);
}

2
resources/js/Components/FormGroup.tsx

@ -2,7 +2,7 @@ import { PropsWithChildren } from 'react';
export default function FormGroup({ children }: PropsWithChildren) {
return(
<div className="form-group flex my-2">
<div className="form-group flex items-center my-2">
{ children }
</div>
);

17
resources/js/Components/SuccessButton.tsx

@ -0,0 +1,17 @@
import { ButtonHTMLAttributes } from 'react';
export default function SuccessButton({ className = '', disabled, children, ...props }: ButtonHTMLAttributes<HTMLButtonElement>) {
return (
<button
{...props}
className={
`flex items-center btn bg-success-main outline-none text-white hover:bg-success-hover active:bg-success-pressed ${
disabled && 'opacity-25'
} ` + className
}
disabled={disabled}
>
{children}
</button>
);
}

196
resources/js/Pages/AppFramework/AddModal.tsx

@ -0,0 +1,196 @@
import { FormEventHandler, Fragment, PropsWithChildren, useEffect, useState } from 'react';
import { Dialog, Transition } from '@headlessui/react';
import { FileText, Box, XCircle, ArrowRightCircle, CheckCircle, XOctagon, Code } from 'react-feather';
import { useForm } from '@inertiajs/react';
import FormGroup from '@/Components/FormGroup';
import InputLabel from '@/Components/InputLabel';
import TextInput from '@/Components/TextInput';
import InputError from '@/Components/InputError';
import ErrorButton from '@/Components/ErrorButton';
import PrimaryButton from '@/Components/PrimaryButton';
import axios from 'axios';
import Modal from '@/Components/Modal';
import ModalButton from '@/Components/ModalButton';
export default function AddFrameworkModal({
show = false,
maxWidth = '2xl',
closeable = true,
onClose = () => {},
onFrameworkAdded = () => {},
className = '',
}: PropsWithChildren<{
show: boolean;
maxWidth?: 'sm' | 'md' | 'lg' | 'xl' | '2xl';
closeable?: boolean;
onClose: CallableFunction;
onFrameworkAdded: CallableFunction;
className?: string;
}>) {
const close = () => {
if (closeable) {
onClose();
}
};
const maxWidthClass = {
sm: 'sm:max-w-sm',
md: 'sm:max-w-md',
lg: 'sm:max-w-lg',
xl: 'sm:max-w-xl',
'2xl': 'sm:max-w-2xl',
}[maxWidth];
const { data, setData, reset, errors } = useForm({
frameworkName: '',
version:'',
});
useEffect(() => {
return () => {
reset('frameworkName');
reset('version');
};
}, []);
const [status, setStatus] = useState('');
const [feedback, setFeedback] = useState('');
const [isSuccessModalOpen, setIsSuccessModalOpen] = useState(false);
const [isErrorModalOpen, setIsErrorModalOpen] = useState(false);
const handleCloseSuccessModal = () => {
setIsSuccessModalOpen(false);
onFrameworkAdded();
close();
}
const submit: FormEventHandler = async (e) => {
e.preventDefault();
try {
const response = await axios.post(route('framework.add'), data);
setStatus('Success')
setFeedback(response.data.message);
setIsSuccessModalOpen(true);
} catch (error) {
setStatus('Error')
if (axios.isAxiosError(error)) {
setFeedback(error.response?.data.message || 'An error occurred');
} else {
setFeedback('An unexpected error occurred');
}
setIsErrorModalOpen(true);
}
};
return (
<Transition show={show} as={Fragment} leave="duration-200">
<Dialog
as="div"
id="modal"
className="fixed inset-0 flex overflow-y-auto px-4 py-6 sm:px-0 items-center z-50 transform transition-all"
onClose={close}
>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="absolute inset-0 bg-neutral-0 opacity-50" />
</Transition.Child>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
enterTo="opacity-100 translate-y-0 sm:scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
<Dialog.Panel
className={`mb-6 bg-white border-4 rounded-lg overflow-hidden shadow-xl items-center transform transition-all sm:w-full sm:mx-auto bg-neutral-white ${maxWidthClass} ${className}`}
>
<div className="p-4 flex flex-col">
<h1 className="font-bold text-xl text-primary-main flex items-center my-2"><FileText className="stroke-primary-main mr-2" />Add New Framework</h1>
<div className="divider"></div>
<form id="AddFrameworkForm">
<div>
<FormGroup>
<InputLabel htmlFor="frameworkName"><Box className='stroke-neutral-10' /></InputLabel>
<label htmlFor="frameworkName" className="mx-2 font-semibold">App Framework Name:</label>
</FormGroup>
<TextInput
id='frameworkName'
name='frameworkName'
value={data.frameworkName}
autoComplete='off'
required
onChange={(e) => setData('frameworkName', e.target.value)}
/>
<InputError message={errors.frameworkName} className="mt-2" />
</div>
<div>
<FormGroup>
<InputLabel htmlFor="version"><Code className='stroke-neutral-10' /></InputLabel>
<label htmlFor="version" className="mx-2 font-semibold">Version:</label>
</FormGroup>
<TextInput
id='version'
name='version'
value={data.version}
autoComplete='off'
required
onChange={(e) => setData('version', e.target.value)}
/>
<InputError message={errors.version} className="mt-2" />
</div>
<div className='flex items-center justify-center mt-3 w-full'>
<ErrorButton type="button" className='w-1/2' onClick={close}>
<XCircle className='stroke-neutral-10' />
Cancel
</ErrorButton>
<PrimaryButton type="button" className='w-1/2' onClick={submit}>
<ArrowRightCircle className='stroke-neutral-10' />
Save
</PrimaryButton>
</div>
</form>
</div>
<Modal show={isSuccessModalOpen} onClose={() => setIsSuccessModalOpen(false)} maxWidth="lg" styling='success'>
<div className="p-4 flex flex-col items-center">
<CheckCircle className='stroke-success-main' size={80} />
<h2 className="text-xl font-bold mt-2">{ status }</h2>
<p className="mt-4">{ feedback }</p>
<ModalButton onClick={handleCloseSuccessModal} className="bg-success-main text-white hover:bg-success-hover active:bg-success-pressed">
Close
</ModalButton>
</div>
</Modal>
<Modal show={isErrorModalOpen} onClose={() => setIsErrorModalOpen(false)} maxWidth="lg" styling='error'>
<div className="p-4 flex flex-col items-center">
<XOctagon className='stroke-error-main' size={80} />
<h2 className="text-xl font-bold mt-2">{ status }</h2>
<p className="mt-4">{ feedback }</p>
<ModalButton onClick={() => setIsErrorModalOpen(false)} className="bg-error-main text-white hover:bg-error-hover active:bg-error-pressed">
Close
</ModalButton>
</div>
</Modal>
</Dialog.Panel>
</Transition.Child>
</Dialog>
</Transition>
);
}

2
resources/js/Pages/Auth/Login.tsx

@ -14,7 +14,7 @@ import axios from 'axios';
import ModalButton from '@/Components/ModalButton';
export default function Login({ status, canResetPassword }: { status?: string, canResetPassword: boolean }) {
const { data, setData, post, processing, errors, reset } = useForm({
const { data, setData, processing, errors, reset } = useForm({
username: '',
password: '',
remember: false,

2
resources/js/Pages/Auth/Register.tsx

@ -211,7 +211,7 @@ export default function Register() {
<CheckCircle className='stroke-success-main' size={80} />
<h2 className="text-xl font-bold mt-2">{ status }</h2>
<p className="mt-4">{ feedback }</p>
<ModalButton onClick={handleCloseSuccessModal} className="bg-success-main hover:bg-success-hover active:bg-success-pressed">
<ModalButton onClick={handleCloseSuccessModal} className="bg-success-main text-white hover:bg-success-hover active:bg-success-pressed">
Login Now
</ModalButton>
</div>

285
resources/js/Pages/RepositoryPage/Create.tsx

@ -0,0 +1,285 @@
import FormGroup from "@/Components/FormGroup";
import InputError from "@/Components/InputError";
import InputLabel from "@/Components/InputLabel";
import TextInput from "@/Components/TextInput";
import Authenticated from "@/Layouts/AuthenticatedLayout";
import { PageProps } from "@/types";
import { Head, Link, useForm } from "@inertiajs/react";
import { Book, Box, CheckCircle, FileText, Paperclip, Plus, PlusCircle, XCircle, XOctagon } from "react-feather";
import 'codemirror/lib/codemirror.css';
import 'codemirror/mode/javascript/javascript';
import PrimaryButton from "@/Components/PrimaryButton";
import Instructions from "./InstructionsForm";
import Modal from "@/Components/Modal";
import ModalButton from "@/Components/ModalButton";
import { FormEventHandler, useEffect, useState } from "react";
import AddCategoryModal from "../SubjectCategory/AddModal";
import axios from "axios";
import SuccessButton from "@/Components/SuccessButton";
import ErrorButton from "@/Components/ErrorButton";
import dayjs from "dayjs";
interface Category {
id: number;
subject_title: string;
description?: string;
}
export default function Create({ auth }: PageProps) {
const thisUser = auth.user;
const { data, setData, processing, errors } = useForm({
pageTitle: '',
category: '',
introduction:'',
});
const [isAddCategoryModalOpen, setIsAddCategoryModalOpen] = useState(false);
const handleChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
const selectedValue = e.target.value;
setData('category', selectedValue);
if (selectedValue === "0") {
setIsAddCategoryModalOpen(true);
}
}
const [status, setStatus] = useState('');
const [feedback, setFeedback] = useState('');
const [isSuccessModalOpen, setIsSuccessModalOpen] = useState(false);
const [isErrorModalOpen, setIsErrorModalOpen] = useState(false);
const [categories, setCategories] = useState<Category[]>([]);
const fetchCategories = async () => {
try {
const response = await axios.get<Category[]>(route('categories.index'));
setCategories(response.data);
} catch (e) {
setStatus('Function Failure');
setFeedback('Failed to fetch categories');
setIsErrorModalOpen(true);
}
};
useEffect(() => {
if (thisUser.role_id !== 1) {
setStatus('Warning');
setFeedback('You are unauthorized to access this page');
setIsErrorModalOpen(true);
} else {
fetchCategories();
}
}, [thisUser.role_id]);
const handleCloseSuccessModal = () => {
setIsSuccessModalOpen(false);
window.location.href = '/dashboard';
}
const handleCloseErrorModal = () => {
setIsErrorModalOpen(false);
window.location.href = '/dashboard';
};
const [instructions, setInstructions] = useState<{ id: number; steps: any[]; frameworkID: string; }[]>([]);
const [nextId, setNextId] = useState<number>(0);
const handleAddInstruction = () => {
setInstructions([...instructions, { id: nextId, steps: [], frameworkID: '' }]);
setNextId(nextId + 1);
};
const handleRemoveInstruction = (id: number) => {
setInstructions(prevInstructions =>
prevInstructions.filter(instruction => instruction.id !== id)
);
};
const handleUpdateInstruction = (id: number, steps: any[], frameworkID: string) => {
setInstructions(prevInstructions =>
prevInstructions.map(instruction =>
instruction.id === id ? { ...instruction, steps, frameworkID } : instruction
)
);
};
const submit: FormEventHandler = async (e) => {
e.preventDefault();
if (data.category==="0" || data.category==="") {
alert('Please choose a valid category.')
return
}
try {
const response = await axios.post(route('page.add'), data);
const pageID = response.data.page.id;
await Promise.all(instructions.map(async (instruction) => {
if (instruction.frameworkID === "0" || instruction.frameworkID === "") {
alert('Please choose a valid framework for each instruction.');
return;
}
const timestamp = dayjs().format('YYYYMMDDHHmmss');
const jsonString = JSON.stringify({ steps: instruction.steps });
const blob = new Blob([jsonString], { type: 'application/json' });
const file = new File([blob], `${timestamp}.json`, { type: 'application/json' });
const formData = new FormData();
formData.append('file', file);
formData.append('frameworkID', instruction.frameworkID);
formData.append('pageID', pageID.toString());
await axios.post(route('instruction.add'), formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
});
}));
setStatus('Success');
setFeedback(response.data.message);
setIsSuccessModalOpen(true);
} catch (error) {
setStatus('Error')
if (axios.isAxiosError(error)) {
setFeedback(error.response?.data.message || 'An error occurred');
} else {
setFeedback('An unexpected error occurred');
}
setIsErrorModalOpen(true);
}
}
const renderIfAdmin = () => {
if (thisUser.role_id === 1) {
return (
<div>
<div className="breadcrumbs text-sm">
<ul>
<li><Link href="/dashboard">Home</Link></li>
<li><a>Repository Pages</a></li>
<li>Add New Repository Page</li>
</ul>
</div>
<h1 className="font-bold text-xl text-primary-main flex items-center my-2"><FileText className="stroke-primary-main mr-2" />Create Repository Page</h1>
<div className="divider"></div>
<form id="CreateRepositoryPageForm" onSubmit={submit}>
<div>
<FormGroup>
<InputLabel htmlFor="pageTitle"><Box className='stroke-neutral-10' /></InputLabel>
<label htmlFor="pageTitle" className="mx-2 font-semibold">Page Title:</label>
</FormGroup>
<TextInput
id='pageTitle'
name='pageTitle'
value={data.pageTitle}
autoComplete='off'
required
onChange={(e) => setData('pageTitle', e.target.value)}
/>
<InputError message={errors.pageTitle} className="mt-2" />
</div>
<div>
<FormGroup>
<InputLabel htmlFor="category"><Book className='stroke-neutral-10' /></InputLabel>
<label htmlFor="category" className="mx-2 font-semibold">Subject Category:</label>
</FormGroup>
<select name="category" id="category" value={data.category} onChange={handleChange}
className='px-2 py-0 flex-grow w-full h-[30px] bg-primary-background border border-primary-hover focus:outline-none focus:bg-neutral-10 focus:border focus:border-primary-hover'>
<option value="" selected disabled></option>
{categories.map(category => (
<option key={category.id} value={category.id}>
{category.subject_title}
</option>
))}
<option value="0" className="text-secondary-main font-semibold">Add new framework</option>
</select>
</div>
<div>
<FormGroup>
<InputLabel htmlFor="description"><Paperclip className='stroke-neutral-10' /></InputLabel>
<label htmlFor="description" className="mx-2 font-semibold">Introduction:</label>
</FormGroup>
<textarea className='px-2 flex-grow w-full bg-primary-background border border-primary-hover focus:outline-none focus:bg-neutral-10 focus:border-2 focus:border-primary-hover '
onChange={(e) => setData('introduction', e.target.value)} />
</div>
{instructions.map((instruction) => (
<Instructions
key={instruction.id}
instruction_id={instruction.id}
onDelete={handleRemoveInstruction}
onUpdate={handleUpdateInstruction}
/>
))}
<PrimaryButton type='button' disabled={processing} onClick={handleAddInstruction} className="w-full">
<Plus className='stroke-neutral-10' />
Add Instruction
</PrimaryButton>
<div className="divider"></div>
<div className="flex">
<ErrorButton className="w-1/2" onClick={() => window.location.href = '/dashboard'}>
<XCircle className='stroke-neutral-10' />
Cancel
</ErrorButton>
<SuccessButton type="submit" className="w-1/2">
<PlusCircle className='stroke-neutral-10' />
Save Repository Page
</SuccessButton>
</div>
</form>
</div>
);
}
}
return (
<Authenticated user={thisUser}>
<Head title="Repository Page" />
<div id="CreateRepositoryPage" className="drawer lg:drawer-open">
<input id="my-drawer-2" type="checkbox" className="drawer-toggle" />
<div className="drawer-content flex flex-row justify-between p-6 bg-neutral-20">
<div className="h-full w-full bg-neutral-10 shadow-md p-6 rounded-lg">
{renderIfAdmin()}
</div>
</div>
<div className="drawer-side">
<label htmlFor="my-drawer-2" aria-label="close sidebar" className="drawer-overlay"></label>
<ul className="menu p-4 w-56 h-full bg-primary-background text-base-content">
{/* Sidebar content here */}
<li><a>Sidebar Item 1</a></li>
<li><a>Sidebar Item 2</a></li>
</ul>
</div>
</div>
<AddCategoryModal show={isAddCategoryModalOpen} onClose={() => setIsAddCategoryModalOpen(false)} onCategoryAdded={fetchCategories} />
<Modal show={isSuccessModalOpen} onClose={() => setIsSuccessModalOpen(false)} maxWidth="lg" styling='success'>
<div className="p-4 flex flex-col items-center">
<CheckCircle className='stroke-success-main' size={80} />
<h2 className="text-xl font-bold mt-2">{ status }</h2>
<p className="mt-4">{ feedback }</p>
<ModalButton onClick={handleCloseSuccessModal} className="bg-success-main text-white hover:bg-success-hover active:bg-success-pressed">
Close
</ModalButton>
</div>
</Modal>
<Modal show={isErrorModalOpen} onClose={() => setIsErrorModalOpen(false)} maxWidth="lg" styling='error'>
<div className="p-4 flex flex-col items-center">
<XOctagon className='stroke-error-main' size={80} />
<h2 className="text-xl font-bold mt-2">{ status }</h2>
<p className="mt-4">{ feedback }</p>
<ModalButton onClick={handleCloseErrorModal} className="bg-error-main text-white hover:bg-error-hover active:bg-error-pressed">
Close
</ModalButton>
</div>
</Modal>
</Authenticated>
);
}

170
resources/js/Pages/RepositoryPage/InstructionsForm.tsx

@ -0,0 +1,170 @@
import { useEffect, useState } from 'react';
import ReactQuill from 'react-quill';
import 'react-quill/dist/quill.snow.css';
import CodeMirror from '@uiw/react-codemirror';
import { html } from '@codemirror/lang-html';
import { javascript } from '@codemirror/lang-javascript';
import { php } from '@codemirror/lang-php';
import 'codemirror/lib/codemirror.css';
import 'codemirror/theme/material.css';
import PrimaryButton from '@/Components/PrimaryButton';
import { Code, Download, Minus, MinusCircle, Plus } from 'react-feather';
import FormGroup from '@/Components/FormGroup';
import InputLabel from '@/Components/InputLabel';
import TextInput from '@/Components/TextInput';
import AddFrameworkModal from '../AppFramework/AddModal';
import axios from 'axios';
interface InstructionsProps {
instruction_id: number;
onDelete: (instruction_id: number) => void;
onUpdate: (instruction_id: number, steps: any[], frameworkID: string) => void;
}
interface Framework {
id: number;
framework_name: string;
version: string;
}
export default function Instructions({ instruction_id, onDelete, onUpdate }: InstructionsProps) {
const [frameworkID, setFrameworkID] = useState('');
const [frameworks, setFrameworks] = useState<Framework[]>([]);
const fetchFrameworks = async () => {
try {
const response = await axios.get<Framework[]>(route('frameworks.index'));
setFrameworks(response.data);
} catch (error) {
console.error('Failed to fetch frameworks', error);
}
};
useEffect(() => {
fetchFrameworks();
}, []);
const [isAddFrameworkModalOpen, setIsAddFrameworkModalOpen] = useState(false);
const handleChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
const selectedValue = e.target.value;
setFrameworkID(selectedValue);
if (selectedValue === "0") {
setIsAddFrameworkModalOpen(true);
}
}
const [steps, setSteps] = useState<Array<{ title: string, content: string, code: string }>>([]);
const handleAddStep = () => {
setSteps([...steps, { title: '', content: '', code: '' }]);
};
const handleRemoveStep = (step_id: number) => {
setSteps(prevSteps => prevSteps.filter((_, index) => index !== step_id));
};
const handleTitleChange = (step_id: number, title: string) => {
const newSteps = [...steps];
newSteps[step_id].title = title;
setSteps(newSteps);
onUpdate(instruction_id, newSteps, frameworkID);
};
const handleContentChange = (step_id: number, content: string) => {
const newSteps = [...steps];
newSteps[step_id].content = content;
setSteps(newSteps);
onUpdate(instruction_id, newSteps, frameworkID);
};
const handleCodeChange = (step_id: number, code: string) => {
const newSteps = [...steps];
newSteps[step_id].code = code;
setSteps(newSteps);
onUpdate(instruction_id, newSteps, frameworkID);
};
return (
<div className="h-full w-full bg-neutral-10 shadow-md my-4 p-6 rounded-lg flex flex-col items-start">
<div className='flex w-full justify-between items-center'>
<h1 className="font-bold text-lg text-secondary-main flex items-center my-2">Instructions Editor</h1>
<button className="btn btn-link text-error-main p-0 no-underline" onClick={() => onDelete(instruction_id)}>
<Minus size={20} />
Remove Instruction
</button>
</div>
<div className='w-full'>
<FormGroup>
<InputLabel htmlFor="frameworkID"><Code className='stroke-neutral-10' /></InputLabel>
<label htmlFor="frameworkID" className="ml-2 w-1/4">Application Framework:</label>
<select name="frameworkID" id="frameworkID" value={frameworkID} onChange={handleChange}
className='px-2 py-0 flex-grow w-full h-[30px] bg-primary-background border border-primary-hover focus:outline-none focus:bg-neutral-10 focus:border focus:border-primary-hover'>
<option value="" selected disabled></option>
{frameworks.map(framework => (
<option key={framework.id} value={framework.id}>
{framework.framework_name}
</option>
))}
<option value="0" className="text-secondary-main font-semibold">Add new framework</option>
</select>
</FormGroup>
</div>
<div className='w-full my-4'>
{steps.map((step, step_id) => (
<div key={step_id}>
<div className='flex justify-between items-center'>
<div className='flex w-full items-center'>
<h3 className='font-semibold text-lg w-1/7'>Step {step_id + 1}</h3>
<TextInput
id='stepTitle'
name='stepTitle'
value={step.title}
placeholder='Step Title'
autoComplete='off'
required
onChange={(e) => handleTitleChange(step_id, e.target.value)}
className='w-1/4 mx-4'
/>
</div>
<button type="button" className="btn btn-link text-error-main p-0 no-underline" onClick={() => handleRemoveStep(step_id)}>
<Minus size={20} />
Remove Step
</button>
</div>
<p>Textual Instruction:</p>
<ReactQuill
value={step.content}
onChange={(content) => handleContentChange(step_id, content)}
className="h-1/2"
modules={{
toolbar: [
['bold', 'italic', 'underline'],
[{'list': 'ordered'}, {'list': 'bullet'}],
['link', 'image', 'video']
]
}}
/>
<p className='mt-2'>Code Snippet:</p>
<CodeMirror
value={step.code}
extensions={[php(), javascript(), html()]}
onChange={(value) => handleCodeChange(step_id, value)}
/>
<div className="divider divider-neutral-20"></div>
</div>
))}
</div>
<PrimaryButton type="button" onClick={handleAddStep} className='w-full'>
<Plus className='stroke-neutral-10' />
Add Steps
</PrimaryButton>
<AddFrameworkModal show={isAddFrameworkModalOpen} onClose={() => setIsAddFrameworkModalOpen(false)} onFrameworkAdded={fetchFrameworks} />
</div>
);
};

189
resources/js/Pages/SubjectCategory/AddModal.tsx

@ -0,0 +1,189 @@
import { FormEventHandler, Fragment, PropsWithChildren, useEffect, useState } from 'react';
import { Dialog, Transition } from '@headlessui/react';
import { FileText, Box, Info, XCircle, ArrowRightCircle, CheckCircle, XOctagon } from 'react-feather';
import { useForm } from '@inertiajs/react';
import FormGroup from '@/Components/FormGroup';
import InputLabel from '@/Components/InputLabel';
import TextInput from '@/Components/TextInput';
import InputError from '@/Components/InputError';
import ErrorButton from '@/Components/ErrorButton';
import PrimaryButton from '@/Components/PrimaryButton';
import axios from 'axios';
import Modal from '@/Components/Modal';
import ModalButton from '@/Components/ModalButton';
export default function AddCategoryModal({
show = false,
maxWidth = '2xl',
closeable = true,
onClose = () => {},
onCategoryAdded = () => {},
className = '',
}: PropsWithChildren<{
show: boolean;
maxWidth?: 'sm' | 'md' | 'lg' | 'xl' | '2xl';
closeable?: boolean;
onClose: CallableFunction;
onCategoryAdded: CallableFunction;
className?: string;
}>) {
const close = () => {
if (closeable) {
onClose();
}
};
const maxWidthClass = {
sm: 'sm:max-w-sm',
md: 'sm:max-w-md',
lg: 'sm:max-w-lg',
xl: 'sm:max-w-xl',
'2xl': 'sm:max-w-2xl',
}[maxWidth];
const { data, setData, errors, reset } = useForm({
subjectTitle: '',
description:'',
});
useEffect(() => {
return () => {
reset('subjectTitle');
reset('description');
};
}, []);
const [status, setStatus] = useState('');
const [feedback, setFeedback] = useState('');
const [isSuccessModalOpen, setIsSuccessModalOpen] = useState(false);
const [isErrorModalOpen, setIsErrorModalOpen] = useState(false);
const handleCloseSuccessModal = () => {
setIsSuccessModalOpen(false);
onCategoryAdded();
close();
}
const submit: FormEventHandler = async (e) => {
e.preventDefault();
try {
const response = await axios.post(route('category.add'), data);
setStatus('Success')
setFeedback(response.data.message);
setIsSuccessModalOpen(true);
} catch (error) {
setStatus('Error')
if (axios.isAxiosError(error)) {
setFeedback(error.response?.data.message || 'An error occurred');
} else {
setFeedback('An unexpected error occurred');
}
setIsErrorModalOpen(true);
}
};
return (
<Transition show={show} as={Fragment} leave="duration-200">
<Dialog
as="div"
id="modal"
className="fixed inset-0 flex overflow-y-auto px-4 py-6 sm:px-0 items-center z-50 transform transition-all"
onClose={close}
>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="absolute inset-0 bg-neutral-0 opacity-50" />
</Transition.Child>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
enterTo="opacity-100 translate-y-0 sm:scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
<Dialog.Panel
className={`mb-6 bg-white border-4 rounded-lg overflow-hidden shadow-xl items-center transform transition-all sm:w-full sm:mx-auto bg-neutral-white ${maxWidthClass} ${className}`}
>
<div className="p-4 flex flex-col">
<h1 className="font-bold text-xl text-primary-main flex items-center my-2"><FileText className="stroke-primary-main mr-2" />Add New Category</h1>
<div className="divider"></div>
<form id="AddCategoryForm">
<div>
<FormGroup>
<InputLabel htmlFor="subjectTitle"><Box className='stroke-neutral-10' /></InputLabel>
<label htmlFor="subjectTitle" className="mx-2 font-semibold">Subject Category Title:</label>
</FormGroup>
<TextInput
id='subjectTitle'
name='subjectTitle'
value={data.subjectTitle}
autoComplete='off'
required
onChange={(e) => setData('subjectTitle', e.target.value)}
/>
<InputError message={errors.subjectTitle} className="mt-2" />
</div>
<div>
<FormGroup>
<InputLabel htmlFor="category"><Info className='stroke-neutral-10' /></InputLabel>
<label htmlFor="category" className="mx-2 font-semibold">Description:</label>
</FormGroup>
<textarea className="px-2 flex-grow w-full bg-primary-background border border-primary-hover focus:outline-none focus:bg-neutral-10 focus:border-2 focus:border-primary-hover"
onChange={(e) => setData('description', e.target.value)}>
</textarea>
</div>
<div className='flex items-center justify-center mt-3 w-full'>
<ErrorButton type="button" className='w-1/2' onClick={close}>
<XCircle className='stroke-neutral-10' />
Cancel
</ErrorButton>
<PrimaryButton type="button" onClick={submit} className='w-1/2'>
<ArrowRightCircle className='stroke-neutral-10' />
Save
</PrimaryButton>
</div>
</form>
</div>
<Modal show={isSuccessModalOpen} onClose={() => setIsSuccessModalOpen(false)} maxWidth="lg" styling='success'>
<div className="p-4 flex flex-col items-center">
<CheckCircle className='stroke-success-main' size={80} />
<h2 className="text-xl font-bold mt-2">{ status }</h2>
<p className="mt-4">{ feedback }</p>
<ModalButton onClick={handleCloseSuccessModal} className="bg-success-main text-white hover:bg-success-hover active:bg-success-pressed">
Close
</ModalButton>
</div>
</Modal>
<Modal show={isErrorModalOpen} onClose={() => setIsErrorModalOpen(false)} maxWidth="lg" styling='error'>
<div className="p-4 flex flex-col items-center">
<XOctagon className='stroke-error-main' size={80} />
<h2 className="text-xl font-bold mt-2">{ status }</h2>
<p className="mt-4">{ feedback }</p>
<ModalButton onClick={() => setIsErrorModalOpen(false)} className="bg-error-main text-white hover:bg-error-hover active:bg-error-pressed">
Close
</ModalButton>
</div>
</Modal>
</Dialog.Panel>
</Transition.Child>
</Dialog>
</Transition>
);
}

25
routes/auth.php

@ -9,6 +9,10 @@ use App\Http\Controllers\Auth\PasswordController;
use App\Http\Controllers\Auth\PasswordResetLinkController;
use App\Http\Controllers\Auth\RegisteredUserController;
use App\Http\Controllers\Auth\VerifyEmailController;
use App\Http\Controllers\CategoryController;
use App\Http\Controllers\FrameworkController;
use App\Http\Controllers\InstructionController;
use App\Http\Controllers\PageController;
use Illuminate\Support\Facades\Route;
Route::middleware('guest')->group(function () {
@ -56,4 +60,25 @@ Route::middleware('auth')->group(function () {
Route::post('logout', [AuthenticatedSessionController::class, 'destroy'])
->name('logout');
Route::get('page', [PageController::class, 'create'])
->name('page.create');
Route::post('page', [PageController::class, 'store'])
->name('page.add');
Route::post('category', [CategoryController::class, 'store'])
->name('category.add');
Route::get('categories', [CategoryController::class, 'index'])
->name('categories.index');
Route::post('framework', [FrameworkController::class, 'store'])
->name('framework.add');
Route::get('frameworks', [FrameworkController::class, 'index'])
->name('frameworks.index');
Route::post('instruction', [InstructionController::class, 'store'])
->name('instruction.add');
});
Loading…
Cancel
Save