Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
3ac11aa
style: update (dark) sidebar css variables
lorenzocorallo Jun 4, 2026
aaed723
feat: sidebar boilerplate and breadcrumb header
lorenzocorallo Jun 4, 2026
da6760c
some refactor
lorenzocorallo Jun 4, 2026
1d485af
feat: use collapsible
lorenzocorallo Jun 4, 2026
ef68226
fix: add tg/grants and remove boilerplate
lorenzocorallo Jun 5, 2026
c8ad53a
feat: storage hooks and cookies utility
lorenzocorallo Jun 5, 2026
24828cd
feat: user nav, category open state persistence
lorenzocorallo Jun 5, 2026
1a5d70d
fix: cookie expiration, use <Link> instead of <a>
lorenzocorallo Jun 5, 2026
fba8bd3
chore: remove zustand, remove old i18n, biome fixes
lorenzocorallo Jun 5, 2026
73acf8f
feat: sidebar item icons, type cleanup
lorenzocorallo Jun 5, 2026
6099b46
feat: remove category pages, adjust page padding
lorenzocorallo Jun 5, 2026
ec3c62e
chore: biome
lorenzocorallo Jun 5, 2026
454e9db
style: adjust destructive color variable
lorenzocorallo Jun 5, 2026
fc32ad7
style: container mx-auto by default
lorenzocorallo Jun 5, 2026
51e171e
fix: remove category pages, dont create link for category in breadcrumb
lorenzocorallo Jun 5, 2026
6362566
fix: re-render issue with the use-cookie-storage hook
lorenzocorallo Jun 5, 2026
075f156
fix: same as previous commit, but for use-session-storage
lorenzocorallo Jun 5, 2026
21c64c5
fix: coderabbit suggestion
lorenzocorallo Jun 5, 2026
e9fd9e7
fix: match also subroutes in category items
lorenzocorallo Jun 5, 2026
60d3617
fix: catch JSON parse error
lorenzocorallo Jun 5, 2026
2510a36
fix: rename column in groups
lorenzocorallo Jun 5, 2026
f6e4fa3
style: move breadcrumb to center
lorenzocorallo Jun 5, 2026
fd76b34
fix: cleanup home
lorenzocorallo Jun 5, 2026
d7388b5
style: button icon svg size
lorenzocorallo Jun 5, 2026
ba20b0b
Merge branch 'main' into sidebar
lorenzocorallo Jun 6, 2026
57d3a23
style: fix padding mismatch between pages and loadings
lorenzocorallo Jun 6, 2026
5003e80
feat: loading skeleton for account page
lorenzocorallo Jun 6, 2026
0b1596c
perf: make dashboard homepage static for now
lorenzocorallo Jun 6, 2026
c266bae
feat: add associations management with card component and delete dialog
BIA3IA Jun 9, 2026
c78ecdb
feat: add No association found message for empty state
BIA3IA Jun 9, 2026
dff8b4d
Connect getAllAssociations and move client logic from page
BIA3IA Jun 9, 2026
c3eb226
Refactor AssociationsList integration
BIA3IA Jun 9, 2026
009bc1a
feat: update association card to display initials when logo is not pr…
BIA3IA Jun 9, 2026
a24b0af
refactor: integrate AssociationsList functionality directly into Asso…
BIA3IA Jun 9, 2026
2e6279e
feat: implement create, edit, and delete association functionalities
BIA3IA Jun 9, 2026
cdcf54a
fix: improve getInitials function to remove "-"
BIA3IA Jun 9, 2026
37b7e07
feat: error handling for delete, save, edit
BIA3IA Jun 9, 2026
ff68966
feat: add loading state to save and cancel actions
BIA3IA Jun 9, 2026
aee00a8
feat: update button variants for success and error states in UI compo…
BIA3IA Jun 9, 2026
5b24e28
feat: implement association links management with dialog and save fun…
BIA3IA Jun 10, 2026
585f6f6
fix: review
lorenzocorallo Jun 10, 2026
4797e44
fix: bring back user details page, link from user list
lorenzocorallo Jun 10, 2026
61bfddb
feat: add back user search, make container flex(col) with pb-6
lorenzocorallo Jun 10, 2026
ce7da3c
fix: missing notFound in user-details, loading style
lorenzocorallo Jun 10, 2026
8f351d4
fix: not found page
lorenzocorallo Jun 10, 2026
df0ae86
fix: add a11y to user details button
lorenzocorallo Jun 10, 2026
73f8223
feat: update icon for Web section in navigation
BIA3IA Jun 10, 2026
1df7f77
Merge commit 'df0ae868e2d0e0381ec4e74cff75d0df42367827' into web/asso…
BIA3IA Jun 10, 2026
aecb7d0
Merge commit '05f48c88c4525c16f54bcb77bdf6db625076a8f5' into web/asso…
BIA3IA Jun 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 109 additions & 0 deletions src/app/dashboard/(active)/web/associations/association-links.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
"use client"

import { Link, Save } from "lucide-react"
import { useState } from "react"
import { Button } from "@/components/ui/button"
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { LINK_FIELDS } from "./constants"
import type { AssociationLinks } from "./types"

function normalizeLinks(links: AssociationLinks) {
return Object.fromEntries(
LINK_FIELDS.map((field) => {
const value = links[field.key]?.trim()
return [field.key, value || null]
})
) as AssociationLinks
}

export function AssociationLinksDialog({
name,
links,
onSave,
}: {
name: string
links: AssociationLinks
onSave: (links: AssociationLinks) => boolean | Promise<boolean>
}) {
const [open, setOpen] = useState(false)
const [pending, setPending] = useState(false)
const [draftLinks, setDraftLinks] = useState(links)

function handleOpenChange(value: boolean) {
setOpen(value)
if (value) setDraftLinks(links)
}

async function handleSave() {
if (pending) return

setPending(true)
try {
const saved = await onSave(normalizeLinks(draftLinks))
if (saved) setOpen(false)
} finally {
setPending(false)
}
}

return (
<Dialog open={open} onOpenChange={handleOpenChange}>
<DialogTrigger
render={
<Button type="button" variant="outline" size="icon" aria-label={`Edit ${name} links`}>
<Link className="size-4" />
</Button>
}
/>

<DialogContent className="sm:max-w-5xl">
<DialogHeader>
<DialogTitle>{name} links</DialogTitle>
<DialogDescription>Manage the public links for this association.</DialogDescription>
</DialogHeader>

<div className="grid gap-3 md:grid-cols-2">
{LINK_FIELDS.map((field) => {
const FieldIcon = field.icon

return (
<div className="grid gap-3" key={field.key}>
<Label htmlFor={`${name}-${field.key}`}>
<FieldIcon className="size-4" />
{field.label}
</Label>
<Input
id={`${name}-${field.key}`}
value={draftLinks[field.key] ?? ""}
onChange={(event) =>
setDraftLinks((currentLinks) => ({
...currentLinks,
[field.key]: event.target.value,
}))
}
/>
</div>
)
})}
</div>

<DialogFooter>
<Button type="button" disabled={pending} onClick={handleSave}>
<Save className="size-4" />
Save
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}
185 changes: 185 additions & 0 deletions src/app/dashboard/(active)/web/associations/associations-view.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
"use client"

import { PlusIcon } from "lucide-react"
import { useRouter } from "next/navigation"
import { useState } from "react"
import { toast } from "sonner"
import WebHeader from "@/components/web-header"
import { createAssociation, deleteAssociation, editAssociation, editAssociationLinks } from "@/server/actions/web"
import CardAssociation from "./card-association"
import { EMPTY_ASSOCIATION_LINKS } from "./constants"
import type { Association, AssociationLinks } from "./types"

export function AssociationsView({ initialAssociations }: { initialAssociations: Association[] }) {
const router = useRouter()
const [associations, setAssociations] = useState(initialAssociations)
const [editingAssociationId, setEditingAssociationId] = useState<number | null>(null)
const [draftAssociationIds, setDraftAssociationIds] = useState<Set<number>>(new Set())

// Creates a new temporary association, id will not be saved and will be replaced
function handleAdd() {
const association: Association = {
id: Date.now(),
name: "New Association",
logoSvg: null,
descriptionIt: "Description in Italian",
descriptionEn: "Description in English",
links: EMPTY_ASSOCIATION_LINKS,
}

setAssociations((items) => [association, ...items])
setEditingAssociationId(association.id)
setDraftAssociationIds((ids) => new Set(ids).add(association.id))
}
Comment thread
BIA3IA marked this conversation as resolved.

function removeAssociationLocally(id: number) {
setAssociations((items) => items.filter((item) => item.id !== id))
setDraftAssociationIds((ids) => {
const nextIds = new Set(ids)
nextIds.delete(id)
return nextIds
})
setEditingAssociationId((editingId) => (editingId === id ? null : editingId))
}

async function handleDelete(id: number) {
if (draftAssociationIds.has(id)) {
removeAssociationLocally(id)
return
}

try {
const result = await deleteAssociation(id)

if (result.error === "UNAUTHORIZED") {
toast.error("You don't have permission to delete associations.")
return
} else if (result.error === "NOT_FOUND") {
toast.info("This association was already deleted.")
} else {
toast.success("Association deleted successfully.")
}

removeAssociationLocally(id)
router.refresh()
} catch (_e) {
toast.error("There was an error deleting the association.")
}
}
Comment thread
BIA3IA marked this conversation as resolved.

// Draft ne crea una nuova, altrimenti modifica quella esistente
async function handleSave(id: number, values: Association) {
try {
const isDraft = draftAssociationIds.has(id)
const result = isDraft
? await createAssociation({
name: values.name,
descriptionIt: values.descriptionIt,
descriptionEn: values.descriptionEn,
logoSvg: values.logoSvg,
})
: await editAssociation({
id,
name: values.name,
descriptionIt: values.descriptionIt,
descriptionEn: values.descriptionEn,
logoSvg: values.logoSvg,
})

if (result.error === "UNAUTHORIZED") {
toast.error("You don't have permission to save associations.")
return false
} else if (result.error === "NOT_FOUND") {
toast.error("This association does not exist anymore.")
return false
} else if (!result.association) {
toast.error("There was an error saving the association.")
return false
}

// anche qui, se facessi il refresh perderei gli altri edit locali
setAssociations((items) => items.map((item) => (item.id === id ? result.association : item)))
setDraftAssociationIds((ids) => {
const nextIds = new Set(ids)
nextIds.delete(id)
return nextIds
})
setEditingAssociationId((editingId) => (editingId === id ? null : editingId))
toast.success(`Association ${isDraft ? "created" : "updated"} successfully.`)
router.refresh()
// Mi ritorna true cosi poi chiudo l'edit della card
return true
} catch (_e) {
toast.error("There was an error saving the association.")
return false
}
}

async function handleSaveLinks(id: number, links: AssociationLinks) {
try {
const result = await editAssociationLinks({ id, links })

if (result.error === "UNAUTHORIZED") {
toast.error("You don't have permission to save association links.")
return false
} else if (result.error === "NOT_FOUND") {
toast.error("This association does not exist anymore.")
return false
} else if (!result.association) {
toast.error("There was an error saving the association links.")
return false
}

setAssociations((items) =>
items.map((item) => (item.id === id ? { ...item, links: result.association.links } : item))
)
toast.success("Association links updated successfully.")
router.refresh()
return true
} catch (_e) {
toast.error("There was an error saving the association links.")
return false
}
}

return (
<>
<WebHeader
title="Associations"
description="Manage and view associations displayed on the web platform."
action={{
label: "Add Association",
icon: <PlusIcon className="size-4" />,
onClick: handleAdd,
}}
/>

<div className="grid gap-4">
{associations.length === 0 && (
<div className="grid min-h-64 place-items-center">
<p className="text-center text-lg text-muted-foreground">
No associations found. Click "Add Association" to create one.
</p>
</div>
)}
{associations.map((item) => (
<CardAssociation
key={item.id}
id={item.id}
name={item.name}
logoSvg={item.logoSvg}
descriptionIt={item.descriptionIt}
descriptionEn={item.descriptionEn}
links={item.links}
initialEditActive={editingAssociationId === item.id}
isDraft={draftAssociationIds.has(item.id)}
onCancelCreate={() => removeAssociationLocally(item.id)}
onDelete={() => handleDelete(item.id)}
onSave={(values) => handleSave(item.id, values)}
onSaveLinks={(links) => handleSaveLinks(item.id, links)}
/>
))}
</div>
</>
)
}
Loading
Loading