EJ-Dannug
3 months ago
27 changed files with 3498 additions and 32 deletions
-
80app/Http/Controllers/CategoryController.php
-
80app/Http/Controllers/FrameworkController.php
-
85app/Http/Controllers/InstructionController.php
-
86app/Http/Controllers/PageController.php
-
21app/Models/Category.php
-
21app/Models/Framework.php
-
43app/Models/Instruction.php
-
42app/Models/Page.php
-
1database/migrations/2024_06_09_184105_create_roles_table.php
-
29database/migrations/2024_06_27_124215_create_categories_table.php
-
29database/migrations/2024_06_27_202837_create_frameworks_table.php
-
32database/migrations/2024_06_29_044406_create_pages_table.php
-
33database/migrations/2024_06_29_215447_create_instructions_table.php
-
2024package-lock.json
-
12package.json
-
4resources/js/Components/DashboardComponents/ContentStatistics.tsx
-
3resources/js/Components/DashboardComponents/PagesOverview.tsx
-
17resources/js/Components/ErrorButton.tsx
-
2resources/js/Components/FormGroup.tsx
-
17resources/js/Components/SuccessButton.tsx
-
196resources/js/Pages/AppFramework/AddModal.tsx
-
2resources/js/Pages/Auth/Login.tsx
-
2resources/js/Pages/Auth/Register.tsx
-
285resources/js/Pages/RepositoryPage/Create.tsx
-
170resources/js/Pages/RepositoryPage/InstructionsForm.tsx
-
189resources/js/Pages/SubjectCategory/AddModal.tsx
-
25routes/auth.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) |
||||
|
{ |
||||
|
//
|
||||
|
} |
||||
|
} |
@ -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) |
||||
|
{ |
||||
|
//
|
||||
|
} |
||||
|
} |
@ -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) |
||||
|
{ |
||||
|
//
|
||||
|
} |
||||
|
} |
@ -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) |
||||
|
{ |
||||
|
//
|
||||
|
} |
||||
|
} |
@ -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', |
||||
|
]; |
||||
|
} |
@ -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', |
||||
|
]; |
||||
|
} |
@ -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); |
||||
|
} |
||||
|
} |
@ -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); |
||||
|
} |
||||
|
} |
@ -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'); |
||||
|
} |
||||
|
}; |
@ -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'); |
||||
|
} |
||||
|
}; |
@ -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'); |
||||
|
} |
||||
|
}; |
@ -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
File diff suppressed because it is too large
View File
@ -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> |
||||
|
); |
||||
|
} |
@ -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> |
||||
|
); |
||||
|
} |
@ -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> |
||||
|
); |
||||
|
} |
@ -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> |
||||
|
); |
||||
|
} |
@ -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> |
||||
|
); |
||||
|
}; |
@ -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> |
||||
|
); |
||||
|
} |
Write
Preview
Loading…
Cancel
Save
Reference in new issue