From ff73133e05ec946acdb99358e92ce6a342aaa773 Mon Sep 17 00:00:00 2001 From: Manuel Bustillo Date: Sat, 28 Dec 2024 11:42:22 +0100 Subject: [PATCH 1/5] Define a health endpoint for docker compose --- app/api/health/route.ts | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 app/api/health/route.ts diff --git a/app/api/health/route.ts b/app/api/health/route.ts new file mode 100644 index 0000000..6a3d203 --- /dev/null +++ b/app/api/health/route.ts @@ -0,0 +1,5 @@ +import { NextResponse } from "next/server"; + +export function GET() { + return NextResponse.json({}); +} \ No newline at end of file From b7e2bbb46f7ae54e2f07512d9f7f7a88711300d0 Mon Sep 17 00:00:00 2001 From: Manuel Bustillo Date: Sat, 28 Dec 2024 13:00:24 +0100 Subject: [PATCH 2/5] Initial approach to configure affinities between groups --- app/lib/affinities.tsx | 3 ++ app/ui/components/form/affinitySlider.tsx | 34 ++++++++++++++++++++ app/ui/components/group-form-dialog.tsx | 39 +++++++++++++++++++---- app/ui/groups/table.tsx | 7 ++-- 4 files changed, 73 insertions(+), 10 deletions(-) create mode 100644 app/lib/affinities.tsx create mode 100644 app/ui/components/form/affinitySlider.tsx diff --git a/app/lib/affinities.tsx b/app/lib/affinities.tsx new file mode 100644 index 0000000..9480936 --- /dev/null +++ b/app/lib/affinities.tsx @@ -0,0 +1,3 @@ +export class Affinities { + [key:string]: number; +} \ No newline at end of file diff --git a/app/ui/components/form/affinitySlider.tsx b/app/ui/components/form/affinitySlider.tsx new file mode 100644 index 0000000..da82ff9 --- /dev/null +++ b/app/ui/components/form/affinitySlider.tsx @@ -0,0 +1,34 @@ +import { Slider } from 'primereact/slider'; +import React, { useEffect, useState } from 'react'; + +export default function AffinitySlider({ initialValue, onChange }: { initialValue: number, onChange?: (value: number) => void }) { + const [value, setValue] = useState(initialValue); + useEffect(() => {setValue(initialValue)}, [initialValue]); + + const label = (value: number) => { + if (value < 0.2) { + return 'Nemesis'; + } else if (value < 0.5) { + return 'Enemies'; + } else if (value < 0.9) { + return 'Bad vibes'; + } else if (value < 1.1) { + return 'Neutral'; + } else if (value < 1.5) { + return 'Good vibes'; + } else if (value < 1.8) { + return 'Good friends'; + } else { + return 'Besties'; + } + } + + return ( + <> + setValue(e.value)} className='w-80 bg-gray-400' /> + + { label(value) } + + + ) +} \ No newline at end of file diff --git a/app/ui/components/group-form-dialog.tsx b/app/ui/components/group-form-dialog.tsx index d652a13..2a78b67 100644 --- a/app/ui/components/group-form-dialog.tsx +++ b/app/ui/components/group-form-dialog.tsx @@ -2,18 +2,20 @@ 'use client'; +import { AbstractApi } from '@/app/api/abstract-api'; +import { Group, GroupSerializer } from '@/app/lib/group'; import { classNames } from '@/app/ui/components/button'; -import { Dialog } from 'primereact/dialog'; import { ColorPicker } from 'primereact/colorpicker'; +import { Dialog } from 'primereact/dialog'; import { Dropdown } from 'primereact/dropdown'; import { FloatLabel } from 'primereact/floatlabel'; import { InputText } from 'primereact/inputtext'; -import { useState } from 'react'; -import { Group, GroupSerializer } from '@/app/lib/group'; -import { ApiError } from 'next/dist/server/api-utils'; -import { AbstractApi } from '@/app/api/abstract-api'; +import { useEffect, useState } from 'react'; +import AffinitySlider from './form/affinitySlider'; +import { Affinities } from '@/app/lib/affinities'; +import { getSlug } from '@/app/lib/utils'; -export default function GroupFormDialog({groups, onCreate, onHide, group, visible }: { +export default function GroupFormDialog({ groups, onCreate, onHide, group, visible }: { groups: Group[], onCreate?: () => void, onHide: () => void, @@ -25,6 +27,19 @@ export default function GroupFormDialog({groups, onCreate, onHide, group, visibl const [icon, setIcon] = useState(group?.icon || ''); const [color, setColor] = useState(group?.color || ''); const [parentId, setParentId] = useState(group?.parentId || ''); + const [affinities, setAffinities] = useState({}); + + useEffect(() => { + if (group?.id === undefined) { + setAffinities({}); + } else { + fetch(`/api/${getSlug()}/groups/${group?.id}/affinities`) + .then((response) => response.json()) + .then((data) => { + setAffinities(data); + }); + } + }, []); const api = new AbstractApi(); const serializer = new GroupSerializer(); @@ -90,6 +105,18 @@ export default function GroupFormDialog({groups, onCreate, onHide, group, visibl {group?.id !== undefined ? 'Update' : 'Create'} + +
+ Describe the affinity with the rest of the groups + + { + groups.map((group) => +
+ {group.name} + +
) + } +
); diff --git a/app/ui/groups/table.tsx b/app/ui/groups/table.tsx index 41af307..4088cfa 100644 --- a/app/ui/groups/table.tsx +++ b/app/ui/groups/table.tsx @@ -2,13 +2,12 @@ 'use client'; -import { Group, GroupSerializer } from '@/app/lib/group'; -import TableOfContents from '../components/table-of-contents'; -import { MapPinIcon, PencilIcon, TrashIcon } from '@heroicons/react/24/outline'; import { AbstractApi } from '@/app/api/abstract-api'; -import { TreeTable } from 'primereact/treetable'; +import { Group, GroupSerializer } from '@/app/lib/group'; +import { PencilIcon, TrashIcon } from '@heroicons/react/24/outline'; import { Column } from 'primereact/column'; import { TreeNode } from 'primereact/treenode'; +import { TreeTable } from 'primereact/treetable'; export default function GroupsTable({ groups, onUpdate, onEdit }: { groups: Group[], From d307ff6927aa015e1b4f5402340b2f2abaa5c949 Mon Sep 17 00:00:00 2001 From: Manuel Bustillo Date: Sat, 28 Dec 2024 13:04:01 +0100 Subject: [PATCH 3/5] Update status of the parent component --- app/ui/components/form/affinitySlider.tsx | 7 ++----- app/ui/components/group-form-dialog.tsx | 2 +- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/app/ui/components/form/affinitySlider.tsx b/app/ui/components/form/affinitySlider.tsx index da82ff9..b434535 100644 --- a/app/ui/components/form/affinitySlider.tsx +++ b/app/ui/components/form/affinitySlider.tsx @@ -1,9 +1,6 @@ import { Slider } from 'primereact/slider'; -import React, { useEffect, useState } from 'react'; -export default function AffinitySlider({ initialValue, onChange }: { initialValue: number, onChange?: (value: number) => void }) { - const [value, setValue] = useState(initialValue); - useEffect(() => {setValue(initialValue)}, [initialValue]); +export default function AffinitySlider({value , onChange }: { value: number, onChange: (value: number) => void }) { const label = (value: number) => { if (value < 0.2) { @@ -25,7 +22,7 @@ export default function AffinitySlider({ initialValue, onChange }: { initialValu return ( <> - setValue(e.value)} className='w-80 bg-gray-400' /> + onChange(e.value)} className='w-80 bg-gray-400' /> { label(value) } diff --git a/app/ui/components/group-form-dialog.tsx b/app/ui/components/group-form-dialog.tsx index 2a78b67..226d40b 100644 --- a/app/ui/components/group-form-dialog.tsx +++ b/app/ui/components/group-form-dialog.tsx @@ -113,7 +113,7 @@ export default function GroupFormDialog({ groups, onCreate, onHide, group, visib groups.map((group) =>
{group.name} - + setAffinities({...affinities, [group.id || "default"]:value})} />
) } From 52fb808d45af67771a20e7c9ad18efa56bd52ab8 Mon Sep 17 00:00:00 2001 From: Manuel Bustillo Date: Sat, 28 Dec 2024 14:18:14 +0100 Subject: [PATCH 4/5] Define a dialog to configure the affinities between groups --- app/[slug]/dashboard/guests/page.tsx | 17 ++++- app/ui/components/affinities-form-dialog.tsx | 69 ++++++++++++++++++++ app/ui/components/form/affinitySlider.tsx | 6 +- app/ui/components/group-form-dialog.tsx | 30 +-------- app/ui/groups/table.tsx | 6 +- 5 files changed, 91 insertions(+), 37 deletions(-) create mode 100644 app/ui/components/affinities-form-dialog.tsx diff --git a/app/[slug]/dashboard/guests/page.tsx b/app/[slug]/dashboard/guests/page.tsx index beb73f8..0d68edc 100644 --- a/app/[slug]/dashboard/guests/page.tsx +++ b/app/[slug]/dashboard/guests/page.tsx @@ -2,8 +2,10 @@ 'use client'; -import { AbstractApi, } from '@/app/api/abstract-api'; +import { AbstractApi, } from '@/app/api/abstract-api'; +import { Group, GroupSerializer } from '@/app/lib/group'; import { Guest, GuestSerializer } from '@/app/lib/guest'; +import AffinitiesFormDialog from '@/app/ui/components/affinities-form-dialog'; import { classNames } from '@/app/ui/components/button'; import GroupFormDialog from '@/app/ui/components/group-form-dialog'; import GuestFormDialog from '@/app/ui/components/guest-form-dialog'; @@ -12,7 +14,6 @@ import SkeletonTable from '@/app/ui/guests/skeleton-row'; import GuestsTable from '@/app/ui/guests/table'; import { TabPanel, TabView } from 'primereact/tabview'; import { Suspense, useState } from 'react'; -import { Group, GroupSerializer } from '@/app/lib/group'; export default function Page() { @@ -34,6 +35,8 @@ export default function Page() { const [groups, setGroups] = useState>([]); const [groupBeingEdited, setGroupBeingEdited] = useState(undefined); + const [groupAffinitiesBeingEditted, setGroupAffinitiesBeingEditted] = useState(undefined); + const [guestsLoaded, setGuestsLoaded] = useState(false); const [guests, setGuests] = useState>([]); const [guestBeingEdited, setGuestBeingEdited] = useState(undefined); @@ -77,11 +80,19 @@ export default function Page() { onHide={() => { setGroupBeingEdited(undefined) }} /> + { setGroupAffinitiesBeingEditted(undefined) }} + /> + }> - setGroupBeingEdited(group)} + onEditAffinities={(group) => setGroupAffinitiesBeingEditted(group)} /> diff --git a/app/ui/components/affinities-form-dialog.tsx b/app/ui/components/affinities-form-dialog.tsx new file mode 100644 index 0000000..7428233 --- /dev/null +++ b/app/ui/components/affinities-form-dialog.tsx @@ -0,0 +1,69 @@ +/* Copyright (C) 2024 Manuel Bustillo*/ + +'use client'; + +import { Affinities } from '@/app/lib/affinities'; +import { Group } from '@/app/lib/group'; +import { getCsrfToken, getSlug } from '@/app/lib/utils'; +import { classNames } from '@/app/ui/components/button'; +import { Dialog } from 'primereact/dialog'; +import { useEffect, useState } from 'react'; +import AffinitySlider from './form/affinitySlider'; + +export default function AffinitiesFormDialog({ groups, group, visible, onHide }: { + groups: Group[], + group?: Group, + visible: boolean, + onHide: () => void, +}) { + const [affinities, setAffinities] = useState({}); + const [isLoadingAffinities, setIsLoadingAffinities] = useState(true); + + useEffect(() => { + setIsLoadingAffinities(true); + if (group?.id === undefined) { + setAffinities({}); + setIsLoadingAffinities(false) + } else { + fetch(`/api/${getSlug()}/groups/${group?.id}/affinities`) + .then((response) => response.json()) + .then((data) => { + setAffinities(data); + setIsLoadingAffinities(false) + }); + } + }, [group]); + + function submitAffinities() { + const formattedAffinities = Object.entries(affinities).map(([groupId, affinity]) => ({ group_id: groupId, affinity: affinity })); + fetch(`/api/${getSlug()}/groups/${group?.id}/affinities/bulk_update`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-TOKEN': getCsrfToken(), + }, + body: JSON.stringify({ affinities: formattedAffinities }) + }).then(() => { + onHide(); + }); + } + + return ( + + {!isLoadingAffinities &&
+ Describe the affinity with the rest of the groups + + { + groups.filter((currentGroup) => currentGroup.id !== group?.id).map((group) => +
+ {group.name} + setAffinities({ ...affinities, [group.id || "default"]: value })} /> +
) + } + + +
+ } +
+ ); +} \ No newline at end of file diff --git a/app/ui/components/form/affinitySlider.tsx b/app/ui/components/form/affinitySlider.tsx index b434535..56bef94 100644 --- a/app/ui/components/form/affinitySlider.tsx +++ b/app/ui/components/form/affinitySlider.tsx @@ -1,6 +1,6 @@ import { Slider } from 'primereact/slider'; -export default function AffinitySlider({value , onChange }: { value: number, onChange: (value: number) => void }) { +export default function AffinitySlider({ value, onChange }: { value: number, onChange: (value: number) => void }) { const label = (value: number) => { if (value < 0.2) { @@ -22,9 +22,9 @@ export default function AffinitySlider({value , onChange }: { value: number, onC return ( <> - onChange(e.value)} className='w-80 bg-gray-400' /> + onChange(e.value)} className='w-80 bg-gray-400' /> - { label(value) } + {label(value)} ) diff --git a/app/ui/components/group-form-dialog.tsx b/app/ui/components/group-form-dialog.tsx index 226d40b..cc96348 100644 --- a/app/ui/components/group-form-dialog.tsx +++ b/app/ui/components/group-form-dialog.tsx @@ -10,10 +10,7 @@ import { Dialog } from 'primereact/dialog'; import { Dropdown } from 'primereact/dropdown'; import { FloatLabel } from 'primereact/floatlabel'; import { InputText } from 'primereact/inputtext'; -import { useEffect, useState } from 'react'; -import AffinitySlider from './form/affinitySlider'; -import { Affinities } from '@/app/lib/affinities'; -import { getSlug } from '@/app/lib/utils'; +import { useState } from 'react'; export default function GroupFormDialog({ groups, onCreate, onHide, group, visible }: { groups: Group[], @@ -27,19 +24,6 @@ export default function GroupFormDialog({ groups, onCreate, onHide, group, visib const [icon, setIcon] = useState(group?.icon || ''); const [color, setColor] = useState(group?.color || ''); const [parentId, setParentId] = useState(group?.parentId || ''); - const [affinities, setAffinities] = useState({}); - - useEffect(() => { - if (group?.id === undefined) { - setAffinities({}); - } else { - fetch(`/api/${getSlug()}/groups/${group?.id}/affinities`) - .then((response) => response.json()) - .then((data) => { - setAffinities(data); - }); - } - }, []); const api = new AbstractApi(); const serializer = new GroupSerializer(); @@ -105,18 +89,6 @@ export default function GroupFormDialog({ groups, onCreate, onHide, group, visib {group?.id !== undefined ? 'Update' : 'Create'} - -
- Describe the affinity with the rest of the groups - - { - groups.map((group) => -
- {group.name} - setAffinities({...affinities, [group.id || "default"]:value})} /> -
) - } -
); diff --git a/app/ui/groups/table.tsx b/app/ui/groups/table.tsx index 4088cfa..253c271 100644 --- a/app/ui/groups/table.tsx +++ b/app/ui/groups/table.tsx @@ -4,15 +4,16 @@ import { AbstractApi } from '@/app/api/abstract-api'; import { Group, GroupSerializer } from '@/app/lib/group'; -import { PencilIcon, TrashIcon } from '@heroicons/react/24/outline'; +import { AdjustmentsHorizontalIcon, PencilIcon, TrashIcon } from '@heroicons/react/24/outline'; import { Column } from 'primereact/column'; import { TreeNode } from 'primereact/treenode'; import { TreeTable } from 'primereact/treetable'; -export default function GroupsTable({ groups, onUpdate, onEdit }: { +export default function GroupsTable({ groups, onUpdate, onEdit, onEditAffinities }: { groups: Group[], onUpdate: () => void, onEdit: (group: Group) => void, + onEditAffinities: (group: Group) => void, }) { const api = new AbstractApi(); @@ -22,6 +23,7 @@ export default function GroupsTable({ groups, onUpdate, onEdit }: {
{ api.destroy(serializer, group, onUpdate) }} /> onEdit(group)} /> + onEditAffinities(group)} />
); From dc735f1a2cb72921f17813f2304aaf105c3b0155 Mon Sep 17 00:00:00 2001 From: Manuel Bustillo Date: Sat, 28 Dec 2024 18:53:59 +0000 Subject: [PATCH 5/5] Add copyright notice --- app/lib/affinities.tsx | 2 ++ app/ui/components/form/affinitySlider.tsx | 2 ++ 2 files changed, 4 insertions(+) diff --git a/app/lib/affinities.tsx b/app/lib/affinities.tsx index 9480936..6c1023e 100644 --- a/app/lib/affinities.tsx +++ b/app/lib/affinities.tsx @@ -1,3 +1,5 @@ +/* Copyright (C) 2024 Manuel Bustillo*/ + export class Affinities { [key:string]: number; } \ No newline at end of file diff --git a/app/ui/components/form/affinitySlider.tsx b/app/ui/components/form/affinitySlider.tsx index 56bef94..f541e4d 100644 --- a/app/ui/components/form/affinitySlider.tsx +++ b/app/ui/components/form/affinitySlider.tsx @@ -1,3 +1,5 @@ +/* Copyright (C) 2024 Manuel Bustillo*/ + import { Slider } from 'primereact/slider'; export default function AffinitySlider({ value, onChange }: { value: number, onChange: (value: number) => void }) {