diff --git a/next.config.ts b/next.config.ts index 609ecca..28a6a24 100644 --- a/next.config.ts +++ b/next.config.ts @@ -11,8 +11,19 @@ const config: NextConfig = { }, output: "standalone", transpilePackages: ["@t3-oss/env-nextjs", "@t3-oss/env-core"], + images: { + remotePatterns: [ + { + protocol: "https", + hostname: "**.blob.core.windows.net", + }, + ], + }, experimental: { reactCompiler: true, + serverActions: { + bodySizeLimit: "2mb", + }, }, } diff --git a/package.json b/package.json index cc55384..6b20144 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "type": "module", "scripts": { "build": "next build", - "dev": "PORT=3001 next dev --turbo", + "dev": "PORT=3001 next dev", "check": "biome check", "check:fix": "biome check --fix --unsafe", "preview": "next build && next start", @@ -15,6 +15,7 @@ "dependencies": { "@base-ui/react": "^1.3.0", "@better-auth/passkey": "^1.5.5", + "@dnd-kit/react": "^0.4.0", "@hookform/resolvers": "^3.9.1", "@polinetwork/backend": "^0.16.0", "@radix-ui/react-dialog": "^1.1.15", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index eb0e106..c48ff7f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,6 +14,9 @@ importers: '@better-auth/passkey': specifier: ^1.5.5 version: 1.5.5(@better-auth/core@1.5.5(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.12)(nanostores@1.2.0))(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-auth@1.5.5(mongodb@7.1.0)(next@15.5.18(@babel/core@7.29.0)(babel-plugin-react-compiler@1.0.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(better-call@1.3.2(zod@4.3.5))(nanostores@1.2.0) + '@dnd-kit/react': + specifier: ^0.4.0 + version: 0.4.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@hookform/resolvers': specifier: ^3.9.1 version: 3.10.0(react-hook-form@7.55.0(react@18.3.1)) @@ -447,6 +450,27 @@ packages: '@date-fns/tz@1.4.1': resolution: {integrity: sha512-P5LUNhtbj6YfI3iJjw5EL9eUAG6OitD0W3fWQcpQjDRc/QIsL0tRNuO1PcDvPccWL1fSTXXdE1ds+l95DV/OFA==} + '@dnd-kit/abstract@0.4.0': + resolution: {integrity: sha512-loEEJxKT5oLOLeRBJVTO9qpgvvW/Qq902xO20v1JMbpANuN/NLurUdpxIwNpVz+RtOSyzznnbc7lO7psmOhc9A==} + + '@dnd-kit/collision@0.4.0': + resolution: {integrity: sha512-oOHHUkH1h9Vl2m8TwLw/mPHA7Blf+s0PYcRoLNWNBVxDzugJKZo8WdpU58EMu9qkqyQGrR/YTOozGiMPhlqZ5Q==} + + '@dnd-kit/dom@0.4.0': + resolution: {integrity: sha512-mJDKt0BtlHXetZyrvZXh6++aycleIbYWH/OVC4nlszDh8NvW7q8dfsxFllR5RtLKLcykLaI4o545Figfks/HZQ==} + + '@dnd-kit/geometry@0.4.0': + resolution: {integrity: sha512-d1n+CU54V/qF/g792bmJK2oR4f5jOL7Pls2IfC+j9f5UBECpjsYbcPZ/krom/z8LgieqvMh1qrUkdcBjJJ7vpg==} + + '@dnd-kit/react@0.4.0': + resolution: {integrity: sha512-J2/N4CpQf98zJBZhMljDNsc+QR4VtUKU9BRO1+Di4OGaB1qafMC4qZ11xKXOkjw+d7h82FRSXmXCo0c8+VWaWg==} + peerDependencies: + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + + '@dnd-kit/state@0.4.0': + resolution: {integrity: sha512-vVdwOY9VsYdMNa7Z0xQhTXlzHqCcCugGuoM1kzvZhnZ0tYVPRdmIhWfeO6Y2ZoN92JwYAyJRRNl4ICkEe2mneg==} + '@dotenvx/dotenvx@1.61.0': resolution: {integrity: sha512-utL3cpZoFzflyqUkjYbxYujI6STBTmO5LFn4bbin/NZnRWN6wQ7eErhr3/Vpa5h/jicPFC6kTa42r940mQftJQ==} hasBin: true @@ -844,6 +868,9 @@ packages: '@polinetwork/backend@0.16.0': resolution: {integrity: sha512-z/xuzvQv9w8frEtKAw59NvfFEllJnx2lw9YVUwTsP9TXh9tfPu5jP1njVUhFGQe15UE+IxwXH1hV4rO0/9/MLg==} + '@preact/signals-core@1.14.4': + resolution: {integrity: sha512-HNB6HYeYKhQbJ1aKl+YRjrS4+QWHLKX6qKoUsfS/m0vqzsVaEBiZiaKbG/e+NKk2ch5ALQr/ihWaMHxiCuuWHA==} + '@radix-ui/number@1.1.1': resolution: {integrity: sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==} @@ -3770,6 +3797,45 @@ snapshots: '@date-fns/tz@1.4.1': {} + '@dnd-kit/abstract@0.4.0': + dependencies: + '@dnd-kit/geometry': 0.4.0 + '@dnd-kit/state': 0.4.0 + tslib: 2.8.1 + + '@dnd-kit/collision@0.4.0': + dependencies: + '@dnd-kit/abstract': 0.4.0 + '@dnd-kit/geometry': 0.4.0 + tslib: 2.8.1 + + '@dnd-kit/dom@0.4.0': + dependencies: + '@dnd-kit/abstract': 0.4.0 + '@dnd-kit/collision': 0.4.0 + '@dnd-kit/geometry': 0.4.0 + '@dnd-kit/state': 0.4.0 + tslib: 2.8.1 + + '@dnd-kit/geometry@0.4.0': + dependencies: + '@dnd-kit/state': 0.4.0 + tslib: 2.8.1 + + '@dnd-kit/react@0.4.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@dnd-kit/abstract': 0.4.0 + '@dnd-kit/dom': 0.4.0 + '@dnd-kit/state': 0.4.0 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + tslib: 2.8.1 + + '@dnd-kit/state@0.4.0': + dependencies: + '@preact/signals-core': 1.14.4 + tslib: 2.8.1 + '@dotenvx/dotenvx@1.61.0': dependencies: commander: 11.1.0 @@ -4174,6 +4240,8 @@ snapshots: '@polinetwork/backend@0.16.0': {} + '@preact/signals-core@1.14.4': {} + '@radix-ui/number@1.1.1': {} '@radix-ui/primitive@1.1.3': {} diff --git a/src/app/dashboard/(active)/telegram/users/[id]/page.tsx b/src/app/dashboard/(active)/telegram/users/[id]/page.tsx index bc83ede..3f01a78 100644 --- a/src/app/dashboard/(active)/telegram/users/[id]/page.tsx +++ b/src/app/dashboard/(active)/telegram/users/[id]/page.tsx @@ -11,7 +11,7 @@ import { NewGroupAdmin } from "./new-group-admin" export default async function TgUserDetails({ params }: { params: Promise<{ id: string }> }) { const { id } = await params const parsedInt = parseInt(id, 10) - if (isNaN(parsedInt)) notFound() + if (Number.isNaN(parsedInt)) notFound() const data = await getUserDetails(parsedInt) if (!data) notFound() diff --git a/src/app/dashboard/(active)/web/projects/card-project.tsx b/src/app/dashboard/(active)/web/projects/card-project.tsx new file mode 100644 index 0000000..8090790 --- /dev/null +++ b/src/app/dashboard/(active)/web/projects/card-project.tsx @@ -0,0 +1,243 @@ +"use client" + +import { useSortable } from "@dnd-kit/react/sortable" +import { Languages, Link, LucidePencil, Save, Upload, X } from "lucide-react" +import Image from "next/image" +import type { ChangeEvent } from "react" +import { useEffect, useState } from "react" +import { ProjectCategoryMenu } from "@/app/dashboard/(active)/web/projects/category-menu" +import { DeleteDialog } from "@/components/delete-dialog" +import { Badge } from "@/components/ui/badge" +import { Button } from "@/components/ui/button" +import { Card, CardAction, CardContent, CardHeader, CardTitle } from "@/components/ui/card" +import { Input } from "@/components/ui/input" +import { Textarea } from "@/components/ui/textarea" +import { getInitials } from "@/lib/utils" +import { cn } from "@/lib/utils/shadcn" +import type { CardProjectProps } from "./types" + +export default function CardProject(item: CardProjectProps) { + const { ref } = useSortable({ + id: item.id, + index: item.sortableIndex ?? 0, + group: item.category, + disabled: item.sortableIndex === undefined, + }) + const iconInputId = `project-icon-${item.id}` + const [editActive, setEditActive] = useState(item.initialEditActive ?? false) + const [title, setTitle] = useState(item.title) + const [logoFile, setLogoFile] = useState(null) + const [logoPreview, setLogoPreview] = useState(null) + const [descriptionIt, setDescriptionIt] = useState(item.descriptionIt) + const [descriptionEn, setDescriptionEn] = useState(item.descriptionEn) + const [link, setLinks] = useState(item.link) + const [pending, setPending] = useState(false) + const initials = getInitials(title) + + async function handleIconUpload(event: ChangeEvent) { + const file = event.target.files?.[0] + if (!file) return + setLogoFile(file) + setLogoPreview(URL.createObjectURL(file)) + } + + useEffect(() => { + return () => { + if (logoPreview) URL.revokeObjectURL(logoPreview) + } + }, [logoPreview]) + + // If it's draft, remove the card, otherwise reset the values to the original ones + function handleCancelEdit() { + if (item.isDraft) { + item.onCancelCreate?.() + return + } + + setTitle(item.title) + setLogoFile(null) + setLogoPreview(null) + setDescriptionIt(item.descriptionIt) + setDescriptionEn(item.descriptionEn) + setLinks(item.link) + setEditActive(false) + } + + // TODO: forse spostare la cosa salvata per ultima nella lista? Perche poi ordinata per id finisce li + // se gli id sono crescenti. O tipo la creo direttamente ultima e non in cima? Pero poi devi scorrere per editarla + async function saveChanges() { + if (pending) return + + setPending(true) + try { + const saved = await item.onSave({ + id: item.id, + title, + logo: item.logo, + logoFile, + descriptionIt, + descriptionEn, + link, + category: item.category, + }) + if (saved) { + setLogoFile(null) + setLogoPreview(null) + setEditActive(false) + } + } finally { + setPending(false) + } + } + + function renderIcon() { + const logoSource = logoPreview ?? item.logo + if (logoSource) { + return + } + return ( + + ) + } + + return ( + + + + {editActive ? ( + <> + +
+ setTitle(event.target.value)} + className="text-lg font-medium" + /> +
+ + ) : ( + <> + {renderIcon()} + {title} + + )} +
+ + {editActive ? ( + <> + + + + ) : ( + <> + + + + + + )} + +
+ + +
+ + + Link + + setLinks(event.target.value)} + className={cn( + "border-0 bg-transparent p-3 shadow-none", + !editActive && + "cursor-default resize-none focus-visible:border-transparent focus-visible:ring-0 focus-visible:ring-transparent" + )} + /> +
+
+ + + IT + +