From d0560df6c9a8f3a631c4fa5e1ff74572f75f7673 Mon Sep 17 00:00:00 2001 From: Linas Date: Mon, 17 Aug 2026 11:34:06 +0300 Subject: [PATCH] Init commit --- app/client-portal/page.tsx | 73 + app/dashboard/[id]/page.tsx | 205 + app/dashboard/page.tsx | 75 + app/favicon.ico | Bin 25931 -> 0 bytes app/globals.css | 131 +- app/icon.jpg | Bin 0 -> 60182 bytes app/layout.tsx | 6 +- app/login/page.tsx | 123 + app/page.tsx | 65 - components.json | 23 + .../associations/associate-form-dialog.tsx | 184 + components/associations/associate-table.tsx | 102 + .../associations/association-info-card.tsx | 31 + .../associations/association-search.tsx | 36 + .../associations/association-status-badge.tsx | 16 + components/associations/association-table.tsx | 69 + .../create-association-dialog.tsx | 132 + .../associations/edit-association-dialog.tsx | 125 + .../associations/user-accounts-card.tsx | 75 + components/associations/user-form-dialog.tsx | 131 + components/layout/app-header.tsx | 78 + components/layout/page-header.tsx | 23 + components/ui/alert-dialog.tsx | 196 + components/ui/avatar.tsx | 109 + components/ui/badge.tsx | 48 + components/ui/button.tsx | 64 + components/ui/card.tsx | 92 + components/ui/checkbox.tsx | 32 + components/ui/confirm-dialog.tsx | 75 + components/ui/dialog.tsx | 158 + components/ui/dropdown-menu.tsx | 257 + components/ui/form.tsx | 167 + components/ui/input.tsx | 21 + components/ui/label.tsx | 24 + components/ui/select.tsx | 190 + components/ui/separator.tsx | 28 + components/ui/switch.tsx | 35 + components/ui/table.tsx | 116 + middleware.ts | 13 + package-lock.json | 5300 ++++++++++++++++- package.json | 12 +- 41 files changed, 8553 insertions(+), 87 deletions(-) create mode 100644 app/client-portal/page.tsx create mode 100644 app/dashboard/[id]/page.tsx create mode 100644 app/dashboard/page.tsx delete mode 100644 app/favicon.ico create mode 100644 app/icon.jpg create mode 100644 app/login/page.tsx delete mode 100644 app/page.tsx create mode 100644 components.json create mode 100644 components/associations/associate-form-dialog.tsx create mode 100644 components/associations/associate-table.tsx create mode 100644 components/associations/association-info-card.tsx create mode 100644 components/associations/association-search.tsx create mode 100644 components/associations/association-status-badge.tsx create mode 100644 components/associations/association-table.tsx create mode 100644 components/associations/create-association-dialog.tsx create mode 100644 components/associations/edit-association-dialog.tsx create mode 100644 components/associations/user-accounts-card.tsx create mode 100644 components/associations/user-form-dialog.tsx create mode 100644 components/layout/app-header.tsx create mode 100644 components/layout/page-header.tsx create mode 100644 components/ui/alert-dialog.tsx create mode 100644 components/ui/avatar.tsx create mode 100644 components/ui/badge.tsx create mode 100644 components/ui/button.tsx create mode 100644 components/ui/card.tsx create mode 100644 components/ui/checkbox.tsx create mode 100644 components/ui/confirm-dialog.tsx create mode 100644 components/ui/dialog.tsx create mode 100644 components/ui/dropdown-menu.tsx create mode 100644 components/ui/form.tsx create mode 100644 components/ui/input.tsx create mode 100644 components/ui/label.tsx create mode 100644 components/ui/select.tsx create mode 100644 components/ui/separator.tsx create mode 100644 components/ui/switch.tsx create mode 100644 components/ui/table.tsx create mode 100644 middleware.ts diff --git a/app/client-portal/page.tsx b/app/client-portal/page.tsx new file mode 100644 index 0000000..2474901 --- /dev/null +++ b/app/client-portal/page.tsx @@ -0,0 +1,73 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { useRouter } from "next/navigation"; +import { Loader2 } from "lucide-react"; +import { AppHeader } from "@/components/layout/app-header"; +import { PageHeader } from "@/components/layout/page-header"; +import { AssociateTable } from "@/components/associations/associate-table"; +import { AssociationInfoCard } from "@/components/associations/association-info-card"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { getAssociation } from "@/lib/api"; +import type { Association } from "@/lib/models"; + +export default function ClientPortalPage() { + const router = useRouter(); + const [association, setAssociation] = useState(null); + const [loading, setLoading] = useState(true); + const [mounted, setMounted] = useState(false); + + useEffect(() => { + setMounted(true); + }, []); + + useEffect(() => { + if (!mounted) return; + + const associationId = localStorage.getItem("association_id"); + if (!associationId) { + router.push("/login"); + return; + } + + getAssociation(associationId) + .then(setAssociation) + .catch((error) => { + console.error("Failed to load association:", error); + router.push("/login"); + }) + .finally(() => setLoading(false)); + }, [mounted, router]); + + if (!mounted || loading) { + return ( +
+ +
+ ); + } + + if (!association) { + return null; + } + + return ( +
+ +
+ + + + + + + Soci ({association.associates?.length ?? 0}) + + + + + +
+
+ ); +} diff --git a/app/dashboard/[id]/page.tsx b/app/dashboard/[id]/page.tsx new file mode 100644 index 0000000..65c83fa --- /dev/null +++ b/app/dashboard/[id]/page.tsx @@ -0,0 +1,205 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { useParams, useRouter } from "next/navigation"; +import { ArrowLeft, Loader2, Plus, Trash2 } from "lucide-react"; +import { AppHeader } from "@/components/layout/app-header"; +import { PageHeader } from "@/components/layout/page-header"; +import { AssociateFormDialog } from "@/components/associations/associate-form-dialog"; +import { AssociateTable } from "@/components/associations/associate-table"; +import { AssociationInfoCard } from "@/components/associations/association-info-card"; +import { EditAssociationDialog } from "@/components/associations/edit-association-dialog"; +import { UserAccountsCard } from "@/components/associations/user-accounts-card"; +import { UserFormDialog } from "@/components/associations/user-form-dialog"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { ConfirmDialog } from "@/components/ui/confirm-dialog"; +import { deleteAssociation, getAssociation, updateAssociation } from "@/lib/api"; +import type { Associate, Association, User } from "@/lib/models"; + +export default function AssociationDetailPage() { + const router = useRouter(); + const params = useParams(); + const id = params.id as string; + + const [association, setAssociation] = useState(null); + const [loading, setLoading] = useState(true); + + const [associateDialogOpen, setAssociateDialogOpen] = useState(false); + const [editingAssociateIndex, setEditingAssociateIndex] = useState(undefined); + + const [userDialogOpen, setUserDialogOpen] = useState(false); + const [editingUserIndex, setEditingUserIndex] = useState(undefined); + + useEffect(() => { + getAssociation(id) + .then(setAssociation) + .catch((error) => { + console.error("Failed to load association:", error); + router.push("/dashboard"); + }) + .finally(() => setLoading(false)); + }, [id, router]); + + const persist = async (updated: Association) => { + const saved = await updateAssociation(id, updated); + setAssociation(saved); + }; + + const openAddAssociate = () => { + setEditingAssociateIndex(undefined); + setAssociateDialogOpen(true); + }; + + const openEditAssociate = (index: number) => { + setEditingAssociateIndex(index); + setAssociateDialogOpen(true); + }; + + const handleAssociateSubmit = async (associate: Associate) => { + if (!association) return; + const associates = [...(association.associates ?? [])]; + if (editingAssociateIndex !== undefined) { + associates[editingAssociateIndex] = associate; + } else { + associates.push(associate); + } + await persist({ ...association, associates }); + }; + + const handleAssociateDelete = async (index: number) => { + if (!association) return; + const associates = (association.associates ?? []).filter((_, i) => i !== index); + await persist({ ...association, associates }); + }; + + const openAddUser = () => { + setEditingUserIndex(undefined); + setUserDialogOpen(true); + }; + + const openEditUser = (index: number) => { + setEditingUserIndex(index); + setUserDialogOpen(true); + }; + + const handleUserSubmit = async (user: User) => { + if (!association) return; + const users = [...(association.users ?? [])]; + if (editingUserIndex !== undefined) { + const existing = users[editingUserIndex]; + users[editingUserIndex] = { ...user, password: user.password || existing.password }; + } else { + users.push(user); + } + await persist({ ...association, users }); + }; + + const handleUserDelete = async (index: number) => { + if (!association) return; + const users = (association.users ?? []).filter((_, i) => i !== index); + await persist({ ...association, users }); + }; + + const handleDeleteAssociation = async () => { + await deleteAssociation(id); + router.push("/dashboard"); + }; + + if (loading) { + return ( +
+ +
+ ); + } + + if (!association) { + return null; + } + + return ( +
+ +
+
+ + + + + + Elimina + + } + title="Eliminare questa associazione?" + description={`"${association.name}" e tutti i suoi soci verranno eliminati definitivamente. L'operazione non รจ reversibile.`} + confirmLabel="Elimina definitivamente" + onConfirm={handleDeleteAssociation} + /> + + } + /> +
+ + + + + + Soci ({association.associates?.length ?? 0}) + + + + + + + + + + Aggiungi account + + } + /> +
+ + + + +
+ ); +} diff --git a/app/dashboard/page.tsx b/app/dashboard/page.tsx new file mode 100644 index 0000000..f2c08c7 --- /dev/null +++ b/app/dashboard/page.tsx @@ -0,0 +1,75 @@ +"use client"; + +import { useEffect, useMemo, useState } from "react"; +import { useRouter } from "next/navigation"; +import { Loader2 } from "lucide-react"; +import { AppHeader } from "@/components/layout/app-header"; +import { PageHeader } from "@/components/layout/page-header"; +import { AssociationSearch } from "@/components/associations/association-search"; +import { AssociationTable } from "@/components/associations/association-table"; +import { CreateAssociationDialog } from "@/components/associations/create-association-dialog"; +import { getAssociations } from "@/lib/api"; +import type { Association } from "@/lib/models"; + +export default function DashboardPage() { + const router = useRouter(); + const [associations, setAssociations] = useState([]); + const [loading, setLoading] = useState(true); + const [mounted, setMounted] = useState(false); + const [query, setQuery] = useState(""); + + useEffect(() => { + setMounted(true); + }, []); + + useEffect(() => { + if (!mounted) return; + getAssociations() + .then(setAssociations) + .catch((error: Error) => { + if (error.message === "Unauthorized") { + router.push("/login"); + } else { + console.error(error); + } + }) + .finally(() => setLoading(false)); + }, [mounted, router]); + + const filtered = useMemo(() => { + const query_ = query.trim().toLowerCase(); + if (!query_) return associations; + return associations.filter((association) => + [association.name, association.address, association.vatNumber, association.runtsId].some( + (value) => value?.toLowerCase().includes(query_) + ) + ); + }, [associations, query]); + + if (!mounted || loading) { + return ( +
+ +
+ ); + } + + return ( +
+ +
+ router.push(`/dashboard/${association.id}`)} + /> + } + /> + + +
+
+ ); +} diff --git a/app/favicon.ico b/app/favicon.ico deleted file mode 100644 index 718d6fea4835ec2d246af9800eddb7ffb276240c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 25931 zcmeHv30#a{`}aL_*G&7qml|y<+KVaDM2m#dVr!KsA!#An?kSQM(q<_dDNCpjEux83 zLb9Z^XxbDl(w>%i@8hT6>)&Gu{h#Oeyszu?xtw#Zb1mO{pgX9699l+Qppw7jXaYf~-84xW z)w4x8?=youko|}Vr~(D$UXIbiXABHh`p1?nn8Po~fxRJv}|0e(BPs|G`(TT%kKVJAdg5*Z|x0leQq0 zkdUBvb#>9F()jo|T~kx@OM8$9wzs~t2l;K=woNssA3l6|sx2r3+kdfVW@e^8e*E}v zA1y5{bRi+3Z`uD3{F7LgFJDdvm;nJilkzDku>BwXH(8ItVCXk*-lSJnR?-2UN%hJ){&rlvg`CDTj z)Bzo!3v7Ou#83zEDEFcKt(f1E0~=rqeEbTnMvWR#{+9pg%7G8y>u1OVRUSoox-ovF z2Ydma(;=YuBY(eI|04{hXzZD6_f(v~H;C~y5=DhAC{MMS>2fm~1H_t2$56pc$NH8( z5bH|<)71dV-_oCHIrzrT`2s-5w_+2CM0$95I6X8p^r!gHp+j_gd;9O<1~CEQQGS8) zS9Qh3#p&JM-G8rHekNmKVewU;pJRcTAog68KYo^dRo}(M>36U4Us zfgYWSiHZL3;lpWT=zNAW>Dh#mB!_@Lg%$ms8N-;aPqMn+C2HqZgz&9~Eu z4|Kp<`$q)Uw1R?y(~S>ePdonHxpV1#eSP1B;Ogo+-Pk}6#0GsZZ5!||ev2MGdh}_m z{DeR7?0-1^zVs&`AV6Vt;r3`I`OI_wgs*w=eO%_#7Kepl{B@xiyCANc(l zzIyd4y|c6PXWq9-|KM8(zIk8LPk(>a)zyFWjhT!$HJ$qX1vo@d25W<fvZQ2zUz5WRc(UnFMKHwe1| zWmlB1qdbiA(C0jmnV<}GfbKtmcu^2*P^O?MBLZKt|As~ge8&AAO~2K@zbXelK|4T<{|y4`raF{=72kC2Kn(L4YyenWgrPiv z@^mr$t{#X5VuIMeL!7Ab6_kG$&#&5p*Z{+?5U|TZ`B!7llpVmp@skYz&n^8QfPJzL z0G6K_OJM9x+Wu2gfN45phANGt{7=C>i34CV{Xqlx(fWpeAoj^N0Biu`w+MVcCUyU* zDZuzO0>4Z6fbu^T_arWW5n!E45vX8N=bxTVeFoep_G#VmNlQzAI_KTIc{6>c+04vr zx@W}zE5JNSU>!THJ{J=cqjz+4{L4A{Ob9$ZJ*S1?Ggg3klFp!+Y1@K+pK1DqI|_gq z5ZDXVpge8-cs!o|;K73#YXZ3AShj50wBvuq3NTOZ`M&qtjj#GOFfgExjg8Gn8>Vq5 z`85n+9|!iLCZF5$HJ$Iu($dm?8~-ofu}tEc+-pyke=3!im#6pk_Wo8IA|fJwD&~~F zc16osQ)EBo58U7XDuMexaPRjU@h8tXe%S{fA0NH3vGJFhuyyO!Uyl2^&EOpX{9As0 zWj+P>{@}jxH)8|r;2HdupP!vie{sJ28b&bo!8`D^x}TE$%zXNb^X1p@0PJ86`dZyj z%ce7*{^oo+6%&~I!8hQy-vQ7E)0t0ybH4l%KltWOo~8cO`T=157JqL(oq_rC%ea&4 z2NcTJe-HgFjNg-gZ$6!Y`SMHrlj}Etf7?r!zQTPPSv}{so2e>Fjs1{gzk~LGeesX%r(Lh6rbhSo_n)@@G-FTQy93;l#E)hgP@d_SGvyCp0~o(Y;Ee8{ zdVUDbHm5`2taPUOY^MAGOw*>=s7=Gst=D+p+2yON!0%Hk` zz5mAhyT4lS*T3LS^WSxUy86q&GnoHxzQ6vm8)VS}_zuqG?+3td68_x;etQAdu@sc6 zQJ&5|4(I?~3d-QOAODHpZ=hlSg(lBZ!JZWCtHHSj`0Wh93-Uk)_S%zsJ~aD>{`A0~ z9{AG(e|q3g5B%wYKRxiL2Y$8(4w6bzchKuloQW#e&S3n+P- z8!ds-%f;TJ1>)v)##>gd{PdS2Oc3VaR`fr=`O8QIO(6(N!A?pr5C#6fc~Ge@N%Vvu zaoAX2&(a6eWy_q&UwOhU)|P3J0Qc%OdhzW=F4D|pt0E4osw;%<%Dn58hAWD^XnZD= z>9~H(3bmLtxpF?a7su6J7M*x1By7YSUbxGi)Ot0P77`}P3{)&5Un{KD?`-e?r21!4vTTnN(4Y6Lin?UkSM z`MXCTC1@4A4~mvz%Rh2&EwY))LeoT=*`tMoqcEXI>TZU9WTP#l?uFv+@Dn~b(>xh2 z;>B?;Tz2SR&KVb>vGiBSB`@U7VIWFSo=LDSb9F{GF^DbmWAfpms8Sx9OX4CnBJca3 zlj9(x!dIjN?OG1X4l*imJNvRCk}F%!?SOfiOq5y^mZW)jFL@a|r-@d#f7 z2gmU8L3IZq0ynIws=}~m^#@&C%J6QFo~Mo4V`>v7MI-_!EBMMtb%_M&kvAaN)@ZVw z+`toz&WG#HkWDjnZE!6nk{e-oFdL^$YnbOCN}JC&{$#$O27@|Tn-skXr)2ml2~O!5 zX+gYoxhoc7qoU?C^3~&!U?kRFtnSEecWuH0B0OvLodgUAi}8p1 zrO6RSXHH}DMc$&|?D004DiOVMHV8kXCP@7NKB zgaZq^^O<7PoKEp72kby@W0Z!Y*Ay{&vfg#C&gG@YVR9g?FEocMUi1gSN$+V+ayF45{a zuDZDTN}mS|;BO%gEf}pjBfN2-gIrU#G5~cucA;dokXW89%>AyXJJI z9X4UlIWA|ZYHgbI z5?oFk@A=Ik7lrEQPDH!H+b`7_Y~aDb_qa=B2^Y&Ow41cU=4WDd40dp5(QS-WMN-=Y z9g;6_-JdNU;|6cPwf$ak*aJIcwL@1n$#l~zi{c{EW?T;DaW*E8DYq?Umtz{nJ&w-M zEMyTDrC&9K$d|kZe2#ws6)L=7K+{ zQw{XnV6UC$6-rW0emqm8wJoeZK)wJIcV?dST}Z;G0Arq{dVDu0&4kd%N!3F1*;*pW zR&qUiFzK=@44#QGw7k1`3t_d8&*kBV->O##t|tonFc2YWrL7_eqg+=+k;!F-`^b8> z#KWCE8%u4k@EprxqiV$VmmtiWxDLgnGu$Vs<8rppV5EajBXL4nyyZM$SWVm!wnCj-B!Wjqj5-5dNXukI2$$|Bu3Lrw}z65Lc=1G z^-#WuQOj$hwNGG?*CM_TO8Bg-1+qc>J7k5c51U8g?ZU5n?HYor;~JIjoWH-G>AoUP ztrWWLbRNqIjW#RT*WqZgPJXU7C)VaW5}MiijYbABmzoru6EmQ*N8cVK7a3|aOB#O& zBl8JY2WKfmj;h#Q!pN%9o@VNLv{OUL?rixHwOZuvX7{IJ{(EdPpuVFoQqIOa7giLVkBOKL@^smUA!tZ1CKRK}#SSM)iQHk)*R~?M!qkCruaS!#oIL1c z?J;U~&FfH#*98^G?i}pA{ z9Jg36t4=%6mhY(quYq*vSxptes9qy|7xSlH?G=S@>u>Ebe;|LVhs~@+06N<4CViBk zUiY$thvX;>Tby6z9Y1edAMQaiH zm^r3v#$Q#2T=X>bsY#D%s!bhs^M9PMAcHbCc0FMHV{u-dwlL;a1eJ63v5U*?Q_8JO zT#50!RD619#j_Uf))0ooADz~*9&lN!bBDRUgE>Vud-i5ck%vT=r^yD*^?Mp@Q^v+V zG#-?gKlr}Eeqifb{|So?HM&g91P8|av8hQoCmQXkd?7wIJwb z_^v8bbg`SAn{I*4bH$u(RZ6*xUhuA~hc=8czK8SHEKTzSxgbwi~9(OqJB&gwb^l4+m`k*Q;_?>Y-APi1{k zAHQ)P)G)f|AyjSgcCFps)Fh6Bca*Xznq36!pV6Az&m{O8$wGFD? zY&O*3*J0;_EqM#jh6^gMQKpXV?#1?>$ml1xvh8nSN>-?H=V;nJIwB07YX$e6vLxH( zqYwQ>qxwR(i4f)DLd)-$P>T-no_c!LsN@)8`e;W@)-Hj0>nJ-}Kla4-ZdPJzI&Mce zv)V_j;(3ERN3_@I$N<^|4Lf`B;8n+bX@bHbcZTopEmDI*Jfl)-pFDvo6svPRoo@(x z);_{lY<;);XzT`dBFpRmGrr}z5u1=pC^S-{ce6iXQlLGcItwJ^mZx{m$&DA_oEZ)B{_bYPq-HA zcH8WGoBG(aBU_j)vEy+_71T34@4dmSg!|M8Vf92Zj6WH7Q7t#OHQqWgFE3ARt+%!T z?oLovLVlnf?2c7pTc)~cc^($_8nyKwsN`RA-23ed3sdj(ys%pjjM+9JrctL;dy8a( z@en&CQmnV(()bu|Y%G1-4a(6x{aLytn$T-;(&{QIJB9vMox11U-1HpD@d(QkaJdEb zG{)+6Dos_L+O3NpWo^=gR?evp|CqEG?L&Ut#D*KLaRFOgOEK(Kq1@!EGcTfo+%A&I z=dLbB+d$u{sh?u)xP{PF8L%;YPPW53+@{>5W=Jt#wQpN;0_HYdw1{ksf_XhO4#2F= zyPx6Lx2<92L-;L5PD`zn6zwIH`Jk($?Qw({erA$^bC;q33hv!d!>%wRhj# zal^hk+WGNg;rJtb-EB(?czvOM=H7dl=vblBwAv>}%1@{}mnpUznfq1cE^sgsL0*4I zJ##!*B?=vI_OEVis5o+_IwMIRrpQyT_Sq~ZU%oY7c5JMIADzpD!Upz9h@iWg_>>~j zOLS;wp^i$-E?4<_cp?RiS%Rd?i;f*mOz=~(&3lo<=@(nR!_Rqiprh@weZlL!t#NCc zO!QTcInq|%#>OVgobj{~ixEUec`E25zJ~*DofsQdzIa@5^nOXj2T;8O`l--(QyU^$t?TGY^7#&FQ+2SS3B#qK*k3`ye?8jUYSajE5iBbJls75CCc(m3dk{t?- zopcER9{Z?TC)mk~gpi^kbbu>b-+a{m#8-y2^p$ka4n60w;Sc2}HMf<8JUvhCL0B&Btk)T`ctE$*qNW8L$`7!r^9T+>=<=2qaq-;ll2{`{Rg zc5a0ZUI$oG&j-qVOuKa=*v4aY#IsoM+1|c4Z)<}lEDvy;5huB@1RJPquU2U*U-;gu z=En2m+qjBzR#DEJDO`WU)hdd{Vj%^0V*KoyZ|5lzV87&g_j~NCjwv0uQVqXOb*QrQ zy|Qn`hxx(58c70$E;L(X0uZZ72M1!6oeg)(cdKO ze0gDaTz+ohR-#d)NbAH4x{I(21yjwvBQfmpLu$)|m{XolbgF!pmsqJ#D}(ylp6uC> z{bqtcI#hT#HW=wl7>p!38sKsJ`r8}lt-q%Keqy%u(xk=yiIJiUw6|5IvkS+#?JTBl z8H5(Q?l#wzazujH!8o>1xtn8#_w+397*_cy8!pQGP%K(Ga3pAjsaTbbXJlQF_+m+-UpUUent@xM zg%jqLUExj~o^vQ3Gl*>wh=_gOr2*|U64_iXb+-111aH}$TjeajM+I20xw(((>fej-@CIz4S1pi$(#}P7`4({6QS2CaQS4NPENDp>sAqD z$bH4KGzXGffkJ7R>V>)>tC)uax{UsN*dbeNC*v}#8Y#OWYwL4t$ePR?VTyIs!wea+ z5Urmc)X|^`MG~*dS6pGSbU+gPJoq*^a=_>$n4|P^w$sMBBy@f*Z^Jg6?n5?oId6f{ z$LW4M|4m502z0t7g<#Bx%X;9<=)smFolV&(V^(7Cv2-sxbxopQ!)*#ZRhTBpx1)Fc zNm1T%bONzv6@#|dz(w02AH8OXe>kQ#1FMCzO}2J_mST)+ExmBr9cva-@?;wnmWMOk z{3_~EX_xadgJGv&H@zK_8{(x84`}+c?oSBX*Ge3VdfTt&F}yCpFP?CpW+BE^cWY0^ zb&uBN!Ja3UzYHK-CTyA5=L zEMW{l3Usky#ly=7px648W31UNV@K)&Ub&zP1c7%)`{);I4b0Q<)B}3;NMG2JH=X$U zfIW4)4n9ZM`-yRj67I)YSLDK)qfUJ_ij}a#aZN~9EXrh8eZY2&=uY%2N0UFF7<~%M zsB8=erOWZ>Ct_#^tHZ|*q`H;A)5;ycw*IcmVxi8_0Xk}aJA^ath+E;xg!x+As(M#0=)3!NJR6H&9+zd#iP(m0PIW8$ z1Y^VX`>jm`W!=WpF*{ioM?C9`yOR>@0q=u7o>BP-eSHqCgMDj!2anwH?s%i2p+Q7D zzszIf5XJpE)IG4;d_(La-xenmF(tgAxK`Y4sQ}BSJEPs6N_U2vI{8=0C_F?@7<(G; zo$~G=8p+076G;`}>{MQ>t>7cm=zGtfbdDXm6||jUU|?X?CaE?(<6bKDYKeHlz}DA8 zXT={X=yp_R;HfJ9h%?eWvQ!dRgz&Su*JfNt!Wu>|XfU&68iRikRrHRW|ZxzRR^`eIGt zIeiDgVS>IeExKVRWW8-=A=yA`}`)ZkWBrZD`hpWIxBGkh&f#ijr449~m`j6{4jiJ*C!oVA8ZC?$1RM#K(_b zL9TW)kN*Y4%^-qPpMP7d4)o?Nk#>aoYHT(*g)qmRUb?**F@pnNiy6Fv9rEiUqD(^O zzyS?nBrX63BTRYduaG(0VVG2yJRe%o&rVrLjbxTaAFTd8s;<<@Qs>u(<193R8>}2_ zuwp{7;H2a*X7_jryzriZXMg?bTuegABb^87@SsKkr2)0Gyiax8KQWstw^v#ix45EVrcEhr>!NMhprl$InQMzjSFH54x5k9qHc`@9uKQzvL4ihcq{^B zPrVR=o_ic%Y>6&rMN)hTZsI7I<3&`#(nl+3y3ys9A~&^=4?PL&nd8)`OfG#n zwAMN$1&>K++c{^|7<4P=2y(B{jJsQ0a#U;HTo4ZmWZYvI{+s;Td{Yzem%0*k#)vjpB zia;J&>}ICate44SFYY3vEelqStQWFihx%^vQ@Do(sOy7yR2@WNv7Y9I^yL=nZr3mb zXKV5t@=?-Sk|b{XMhA7ZGB@2hqsx}4xwCW!in#C zI@}scZlr3-NFJ@NFaJlhyfcw{k^vvtGl`N9xSo**rDW4S}i zM9{fMPWo%4wYDG~BZ18BD+}h|GQKc-g^{++3MY>}W_uq7jGHx{mwE9fZiPCoxN$+7 zrODGGJrOkcPQUB(FD5aoS4g~7#6NR^ma7-!>mHuJfY5kTe6PpNNKC9GGRiu^L31uG z$7v`*JknQHsYB!Tm_W{a32TM099djW%5e+j0Ve_ct}IM>XLF1Ap+YvcrLV=|CKo6S zb+9Nl3_YdKP6%Cxy@6TxZ>;4&nTneadr z_ES90ydCev)LV!dN=#(*f}|ZORFdvkYBni^aLbUk>BajeWIOcmHP#8S)*2U~QKI%S zyrLmtPqb&TphJ;>yAxri#;{uyk`JJqODDw%(Z=2`1uc}br^V%>j!gS)D*q*f_-qf8&D;W1dJgQMlaH5er zN2U<%Smb7==vE}dDI8K7cKz!vs^73o9f>2sgiTzWcwY|BMYHH5%Vn7#kiw&eItCqa zIkR2~Q}>X=Ar8W|^Ms41Fm8o6IB2_j60eOeBB1Br!boW7JnoeX6Gs)?7rW0^5psc- zjS16yb>dFn>KPOF;imD}e!enuIniFzv}n$m2#gCCv4jM#ArwlzZ$7@9&XkFxZ4n!V zj3dyiwW4Ki2QG{@i>yuZXQizw_OkZI^-3otXC{!(lUpJF33gI60ak;Uqitp74|B6I zgg{b=Iz}WkhCGj1M=hu4#Aw173YxIVbISaoc z-nLZC*6Tgivd5V`K%GxhBsp@SUU60-rfc$=wb>zdJzXS&-5(NRRodFk;Kxk!S(O(a0e7oY=E( zAyS;Ow?6Q&XA+cnkCb{28_1N8H#?J!*$MmIwLq^*T_9-z^&UE@A(z9oGYtFy6EZef LrJugUA?W`A8`#=m diff --git a/app/globals.css b/app/globals.css index a2dc41e..a6168a5 100644 --- a/app/globals.css +++ b/app/globals.css @@ -1,26 +1,131 @@ @import "tailwindcss"; +@import "tw-animate-css"; +@import "shadcn/tailwind.css"; -:root { - --background: #ffffff; - --foreground: #171717; -} +@custom-variant dark (&:is(.dark *)); @theme inline { --color-background: var(--background); --color-foreground: var(--foreground); --font-sans: var(--font-geist-sans); --font-mono: var(--font-geist-mono); + --color-sidebar-ring: var(--sidebar-ring); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar: var(--sidebar); + --color-chart-5: var(--chart-5); + --color-chart-4: var(--chart-4); + --color-chart-3: var(--chart-3); + --color-chart-2: var(--chart-2); + --color-chart-1: var(--chart-1); + --color-ring: var(--ring); + --color-input: var(--input); + --color-border: var(--border); + --color-destructive: var(--destructive); + --color-accent-foreground: var(--accent-foreground); + --color-accent: var(--accent); + --color-muted-foreground: var(--muted-foreground); + --color-muted: var(--muted); + --color-secondary-foreground: var(--secondary-foreground); + --color-secondary: var(--secondary); + --color-primary-foreground: var(--primary-foreground); + --color-primary: var(--primary); + --color-popover-foreground: var(--popover-foreground); + --color-popover: var(--popover); + --color-card-foreground: var(--card-foreground); + --color-card: var(--card); + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); + --radius-2xl: calc(var(--radius) + 8px); + --radius-3xl: calc(var(--radius) + 12px); + --radius-4xl: calc(var(--radius) + 16px); } -@media (prefers-color-scheme: dark) { - :root { - --background: #0a0a0a; - --foreground: #ededed; +:root { + --radius: 0.625rem; + --background: oklch(1 0 0); + --foreground: oklch(0.129 0.042 264.695); + --card: oklch(1 0 0); + --card-foreground: oklch(0.129 0.042 264.695); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.129 0.042 264.695); + --primary: oklch(0.447 0.09 149.3); + --primary-foreground: oklch(0.984 0.003 247.858); + --secondary: oklch(0.968 0.007 247.896); + --secondary-foreground: oklch(0.208 0.042 265.755); + --muted: oklch(0.968 0.007 247.896); + --muted-foreground: oklch(0.554 0.046 257.417); + --accent: oklch(0.955 0.025 149.3); + --accent-foreground: oklch(0.325 0.08 149.3); + --destructive: oklch(0.549 0.16 24.5); + --border: oklch(0.929 0.013 255.508); + --input: oklch(0.929 0.013 255.508); + --ring: oklch(0.447 0.09 149.3 / 50%); + --chart-1: oklch(0.447 0.09 149.3); + --chart-2: oklch(0.549 0.16 24.5); + --chart-3: oklch(0.398 0.07 227.392); + --chart-4: oklch(0.828 0.189 84.429); + --chart-5: oklch(0.769 0.188 70.08); + --sidebar: oklch(0.984 0.003 247.858); + --sidebar-foreground: oklch(0.129 0.042 264.695); + --sidebar-primary: oklch(0.447 0.09 149.3); + --sidebar-primary-foreground: oklch(0.984 0.003 247.858); + --sidebar-accent: oklch(0.955 0.025 149.3); + --sidebar-accent-foreground: oklch(0.325 0.08 149.3); + --sidebar-border: oklch(0.929 0.013 255.508); + --sidebar-ring: oklch(0.447 0.09 149.3 / 50%); +} + +.dark { + --background: oklch(0.129 0.042 264.695); + --foreground: oklch(0.984 0.003 247.858); + --card: oklch(0.208 0.042 265.755); + --card-foreground: oklch(0.984 0.003 247.858); + --popover: oklch(0.208 0.042 265.755); + --popover-foreground: oklch(0.984 0.003 247.858); + --primary: oklch(0.667 0.11 149.3); + --primary-foreground: oklch(0.129 0.042 264.695); + --secondary: oklch(0.279 0.041 260.031); + --secondary-foreground: oklch(0.984 0.003 247.858); + --muted: oklch(0.279 0.041 260.031); + --muted-foreground: oklch(0.704 0.04 256.788); + --accent: oklch(0.3 0.05 149.3); + --accent-foreground: oklch(0.9 0.04 149.3); + --destructive: oklch(0.704 0.16 24.5); + --border: oklch(1 0 0 / 10%); + --input: oklch(1 0 0 / 15%); + --ring: oklch(0.667 0.11 149.3 / 50%); + --chart-1: oklch(0.667 0.11 149.3); + --chart-2: oklch(0.704 0.16 24.5); + --chart-3: oklch(0.769 0.188 70.08); + --chart-4: oklch(0.627 0.265 303.9); + --chart-5: oklch(0.645 0.246 16.439); + --sidebar: oklch(0.208 0.042 265.755); + --sidebar-foreground: oklch(0.984 0.003 247.858); + --sidebar-primary: oklch(0.667 0.11 149.3); + --sidebar-primary-foreground: oklch(0.129 0.042 264.695); + --sidebar-accent: oklch(0.3 0.05 149.3); + --sidebar-accent-foreground: oklch(0.9 0.04 149.3); + --sidebar-border: oklch(1 0 0 / 10%); + --sidebar-ring: oklch(0.667 0.11 149.3 / 50%); +} + +@layer base { + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground; + } + html { + overflow-y: scroll; } } -body { - background: var(--background); - color: var(--foreground); - font-family: Arial, Helvetica, sans-serif; -} + diff --git a/app/icon.jpg b/app/icon.jpg new file mode 100644 index 0000000000000000000000000000000000000000..102753f6e056dc379b51b5821b56326cfdd6ce57 GIT binary patch literal 60182 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 E1G!P}1^@s6 literal 0 HcmV?d00001 diff --git a/app/layout.tsx b/app/layout.tsx index f7fa87e..fc39233 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -13,8 +13,8 @@ const geistMono = Geist_Mono({ }); export const metadata: Metadata = { - title: "Create Next App", - description: "Generated by create next app", + title: "ENCAP - Rete Associativa", + description: "Gestione della rete associativa ENCAP", }; export default function RootLayout({ @@ -23,7 +23,7 @@ export default function RootLayout({ children: React.ReactNode; }>) { return ( - + diff --git a/app/login/page.tsx b/app/login/page.tsx new file mode 100644 index 0000000..14c6e0e --- /dev/null +++ b/app/login/page.tsx @@ -0,0 +1,123 @@ +"use client"; + +import { useState } from "react"; +import { useRouter } from "next/navigation"; +import Image from "next/image"; +import { Loader2, Lock, Mail, TriangleAlert } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardHeader } from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { login } from "@/lib/api"; + +export default function LoginPage() { + const router = useRouter(); + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [error, setError] = useState(""); + const [loading, setLoading] = useState(false); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setError(""); + setLoading(true); + + try { + const response = await login(email, password); + + localStorage.setItem("user_role", response.user.role); + localStorage.setItem("user_email", response.user.email); + localStorage.setItem("association_id", response.association.id || ""); + + if (response.user.role === "CLIENT") { + router.push("/client-portal"); + } else { + router.push("/dashboard"); + } + } catch { + setError("Email o password non corretti"); + } finally { + setLoading(false); + } + }; + + return ( +
+ {/* Tricolor accent bar */} +
+ + + +
+ + {/* Centered login card */} +
+ + + ENCAP +
+

Accedi

+

+ Inserisci le tue credenziali per accedere al portale +

+
+
+ +
+ {error && ( +
+ + {error} +
+ )} + +
+ +
+ + setEmail(e.target.value)} + required + /> +
+
+ +
+ +
+ + setPassword(e.target.value)} + required + /> +
+
+ + +
+
+
+
+
+ ); +} diff --git a/app/page.tsx b/app/page.tsx deleted file mode 100644 index 295f8fd..0000000 --- a/app/page.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import Image from "next/image"; - -export default function Home() { - return ( -
-
- Next.js logo -
-

- To get started, edit the page.tsx file. -

-

- Looking for a starting point or more instructions? Head over to{" "} - - Templates - {" "} - or the{" "} - - Learning - {" "} - center. -

-
- -
-
- ); -} diff --git a/components.json b/components.json new file mode 100644 index 0000000..44b4685 --- /dev/null +++ b/components.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "", + "css": "app/globals.css", + "baseColor": "slate", + "cssVariables": true, + "prefix": "" + }, + "iconLibrary": "lucide", + "rtl": false, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "registries": {} +} diff --git a/components/associations/associate-form-dialog.tsx b/components/associations/associate-form-dialog.tsx new file mode 100644 index 0000000..0c5aebf --- /dev/null +++ b/components/associations/associate-form-dialog.tsx @@ -0,0 +1,184 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { Loader2 } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { Checkbox } from "@/components/ui/checkbox"; +import { + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import type { Associate } from "@/lib/models"; + +const ROLE_OPTIONS: { value: Associate["role"]; label: string }[] = [ + { value: "PRESIDENT", label: "Presidente" }, + { value: "VICE_PRESIDENT", label: "Vicepresidente" }, + { value: "SECRETARY", label: "Segretario" }, + { value: "ASSOCIATE", label: "Socio" }, +]; + +const EMPTY_ASSOCIATE: Associate = { + name: "", + surname: "", + birthDate: "", + address: "", + fiscalCode: "", + italianCitizen: true, + role: "ASSOCIATE", + residencePermitNumber: "", +}; + +export function AssociateFormDialog({ + open, + onOpenChange, + initialValue, + onSubmit, +}: { + open: boolean; + onOpenChange: (open: boolean) => void; + initialValue?: Associate; + onSubmit: (associate: Associate) => Promise | void; +}) { + const [value, setValue] = useState(initialValue ?? EMPTY_ASSOCIATE); + const [saving, setSaving] = useState(false); + + useEffect(() => { + if (open) { + setValue(initialValue ?? EMPTY_ASSOCIATE); + } + }, [open, initialValue]); + + const update = (key: K, fieldValue: Associate[K]) => + setValue((prev) => ({ ...prev, [key]: fieldValue })); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setSaving(true); + try { + await onSubmit(value); + onOpenChange(false); + } catch (error) { + console.error("Failed to save associate:", error); + alert("Impossibile salvare il socio"); + } finally { + setSaving(false); + } + }; + + return ( + + + + {initialValue ? "Modifica socio" : "Aggiungi socio"} + +
+
+
+ + update("name", e.target.value)} required /> +
+
+ + update("surname", e.target.value)} + required + /> +
+ +
+ + update("birthDate", e.target.value)} + required + /> +
+
+ + +
+ +
+ + update("address", e.target.value)} + required + /> +
+ +
+ + update("fiscalCode", e.target.value.toUpperCase())} + required + /> +
+ + {!value.italianCitizen && ( +
+ + update("residencePermitNumber", e.target.value)} + /> +
+ )} + +
+ update("italianCitizen", checked === true)} + /> + +
+
+ + + + + +
+
+
+ ); +} diff --git a/components/associations/associate-table.tsx b/components/associations/associate-table.tsx new file mode 100644 index 0000000..a3c8713 --- /dev/null +++ b/components/associations/associate-table.tsx @@ -0,0 +1,102 @@ +import { Pencil, Trash2 } from "lucide-react"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { ConfirmDialog } from "@/components/ui/confirm-dialog"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import type { Associate } from "@/lib/models"; + +const ROLE_LABELS: Record = { + PRESIDENT: "Presidente", + VICE_PRESIDENT: "Vicepresidente", + SECRETARY: "Segretario", + ASSOCIATE: "Socio", +}; + +export function AssociateTable({ + associates, + onEdit, + onDelete, +}: { + associates: Associate[]; + onEdit?: (index: number) => void; + onDelete?: (index: number) => Promise | void; +}) { + const editable = Boolean(onEdit || onDelete); + + if (associates.length === 0) { + return ( +

+ Nessun socio registrato +

+ ); + } + + return ( + + + + Nome + Ruolo + Codice fiscale + Indirizzo + Cittadinanza + {editable && } + + + + {associates.map((associate, index) => ( + + + {associate.name} {associate.surname} + + + {ROLE_LABELS[associate.role] ?? associate.role} + + + {associate.fiscalCode || "โ€”"} + + + {associate.address || "โ€”"} + + + + {associate.italianCitizen ? "Italiana" : "Straniera"} + + + {editable && ( + +
+ {onEdit && ( + + )} + {onDelete && ( + + + + } + title="Rimuovere il socio?" + description={`${associate.name} ${associate.surname} verrร  rimosso dall'associazione.`} + confirmLabel="Rimuovi" + onConfirm={() => onDelete(index)} + /> + )} +
+
+ )} +
+ ))} +
+
+ ); +} diff --git a/components/associations/association-info-card.tsx b/components/associations/association-info-card.tsx new file mode 100644 index 0000000..33d6111 --- /dev/null +++ b/components/associations/association-info-card.tsx @@ -0,0 +1,31 @@ +import type { ReactNode } from "react"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import type { Association } from "@/lib/models"; +import { AssociationStatusBadge } from "./association-status-badge"; + +export function AssociationInfoCard({ association }: { association: Association }) { + const fields: { label: string; value: ReactNode }[] = [ + { label: "Indirizzo", value: association.address || "โ€”" }, + { label: "Partita IVA", value: association.vatNumber || "โ€”" }, + { label: "Codice RUNTS", value: association.runtsId || "โ€”" }, + { label: "Stato", value: }, + ]; + + return ( + + + Informazioni generali + + +
+ {fields.map((field) => ( +
+
{field.label}
+
{field.value}
+
+ ))} +
+
+
+ ); +} diff --git a/components/associations/association-search.tsx b/components/associations/association-search.tsx new file mode 100644 index 0000000..41390a8 --- /dev/null +++ b/components/associations/association-search.tsx @@ -0,0 +1,36 @@ +"use client"; + +import { Search, X } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; + +export function AssociationSearch({ + value, + onChange, +}: { + value: string; + onChange: (value: string) => void; +}) { + return ( +
+ + onChange(e.target.value)} + placeholder="Cerca per nome, indirizzo, P.IVA o RUNTS..." + className="pl-9 pr-9" + /> + {value && ( + + )} +
+ ); +} diff --git a/components/associations/association-status-badge.tsx b/components/associations/association-status-badge.tsx new file mode 100644 index 0000000..6d530e8 --- /dev/null +++ b/components/associations/association-status-badge.tsx @@ -0,0 +1,16 @@ +import { Badge } from "@/components/ui/badge"; + +export function AssociationStatusBadge({ isShown }: { isShown?: boolean }) { + if (isShown) { + return ( + + Attiva + + ); + } + return ( + + Nascosta + + ); +} diff --git a/components/associations/association-table.tsx b/components/associations/association-table.tsx new file mode 100644 index 0000000..b348e19 --- /dev/null +++ b/components/associations/association-table.tsx @@ -0,0 +1,69 @@ +"use client"; + +import { useRouter } from "next/navigation"; +import { ChevronRight } from "lucide-react"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import type { Association } from "@/lib/models"; +import { AssociationStatusBadge } from "./association-status-badge"; + +export function AssociationTable({ associations }: { associations: Association[] }) { + const router = useRouter(); + + if (associations.length === 0) { + return ( +
+ Nessuna associazione trovata +
+ ); + } + + return ( +
+ + + + Nome + Indirizzo + P.IVA + RUNTS + Stato + + + + + {associations.map((association) => ( + router.push(`/dashboard/${association.id}`)} + > + {association.name} + + {association.address} + + + {association.vatNumber || "โ€”"} + + + {association.runtsId || "โ€”"} + + + + + + + + + ))} + +
+
+ ); +} diff --git a/components/associations/create-association-dialog.tsx b/components/associations/create-association-dialog.tsx new file mode 100644 index 0000000..1a7ef8c --- /dev/null +++ b/components/associations/create-association-dialog.tsx @@ -0,0 +1,132 @@ +"use client"; + +import { useState } from "react"; +import { Loader2, Plus } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { createAssociation } from "@/lib/api"; +import type { Association } from "@/lib/models"; + +export function CreateAssociationDialog({ + onCreated, +}: { + onCreated: (association: Association) => void; +}) { + const [open, setOpen] = useState(false); + const [creating, setCreating] = useState(false); + const [name, setName] = useState(""); + const [address, setAddress] = useState(""); + const [vatNumber, setVatNumber] = useState(""); + const [runtsId, setRuntsId] = useState(""); + + const reset = () => { + setName(""); + setAddress(""); + setVatNumber(""); + setRuntsId(""); + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setCreating(true); + + try { + const association = await createAssociation({ + name, + address, + vatNumber, + runtsId, + isShown: true, + associates: [], + users: [], + }); + reset(); + setOpen(false); + onCreated(association); + } catch (error) { + console.error("Failed to create association:", error); + alert("Impossibile creare l'associazione"); + } finally { + setCreating(false); + } + }; + + return ( + + + + + + + Crea nuova associazione + +
+
+
+ + setName(e.target.value)} + placeholder="Associazione Sportiva Roma" + required + /> +
+ +
+ + setAddress(e.target.value)} + placeholder="Via Roma 10, Milano, 20100" + required + /> +
+ +
+ + setVatNumber(e.target.value)} + placeholder="12345678901" + /> +
+ +
+ + setRuntsId(e.target.value)} + placeholder="RUNTS001" + /> +
+
+ +
+ + +
+
+
+
+ ); +} diff --git a/components/associations/edit-association-dialog.tsx b/components/associations/edit-association-dialog.tsx new file mode 100644 index 0000000..56ee9cc --- /dev/null +++ b/components/associations/edit-association-dialog.tsx @@ -0,0 +1,125 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { Loader2, Pencil } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Switch } from "@/components/ui/switch"; +import { updateAssociation } from "@/lib/api"; +import type { Association } from "@/lib/models"; + +export function EditAssociationDialog({ + association, + onSaved, +}: { + association: Association; + onSaved: (association: Association) => void; +}) { + const [open, setOpen] = useState(false); + const [saving, setSaving] = useState(false); + const [name, setName] = useState(association.name); + const [address, setAddress] = useState(association.address); + const [vatNumber, setVatNumber] = useState(association.vatNumber ?? ""); + const [runtsId, setRuntsId] = useState(association.runtsId ?? ""); + const [isShown, setIsShown] = useState(association.isShown ?? true); + + useEffect(() => { + if (open) { + setName(association.name); + setAddress(association.address); + setVatNumber(association.vatNumber ?? ""); + setRuntsId(association.runtsId ?? ""); + setIsShown(association.isShown ?? true); + } + }, [open, association]); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setSaving(true); + try { + const updated = await updateAssociation(association.id!, { + ...association, + name, + address, + vatNumber, + runtsId, + isShown, + }); + onSaved(updated); + setOpen(false); + } catch (error) { + console.error("Failed to update association:", error); + alert("Impossibile salvare le modifiche"); + } finally { + setSaving(false); + } + }; + + return ( + + + + + + + Modifica associazione + +
+
+
+ + setName(e.target.value)} required /> +
+ +
+ + setAddress(e.target.value)} required /> +
+ +
+ + setVatNumber(e.target.value)} /> +
+ +
+ + setRuntsId(e.target.value)} /> +
+ +
+
+ +

+ Le associazioni non attive non compaiono nell'elenco +

+
+ +
+
+ + + + + +
+
+
+ ); +} diff --git a/components/associations/user-accounts-card.tsx b/components/associations/user-accounts-card.tsx new file mode 100644 index 0000000..1376534 --- /dev/null +++ b/components/associations/user-accounts-card.tsx @@ -0,0 +1,75 @@ +import { Pencil, Trash2 } from "lucide-react"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { ConfirmDialog } from "@/components/ui/confirm-dialog"; +import type { User } from "@/lib/models"; + +const ROLE_LABELS: Record = { + ADMIN: "Amministratore", + MANAGER: "Gestore", + CLIENT: "Associazione", +}; + +export function UserAccountsCard({ + users, + actions, + onEdit, + onDelete, +}: { + users: User[]; + actions?: React.ReactNode; + onEdit?: (index: number) => void; + onDelete?: (index: number) => Promise | void; +}) { + const editable = Boolean(onEdit || onDelete); + + return ( + + + Account di accesso ({users.length}) + {actions} + + + {users.length === 0 ? ( +

+ Nessun account configurato +

+ ) : ( +
    + {users.map((user, index) => ( +
  • +
    + {user.email} + {ROLE_LABELS[user.role] ?? user.role} +
    + {editable && ( +
    + {onEdit && ( + + )} + {onDelete && ( + + + + } + title="Rimuovere l'account?" + description={`${user.email} non potrร  piรน accedere al portale.`} + confirmLabel="Rimuovi" + onConfirm={() => onDelete(index)} + /> + )} +
    + )} +
  • + ))} +
+ )} +
+
+ ); +} diff --git a/components/associations/user-form-dialog.tsx b/components/associations/user-form-dialog.tsx new file mode 100644 index 0000000..b9e48df --- /dev/null +++ b/components/associations/user-form-dialog.tsx @@ -0,0 +1,131 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { Loader2 } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import type { User } from "@/lib/models"; + +const ROLE_OPTIONS: { value: User["role"]; label: string }[] = [ + { value: "CLIENT", label: "Associazione" }, + { value: "MANAGER", label: "Gestore" }, + { value: "ADMIN", label: "Amministratore" }, +]; + +const EMPTY_USER: User = { email: "", password: "", role: "CLIENT" }; + +export function UserFormDialog({ + open, + onOpenChange, + initialValue, + onSubmit, +}: { + open: boolean; + onOpenChange: (open: boolean) => void; + initialValue?: User; + onSubmit: (user: User) => Promise | void; +}) { + const [value, setValue] = useState(initialValue ?? EMPTY_USER); + const [saving, setSaving] = useState(false); + const isEdit = Boolean(initialValue); + + useEffect(() => { + if (open) { + setValue(initialValue ?? EMPTY_USER); + } + }, [open, initialValue]); + + const update = (key: K, fieldValue: User[K]) => + setValue((prev) => ({ ...prev, [key]: fieldValue })); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setSaving(true); + try { + await onSubmit(value); + onOpenChange(false); + } catch (error) { + console.error("Failed to save user:", error); + alert("Impossibile salvare l'account"); + } finally { + setSaving(false); + } + }; + + return ( + + + + {isEdit ? "Modifica account" : "Nuovo account di accesso"} + +
+
+ + update("email", e.target.value)} + required + /> +
+ +
+ + update("password", e.target.value)} + placeholder={isEdit ? "Lascia vuoto per non modificarla" : undefined} + required={!isEdit} + /> +
+ +
+ + +
+ + + + + +
+
+
+ ); +} diff --git a/components/layout/app-header.tsx b/components/layout/app-header.tsx new file mode 100644 index 0000000..6a9b888 --- /dev/null +++ b/components/layout/app-header.tsx @@ -0,0 +1,78 @@ +"use client"; + +import Image from "next/image"; +import Link from "next/link"; +import { useRouter } from "next/navigation"; +import { LogOut } from "lucide-react"; +import { Avatar, AvatarFallback } from "@/components/ui/avatar"; +import { Badge } from "@/components/ui/badge"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { clearStoredUser, useStoredUser } from "@/lib/auth"; + +const ROLE_LABELS: Record = { + ADMIN: "Amministratore", + MANAGER: "Gestore", + CLIENT: "Associazione", +}; + +export function AppHeader() { + const router = useRouter(); + const { user } = useStoredUser(); + + const handleLogout = () => { + clearStoredUser(); + router.push("/login"); + }; + + const homeHref = user?.role === "CLIENT" ? "/client-portal" : "/dashboard"; + + return ( +
+
+ + ENCAP + ENCAP + + + {user && ( + + + + {user.email} + + + {user.email.slice(0, 2).toUpperCase()} + + + + + {user.email} + + {ROLE_LABELS[user.role] ?? user.role} + + + + + + Esci + + + + )} +
+
+ ); +} diff --git a/components/layout/page-header.tsx b/components/layout/page-header.tsx new file mode 100644 index 0000000..0274153 --- /dev/null +++ b/components/layout/page-header.tsx @@ -0,0 +1,23 @@ +import type { ReactNode } from "react"; + +export function PageHeader({ + title, + description, + actions, +}: { + title: string; + description?: string; + actions?: ReactNode; +}) { + return ( +
+
+

{title}

+ {description && ( +

{description}

+ )} +
+ {actions &&
{actions}
} +
+ ); +} diff --git a/components/ui/alert-dialog.tsx b/components/ui/alert-dialog.tsx new file mode 100644 index 0000000..dad3273 --- /dev/null +++ b/components/ui/alert-dialog.tsx @@ -0,0 +1,196 @@ +"use client" + +import * as React from "react" +import { AlertDialog as AlertDialogPrimitive } from "radix-ui" + +import { cn } from "@/lib/utils" +import { Button } from "@/components/ui/button" + +function AlertDialog({ + ...props +}: React.ComponentProps) { + return +} + +function AlertDialogTrigger({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogPortal({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogOverlay({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogContent({ + className, + size = "default", + ...props +}: React.ComponentProps & { + size?: "default" | "sm" +}) { + return ( + + + + + ) +} + +function AlertDialogHeader({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function AlertDialogFooter({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function AlertDialogTitle({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogDescription({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogMedia({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function AlertDialogAction({ + className, + variant = "default", + size = "default", + ...props +}: React.ComponentProps & + Pick, "variant" | "size">) { + return ( + + ) +} + +function AlertDialogCancel({ + className, + variant = "outline", + size = "default", + ...props +}: React.ComponentProps & + Pick, "variant" | "size">) { + return ( + + ) +} + +export { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogMedia, + AlertDialogOverlay, + AlertDialogPortal, + AlertDialogTitle, + AlertDialogTrigger, +} diff --git a/components/ui/avatar.tsx b/components/ui/avatar.tsx new file mode 100644 index 0000000..ea65850 --- /dev/null +++ b/components/ui/avatar.tsx @@ -0,0 +1,109 @@ +"use client" + +import * as React from "react" +import { Avatar as AvatarPrimitive } from "radix-ui" + +import { cn } from "@/lib/utils" + +function Avatar({ + className, + size = "default", + ...props +}: React.ComponentProps & { + size?: "default" | "sm" | "lg" +}) { + return ( + + ) +} + +function AvatarImage({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AvatarFallback({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AvatarBadge({ className, ...props }: React.ComponentProps<"span">) { + return ( + svg]:hidden", + "group-data-[size=default]/avatar:size-2.5 group-data-[size=default]/avatar:[&>svg]:size-2", + "group-data-[size=lg]/avatar:size-3 group-data-[size=lg]/avatar:[&>svg]:size-2", + className + )} + {...props} + /> + ) +} + +function AvatarGroup({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function AvatarGroupCount({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( +
svg]:size-4 group-has-data-[size=lg]/avatar-group:[&>svg]:size-5 group-has-data-[size=sm]/avatar-group:[&>svg]:size-3", + className + )} + {...props} + /> + ) +} + +export { + Avatar, + AvatarImage, + AvatarFallback, + AvatarBadge, + AvatarGroup, + AvatarGroupCount, +} diff --git a/components/ui/badge.tsx b/components/ui/badge.tsx new file mode 100644 index 0000000..6eb2a05 --- /dev/null +++ b/components/ui/badge.tsx @@ -0,0 +1,48 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" +import { Slot } from "radix-ui" + +import { cn } from "@/lib/utils" + +const badgeVariants = cva( + "inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-full border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3", + { + variants: { + variant: { + default: "bg-primary text-primary-foreground [a&]:hover:bg-primary/90", + secondary: + "bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90", + destructive: + "bg-destructive text-white focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40 [a&]:hover:bg-destructive/90", + outline: + "border-border text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground", + ghost: "[a&]:hover:bg-accent [a&]:hover:text-accent-foreground", + link: "text-primary underline-offset-4 [a&]:hover:underline", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +function Badge({ + className, + variant = "default", + asChild = false, + ...props +}: React.ComponentProps<"span"> & + VariantProps & { asChild?: boolean }) { + const Comp = asChild ? Slot.Root : "span" + + return ( + + ) +} + +export { Badge, badgeVariants } diff --git a/components/ui/button.tsx b/components/ui/button.tsx new file mode 100644 index 0000000..b5ea4ab --- /dev/null +++ b/components/ui/button.tsx @@ -0,0 +1,64 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" +import { Slot } from "radix-ui" + +import { cn } from "@/lib/utils" + +const buttonVariants = cva( + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", + { + variants: { + variant: { + default: "bg-primary text-primary-foreground hover:bg-primary/90", + destructive: + "bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", + outline: + "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50", + secondary: + "bg-secondary text-secondary-foreground hover:bg-secondary/80", + ghost: + "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: "h-9 px-4 py-2 has-[>svg]:px-3", + xs: "h-6 gap-1 rounded-md px-2 text-xs has-[>svg]:px-1.5 [&_svg:not([class*='size-'])]:size-3", + sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", + lg: "h-10 rounded-md px-6 has-[>svg]:px-4", + icon: "size-9", + "icon-xs": "size-6 rounded-md [&_svg:not([class*='size-'])]:size-3", + "icon-sm": "size-8", + "icon-lg": "size-10", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +) + +function Button({ + className, + variant = "default", + size = "default", + asChild = false, + ...props +}: React.ComponentProps<"button"> & + VariantProps & { + asChild?: boolean + }) { + const Comp = asChild ? Slot.Root : "button" + + return ( + + ) +} + +export { Button, buttonVariants } diff --git a/components/ui/card.tsx b/components/ui/card.tsx new file mode 100644 index 0000000..681ad98 --- /dev/null +++ b/components/ui/card.tsx @@ -0,0 +1,92 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +function Card({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardTitle({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardDescription({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardAction({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardContent({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +export { + Card, + CardHeader, + CardFooter, + CardTitle, + CardAction, + CardDescription, + CardContent, +} diff --git a/components/ui/checkbox.tsx b/components/ui/checkbox.tsx new file mode 100644 index 0000000..f5a7e43 --- /dev/null +++ b/components/ui/checkbox.tsx @@ -0,0 +1,32 @@ +"use client" + +import * as React from "react" +import { CheckIcon } from "lucide-react" +import { Checkbox as CheckboxPrimitive } from "radix-ui" + +import { cn } from "@/lib/utils" + +function Checkbox({ + className, + ...props +}: React.ComponentProps) { + return ( + + + + + + ) +} + +export { Checkbox } diff --git a/components/ui/confirm-dialog.tsx b/components/ui/confirm-dialog.tsx new file mode 100644 index 0000000..26efbf0 --- /dev/null +++ b/components/ui/confirm-dialog.tsx @@ -0,0 +1,75 @@ +"use client"; + +import { useState, type ReactNode } from "react"; +import { Loader2 } from "lucide-react"; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, +} from "@/components/ui/alert-dialog"; + +export function ConfirmDialog({ + trigger, + title, + description, + confirmLabel = "Conferma", + loadingLabel = "Attendere...", + destructive = true, + onConfirm, +}: { + trigger: ReactNode; + title: string; + description: string; + confirmLabel?: string; + loadingLabel?: string; + destructive?: boolean; + onConfirm: () => Promise | void; +}) { + const [open, setOpen] = useState(false); + const [loading, setLoading] = useState(false); + + const handleConfirm = async () => { + setLoading(true); + try { + await onConfirm(); + setOpen(false); + } catch (error) { + console.error(error); + alert("Si รจ verificato un errore. Riprova."); + } finally { + setLoading(false); + } + }; + + return ( + + {trigger} + + + {title} + {description} + + + Annulla + { + e.preventDefault(); + handleConfirm(); + }} + > + {loading && } + {loading ? loadingLabel : confirmLabel} + + + + + ); +} diff --git a/components/ui/dialog.tsx b/components/ui/dialog.tsx new file mode 100644 index 0000000..80d7ad6 --- /dev/null +++ b/components/ui/dialog.tsx @@ -0,0 +1,158 @@ +"use client" + +import * as React from "react" +import { XIcon } from "lucide-react" +import { Dialog as DialogPrimitive } from "radix-ui" + +import { cn } from "@/lib/utils" +import { Button } from "@/components/ui/button" + +function Dialog({ + ...props +}: React.ComponentProps) { + return +} + +function DialogTrigger({ + ...props +}: React.ComponentProps) { + return +} + +function DialogPortal({ + ...props +}: React.ComponentProps) { + return +} + +function DialogClose({ + ...props +}: React.ComponentProps) { + return +} + +function DialogOverlay({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DialogContent({ + className, + children, + showCloseButton = true, + ...props +}: React.ComponentProps & { + showCloseButton?: boolean +}) { + return ( + + + + {children} + {showCloseButton && ( + + + Close + + )} + + + ) +} + +function DialogHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function DialogFooter({ + className, + showCloseButton = false, + children, + ...props +}: React.ComponentProps<"div"> & { + showCloseButton?: boolean +}) { + return ( +
+ {children} + {showCloseButton && ( + + + + )} +
+ ) +} + +function DialogTitle({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DialogDescription({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogOverlay, + DialogPortal, + DialogTitle, + DialogTrigger, +} diff --git a/components/ui/dropdown-menu.tsx b/components/ui/dropdown-menu.tsx new file mode 100644 index 0000000..ae1fcf6 --- /dev/null +++ b/components/ui/dropdown-menu.tsx @@ -0,0 +1,257 @@ +"use client" + +import * as React from "react" +import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react" +import { DropdownMenu as DropdownMenuPrimitive } from "radix-ui" + +import { cn } from "@/lib/utils" + +function DropdownMenu({ + ...props +}: React.ComponentProps) { + return +} + +function DropdownMenuPortal({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DropdownMenuTrigger({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DropdownMenuContent({ + className, + sideOffset = 4, + ...props +}: React.ComponentProps) { + return ( + + + + ) +} + +function DropdownMenuGroup({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DropdownMenuItem({ + className, + inset, + variant = "default", + ...props +}: React.ComponentProps & { + inset?: boolean + variant?: "default" | "destructive" +}) { + return ( + + ) +} + +function DropdownMenuCheckboxItem({ + className, + children, + checked, + ...props +}: React.ComponentProps) { + return ( + + + + + + + {children} + + ) +} + +function DropdownMenuRadioGroup({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DropdownMenuRadioItem({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + + + + + + {children} + + ) +} + +function DropdownMenuLabel({ + className, + inset, + ...props +}: React.ComponentProps & { + inset?: boolean +}) { + return ( + + ) +} + +function DropdownMenuSeparator({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DropdownMenuShortcut({ + className, + ...props +}: React.ComponentProps<"span">) { + return ( + + ) +} + +function DropdownMenuSub({ + ...props +}: React.ComponentProps) { + return +} + +function DropdownMenuSubTrigger({ + className, + inset, + children, + ...props +}: React.ComponentProps & { + inset?: boolean +}) { + return ( + + {children} + + + ) +} + +function DropdownMenuSubContent({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { + DropdownMenu, + DropdownMenuPortal, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuLabel, + DropdownMenuItem, + DropdownMenuCheckboxItem, + DropdownMenuRadioGroup, + DropdownMenuRadioItem, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuSub, + DropdownMenuSubTrigger, + DropdownMenuSubContent, +} diff --git a/components/ui/form.tsx b/components/ui/form.tsx new file mode 100644 index 0000000..b438928 --- /dev/null +++ b/components/ui/form.tsx @@ -0,0 +1,167 @@ +"use client" + +import * as React from "react" +import type { Label as LabelPrimitive } from "radix-ui" +import { Slot } from "radix-ui" +import { + Controller, + FormProvider, + useFormContext, + useFormState, + type ControllerProps, + type FieldPath, + type FieldValues, +} from "react-hook-form" + +import { cn } from "@/lib/utils" +import { Label } from "@/components/ui/label" + +const Form = FormProvider + +type FormFieldContextValue< + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath, +> = { + name: TName +} + +const FormFieldContext = React.createContext( + {} as FormFieldContextValue +) + +const FormField = < + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath, +>({ + ...props +}: ControllerProps) => { + return ( + + + + ) +} + +const useFormField = () => { + const fieldContext = React.useContext(FormFieldContext) + const itemContext = React.useContext(FormItemContext) + const { getFieldState } = useFormContext() + const formState = useFormState({ name: fieldContext.name }) + const fieldState = getFieldState(fieldContext.name, formState) + + if (!fieldContext) { + throw new Error("useFormField should be used within ") + } + + const { id } = itemContext + + return { + id, + name: fieldContext.name, + formItemId: `${id}-form-item`, + formDescriptionId: `${id}-form-item-description`, + formMessageId: `${id}-form-item-message`, + ...fieldState, + } +} + +type FormItemContextValue = { + id: string +} + +const FormItemContext = React.createContext( + {} as FormItemContextValue +) + +function FormItem({ className, ...props }: React.ComponentProps<"div">) { + const id = React.useId() + + return ( + +
+ + ) +} + +function FormLabel({ + className, + ...props +}: React.ComponentProps) { + const { error, formItemId } = useFormField() + + return ( +