feat: update Next.js configuration and package dependencies
- Adjusted `reactStrictMode` in `next.config.ts` to reduce strict mode double mounts during development, addressing hydration issues. - Modified `dev` script in `package.json` to suppress hydration warnings, and added a `dev:strict` script for strict mode development. - Added new dependencies: `@polar-sh/better-auth` and `@polar-sh/sdk` for enhanced authentication and SDK functionalities. - Updated `pnpm-lock.yaml` to reflect new package versions and dependencies.
This commit is contained in:
@@ -6,7 +6,6 @@ import { useState } from "react";
|
|||||||
import { useTheme } from "next-themes";
|
import { useTheme } from "next-themes";
|
||||||
import { useMutation, useQuery } from "convex/react";
|
import { useMutation, useQuery } from "convex/react";
|
||||||
import {
|
import {
|
||||||
ArrowUpRight,
|
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
Coins,
|
Coins,
|
||||||
LayoutTemplate,
|
LayoutTemplate,
|
||||||
@@ -31,9 +30,9 @@ import {
|
|||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { api } from "@/convex/_generated/api";
|
import { api } from "@/convex/_generated/api";
|
||||||
import { authClient } from "@/lib/auth-client";
|
import { authClient } from "@/lib/auth-client";
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
import { CreditOverview } from "@/components/dashboard/credit-overview";
|
import { CreditOverview } from "@/components/dashboard/credit-overview";
|
||||||
import { RecentTransactions } from "@/components/dashboard/recent-transactions";
|
import { RecentTransactions } from "@/components/dashboard/recent-transactions";
|
||||||
|
import CanvasCard from "@/components/dashboard/canvas-card";
|
||||||
|
|
||||||
|
|
||||||
function getInitials(nameOrEmail: string) {
|
function getInitials(nameOrEmail: string) {
|
||||||
@@ -212,24 +211,11 @@ export default function DashboardPage() {
|
|||||||
) : (
|
) : (
|
||||||
<div className="grid gap-3 sm:grid-cols-3">
|
<div className="grid gap-3 sm:grid-cols-3">
|
||||||
{canvases.map((canvas) => (
|
{canvases.map((canvas) => (
|
||||||
<button
|
<CanvasCard
|
||||||
key={canvas._id}
|
key={canvas._id}
|
||||||
type="button"
|
canvas={canvas}
|
||||||
onClick={() => router.push(`/canvas/${canvas._id}`)}
|
onNavigate={(id) => router.push(`/canvas/${id}`)}
|
||||||
className={cn(
|
/>
|
||||||
"group flex cursor-pointer items-center gap-4 rounded-xl border bg-card p-4 text-left shadow-sm shadow-foreground/3 transition-all",
|
|
||||||
"hover:bg-muted/60 hover:shadow-md hover:shadow-foreground/4",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div className="flex size-10 shrink-0 items-center justify-center rounded-lg bg-primary/8 text-sm font-semibold text-primary">
|
|
||||||
{canvas.name.slice(0, 1).toUpperCase()}
|
|
||||||
</div>
|
|
||||||
<div className="min-w-0 flex-1">
|
|
||||||
<p className="truncate text-sm font-medium">{canvas.name}</p>
|
|
||||||
<p className="mt-0.5 text-xs text-muted-foreground">Canvas</p>
|
|
||||||
</div>
|
|
||||||
<ArrowUpRight className="size-4 text-muted-foreground/0 transition-colors group-hover:text-muted-foreground" />
|
|
||||||
</button>
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
149
components/dashboard/canvas-card.tsx
Normal file
149
components/dashboard/canvas-card.tsx
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState, useCallback, useRef } from "react";
|
||||||
|
import { useMutation } from "convex/react";
|
||||||
|
import { ArrowUpRight, MoreHorizontal, Pencil } from "lucide-react";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from "@/components/ui/dropdown-menu";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { api } from "@/convex/_generated/api";
|
||||||
|
import type { Id } from "@/convex/_generated/dataModel";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
interface CanvasCardProps {
|
||||||
|
canvas: { _id: Id<"canvases">; name: string };
|
||||||
|
onNavigate: (id: Id<"canvases">) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function CanvasCard({ canvas, onNavigate }: CanvasCardProps) {
|
||||||
|
const [isEditing, setIsEditing] = useState(false);
|
||||||
|
const [editName, setEditName] = useState(canvas.name);
|
||||||
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
|
const updateCanvas = useMutation(api.canvases.update);
|
||||||
|
|
||||||
|
const handleStartEdit = useCallback(() => {
|
||||||
|
setEditName(canvas.name);
|
||||||
|
setIsEditing(true);
|
||||||
|
setTimeout(() => inputRef.current?.select(), 0);
|
||||||
|
}, [canvas.name]);
|
||||||
|
|
||||||
|
const handleSave = useCallback(async () => {
|
||||||
|
const trimmedName = editName.trim();
|
||||||
|
if (!trimmedName) {
|
||||||
|
toast.error("Name darf nicht leer sein");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (trimmedName === canvas.name) {
|
||||||
|
setIsEditing(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsSaving(true);
|
||||||
|
try {
|
||||||
|
await updateCanvas({ canvasId: canvas._id, name: trimmedName });
|
||||||
|
toast.success("Arbeitsbereich umbenannt");
|
||||||
|
setIsEditing(false);
|
||||||
|
} catch {
|
||||||
|
toast.error("Fehler beim Umbenennen");
|
||||||
|
} finally {
|
||||||
|
setIsSaving(false);
|
||||||
|
}
|
||||||
|
}, [editName, canvas.name, canvas._id, updateCanvas]);
|
||||||
|
|
||||||
|
const handleKeyDown = useCallback(
|
||||||
|
(e: React.KeyboardEvent) => {
|
||||||
|
if (e.key === "Enter") {
|
||||||
|
e.preventDefault();
|
||||||
|
handleSave();
|
||||||
|
} else if (e.key === "Escape") {
|
||||||
|
e.preventDefault();
|
||||||
|
setIsEditing(false);
|
||||||
|
setEditName(canvas.name);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[handleSave, canvas.name]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Prevent duplicate toast: only save on blur if still in editing mode
|
||||||
|
const handleBlur = useCallback(() => {
|
||||||
|
if (!isEditing) return;
|
||||||
|
handleSave();
|
||||||
|
}, [isEditing, handleSave]);
|
||||||
|
|
||||||
|
const handleCardClick = useCallback(() => {
|
||||||
|
if (!isEditing) {
|
||||||
|
onNavigate(canvas._id);
|
||||||
|
}
|
||||||
|
}, [isEditing, onNavigate, canvas._id]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"group relative flex cursor-pointer items-center gap-4 rounded-xl border bg-card p-4 text-left shadow-sm shadow-foreground/3 transition-all",
|
||||||
|
"hover:bg-muted/60 hover:shadow-md hover:shadow-foreground/4",
|
||||||
|
isEditing && "ring-2 ring-primary/50"
|
||||||
|
)}
|
||||||
|
onClick={handleCardClick}
|
||||||
|
>
|
||||||
|
{/* Avatar */}
|
||||||
|
<div className="flex size-10 shrink-0 items-center justify-center rounded-lg bg-primary/8 text-sm font-semibold text-primary">
|
||||||
|
{canvas.name.slice(0, 1).toUpperCase()}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
{isEditing ? (
|
||||||
|
<Input
|
||||||
|
ref={inputRef}
|
||||||
|
value={editName}
|
||||||
|
onChange={(e) => setEditName(e.target.value)}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
onBlur={handleBlur}
|
||||||
|
disabled={isSaving}
|
||||||
|
autoFocus
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
className="h-auto py-0.5 text-sm font-medium bg-transparent border px-1.5 focus-visible:ring-1"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<p className="truncate text-sm font-medium">{canvas.name}</p>
|
||||||
|
)}
|
||||||
|
<p className="mt-0.5 text-xs text-muted-foreground">Canvas</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Actions - positioned to not overlap with content */}
|
||||||
|
{!isEditing && (
|
||||||
|
<div className="flex shrink-0 items-center gap-2 ml-2">
|
||||||
|
<ArrowUpRight className="size-4 text-muted-foreground/0 transition-colors group-hover:text-muted-foreground" />
|
||||||
|
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="size-7 shrink-0 opacity-0 group-hover:opacity-100 transition-opacity"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<MoreHorizontal className="size-4" />
|
||||||
|
<span className="sr-only">Optionen</span>
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end">
|
||||||
|
<DropdownMenuItem onSelect={handleStartEdit}>
|
||||||
|
<Pencil className="size-4" />
|
||||||
|
Umbenennen
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,6 +1,11 @@
|
|||||||
import type { NextConfig } from "next";
|
import type { NextConfig } from "next";
|
||||||
|
|
||||||
const nextConfig: NextConfig = {
|
const nextConfig: NextConfig = {
|
||||||
|
// Reduziert in der Entwicklung Strict-Mode-Doppel-Mounts (häufige Ursache für
|
||||||
|
// „Hydration“-Lärm). Echte Server/Client-Mismatches können weiterhin auftreten;
|
||||||
|
// dann `pnpm dev:strict` zum Debuggen oder Ursache beheben.
|
||||||
|
reactStrictMode:
|
||||||
|
process.env.NEXT_DEV_SUPPRESS_HYDRATION === "1" ? false : null,
|
||||||
images: {
|
images: {
|
||||||
remotePatterns: [
|
remotePatterns: [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -3,7 +3,8 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev",
|
"dev": "NEXT_DEV_SUPPRESS_HYDRATION=1 next dev",
|
||||||
|
"dev:strict": "next dev",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "eslint"
|
"lint": "eslint"
|
||||||
@@ -14,6 +15,8 @@
|
|||||||
"@dnd-kit/core": "^6.3.1",
|
"@dnd-kit/core": "^6.3.1",
|
||||||
"@dnd-kit/utilities": "^3.2.2",
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
"@napi-rs/canvas": "^0.1.97",
|
"@napi-rs/canvas": "^0.1.97",
|
||||||
|
"@polar-sh/better-auth": "^1.8.3",
|
||||||
|
"@polar-sh/sdk": "^0.46.7",
|
||||||
"@xyflow/react": "^12.10.1",
|
"@xyflow/react": "^12.10.1",
|
||||||
"better-auth": "^1.5.6",
|
"better-auth": "^1.5.6",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
|
|||||||
555
pnpm-lock.yaml
generated
555
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user