From f166e8179b8b291ad356fb4c9e11095d13140948 Mon Sep 17 00:00:00 2001 From: ciraganenicole <ciraganenicole@gmail.com> Date: Wed, 4 Oct 2023 08:56:29 +0200 Subject: [PATCH 1/2] Port ballot to the dashboard --- main/app/DataTransferObjects/BallotData.php | 4 +- main/app/Enums/ModelStatusEnum.php | 16 +- .../Dashboard/BallotController.php | 45 ++++- .../Dashboard/DashboardController.php | 6 +- .../Dashboard/SnapshotController.php | 5 +- main/app/Models/Ballot.php | 19 +- .../js/Pages/Dashboard/Ballots/Create.vue | 152 +--------------- .../js/Pages/Dashboard/Ballots/Edit.vue | 48 +---- .../js/Pages/Dashboard/Ballots/Index.vue | 79 -------- .../Dashboard/Ballots/Partials/BallotCard.vue | 88 +++++++++ .../Dashboard/Ballots/Partials/BallotForm.vue | 133 ++++++++++++++ .../Dashboard/Ballots/Partials/BallotList.vue | 161 ++++++++++++++++ .../Ballots/Partials/BallotStatusBadge.vue | 28 +++ .../Dashboard/Ballots/Partials/Step1.vue | 172 ++++++++++++++++++ .../Ballots/{ => Partials}/Step2.vue | 9 +- .../Ballots/{ => Partials}/Step3.vue | 12 +- .../Dashboard/Ballots/Partials/Step4.vue | 14 ++ .../Ballots/Services/admin-ballot-service.ts | 7 + .../js/Pages/Dashboard/Ballots/Step1.vue | 38 ---- .../js/Pages/Dashboard/Ballots/Step4.vue | 20 -- .../js/Pages/Dashboard/Ballots/View.vue | 32 ++++ main/resources/js/Pages/Dashboard/Home.vue | 20 ++ .../js/Pages/Dashboard/Snapshot/Create.vue | 2 +- main/resources/js/models/alert.ts | 6 + .../js/shared/Services/admin-service.ts | 8 + main/resources/js/shared/Services/alert.ts | 12 ++ main/resources/js/stores/ballot-store.ts | 118 ++++++------ .../resources/js/stores/global-alert-store.ts | 25 +++ main/resources/js/types/generated.d.ts | 1 + main/resources/js/utils/alert.ts | 11 ++ main/routes/web.php | 15 +- 31 files changed, 883 insertions(+), 423 deletions(-) delete mode 100644 main/resources/js/Pages/Dashboard/Ballots/Index.vue create mode 100644 main/resources/js/Pages/Dashboard/Ballots/Partials/BallotCard.vue create mode 100644 main/resources/js/Pages/Dashboard/Ballots/Partials/BallotForm.vue create mode 100644 main/resources/js/Pages/Dashboard/Ballots/Partials/BallotList.vue create mode 100644 main/resources/js/Pages/Dashboard/Ballots/Partials/BallotStatusBadge.vue create mode 100644 main/resources/js/Pages/Dashboard/Ballots/Partials/Step1.vue rename main/resources/js/Pages/Dashboard/Ballots/{ => Partials}/Step2.vue (62%) rename main/resources/js/Pages/Dashboard/Ballots/{ => Partials}/Step3.vue (62%) create mode 100644 main/resources/js/Pages/Dashboard/Ballots/Partials/Step4.vue create mode 100644 main/resources/js/Pages/Dashboard/Ballots/Services/admin-ballot-service.ts delete mode 100644 main/resources/js/Pages/Dashboard/Ballots/Step1.vue delete mode 100644 main/resources/js/Pages/Dashboard/Ballots/Step4.vue create mode 100644 main/resources/js/Pages/Dashboard/Ballots/View.vue create mode 100644 main/resources/js/models/alert.ts create mode 100644 main/resources/js/shared/Services/admin-service.ts create mode 100644 main/resources/js/shared/Services/alert.ts create mode 100644 main/resources/js/stores/global-alert-store.ts create mode 100644 main/resources/js/utils/alert.ts diff --git a/main/app/DataTransferObjects/BallotData.php b/main/app/DataTransferObjects/BallotData.php index b3e8037..201b05f 100644 --- a/main/app/DataTransferObjects/BallotData.php +++ b/main/app/DataTransferObjects/BallotData.php @@ -9,10 +9,12 @@ class BallotData extends Data { public function __construct( + public int $id, public int $user_id, public string $title, public string $description, - public string $status, + public $status, + public ?string $isPublished, public string $started_at, public string $ended_at, ) { diff --git a/main/app/Enums/ModelStatusEnum.php b/main/app/Enums/ModelStatusEnum.php index 686a9ad..2a4b539 100644 --- a/main/app/Enums/ModelStatusEnum.php +++ b/main/app/Enums/ModelStatusEnum.php @@ -2,16 +2,14 @@ namespace App\Enums; -enum ModelStatusEnum: String +enum ModelStatusEnum: string { - public static function getValues() - { - $possibleStatuses = [ - 'draft', - 'pending', - 'published', - ]; + case DRAFT = 'draft'; + case PENDING = 'pending'; + case PUBLISHED = 'published'; - return $possibleStatuses; + public static function values(): array + { + return array_column(self::cases(), 'value'); } } diff --git a/main/app/Http/Controllers/Dashboard/BallotController.php b/main/app/Http/Controllers/Dashboard/BallotController.php index 6500908..5f4eab2 100644 --- a/main/app/Http/Controllers/Dashboard/BallotController.php +++ b/main/app/Http/Controllers/Dashboard/BallotController.php @@ -7,6 +7,9 @@ use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; use Inertia\Inertia; +use App\DataTransferObjects\BallotData; +use Illuminate\Support\Facades\Redirect; +use Illuminate\Support\Facades\Gate; class BallotController extends Controller @@ -26,7 +29,7 @@ public function index(Request $request) public function create() { return Inertia::render( - 'Dashboard/Ballots/Create' + 'Dashboard/Ballots/Create', [] ); } @@ -36,27 +39,51 @@ public function store(Request $request) $validatedData = $request->validate([ 'title' => 'required|string|min:3|max:255', 'description' => 'required|string|min:10', + 'started_at' => 'required', + 'ended_at' => 'required', + 'status' => 'required', ]); $ballot = Ballot::create([ 'user_id' => $user->id, 'title' => $validatedData['title'], 'description' => $validatedData['description'], + 'started_at' => $validatedData['started_at'], + 'ended_at' => $validatedData['ended_at'], + 'status' => $validatedData['status'], ]); $ballot->save(); - return to_route('dashboard.ballots.index'); + //return to_route('dashboard.home'); } - public function edit(Request $request) + public function view(Request $request, Ballot $ballot) + { + $user = Auth::user(); + + return Inertia::render( + 'Dashboard/Ballots/View', [ + 'ballot' => BallotData::from([ + 'id' => $ballot->id, + 'user_id' => $user->id, + 'title' => $ballot->title, + 'description' => $ballot->description, + 'status' => $ballot->status, + 'started_at' => $ballot->started_at, + 'ended_at' => $ballot->ended_at + ]), + ] + ); + } + + public function edit(Request $request, Ballot $ballot) { - $ballot = Ballot::find($request->input('id')); return Inertia::render( 'Dashboard/Ballots/Edit', [ - 'ballot' => $ballot, + 'ballot' => BallotData::from($ballot), ] ); } @@ -67,18 +94,20 @@ public function update(Request $request, Ballot $ballot) $validatedData = $request->validate([ 'title' => 'required|string|min:3|max:255', 'description' => 'required|string|min:10', + 'started_at' => 'required', + 'ended_at' => 'required', + 'status' => 'required', ]); $ballot->update($validatedData); - // return response()->json(['message' => 'Ballot updated successfully', 'ballot' => BallotData::from( $ballot)], 201); - return to_route('dashboard.ballots.index'); + return to_route('dashboard.ballots.view', $ballot); } public function delete(Ballot $ballot) { $ballot->delete(); - return to_route('dashboard.ballots.index'); + return to_route('dashboard.home'); } } diff --git a/main/app/Http/Controllers/Dashboard/DashboardController.php b/main/app/Http/Controllers/Dashboard/DashboardController.php index 8c34a11..511b117 100644 --- a/main/app/Http/Controllers/Dashboard/DashboardController.php +++ b/main/app/Http/Controllers/Dashboard/DashboardController.php @@ -3,7 +3,6 @@ namespace App\Http\Controllers\Dashboard; use App\DataTransferObjects\BallotData; -use App\DataTransferObjects\SnapshotData; use App\Http\Controllers\Controller; use App\Models\Ballot; use App\Models\Snapshot; @@ -15,13 +14,14 @@ class DashboardController extends Controller { - public function index(Request $request): Response + public function index(Request $request, Ballot $ballot ): Response { $snapshots = Snapshot::all(); - + $ballots = BallotData::collection(Ballot::all()); return Inertia::render('Dashboard/Home') ->with([ 'snapshots' => $snapshots, + 'ballots' => $ballots, ]); } } diff --git a/main/app/Http/Controllers/Dashboard/SnapshotController.php b/main/app/Http/Controllers/Dashboard/SnapshotController.php index 5d42351..2c397bc 100644 --- a/main/app/Http/Controllers/Dashboard/SnapshotController.php +++ b/main/app/Http/Controllers/Dashboard/SnapshotController.php @@ -39,11 +39,14 @@ public function store(Request $request) 'user_id' => $user->id, 'title' => $validatedData['title'], 'description' => $validatedData['description'], + 'policy_id' => 23, + 'status' => 'draft', + 'type' => 'file', ]); $snapshot->save(); - return to_route('dashboard.snapshot.index'); + return to_route('dashboard.snapshots.index'); } public function read(Request $request) diff --git a/main/app/Models/Ballot.php b/main/app/Models/Ballot.php index 0d062c2..6ba8e6a 100644 --- a/main/app/Models/Ballot.php +++ b/main/app/Models/Ballot.php @@ -4,8 +4,11 @@ use App\Models\Traits\HasModel; use App\Models\Traits\HasOwner; +use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; +use App\Enums\ModelStatusEnum; +use App\Enums\BallotTypeEnum; class Ballot extends Model { @@ -17,6 +20,20 @@ class Ballot extends Model 'description', 'status', 'policy_id', - 'type', + 'isPublished', + 'started_at', + 'ended_at', ]; + + protected $casts = [ + 'status' => ModelStatusEnum::class, + 'started_at' => 'datetime:Y-m-d H:i:s', + 'ended_at' => 'datetime:Y-m-d H:i:s', + ]; + + public function isPublished(): Attribute + { + return Attribute::make(get: fn () => $this->status == 'published' ? true : false); + } } + diff --git a/main/resources/js/Pages/Dashboard/Ballots/Create.vue b/main/resources/js/Pages/Dashboard/Ballots/Create.vue index 329ec69..f3f052a 100644 --- a/main/resources/js/Pages/Dashboard/Ballots/Create.vue +++ b/main/resources/js/Pages/Dashboard/Ballots/Create.vue @@ -1,149 +1,7 @@ -<template> - <AppLayout title="Dashboard"> - <div class="py-12"> - <div class="mx-auto max-w-7xl sm:px-6 lg:px-8"> - <div class="flex flex-col w-[100%] bg-white"> - <div class="bg-gray-50 px-20 py-10"> - <div class="flex items-center justify-center"> - <div - v-for="(stepName, stepNumber) in stepNames" - :key="stepNumber" - class="flex-col items-center" - > - <div class="relative flex items-center mb-4"> - <span - class="rounded-tl-full rounded-bl-full p-2 bg-primary-950 text-white w-8 h-8 flex items-center justify-center mr-[-4px]" - :class="{ 'bg-opacity-50': step !== stepName.id }" - >{{ stepName.id }}</span> - <span - class="rounded-full p-2 bg-primary-950 text-white w-20 h-16 flex items-center justify-center" - :class="{ 'bg-opacity-50': step !== stepName.id }" - > - <i - v-if="stepName.id === 1" - class="fas fa-user" - ></i> - <i - v-else-if="stepName.id === 2" - class="fas fa-question" - ></i> - <i - v-else-if="stepName.id === 3" - class="fas fa-shield-alt" - ></i> - <i - v-else - class="fas fa-check" - ></i> - </span> - <svg - v-if="stepNumber < 3" - class="h-[2px] w-[100%] bg-primary-950" - :class="{ 'bg-opacity-50': step !== stepName.id }" - :style="{ left: `${((stepName.id - 1) / 3) * 100}%` }" - ></svg> - </div> - <span - class="text-xl text-slate-600 ml-4" - :class="{ 'text-primary-950': step === stepNumber }" - > - {{ stepName.name }} - </span> - </div> - </div> - </div> - <div class="flex items-center px-20 py-10"> - <div class="w-[40%] border-[1px] border-primary-950 rounded-lg p-8"> - <h1 class="mb-12 text-2xl font-semibold text-slate-600"> - Create new ballot - </h1> - <component :is="currentStepComponent"/> - </div> - <div class="w-[30%] p-20">Sidebar</div> - </div> - <hr/> - <div class="flex flex-row items-center justify-between px-20 py-4"> - <button - v-if="step !== 1" - class="text-primary-950 border-primary-950 border-[1px] py-4 px-8 text-lg mt-10 rounded-[10px] flex items-center justify-between w-[140px]" - @click="prevStep" - :disabled="step === 1" - > - <i class="fas fa-arrow-left"></i> - <p>Back</p> - </button> - <button - v-if="step !== 4" - class="bg-primary-950 text-white p-4 text-lg mt-10 rounded-[10px] flex items-center justify-between w-[140px]" - @click="nextStep" - :disabled="step === 4" - > - <p>Next step</p> - <i class="fas fa-arrow-right"></i> - </button> - </div> - </div> - </div> - </div> - </AppLayout> -</template> - <script setup lang="ts"> -import {useForm} from '@inertiajs/vue3'; -import {useFormDataStore} from '@/stores/ballot-store'; -import {computed} from 'vue'; - -import Step1 from './Step1.vue'; -import Step2 from './Step2.vue'; -import Step3 from './Step3.vue'; -import Step4 from './Step4.vue'; -import AppLayout from "@/Layouts/AppLayout.vue"; - -const formDataStore = useFormDataStore(); -const step = computed(() => formDataStore.step); - -const stepNames = computed(() => [ - {id: 1, name: 'Intro'}, - {id: 2, name: 'Questions'}, - {id: 3, name: 'Policy'}, - {id: 4, name: 'Publish'}, - -]); - -const nextStep = () => { - formDataStore.nextStep(); -}; - -const prevStep = () => { - formDataStore.prevStep(); -}; - -const currentStepComponent = computed(() => { - switch (step.value) { - case 1: - return Step1; - case 2: - return Step2; - case 3: - return Step3; - case 4: - return Step4; - default: - return null; - } -}); - -const form = useForm({ - title: formDataStore.formData.title || '', - description: formDataStore.formData.description || '', - question: formDataStore.formData.question || '', - policy: formDataStore.formData.policy || '', - publish: formDataStore.formData.publish || false -}); - -const submit = () => { - form.post(route('dashboard.ballots.store'), {}); - -} - +import BallotForm from './Partials/BallotForm.vue'; </script> + +<template> + <BallotForm class="w-full" /> +</template> diff --git a/main/resources/js/Pages/Dashboard/Ballots/Edit.vue b/main/resources/js/Pages/Dashboard/Ballots/Edit.vue index 57fccbc..0facf95 100644 --- a/main/resources/js/Pages/Dashboard/Ballots/Edit.vue +++ b/main/resources/js/Pages/Dashboard/Ballots/Edit.vue @@ -1,50 +1,16 @@ <script setup lang="ts"> -import { useForm } from '@inertiajs/inertia-vue3'; +import BallotForm from './Partials/BallotForm.vue'; +import BallotData = App.DataTransferObjects.BallotData; - -const props = withDefaults( - defineProps<{ - ballot?: {'id': number, 'title': string, 'description': string}; - }>(), - {} -); - -const form = useForm({ - id: props.ballot.id, - title: props.ballot.title, - description: props.ballot.description -}); - -const submit = () => { - form.put(route('dashboard.ballots.update', {'ballot': props.ballot.id}))}; +const props = defineProps<{ + ballot: BallotData; +}>(); </script> <template> - <div class="flex items-center justify-center h-screen"> + <BallotForm :ballot="props.ballot" class="w-full" /> +</template> - <form @submit.prevent="submit" class="border-2 border-slate-400 rounded-[15px] w-[40%] p-16"> - <h1 class="mb-12 text-2xl font-semibold text-slate-600">Edit ballot</h1> - <div class="flex flex-col"> - <label for="title" class="mb-4 text-lg text-slate-600">Title</label> - <input - v-model="form.title" - type="text" - name="title" - id="title" - required - class="rounded-[10px] border-slate-400 p-4 w-[100%]" - /> - </div> - <div class="flex flex-col"> - <label for="description" class="my-4 text-lg text-slate-600">Description </label> - <textarea v-model="form.description" id="description" required class="rounded-[10px] border-slate-400 p-4 w-[100%] h-[200px]"></textarea> - </div> - <div> - <button type="submit" class="bg-blue-600 text-white p-4 text-lg mt-10 rounded-[10px] w-[30%]">Submit</button> - </div> - </form> - </div> - </template> diff --git a/main/resources/js/Pages/Dashboard/Ballots/Index.vue b/main/resources/js/Pages/Dashboard/Ballots/Index.vue deleted file mode 100644 index c4e2109..0000000 --- a/main/resources/js/Pages/Dashboard/Ballots/Index.vue +++ /dev/null @@ -1,79 +0,0 @@ -<script setup lang="ts"> -import { Inertia } from "@inertiajs/inertia"; -import { Head, Link } from "@inertiajs/vue3"; - -defineProps({ ballots: Array }); - -function destroy(id) { - if (confirm("Are you sure you want to Delete")) { - Inertia.delete(route('dashboard.ballots.delete', id)); - } -} - -function updateStatus(id) { - if (confirm("Are you sure you want to publish")) { - Inertia.post(route('dashboard.ballots.store', id)); - } -} -</script> - -<template> - <Head title="Ballots" /> - - <div class="p-16"> - <div class="mb-16"> - <a - :href="route('dashboard.ballots.create.view')" - class="text-xl text-white font-semibold bg-primary-950 p-4 rounded-[15px]" - >Create new ballot</a - > - </div> - - <section class="grid grid-cols-3 gap-20"> - <div v-for="ballot in ballots" :key="ballot.id"> - <div - class="w-full bg-white border border-slate-200 rounded-[15px] shadow-lg" - > - <ul - class="flex flex-row items-center justify-between text-sm font-medium text-center text-gray-500 border-b border-slate-200 rounded-t-[15px] p-4" - > - <li class="flex flex-row items-center"> - <div - class="mr-6 bg-orange-50 text-orange-900 px-3 rounded-[30px] published:bg-green-50 published:text-green-900" - > - {{ ballot.status }} - </div> - </li> - <li class="flex flex-row items-center gap-4"> - <Link - :href="route('dashboard.ballots.edit', {'id': ballot.id})" - class="px-6 py-1 bg-yellow-50 text-yellow-900 rounded-lg" - >Edit</Link - > - <button - class="bg-red-100 text-red-900 px-3 py-1 rounded-lg" - @click="destroy(ballot.id)" - > - Delete - </button> - </li> - </ul> - <div - class="p-6 bg-white rounded-[15px] dark:bg-gray-800 h-[250px]" - > - <h2 - class="mb-6 text-xl font-extrabold tracking-tight text-slate-600 dark:text-white" - > - {{ ballot.title }} - </h2> - <p - class="mb-2 text-gray-500 dark:text-gray-400 text-sm overflow-wrap" - > - {{ ballot.description }} - </p> - </div> - </div> - </div> - </section> - </div> -</template> diff --git a/main/resources/js/Pages/Dashboard/Ballots/Partials/BallotCard.vue b/main/resources/js/Pages/Dashboard/Ballots/Partials/BallotCard.vue new file mode 100644 index 0000000..82a0067 --- /dev/null +++ b/main/resources/js/Pages/Dashboard/Ballots/Partials/BallotCard.vue @@ -0,0 +1,88 @@ +<script setup lang="ts"> +import BallotData = App.DataTransferObjects.BallotData; +import { Link } from '@inertiajs/vue3'; + +defineProps<{ + ballot: BallotData; +}>(); + +</script> + + +<template> + <section> + <div class="relative"> + <div + class="overflow-hidden border border-primary-950 rounded-lg shadow-sm dark:border-gray-700 focus-within:border-indigo-500 dark:focus-within:border-indigo-600 focus-within:ring-1 focus-within:ring-indigo-500 dark:focus-within:ring-indigo-500"> + <header + class="block w-full p-4 text-gray-900 border-0 resize-none dark:text-gray-100 xl:p-4 sm:text-sm sm:leading-6 bg-primary-50/20 dark:bg-gray-900"> + <div class="flex flex-wrap items-center justify-start gap-2 sm:flex-nowrap"> + <h2 class="text-xl font-bold mb-4"> + {{ ballot.title }} + </h2> + <!-- <BallotStatusBadge :ballot="ballot"></BallotStatusBadge> --> + </div> + <p class="h-20 text-lg"> + {{ ballot.description }} + </p> + </header> + + <div class="flex items-center gap-8 px-2 py-4 xl:px-3"> + <div class="block text-sm font-medium leading-6 text-gray-900 dark:text-gray-300 w-44"> + Ballot Start + </div> + <p class="relative block w-full flex flex-1 border-0 p-2.5 sm:text-sm sm:leading-6 capitalize font-medium text-gray-900 dark:text-gray-100 placeholder:text-gray-400 focus:ring-0 bg-primary-50/20 dark:bg-gray-900 rounded-lg"> + {{ ballot.started_at }} + </p> + </div> + + <div class="flex items-center gap-8 px-2 py-4 xl:px-3"> + <div class="block text-sm font-medium leading-6 text-gray-900 dark:text-gray-300 w-44"> + Ballot End + </div> + <p class="relative block w-full flex flex-1 border-0 p-2.5 sm:text-sm sm:leading-6 capitalize font-medium text-gray-900 dark:text-gray-100 placeholder:text-gray-400 focus:ring-0 bg-primary-50/20 dark:bg-gray-900 rounded-lg"> + {{ ballot.ended_at }} + </p> + </div> + + <div class="flex items-center gap-8 px-2 py-4 xl:px-3"> + <div class="block text-sm font-medium leading-6 text-gray-900 dark:text-gray-300 w-44"> + Ballot Status + </div> + <p class="relative block w-full flex flex-1 border-0 p-2.5 sm:text-sm sm:leading-6 capitalize font-medium text-gray-900 dark:text-gray-100 placeholder:text-gray-400 focus:ring-0 bg-primary-50/20 dark:bg-gray-900 rounded-lg"> + {{ ballot.status }} + </p> + </div> + + + <div aria-hidden="true"> + <div class="py-3"> + <div class="h-10"/> + </div> + <div class="h-px"/> + <div class="py-3"> + <div class="py-px"> + <div class="h-10"/> + </div> + </div> + </div> + </div> + + <div v-if="ballot.title" class="absolute bottom-0 inset-x-px"> + <div + class="flex items-center justify-between px-2 py-3 space-x-3 border-t border-gray-200 dark:border-gray-700 sm:px-3"> + <div class="flex"> + + </div> + <div class="flex-shrink-0"> + <Link as="button" + :href="route( 'dashboard.ballots.edit', { ballot: ballot.id})" + class="inline-flex items-center px-6 py-2 text-sm font-semibold text-white bg-primary-950 rounded-md shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"> + <span>Edit</span> + </Link> + </div> + </div> + </div> + </div> + </section> +</template> \ No newline at end of file diff --git a/main/resources/js/Pages/Dashboard/Ballots/Partials/BallotForm.vue b/main/resources/js/Pages/Dashboard/Ballots/Partials/BallotForm.vue new file mode 100644 index 0000000..b7b3046 --- /dev/null +++ b/main/resources/js/Pages/Dashboard/Ballots/Partials/BallotForm.vue @@ -0,0 +1,133 @@ +<template> + <AppLayout title="Dashboard"> + <div class="py-12"> + <div class="mx-auto max-w-7xl sm:px-6 lg:px-8"> + <div class="flex flex-col w-[100%] bg-white"> + <div class="bg-gray-50 px-10 py-4"> + <div class="flex items-center justify-center"> + <div + v-for="(stepName, stepNumber) in stepNames" + :key="stepNumber" + class="flex-col items-center" + > + <div class="relative flex items-center mb-2"> + <span + class="rounded-tl-full rounded-bl-full p-2 bg-primary-950 text-white w-8 h-8 flex items-center justify-center mr-[-4px]" + :class="{ + 'bg-opacity-50': + step !== stepName.id, + }" + + >{{ stepName.id }}</span + > + <span + class="rounded-full p-2 bg-primary-950 text-white w-20 h-16 flex items-center justify-center" + :class="{ + 'bg-opacity-50': + step !== stepName.id, + }" + @click="changeStep()" + + > + <UserIcon class="w-6 h-6" v-if="stepName.id === 1"/> + <QuestionMarkCircleIcon class="w-6 h-6" v-else-if="stepName.id === 2"/> + <DocumentIcon class="w-6 h-6" v-else-if="stepName.id === 3"/> + <CheckCircleIcon class="w-6 h-6" v-else /> + </span> + <svg + v-if="stepNumber < 3" + class="h-[2px] w-[100%] bg-primary-950" + :class="{ + 'bg-opacity-50': + step !== stepName.id, + }" + :style="{ + left: `${ + ((stepName.id - 1) / 3) * 100 + }%`, + }" + ></svg> + </div> + <span + class="text-xl text-slate-600 ml-4" + :class="{ + 'text-primary-950': step === stepNumber, + }" + > + {{ stepName.name }} + </span> + </div> + </div> + </div> + <h1 class="text-xl font-semibold text-slate-600 p-8"> + {{ ballot ? 'Edit Ballot' : 'Create new ballot' }} + </h1> + <div class="flex items-center px-8 mb-4 gap-16"> + <div + class="w-[60%] border-[1px] border-primary-950 rounded-lg" + > + <!-- <component :is="currentStepComponent" /> --> + <div v-if="step == 1"> + <Step1 :ballot="ballot"></Step1> + </div> + <div v-else-if="step == 2"> + <Step2></Step2> + </div> + <div v-else-if="step == 3"> + <Step3></Step3> + </div> + <div v-else-if="step == 4"> + <Step4></Step4> + </div> + </div> + <div class="w-[35%] p-20 bg-primary-50/30 h-[560px] rounded-lg">Sidebar</div> + </div> + <hr /> + </div> + </div> + </div> + </AppLayout> +</template> + +<script setup lang="ts"> +import { useBallotStore } from "@/stores/ballot-store"; +import {storeToRefs} from 'pinia'; +import { computed, ref } from "vue"; +import { UserIcon, QuestionMarkCircleIcon, CheckCircleIcon, DocumentIcon } from '@heroicons/vue/24/solid'; + +import Step1 from "./Step1.vue"; +import Step2 from "./Step2.vue"; +import Step3 from "./Step3.vue"; +import Step4 from "./Step4.vue"; +import AppLayout from "@/Layouts/AppLayout.vue"; +import BallotData=App.DataTransferObjects.BallotData + +const props = defineProps<{ + ballot?: BallotData; +}>(); + +let propBallot = ref(props.ballot); +const { nextStep, previousStep } = useBallotStore(); + +const formD = useBallotStore(); +formD.uploadBallotData(propBallot.value); +formD.setStep(); +const { step, ballot } = storeToRefs(formD); + + +const stepNames = computed(() => [ + { id: 1, name: "Create ballot" }, + { id: 2, name: "Add questions" }, + { id: 3, name: "Add policy" }, + { id: 4, name: "Publish" }, +]); + +const changeStep = () => { + if (step.value < 4) { + nextStep(); + } else if (step.value > 1){ + previousStep(); + } + } +</script> + \ No newline at end of file diff --git a/main/resources/js/Pages/Dashboard/Ballots/Partials/BallotList.vue b/main/resources/js/Pages/Dashboard/Ballots/Partials/BallotList.vue new file mode 100644 index 0000000..23fbe3f --- /dev/null +++ b/main/resources/js/Pages/Dashboard/Ballots/Partials/BallotList.vue @@ -0,0 +1,161 @@ +<template> + <div class="overflow-x-auto"> + <ul + role="list" + class="grid grid-cols-1 gap-x-6 gap-y-8 lg:grid-cols-3 xl:gap-x-8" + style="width: max-content; min-width: 100%" + > + <li v-for="ballot in ballots" :key="ballot.id"> + <div + class="w-full bg-white border border-slate-200 rounded-[15px] shadow-lg" + > + <ul + class="flex flex-row items-center justify-between text-sm font-medium text-center text-gray-500 border-b border-slate-200 rounded-t-[15px] p-4 bg-primary-50/10" + > + <li class="text-slate-900 text-xl font-bold"> + {{ ballot.title }} + </li> + + <li class="flex flex-row items-center"> + <div class="flex flex-row items-center gap-2"> + <div + class="bg-orange-400 text-orange-900 rounded-full p-[4px]" + > + <div + class="bg-orange-900 text-orange-900 rounded-full w-[6px] h-[6px]" + /> + </div> + <p class="text-lg text-orange-900">Pending</p> + </div> + + <div class="hidden sm:flex sm:items-center sm:ml-6"> + <div class="relative"> + <Dropdown align="right" width="60"> + <template #trigger> + <button type="button"> + <svg + class="ml-2 -mr-0.5 h-4 w-4" + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" + stroke-width="1.5" + stroke="currentColor" + > + <path + stroke-linecap="round" + stroke-linejoin="round" + stroke-width="2" + d="M16 12a2 2 0 11-4 0 2 2 0 014 0zm-8 0a2 2 0 11-4 0 2 2 0 014 0zm16 0a2 2 0 11-4 0 2 2 0 014 0z" + /> + </svg> + </button> + </template> + + <template #content> + <div + class="flex flex-col items-start gap-4 p-4 w-40 right-0" + > + <DropdownLink + :href="view(ballot.id)" + > + View + </DropdownLink> + + <DropdownLink + :href=" + route( + 'dashboard.ballots.edit', + { id: ballot.id } + ) + " + > + Edit + </DropdownLink> + + <button + @click="destroy(ballot.id)" + class="text-red-600 dark:text-red-400 hover:bg-red-100 hover:text-red-800 ml-4" + > + Delete + </button> + </div> + </template> + </Dropdown> + </div> + </div> + </li> + </ul> + <div + class="p-6 bg-white rounded-[15px] dark:bg-gray-800 h-[250px]" + > + <ul> + <li> + <div + class="flex flex-row items-center justify-between py-4" + > + <h3>Ballot opens</h3> + <p>{{ ballot.started_at }}</p> + </div> + <hr class="h-4" /> + </li> + <li> + <div + class="flex flex-row items-center justify-between py-4" + > + <h3>Ballot ends</h3> + <p>{{ ballot.ended_at }}</p> + </div> + <hr class="h-4" /> + </li> + <li> + <div + class="flex flex-row items-center justify-between py-2" + > + <h3>Total votes</h3> + <p>0</p> + </div> + </li> + </ul> + </div> + </div> + </li> + + <li + class="py-16 overflow-hidden border border-gray-400 border-dashed rounded-xl dark:border-gray-700 hover:border-indigo-600" + > + <Link + as="button" + :href="route('dashboard.ballots.create.view')" + class="flex flex-col items-center justify-center w-full h-full gap-2 px-6 py-4 leading-6 text-gray-500 text-md xl:text-xl dark:text-gray-400" + > + <PlusIcon class="w-6 h-6" /> + <span>Create ballot</span> + </Link> + </li> + </ul> + </div> +</template> + +<script setup lang="ts"> +import { Link } from "@inertiajs/vue3"; +import { Inertia } from "@inertiajs/inertia"; +import { PlusIcon } from "@heroicons/vue/24/solid"; +import Dropdown from "@/Components/Dropdown.vue"; +import DropdownLink from "@/Components/DropdownLink.vue"; +import BallotData = App.DataTransferObjects.BallotData; + +defineProps<{ + ballots: BallotData[]; +}>(); + +function destroy(id) { + if (confirm("Are you sure you want to Delete")) { + Inertia.delete(route("dashboard.ballots.delete", id)); + } +} + +function view(id) { + let link = route("dashboard.ballots.view", { ballot: id }); + return link; +} +</script> diff --git a/main/resources/js/Pages/Dashboard/Ballots/Partials/BallotStatusBadge.vue b/main/resources/js/Pages/Dashboard/Ballots/Partials/BallotStatusBadge.vue new file mode 100644 index 0000000..7d52e2d --- /dev/null +++ b/main/resources/js/Pages/Dashboard/Ballots/Partials/BallotStatusBadge.vue @@ -0,0 +1,28 @@ +<script lang="ts" setup> +import BallotData = App.DataTransferObjects.BallotData; +import {computed} from "vue"; + +const props = defineProps<{ + ballot: BallotData; +}>(); + +let status = computed(() => { + switch (props.ballot.status) { + case 'published': + return props.ballot?.status ? 'Pending' : 'Published'; + default: + return props.ballot.status; + } +}); + +</script> +<template> + <span class="flex items-center gap-x-1.5"> + <span class="flex-none rounded-full p-1"> + <div class="h-1.5 w-1.5 rounded-full" /> + </span> + <span class="text-xs leading-5 text-gray-300 capitalize"> + {{ status }} + </span> + </span> +</template> diff --git a/main/resources/js/Pages/Dashboard/Ballots/Partials/Step1.vue b/main/resources/js/Pages/Dashboard/Ballots/Partials/Step1.vue new file mode 100644 index 0000000..826b31a --- /dev/null +++ b/main/resources/js/Pages/Dashboard/Ballots/Partials/Step1.vue @@ -0,0 +1,172 @@ +<template> + <form @submit.prevent="submitForm"> + <div class="flex flex-col bg-primary-100/10"> + <input + v-model="form.title" + type="text" + name="title" + id="title" + placeholder="Title" + required + class="block w-full border-0 py-3 px-6 text-lg font-medium text-gray-900 dark:text-gray-100 placeholder:text-gray-400 focus:ring-0 bg-primary-50/10 dark:bg-gray-900" + /> + <textarea + v-model="form.description" + id="description" + placeholder="Write a description" + required + class="block w-full h-[100px] py-3 px-6 text-gray-900 bg-primary-50/10 border-0 rounded-md resize-none text-md xl:text-lg dark:text-gray-100 placeholder:text-gray-400 focus:ring-0 sm:text-sm sm:leading-6 dark:bg-gray-900" + ></textarea> + </div> + + <div class="flex flex-col p-4"> + <div> + <div class="flex items-center gap-8 px-2 py-4 xl:px-3"> + <label for="title" class="block text-sm font-medium leading-6 text-gray-900 dark:text-gray-300 w-44">Start Date & Time</label> + <input + v-model="form.started_at" + type="datetime-local" + name="version" + id="version" + placeholder="Datetime" + class="relative w-full flex flex-1 border-0 sm:text-sm sm:leading-6 font-medium text-gray-900 dark:text-gray-100 placeholder:text-gray-400 bg-primary-50/10 dark:bg-gray-900 ring-1 ring-gray-300 dark:ring-gray-700 focus:ring-2 focus:ring-indigo-600 dark:focus:ring-indigo-700 rounded-lg"> + </div> + <div class="flex items-center gap-8 px-2 py-4 xl:px-3"> + <label for="title" class="block text-sm font-medium leading-6 text-gray-900 dark:text-gray-300 w-44">End Date & Time</label> + <input + v-model="form.ended_at" + type="datetime-local" + name="version" + id="version" + placeholder="Datetime" + class="relative w-full flex flex-1 border-0 sm:text-sm sm:leading-6 font-medium text-gray-900 dark:text-gray-100 placeholder:text-gray-400 bg-primary-50/10 dark:bg-gray-900 ring-1 ring-gray-300 dark:ring-gray-700 focus:ring-2 focus:ring-indigo-600 dark:focus:ring-indigo-700 rounded-lg"> + </div> + + <Listbox as="div" @update:modelValue="value => form.status = value" + class="flex items-center gap-8 px-2 py-2 border-t border-gray-200 xl:px-3 dark:border-gray-700"> + <ListboxLabel class="block text-sm font-medium leading-6 text-gray-900 dark:text-gray-300 w-44"> + Ballot Status + </ListboxLabel> + <div class="relative flex flex-1 mt-2"> + <ListboxButton + class="relative w-full cursor-default rounded-md bg-primary-50/10 dark:bg-gray-900 py-1.5 pl-3 pr-10 text-left text-gray-900 dark:text-gray-100 shadow-sm ring-1 ring-inset ring-gray-300 dark:ring-gray-700 focus:outline-none focus:ring-2 focus:ring-indigo-600 dark:focus:ring-indigo-700 sm:text-sm sm:leading-6"> + <span class="block capitalize truncate">{{ form.status }}</span> + <span class="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none"> + <ChevronUpDownIcon class="w-5 h-5 text-gray-400" aria-hidden="true"/> + </span> + </ListboxButton> + + <transition leave-active-class="transition duration-100 ease-in" + leave-from-class="opacity-100" leave-to-class="opacity-0"> + <ListboxOptions + class="absolute z-10 w-full py-1 mt-1 overflow-auto text-base bg-primary-50 rounded-md shadow-lg max-h-60 dark:bg-gray-700 ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm"> + <ListboxOption as="template" v-for="status in Object.values(ballotStatuses)" :key="status" + :value="status" v-slot="{ active, selected }"> + <li :class="[active ? 'bg-primary-950 text-white' : 'text-gray-900', 'relative cursor-default select-none py-2 pl-3 pr-9']"> + <span class="capitalize" + :class="[selected ? 'font-semibold' : 'font-normal', 'block truncate']"> + {{status }} + </span> + + <span v-if="selected" + :class="[active ? 'text-white' : 'text-indigo-600', 'absolute inset-y-0 right-0 flex items-center pr-4']"> + <CheckIcon class="w-5 h-5" aria-hidden="true"/> + </span> + </li> + </ListboxOption> + </ListboxOptions> + </transition> + </div> + </Listbox> + </div> + + <div class="flex justify-end px-2 py-2 space-x-2 border-t border-gray-200 flex-nowrap sm:px-3 dark:border-gray-700"> + <div class="flex-shrink-0" data-headlessui-state=""> + <label id="headlessui-listbox-label-30" data-headlessui-state="" class="sr-only">Assign</label> + <div class="relative"> + <button id="headlessui-listbox-button-31" type="button" aria-haspopup="listbox" aria-expanded="false" data-headlessui-state="" class="relative inline-flex items-center px-2 py-2 text-sm font-medium text-gray-500 rounded-full whitespace-nowrap bg-gray-50 dark:bg-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-900 sm:px-3" aria-labelledby="headlessui-listbox-label-30 headlessui-listbox-button-31"> + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" class="flex-shrink-0 w-5 h-5 text-gray-300 dark:text-gray-100 sm:-ml-1"> + <path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-5.5-2.5a2.5 2.5 0 11-5 0 2.5 2.5 0 015 0zM10 12a5.99 5.99 0 00-4.793 2.39A6.483 6.483 0 0010 16.5a6.483 6.483 0 004.793-2.11A5.99 5.99 0 0010 12z" clip-rule="evenodd"></path> + </svg> + <span class="hidden truncate sm:ml-2 sm:block">Assign</span> + </button> + </div> + </div> + </div> + <div class="flex items-center justify-between px-2 py-2 space-x-3 border-t border-gray-200 dark:border-gray-700 sm:px-3"> + <div class="flex"></div> + <div class="flex-shrink-0"> + <button + type="submit" + class="inline-flex items-center px-3 py-2 text-sm font-semibold text-white bg-primary-950 rounded-md shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"> + {{ ballot ? 'Edit' : 'Create' }} + </button> + </div> + </div> + </div> +</form> +</template> + +<script setup lang="ts"> +import { useForm } from "@inertiajs/vue3"; +import AlertService from "@/shared/Services/alert" +import {Listbox, ListboxButton, ListboxLabel, ListboxOption, ListboxOptions} from '@headlessui/vue' +import BallotData = App.DataTransferObjects.BallotData; +import { ChevronUpDownIcon } from '@heroicons/vue/20/solid' +import { useBallotStore } from "@/stores/ballot-store"; +import { ref } from "vue"; +import AdminBallotService from "../Services/admin-ballot-service" + + +const props = defineProps<{ + status?: String; + ballot?: BallotData; +}>(); + +const ballotStatuses = ref({}); +AdminBallotService.getBallotStatuses().then((statuses) => { + ballotStatuses.value = statuses; +}); + +const form = useForm({ + title: props?.ballot?.title ?? '', + description: props?.ballot?.description ?? '', + status: props?.ballot?.status ?? 'Select Status', + type: props?.ballot?.type ?? 'Select Type', + started_at: props?.ballot?.started_at, + ended_at: props?.ballot?.ended_at, +}); + +const { nextStep } = useBallotStore(); + +const submitForm = () => { + if (!props.ballot?.id) { + form.post(route('dashboard.ballots.store'), { + onSuccess: () => { + AlertService.show(['Ballot created successfully'], 'success'); + nextStep(); + }, + onError: (errors) => { + AlertService.show( + Object + .entries(errors) + .map(([key, value]) => value) + ); + }, + }); + } else { + form.put(route('dashboard.ballots.update', {ballot: props.ballot?.id}), { + onSuccess: () => { + AlertService.show(['Ballot updated successfully'], 'success'); + }, + onError: (errors) => { + AlertService.show( + Object + .entries(errors) + .map(([key, value]) => value) + ); + }, + }); + } +} +</script> diff --git a/main/resources/js/Pages/Dashboard/Ballots/Step2.vue b/main/resources/js/Pages/Dashboard/Ballots/Partials/Step2.vue similarity index 62% rename from main/resources/js/Pages/Dashboard/Ballots/Step2.vue rename to main/resources/js/Pages/Dashboard/Ballots/Partials/Step2.vue index 00a4e28..3337c70 100644 --- a/main/resources/js/Pages/Dashboard/Ballots/Step2.vue +++ b/main/resources/js/Pages/Dashboard/Ballots/Partials/Step2.vue @@ -4,7 +4,7 @@ >Question </label> <textarea - v-model="formDataStore.formData.question" + v-model="form.question" id="description" required class="rounded-[10px] border-slate-400 p-4 w-[100%] h-[150px]" @@ -13,12 +13,11 @@ </template> <script setup lang="ts"> -import {useFormDataStore} from '@/stores/ballot-store'; +import { useForm } from '@inertiajs/vue3'; -const formDataStore = useFormDataStore(); -formDataStore.updateFormData({ - question: formDataStore.formData.question +const form = useForm({ + question: '' }); </script> diff --git a/main/resources/js/Pages/Dashboard/Ballots/Step3.vue b/main/resources/js/Pages/Dashboard/Ballots/Partials/Step3.vue similarity index 62% rename from main/resources/js/Pages/Dashboard/Ballots/Step3.vue rename to main/resources/js/Pages/Dashboard/Ballots/Partials/Step3.vue index 2f994d6..0f20975 100644 --- a/main/resources/js/Pages/Dashboard/Ballots/Step3.vue +++ b/main/resources/js/Pages/Dashboard/Ballots/Partials/Step3.vue @@ -4,7 +4,7 @@ >Policy </label> <textarea - v-model="formDataStore.formData.policy" + v-model="form.policy" id="description" required class="rounded-[10px] border-slate-400 p-4 w-[100%] h-[150px]" @@ -13,13 +13,9 @@ </template> <script setup lang="ts"> -import {useFormDataStore} from '@/stores/ballot-store'; +import { useForm } from '@inertiajs/vue3'; - -const formDataStore = useFormDataStore(); - -formDataStore.updateFormData({ - policy: formDataStore.formData.policy +const form = useForm({ + policy: '' }); - </script> diff --git a/main/resources/js/Pages/Dashboard/Ballots/Partials/Step4.vue b/main/resources/js/Pages/Dashboard/Ballots/Partials/Step4.vue new file mode 100644 index 0000000..2f67a0c --- /dev/null +++ b/main/resources/js/Pages/Dashboard/Ballots/Partials/Step4.vue @@ -0,0 +1,14 @@ +<template> + <button @click="submit" class="text-xl text-white font-semibold bg-primary-950 p-4 rounded-[15px]">Publish</button> +</template> + +<script setup lang="ts"> + +let publish = false + +const submit = () => { + publish = true +} + + +</script> diff --git a/main/resources/js/Pages/Dashboard/Ballots/Services/admin-ballot-service.ts b/main/resources/js/Pages/Dashboard/Ballots/Services/admin-ballot-service.ts new file mode 100644 index 0000000..db9e7fe --- /dev/null +++ b/main/resources/js/Pages/Dashboard/Ballots/Services/admin-ballot-service.ts @@ -0,0 +1,7 @@ +import AdminService from "@/shared/Services/admin-service"; + +export default class AdminBallotService { + public static async getBallotStatuses(): Promise<string[]> { + return AdminService.getEnums('model-status'); + } +} \ No newline at end of file diff --git a/main/resources/js/Pages/Dashboard/Ballots/Step1.vue b/main/resources/js/Pages/Dashboard/Ballots/Step1.vue deleted file mode 100644 index eaeedca..0000000 --- a/main/resources/js/Pages/Dashboard/Ballots/Step1.vue +++ /dev/null @@ -1,38 +0,0 @@ -<template> - <div class="flex flex-col"> - <label for="title" class="mb-4 text-lg text-slate-600" - >Title</label - > - <input - v-model="formDataStore.formData.title" - type="text" - name="title" - id="title" - required - class="rounded-[10px] border-slate-400 p-4 w-[100%]" - /> - </div> - <div class="flex flex-col"> - <label for="description" class="my-4 text-lg text-slate-600" - >Description - </label> - <textarea - v-model="formDataStore.formData.description" - id="description" - required - class="rounded-[10px] border-slate-400 p-4 w-[100%] h-[150px]" - ></textarea> - </div> -</template> - -<script setup lang="ts"> -import {useFormDataStore} from '@/stores/ballot-store'; - - -const formDataStore = useFormDataStore(); - -formDataStore.updateFormData({ - title: formDataStore.formData.title, - description: formDataStore.formData.description, -}); -</script> diff --git a/main/resources/js/Pages/Dashboard/Ballots/Step4.vue b/main/resources/js/Pages/Dashboard/Ballots/Step4.vue deleted file mode 100644 index bcd3a15..0000000 --- a/main/resources/js/Pages/Dashboard/Ballots/Step4.vue +++ /dev/null @@ -1,20 +0,0 @@ -<template> - <button @click="submit" class="text-xl text-white font-semibold bg-primary-950 p-4 rounded-[15px]">Publish</button> -</template> - -<script setup lang="ts"> -import {useFormDataStore} from '@/stores/ballot-store'; - - -const formDataStore = useFormDataStore(); - -formDataStore.updateFormData({ - publish: formDataStore.formData.publish -}); - -const submit = () => { - formDataStore.formData.publish = true -} - - -</script> diff --git a/main/resources/js/Pages/Dashboard/Ballots/View.vue b/main/resources/js/Pages/Dashboard/Ballots/View.vue new file mode 100644 index 0000000..37addc0 --- /dev/null +++ b/main/resources/js/Pages/Dashboard/Ballots/View.vue @@ -0,0 +1,32 @@ +<script setup lang="ts"> +import AppLayout from '@/Layouts/AppLayout.vue'; +import BallotCard from "@/Pages/Dashboard/Ballots/Partials/BallotCard.vue"; +import BallotData = App.DataTransferObjects.BallotData; + +defineProps<{ + ballot: BallotData; +}>(); +</script> + +<template> + + <AppLayout title="Dashboard"> + <template #header> + <div class="flex flex-row justify-between"> + <h2 class="text-xl font-semibold leading-tight text-gray-800 dark:text-gray-200">Viewing + <b>{{ ballot.title }}</b> + Ballot</h2> + </div> + </template> + + <div class="py-12"> + <div class="mx-auto space-y-6 max-w-7xl sm:px-6 lg:px-8"> + <div class="flex gap-8 p-4 bg-white shadow sm:p-8 dark:bg-gray-800 sm:rounded-lg"> + <BallotCard :ballot="ballot" class="w-2/3 max-w-xl" /> + </div> + + + </div> + </div> + </AppLayout> +</template> diff --git a/main/resources/js/Pages/Dashboard/Home.vue b/main/resources/js/Pages/Dashboard/Home.vue index 79f0c06..34b3321 100644 --- a/main/resources/js/Pages/Dashboard/Home.vue +++ b/main/resources/js/Pages/Dashboard/Home.vue @@ -1,6 +1,24 @@ <template> <AppLayout title="Dashboard"> <div class="py-12"> + + <div class="mx-auto max-w-7xl sm:px-6 lg:px-8 mb-8"> + <div class="overflow-hidden bg-white shadow-xl dark:bg-gray-800 sm:rounded-lg"> + + <div class="py-12"> + <div class="max-w-7xl mx-auto sm:px-6 lg:px-8"> + <div class="sm:rounded-lg"> + <h2 class="font-semibold text-lg xl:text-xl text-gray-800 dark:text-gray-200 leading-tight mb-4"> + Ballots + </h2> + <BallotList :ballots=props.ballots /> + </div> + </div> + </div> + </div> + </div> + + <div class="mx-auto max-w-7xl sm:px-6 lg:px-8"> <div class="overflow-hidden bg-white shadow-xl dark:bg-gray-800 sm:rounded-lg"> @@ -22,9 +40,11 @@ <script setup lang="ts"> import AppLayout from '@/Layouts/AppLayout.vue'; import SnapshotList from "@/Pages/Dashboard/Snapshot/Partials/SnapshotList.vue"; +import BallotList from "@/Pages/Dashboard/Ballots/Partials/BallotList.vue" const props = defineProps<{ snapshots: any; + ballots: any; }>(); </script> diff --git a/main/resources/js/Pages/Dashboard/Snapshot/Create.vue b/main/resources/js/Pages/Dashboard/Snapshot/Create.vue index f148bd9..a63c9ca 100644 --- a/main/resources/js/Pages/Dashboard/Snapshot/Create.vue +++ b/main/resources/js/Pages/Dashboard/Snapshot/Create.vue @@ -37,7 +37,7 @@ }); const submit = () => { - form.post(route('dashboard.snapshot.store'), { + form.post(route('dashboard.snapshots.store'), { }); } diff --git a/main/resources/js/models/alert.ts b/main/resources/js/models/alert.ts new file mode 100644 index 0000000..2befd38 --- /dev/null +++ b/main/resources/js/models/alert.ts @@ -0,0 +1,6 @@ +export default interface Alert{ + message:string, + type :string, + show : boolean, + showTime?:number +} \ No newline at end of file diff --git a/main/resources/js/shared/Services/admin-service.ts b/main/resources/js/shared/Services/admin-service.ts new file mode 100644 index 0000000..1007204 --- /dev/null +++ b/main/resources/js/shared/Services/admin-service.ts @@ -0,0 +1,8 @@ +import axios from "axios"; +export default class AdminService { + public static async getEnums(collection: string): Promise<string[]> { + const response = await axios.get(route('dashboard.enums', { collection })); + return response.data; + } + +} \ No newline at end of file diff --git a/main/resources/js/shared/Services/alert.ts b/main/resources/js/shared/Services/alert.ts new file mode 100644 index 0000000..ed001ff --- /dev/null +++ b/main/resources/js/shared/Services/alert.ts @@ -0,0 +1,12 @@ +import { useGlobalAlert } from "@/stores/global-alert-store"; +import setAlert from "@/utils/alert"; +import axios from "axios"; + +export default class AlertService { + static show(alerts: string [], type: string = 'info') { + const alertStore = useGlobalAlert(); + for (const message of alerts) { + alertStore.showAlert(setAlert(message, type)); + } + } +} diff --git a/main/resources/js/stores/ballot-store.ts b/main/resources/js/stores/ballot-store.ts index 2b79f0a..d72dfb1 100644 --- a/main/resources/js/stores/ballot-store.ts +++ b/main/resources/js/stores/ballot-store.ts @@ -1,65 +1,67 @@ -import { defineStore } from 'pinia'; +import {defineStore} from 'pinia'; +import { ref, computed, watch} from 'vue'; import {useStorage} from "@vueuse/core"; -type StepData = { - title?: string; - description?: string; - question?: string; - policy?: string; - publish?: boolean; -}; +export const useBallotStore = defineStore('ballot-store', () => { -export const useFormDataStore = defineStore('form',{ - state: () => ({ - step: 1, - formData: useStorage('form-data', { - title: '', - description: '', - question: '', - policy: '', - publish: false, - }), - }), - getters: { - currentStepData(state) { - const { step, formData } = state; - switch (step) { - case 1: - return { - title: formData.title, - description: formData.description , - } - case 2: - return { - question: formData.question, - } - case 3: - return { - policy: formData.policy, - } - case 4: - return { - publish: formData.publish , - } - default: - return {} - } - }, - }, + interface BallotData { + title?: string; + description?: string; + started_at?: string, + ended_at?: string, + status?: any, + question?: string; + policy?: string; + publish?: boolean; + hasQuestion?: boolean; + hasPolicy?: boolean; + isPublished?: boolean; + }; - actions: { - updateFormData(data: Partial<StepData>) { - this.formData = { ...this.formData, ...data }; - }, - nextStep() { - this.step++; - }, - prevStep() { - this.step--; - }, - }, -} -); + let formData = ref<object|null>(null); + let ballot = ref<BallotData|null>(null); + let step = ref<number>(1); + + function uploadFormData(form: any) + { + formData.value = form; + } + + function uploadBallotData(form: any) + { + ballot.value = form; + } + + function setStep() { + + if (ballot.value?.question && !ballot.value?.policy) { + step.value = 2; + } else if (ballot.value?.policy && !ballot.value?.publish){ + step.value = 3; + } + } + + // Next Step method + function nextStep() { + step.value++; + } + + // Previous Step method + function previousStep() { + step.value--; + } + + return { + formData, + ballot, + step, + setStep, + uploadFormData, + uploadBallotData, + nextStep, + previousStep, + } +}); diff --git a/main/resources/js/stores/global-alert-store.ts b/main/resources/js/stores/global-alert-store.ts new file mode 100644 index 0000000..f52e73e --- /dev/null +++ b/main/resources/js/stores/global-alert-store.ts @@ -0,0 +1,25 @@ +import {defineStore} from 'pinia'; +import {computed, ref, Ref} from 'vue'; +import Alert from '@/models/alert'; + +export const useGlobalAlert = defineStore('global-alert', () => { + let alerts: Ref<Alert[]> = ref([]); + + function showAlert(alertModel: Alert) { + alerts.value = [...alerts.value,{...alertModel}]; + setTimeout(() => { + alerts.value = []; + }, 5000); + } + + function closeAlert(index:number) { + alerts.value.length==1 ? alerts.value = [] : alerts.value.splice(index, 1); + } + + return { + showAlert, + closeAlert, + alerts + } +}); + diff --git a/main/resources/js/types/generated.d.ts b/main/resources/js/types/generated.d.ts index 319bf8a..144d7cb 100644 --- a/main/resources/js/types/generated.d.ts +++ b/main/resources/js/types/generated.d.ts @@ -4,6 +4,7 @@ user_id: number; title: string; description: string; status: string; +type: string; started_at: string; ended_at: string; }; diff --git a/main/resources/js/utils/alert.ts b/main/resources/js/utils/alert.ts new file mode 100644 index 0000000..19d1bee --- /dev/null +++ b/main/resources/js/utils/alert.ts @@ -0,0 +1,11 @@ +import Alert from "@/models/alert" + +function setAlert(message: string, type: string) { + let notification = {} as Alert + notification.message = message; + notification.type = type; + notification.show = true; + return notification; +} + +export default setAlert; diff --git a/main/routes/web.php b/main/routes/web.php index 4abf509..e8c729f 100644 --- a/main/routes/web.php +++ b/main/routes/web.php @@ -8,7 +8,7 @@ use Illuminate\Foundation\Application; use Illuminate\Support\Facades\Route; use Inertia\Inertia; - +use Illuminate\Support\Str; /* |-------------------------------------------------------------------------- | Web Routes @@ -55,7 +55,9 @@ Route::post('/store', [BallotController::class, 'store'])->name('store'); - Route::get('/edit', [BallotController::class, 'edit'])->name('edit'); + Route::get('/view/{ballot}', [BallotController::class, 'view'])->name('view'); + + Route::get('/edit/{ballot}', [BallotController::class, 'edit'])->name('edit'); Route::put('/update/{ballot}', [BallotController::class, 'update'])->name('update'); @@ -69,10 +71,17 @@ Route::post('/store', [SnapshotController::class, 'store'])->name('store'); - Route::get('/edit', [SnapshotCotroller::class, 'edit'])->name('edit'); + Route::get('/edit', [SnapshotController::class, 'edit'])->name('edit'); Route::put('/update/{snapshot}', [SnapshotController::class, 'update'])->name(''); Route::delete('/delete/{snapshot}', [SnapshotController::class, 'delete'])->name(''); }); + + Route::get('/enums/{collection}', function () { + $collection = request()->route('collection'); + $collection = 'App\\Enums\\' . Str::studly($collection). 'Enum'; + + return array_column($collection::cases(), 'value', 'name'); + })->name('enums'); }); -- GitLab From aee0f92742cd18d872233c1a36970c6c44300028 Mon Sep 17 00:00:00 2001 From: ciraganenicole <ciraganenicole@gmail.com> Date: Wed, 4 Oct 2023 09:02:50 +0200 Subject: [PATCH 2/2] Update the button colors --- main/resources/js/Pages/Dashboard/Ballots/Partials/Step1.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main/resources/js/Pages/Dashboard/Ballots/Partials/Step1.vue b/main/resources/js/Pages/Dashboard/Ballots/Partials/Step1.vue index 826b31a..945ee53 100644 --- a/main/resources/js/Pages/Dashboard/Ballots/Partials/Step1.vue +++ b/main/resources/js/Pages/Dashboard/Ballots/Partials/Step1.vue @@ -98,7 +98,7 @@ <div class="flex-shrink-0"> <button type="submit" - class="inline-flex items-center px-3 py-2 text-sm font-semibold text-white bg-primary-950 rounded-md shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"> + class="inline-flex items-center px-3 py-2 text-sm font-semibold text-white bg-primary-950 rounded-md shadow-sm hover:bg-primary-50 hover:text-black focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"> {{ ballot ? 'Edit' : 'Create' }} </button> </div> -- GitLab