From 6e0233682706603312f99a09eb94c18b6170975f Mon Sep 17 00:00:00 2001 From: Kenan KARAER Date: Thu, 8 Jan 2026 21:13:12 +0300 Subject: [PATCH 1/6] Fix: Restore missing UI components and optimize config --- components/ui/avatar.tsx | 53 +++++ components/ui/dropdown-menu.tsx | 257 +++++++++++++++++++++++ next.config.mjs | 11 +- package-lock.json | 347 ++++++++++++++++++++++++++++++++ package.json | 2 + scripts/test-supabase.js | 50 +++++ 6 files changed, 719 insertions(+), 1 deletion(-) create mode 100644 components/ui/avatar.tsx create mode 100644 components/ui/dropdown-menu.tsx create mode 100644 scripts/test-supabase.js diff --git a/components/ui/avatar.tsx b/components/ui/avatar.tsx new file mode 100644 index 0000000..71e428b --- /dev/null +++ b/components/ui/avatar.tsx @@ -0,0 +1,53 @@ +"use client" + +import * as React from "react" +import * as AvatarPrimitive from "@radix-ui/react-avatar" + +import { cn } from "@/lib/utils" + +function Avatar({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AvatarImage({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AvatarFallback({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { Avatar, AvatarImage, AvatarFallback } diff --git a/components/ui/dropdown-menu.tsx b/components/ui/dropdown-menu.tsx new file mode 100644 index 0000000..bbe6fb0 --- /dev/null +++ b/components/ui/dropdown-menu.tsx @@ -0,0 +1,257 @@ +"use client" + +import * as React from "react" +import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu" +import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react" + +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/next.config.mjs b/next.config.mjs index 4678774..b71f58b 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -1,4 +1,13 @@ /** @type {import('next').NextConfig} */ -const nextConfig = {}; +const nextConfig = { + reactStrictMode: true, + webpack: (config) => { + // Suppress cache serialization warnings + config.infrastructureLogging = { + level: 'error', + }; + return config; + }, +}; export default nextConfig; diff --git a/package-lock.json b/package-lock.json index 5eeac68..06ec794 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,9 @@ "version": "0.1.0", "dependencies": { "@hookform/resolvers": "^5.2.2", + "@radix-ui/react-avatar": "^1.1.11", "@radix-ui/react-dialog": "^1.1.15", + "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-label": "^2.1.8", "@radix-ui/react-navigation-menu": "^1.2.14", "@radix-ui/react-separator": "^1.1.8", @@ -152,6 +154,44 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@floating-ui/core": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz", + "integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz", + "integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.3", + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.6.tgz", + "integrity": "sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.7.4" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", + "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", + "license": "MIT" + }, "node_modules/@hookform/resolvers": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-5.2.2.tgz", @@ -526,6 +566,94 @@ "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", "license": "MIT" }, + "node_modules/@radix-ui/react-arrow": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", + "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-avatar": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.1.11.tgz", + "integrity": "sha512-0Qk603AHGV28BOBO34p7IgD5m+V5Sg/YovfayABkoDDBM5d3NCx0Mp4gGrjzLGes1jV5eNOE1r3itqOR33VC6Q==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-context": "1.1.3", + "@radix-ui/react-primitive": "2.1.4", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-is-hydrated": "0.1.0", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-avatar/node_modules/@radix-ui/react-context": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.3.tgz", + "integrity": "sha512-ieIFACdMpYfMEjF0rEf5KLvfVyIkOz6PDGyNnP+u+4xQ6jny3VCgA4OgXOwNx2aUkxn8zx9fiVcM8CfFYv9Lxw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-avatar/node_modules/@radix-ui/react-primitive": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.4.tgz", + "integrity": "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-collection": { "version": "1.1.7", "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", @@ -696,6 +824,35 @@ } } }, + "node_modules/@radix-ui/react-dropdown-menu": { + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.16.tgz", + "integrity": "sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-menu": "2.1.16", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-focus-guards": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz", @@ -800,6 +957,64 @@ } } }, + "node_modules/@radix-ui/react-menu": { + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.16.tgz", + "integrity": "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-navigation-menu": { "version": "1.2.14", "resolved": "https://registry.npmjs.org/@radix-ui/react-navigation-menu/-/react-navigation-menu-1.2.14.tgz", @@ -836,6 +1051,38 @@ } } }, + "node_modules/@radix-ui/react-popper": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz", + "integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-rect": "1.1.1", + "@radix-ui/react-use-size": "1.1.1", + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-portal": { "version": "1.1.9", "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", @@ -925,6 +1172,37 @@ } } }, + "node_modules/@radix-ui/react-roving-focus": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz", + "integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-separator": { "version": "1.1.8", "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.8.tgz", @@ -1059,6 +1337,24 @@ } } }, + "node_modules/@radix-ui/react-use-is-hydrated": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-is-hydrated/-/react-use-is-hydrated-0.1.0.tgz", + "integrity": "sha512-U+UORVEq+cTnRIaostJv9AGdV3G6Y+zbVd+12e18jQ5A3c0xL03IhnHuiU4UV69wolOQp5GfR58NW/EgdQhwOA==", + "license": "MIT", + "dependencies": { + "use-sync-external-store": "^1.5.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-use-layout-effect": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", @@ -1089,6 +1385,42 @@ } } }, + "node_modules/@radix-ui/react-use-rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz", + "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==", + "license": "MIT", + "dependencies": { + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-size": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", + "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-visually-hidden": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz", @@ -1112,6 +1444,12 @@ } } }, + "node_modules/@radix-ui/rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz", + "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", + "license": "MIT" + }, "node_modules/@rtsao/scc": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", @@ -6867,6 +7205,15 @@ } } }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", diff --git a/package.json b/package.json index 52f8d22..f69796d 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,9 @@ }, "dependencies": { "@hookform/resolvers": "^5.2.2", + "@radix-ui/react-avatar": "^1.1.11", "@radix-ui/react-dialog": "^1.1.15", + "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-label": "^2.1.8", "@radix-ui/react-navigation-menu": "^1.2.14", "@radix-ui/react-separator": "^1.1.8", diff --git a/scripts/test-supabase.js b/scripts/test-supabase.js new file mode 100644 index 0000000..4360e74 --- /dev/null +++ b/scripts/test-supabase.js @@ -0,0 +1,50 @@ +const fs = require('fs'); +const path = require('path'); +const { createClient } = require('@supabase/supabase-js'); + +const envPath = path.resolve(__dirname, '../.env.local'); + +console.log('Reading env from:', envPath); + +try { + const envContent = fs.readFileSync(envPath, 'utf8'); + const envVars = {}; + envContent.split('\n').forEach(line => { + const parts = line.split('='); + if (parts.length >= 2) { + const key = parts[0].trim(); + const value = parts.slice(1).join('=').trim().replace(/"/g, ''); + envVars[key] = value; + } + }); + + const supabaseUrl = envVars['NEXT_PUBLIC_SUPABASE_URL']; + const supabaseKey = envVars['NEXT_PUBLIC_SUPABASE_ANON_KEY']; + + if (!supabaseUrl || !supabaseKey) { + console.error('❌ Missing Supabase Environment Variables in .env.local'); + console.log('URL:', supabaseUrl ? 'Set' : 'Missing'); + console.log('Key:', supabaseKey ? 'Set' : 'Missing'); + process.exit(1); + } + + console.log('Checking connection to:', supabaseUrl); + const supabase = createClient(supabaseUrl, supabaseKey); + + (async () => { + try { + const { error } = await supabase.auth.getSession(); + if (error) { + console.error('❌ Connection Failed:', error.message); + process.exit(1); + } else { + console.log('✅ Connection Successful! Supabase is reachable and keys are valid.'); + } + } catch (err) { + console.error('❌ Unexpected Error:', err); + } + })(); + +} catch (err) { + console.error('❌ Could not read .env.local:', err.message); +} -- 2.49.1 From ddf28e1892e4c644be85789a650cafef56f7ddab Mon Sep 17 00:00:00 2001 From: Kenan KARAER Date: Thu, 8 Jan 2026 23:56:28 +0300 Subject: [PATCH 2/6] =?UTF-8?q?Site=20Y=C3=B6netimi,Kullan=C4=B1c=C4=B1=20?= =?UTF-8?q?Giri=C5=9Fi,Karanl=C4=B1k=20mod=20=C3=B6zellikleri=20db=20ba?= =?UTF-8?q?=C4=9Flant=C4=B1lar=C4=B1.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- PLANLAMA.md | 23 ++ .../dashboard/categories/[id]/page.tsx | 22 ++ .../dashboard/categories/actions.ts | 107 ++++++++++ .../dashboard/categories/new/page.tsx | 9 + app/(dashboard)/dashboard/categories/page.tsx | 78 +++++++ app/(dashboard)/dashboard/page.tsx | 180 +++++++++------- .../dashboard/products/[productId]/page.tsx | 33 +++ app/(dashboard)/dashboard/products/actions.ts | 51 +++++ .../dashboard/products/new/page.tsx | 14 ++ app/(dashboard)/dashboard/products/page.tsx | 75 +++++++ app/(dashboard)/dashboard/profile/page.tsx | 50 +++++ app/(dashboard)/dashboard/settings/actions.ts | 42 ++++ app/(dashboard)/dashboard/settings/page.tsx | 45 ++++ .../dashboard/users/[userId]/page.tsx | 69 ++++++ app/(dashboard)/dashboard/users/actions.ts | 133 ++++++++++++ app/(dashboard)/dashboard/users/new/page.tsx | 13 ++ app/(dashboard)/dashboard/users/page.tsx | 87 ++++++++ app/(public)/login/page.tsx | 5 +- app/layout.tsx | 31 ++- components/dashboard/appearance-form.tsx | 57 +++++ components/dashboard/category-form.tsx | 186 ++++++++++++++++ components/dashboard/product-form.tsx | 200 +++++++++++++++++ components/dashboard/sidebar.tsx | 12 +- components/dashboard/site-settings-form.tsx | 164 ++++++++++++++ components/dashboard/user-form.tsx | 201 ++++++++++++++++++ components/dashboard/user-nav.tsx | 22 +- components/modals/alert-modal.tsx | 57 +++++ components/theme-provider.tsx | 9 + components/ui/badge.tsx | 46 ++++ components/ui/dialog.tsx | 143 +++++++++++++ components/ui/select.tsx | 190 +++++++++++++++++ components/ui/switch.tsx | 31 +++ lib/site-settings.ts | 8 + lib/supabase-browser.ts | 8 + make_admin.sql | 8 + package-lock.json | 120 +++++++++++ package.json | 4 + supabase_schema_additions.sql | 58 +++++ supabase_schema_categories.sql | 44 ++++ supabase_schema_products_category_fk.sql | 6 + 40 files changed, 2545 insertions(+), 96 deletions(-) create mode 100644 PLANLAMA.md create mode 100644 app/(dashboard)/dashboard/categories/[id]/page.tsx create mode 100644 app/(dashboard)/dashboard/categories/actions.ts create mode 100644 app/(dashboard)/dashboard/categories/new/page.tsx create mode 100644 app/(dashboard)/dashboard/categories/page.tsx create mode 100644 app/(dashboard)/dashboard/products/[productId]/page.tsx create mode 100644 app/(dashboard)/dashboard/products/actions.ts create mode 100644 app/(dashboard)/dashboard/products/new/page.tsx create mode 100644 app/(dashboard)/dashboard/products/page.tsx create mode 100644 app/(dashboard)/dashboard/profile/page.tsx create mode 100644 app/(dashboard)/dashboard/settings/actions.ts create mode 100644 app/(dashboard)/dashboard/settings/page.tsx create mode 100644 app/(dashboard)/dashboard/users/[userId]/page.tsx create mode 100644 app/(dashboard)/dashboard/users/actions.ts create mode 100644 app/(dashboard)/dashboard/users/new/page.tsx create mode 100644 app/(dashboard)/dashboard/users/page.tsx create mode 100644 components/dashboard/appearance-form.tsx create mode 100644 components/dashboard/category-form.tsx create mode 100644 components/dashboard/product-form.tsx create mode 100644 components/dashboard/site-settings-form.tsx create mode 100644 components/dashboard/user-form.tsx create mode 100644 components/modals/alert-modal.tsx create mode 100644 components/theme-provider.tsx create mode 100644 components/ui/badge.tsx create mode 100644 components/ui/dialog.tsx create mode 100644 components/ui/select.tsx create mode 100644 components/ui/switch.tsx create mode 100644 lib/site-settings.ts create mode 100644 lib/supabase-browser.ts create mode 100644 make_admin.sql create mode 100644 supabase_schema_additions.sql create mode 100644 supabase_schema_categories.sql create mode 100644 supabase_schema_products_category_fk.sql diff --git a/PLANLAMA.md b/PLANLAMA.md new file mode 100644 index 0000000..3c80e55 --- /dev/null +++ b/PLANLAMA.md @@ -0,0 +1,23 @@ +# ParaKasa Geliştirme Planı (Revize Edildi) + +**Vizyon:** Proje, tek bir çatı altında iki farklı uygulama gibi çalışacaktır. + +## 1. Web Sitesi (Public) +- Herkese açık kurumsal web sitesi. +- Ürünler, kategoriler ve iletişim bilgileri sergilenecek. +- Veriler Yönetim Panelinden (CMS) çekilecek. + +## 2. Yönetim Paneli (Private) +- Sadece firma yetkililerinin giriş yapabileceği kapalı devre sistem. +- **Fonksiyonlar:** + - **CMS:** Web sitesinin içeriğini (ürünler, slider, metinler) yönetme. + - **ERP (İç Yönetim):** Stok takibi, gelir/gider hesapları, sipariş yönetimi. +- **Durum:** Bu bölümün tasarımı ve akışı tamamen baştan kurgulanacak. Şimdilik mevcut haliyle donduruldu. + +--- + +## Mevcut Durum (Tamamlananlar) +- [x] Kullanıcı Yönetimi (Admin Ekle/Sil). +- [x] Temel Site Ayarları (Başlık, İletişim). +- [x] Ürün Yönetimi (Temel CRUD). +- [x] Kategori Yönetimi (Arayüz hazır, veritabanı bekleniyor). diff --git a/app/(dashboard)/dashboard/categories/[id]/page.tsx b/app/(dashboard)/dashboard/categories/[id]/page.tsx new file mode 100644 index 0000000..99bd894 --- /dev/null +++ b/app/(dashboard)/dashboard/categories/[id]/page.tsx @@ -0,0 +1,22 @@ +import { createClient } from "@/lib/supabase-server" +import { CategoryForm } from "@/components/dashboard/category-form" +import { notFound } from "next/navigation" + +export default async function EditCategoryPage({ params }: { params: { id: string } }) { + const supabase = createClient() + const { data: category } = await supabase + .from('categories') + .select('*') + .eq('id', params.id) + .single() + + if (!category) { + notFound() + } + + return ( +
+ +
+ ) +} diff --git a/app/(dashboard)/dashboard/categories/actions.ts b/app/(dashboard)/dashboard/categories/actions.ts new file mode 100644 index 0000000..c4d9451 --- /dev/null +++ b/app/(dashboard)/dashboard/categories/actions.ts @@ -0,0 +1,107 @@ +"use server" + +import { createClient } from "@/lib/supabase-server" +import { createClient as createSupabaseClient } from "@supabase/supabase-js" +import { revalidatePath } from "next/cache" + +// Admin client for privileged operations +const supabaseAdmin = createSupabaseClient( + process.env.NEXT_PUBLIC_SUPABASE_URL!, + process.env.SUPABASE_SERVICE_ROLE_KEY!, + { + auth: { + autoRefreshToken: false, + persistSession: false + } + } +) + +export async function createCategory(data: { name: string, description?: string, image_url?: string }) { + const supabase = createClient() + + // Check admin + const { data: { user } } = await supabase.auth.getUser() + if (!user) return { error: "Oturum açmanız gerekiyor." } + + // Check if current user is admin + const { data: profile } = await supabase.from('profiles').select('role').eq('id', user.id).single() + if (profile?.role !== 'admin') return { error: "Yetkisiz işlem." } + + // Generate slug from name + const slug = data.name.toLowerCase() + .replace(/ğ/g, 'g') + .replace(/ü/g, 'u') + .replace(/ş/g, 's') + .replace(/ı/g, 'i') + .replace(/ö/g, 'o') + .replace(/ç/g, 'c') + .replace(/[^a-z0-9]/g, '-') + .replace(/-+/g, '-') + .replace(/^-|-$/g, '') + + const { error } = await supabaseAdmin.from('categories').insert({ + name: data.name, + slug: slug, + description: data.description, + image_url: data.image_url + }) + + if (error) return { error: "Kategori oluşturulamadı: " + error.message } + + revalidatePath("/dashboard/categories") + return { success: true } +} + +export async function updateCategory(id: string, data: { name: string, description?: string, image_url?: string }) { + const supabase = createClient() + + // Check admin + const { data: { user } } = await supabase.auth.getUser() + if (!user) return { error: "Oturum açmanız gerekiyor." } + + // Check if current user is admin + const { data: profile } = await supabase.from('profiles').select('role').eq('id', user.id).single() + if (profile?.role !== 'admin') return { error: "Yetkisiz işlem." } + + const slug = data.name.toLowerCase() + .replace(/ğ/g, 'g') + .replace(/ü/g, 'u') + .replace(/ş/g, 's') + .replace(/ı/g, 'i') + .replace(/ö/g, 'o') + .replace(/ç/g, 'c') + .replace(/[^a-z0-9]/g, '-') + .replace(/-+/g, '-') + .replace(/^-|-$/g, '') + + const { error } = await supabaseAdmin.from('categories').update({ + name: data.name, + slug: slug, + description: data.description, + image_url: data.image_url + }).eq('id', id) + + if (error) return { error: "Kategori güncellenemedi: " + error.message } + + revalidatePath("/dashboard/categories") + return { success: true } +} + +export async function deleteCategory(id: string) { + const supabase = createClient() + + // Check admin + const { data: { user } } = await supabase.auth.getUser() + if (!user) return { error: "Oturum açmanız gerekiyor." } + + // Check if current user is admin + const { data: profile } = await supabase.from('profiles').select('role').eq('id', user.id).single() + if (profile?.role !== 'admin') return { error: "Yetkisiz işlem." } + + const { error } = await supabaseAdmin.from('categories').delete().eq('id', id) + + if (error) return { error: "Kategori silinemedi: " + error.message } + + revalidatePath("/dashboard/categories") + return { success: true } +} diff --git a/app/(dashboard)/dashboard/categories/new/page.tsx b/app/(dashboard)/dashboard/categories/new/page.tsx new file mode 100644 index 0000000..b484e34 --- /dev/null +++ b/app/(dashboard)/dashboard/categories/new/page.tsx @@ -0,0 +1,9 @@ +import { CategoryForm } from "@/components/dashboard/category-form" + +export default function NewCategoryPage() { + return ( +
+ +
+ ) +} diff --git a/app/(dashboard)/dashboard/categories/page.tsx b/app/(dashboard)/dashboard/categories/page.tsx new file mode 100644 index 0000000..3e7bfed --- /dev/null +++ b/app/(dashboard)/dashboard/categories/page.tsx @@ -0,0 +1,78 @@ +import { createClient } from "@/lib/supabase-server" +import { format } from "date-fns" +import { tr } from "date-fns/locale" +import Link from "next/link" +import { Plus } from "lucide-react" +import { Button } from "@/components/ui/button" +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table" + +export default async function CategoriesPage() { + const supabase = createClient() + const { data: categories } = await supabase + .from('categories') + .select('*') + .order('created_at', { ascending: false }) + + return ( +
+
+
+

Kategoriler ({categories?.length || 0})

+

+ Sitenizdeki ürün kategorilerini yönetin. +

+
+ + + +
+ +
+ + + + Ad + Slug + Oluşturulma Tarihi + İşlemler + + + + {categories?.map((category) => ( + + {category.name} + {category.slug} + + {format(new Date(category.created_at), "d MMMM yyyy", { locale: tr })} + + + + + + + + ))} + {(!categories || categories.length === 0) && ( + + + Kategori bulunamadı. + + + )} + +
+
+
+ ) +} diff --git a/app/(dashboard)/dashboard/page.tsx b/app/(dashboard)/dashboard/page.tsx index 83e7937..e4deb2a 100644 --- a/app/(dashboard)/dashboard/page.tsx +++ b/app/(dashboard)/dashboard/page.tsx @@ -1,126 +1,126 @@ - +import { createClient } from "@/lib/supabase-server" import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" -import { DollarSign, ShoppingCart, Users, CreditCard } from "lucide-react" +import { DollarSign, ShoppingCart, Users, CreditCard, Package } from "lucide-react" +import Link from "next/link" +import { Button } from "@/components/ui/button" + +export default async function DashboardPage() { + const supabase = createClient() + + // Fetch real data + const { data: products } = await supabase + .from("products") + .select("*") + .order("created_at", { ascending: false }) + + const totalProducts = products?.length || 0 + const totalValue = products?.reduce((acc, product) => acc + (Number(product.price) || 0), 0) || 0 + const recentProducts = products?.slice(0, 5) || [] + + // Calculate unique categories + const categories = new Set(products?.map(p => p.category)).size -export default function DashboardPage() { return ( -
+

Genel Bakış

+
+ + + +
- {/* Stats Grid */}
- Toplam Gelir + Toplam Ürün Değeri -
₺45,231.89
-

+20.1% geçen aya göre

+
₺{totalValue.toLocaleString('tr-TR', { minimumFractionDigits: 2 })}
+

Stoktaki toplam varlık

- Abonelikler + Toplam Ürün + + + +
{totalProducts}
+

Kayıtlı ürün sayısı

+
+
+ + + Kategoriler + + + +
{categories}
+

Aktif kategori

+
+
+ + + Son Güncelleme -
+2350
-

+180.1% geçen aya göre

-
-
- - - Satışlar - - - -
+12,234
-

+19% geçen aya göre

-
-
- - - Aktif Şimdi - - - -
+573
-

+201 son bir saatte

+
Şimdi
+

Canlı veri akışı

- {/* Recent Sales / Activity */} - Son Hareketler + Son Eklenen Ürünler - Bu ay 265+ satış yaptınız. + En son eklenen {recentProducts.length} ürün. - {/* Mock List */}
-
-
- OM + {recentProducts.map((product) => ( +
+
+ {product.name.substring(0, 2).toUpperCase()} +
+
+

{product.name}

+

{product.category}

+
+
₺{Number(product.price).toLocaleString('tr-TR')}
-
-

Ozan Mehmet

-

ozan@email.com

-
-
+₺1,999.00
-
-
-
- -
-
-

Ayşe Özdemir

-

ayse@email.com

-
-
+₺39.00
-
-
-
- MK -
-
-

Mehmet Kaya

-

mehmet@email.com

-
-
+₺299.00
-
+ ))} + {recentProducts.length === 0 && ( +
Henüz ürün yok.
+ )}
- {/* Recent Products or Other Info */} + {/* Placeholder for future features or quick actions */} - Son Eklenen Ürünler + Hızlı İşlemler - Stoğa yeni giren ürünler. + Yönetim paneli kısayolları. -
-
- Çelik Kasa EV-100 - Stokta -
-
- Ofis Tipi XYZ - Azaldı -
-
- Otel Kasası H-20 - Stokta -
+
+ + + +
@@ -128,3 +128,23 @@ export default function DashboardPage() {
) } + +function PlusIcon(props: any) { + return ( + + + + + ) +} diff --git a/app/(dashboard)/dashboard/products/[productId]/page.tsx b/app/(dashboard)/dashboard/products/[productId]/page.tsx new file mode 100644 index 0000000..c92c6bb --- /dev/null +++ b/app/(dashboard)/dashboard/products/[productId]/page.tsx @@ -0,0 +1,33 @@ +import { createClient } from "@/lib/supabase-server" +import { ProductForm } from "@/components/dashboard/product-form" +import { notFound } from "next/navigation" + +interface ProductEditPageProps { + params: { + productId: string + } +} + +export default async function ProductEditPage({ params }: ProductEditPageProps) { + const supabase = createClient() + const { data: product } = await supabase + .from("products") + .select("*") + .eq("id", params.productId) + .single() + + if (!product) { + notFound() + } + + return ( +
+
+

Ürün Düzenle

+
+
+ +
+
+ ) +} diff --git a/app/(dashboard)/dashboard/products/actions.ts b/app/(dashboard)/dashboard/products/actions.ts new file mode 100644 index 0000000..71acb65 --- /dev/null +++ b/app/(dashboard)/dashboard/products/actions.ts @@ -0,0 +1,51 @@ +"use server" + +import { createClient } from "@/lib/supabase-server" +import { revalidatePath } from "next/cache" + +export async function createProduct(data: any) { + const supabase = createClient() + + // Validate data manually or use Zod schema here again securely + // For simplicity, we assume data is coming from the strongly typed Client Form + // In production, ALWAYS validate server-side strictly. + + try { + const { error } = await supabase.from("products").insert({ + name: data.name, + category: data.category, + description: data.description, + price: data.price, + image_url: data.image_url, + }) + + if (error) throw error + + revalidatePath("/dashboard/products") + return { success: true } + } catch (error: any) { + return { success: false, error: error.message } + } +} + +export async function updateProduct(id: number, data: any) { + const supabase = createClient() + + try { + const { error } = await supabase.from("products").update({ + name: data.name, + category: data.category, + description: data.description, + price: data.price, + image_url: data.image_url, + }).eq("id", id) + + if (error) throw error + + revalidatePath("/dashboard/products") + revalidatePath(`/dashboard/products/${id}`) + return { success: true } + } catch (error: any) { + return { success: false, error: error.message } + } +} diff --git a/app/(dashboard)/dashboard/products/new/page.tsx b/app/(dashboard)/dashboard/products/new/page.tsx new file mode 100644 index 0000000..b2d4312 --- /dev/null +++ b/app/(dashboard)/dashboard/products/new/page.tsx @@ -0,0 +1,14 @@ +import { ProductForm } from "@/components/dashboard/product-form" + +export default function NewProductPage() { + return ( +
+
+

Yeni Ürün Ekle

+
+
+ +
+
+ ) +} diff --git a/app/(dashboard)/dashboard/products/page.tsx b/app/(dashboard)/dashboard/products/page.tsx new file mode 100644 index 0000000..c786446 --- /dev/null +++ b/app/(dashboard)/dashboard/products/page.tsx @@ -0,0 +1,75 @@ +import { createClient } from "@/lib/supabase-server" +import { Button } from "@/components/ui/button" +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table" +import { Plus } from "lucide-react" +import Link from "next/link" +import { Badge } from "@/components/ui/badge" + +export default async function ProductsPage() { + const supabase = createClient() + const { data: products } = await supabase + .from("products") + .select("*") + .order("created_at", { ascending: false }) + + return ( +
+
+

Ürünler

+
+ + + +
+
+ +
+ + + + Ad + Kategori + Fiyat + İşlemler + + + + {products?.length === 0 ? ( + + + Henüz ürün eklenmemiş. + + + ) : ( + products?.map((product) => ( + + {product.name} + + + {product.category} + + + ₺{product.price} + + + + + + + )) + )} + +
+
+
+ ) +} diff --git a/app/(dashboard)/dashboard/profile/page.tsx b/app/(dashboard)/dashboard/profile/page.tsx new file mode 100644 index 0000000..57526dd --- /dev/null +++ b/app/(dashboard)/dashboard/profile/page.tsx @@ -0,0 +1,50 @@ +import { createClient } from "@/lib/supabase-server" +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar" +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import { Label } from "@/components/ui/label" + +export default async function ProfilePage() { + const supabase = createClient() + const { data: { user } } = await supabase.auth.getUser() + + return ( +
+
+

Profil

+
+ +
+ + + Genel Bilgiler + + Kişisel profil bilgileriniz. + + + +
+ + + PK + + +
+ +
+ + +

E-posta adresi değiştirilemez.

+
+ +
+ + +
+
+
+
+
+ ) +} diff --git a/app/(dashboard)/dashboard/settings/actions.ts b/app/(dashboard)/dashboard/settings/actions.ts new file mode 100644 index 0000000..f26c8da --- /dev/null +++ b/app/(dashboard)/dashboard/settings/actions.ts @@ -0,0 +1,42 @@ +"use server" + +import { createClient } from "@/lib/supabase-server" +import { revalidatePath } from "next/cache" + +export async function updateSiteSettings(data: { + site_title: string + site_description: string + contact_email: string + contact_phone: string + currency: string +}) { + const supabase = createClient() + + // Check admin is already handled by RLS on database level, but we can double check here + const { data: { user } } = await supabase.auth.getUser() + if (!user) return { error: "Oturum açmanız gerekiyor." } + + // We update the single row where id is likely 1 or just the first row + // Since we initialized it with one row, we can just update match on something true or fetch id first. + // Easier: Update all rows (there should only be one) or fetch the specific ID first. + + // Let's first get the ID just to be precise + const { data: existing } = await supabase.from('site_settings').select('id').single() + + if (!existing) { + return { error: "Ayarlar bulunamadı." } + } + + const { error } = await supabase + .from('site_settings') + .update(data) + .eq('id', existing.id) + + if (error) { + return { error: "Ayarlar güncellenemedi: " + error.message } + } + + revalidatePath("/dashboard/settings") + revalidatePath("/") // Revalidate home as it might use these settings + return { success: true } +} diff --git a/app/(dashboard)/dashboard/settings/page.tsx b/app/(dashboard)/dashboard/settings/page.tsx new file mode 100644 index 0000000..5cced11 --- /dev/null +++ b/app/(dashboard)/dashboard/settings/page.tsx @@ -0,0 +1,45 @@ +import { createClient } from "@/lib/supabase-server" +import { SiteSettingsForm } from "@/components/dashboard/site-settings-form" +import { AppearanceForm } from "@/components/dashboard/appearance-form" +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" +import { Label } from "@/components/ui/label" +import { Switch } from "@/components/ui/switch" +import { Button } from "@/components/ui/button" + +export default async function SettingsPage() { + const supabase = createClient() + + // Fetch site settings + const { data: settings } = await supabase + .from('site_settings') + .select('*') + .single() + + return ( +
+

Ayarlar

+ + {/* Site General Settings */} +
+ +
+ +
+ + + + + Hesap Güvenliği + + Şifre ve oturum yönetimi. + + + + + + + +
+
+ ) +} diff --git a/app/(dashboard)/dashboard/users/[userId]/page.tsx b/app/(dashboard)/dashboard/users/[userId]/page.tsx new file mode 100644 index 0000000..545a572 --- /dev/null +++ b/app/(dashboard)/dashboard/users/[userId]/page.tsx @@ -0,0 +1,69 @@ +import { createClient } from "@/lib/supabase-server" +import { UserForm } from "@/components/dashboard/user-form" +import { notFound } from "next/navigation" + +export default async function EditUserPage({ params }: { params: { userId: string } }) { + const supabase = createClient() + + // Fetch profile + const { data: profile } = await supabase + .from('profiles') + .select('*') + .eq('id', params.userId) + .single() + + if (!profile) { + notFound() + } + + // We also need the email, which is in auth.users, but we can't select from there easily with RLS/Client if not admin API + // However, our logged in user IS admin, but RLS on auth.users is usually strict. + // Let's see if we can get it via RPC or if the profile should store email (bad practice duplication, but helpful). + // Actually, `supabaseAdmin` in a server action can get it, but here we are in a Page (Server Component). + // We can use `supabaseAdmin` here too if we create a utility for it or just import createClient from supabase-js with admin key. + + // WORKAROUND: For now, let's assume we might need a server function to fetch full user details including email + // OR we just update the profile part. But the user wants to update email probably. + // Let's write a small server action/function to fetch this data securely to render the form. + + // Better: Helper function to get user details + const userDetails = await getUserDetails(params.userId) + + return ( +
+
+

Kullanıcı Düzenle

+
+ +
+ ) +} + +// Helper to get admin-level data for the form +import { createClient as createSupabaseClient } from "@supabase/supabase-js" + +async function getUserDetails(userId: string) { + const supabaseAdmin = createSupabaseClient( + process.env.NEXT_PUBLIC_SUPABASE_URL!, + process.env.SUPABASE_SERVICE_ROLE_KEY!, + { auth: { autoRefreshToken: false, persistSession: false } } + ) + + const { data: { user }, error } = await supabaseAdmin.auth.admin.getUserById(userId) + const { data: profile } = await supabaseAdmin.from('profiles').select('*').eq('id', userId).single() + + if (error || !user || !profile) return undefined + + // Split full name + const parts = (profile.full_name || "").split(' ') + const firstName = parts[0] || "" + const lastName = parts.slice(1).join(' ') || "" + + return { + id: userId, + firstName, + lastName, + email: user.email || "", + role: profile.role as "admin" | "user" + } +} diff --git a/app/(dashboard)/dashboard/users/actions.ts b/app/(dashboard)/dashboard/users/actions.ts new file mode 100644 index 0000000..1d49868 --- /dev/null +++ b/app/(dashboard)/dashboard/users/actions.ts @@ -0,0 +1,133 @@ +"use server" + +import { createClient } from "@/lib/supabase-server" +import { createClient as createSupabaseClient } from "@supabase/supabase-js" +import { revalidatePath } from "next/cache" +import { redirect } from "next/navigation" + +// WARNING: specialized client for admin actions only +// This requires SUPABASE_SERVICE_ROLE_KEY to be set in .env.local +const supabaseAdmin = createSupabaseClient( + process.env.NEXT_PUBLIC_SUPABASE_URL!, + process.env.SUPABASE_SERVICE_ROLE_KEY!, + { + auth: { + autoRefreshToken: false, + persistSession: false + } + } +) + +export async function createUser(firstName: string, lastName: string, email: string, password: string, role: 'admin' | 'user') { + const supabase = createClient() + + // 1. Check if current user is admin + const { data: { user: currentUser } } = await supabase.auth.getUser() + if (!currentUser) return { error: "Oturum açmanız gerekiyor." } + + const { data: profile } = await supabase + .from('profiles') + .select('role') + .eq('id', currentUser.id) + .single() + + if (!profile || profile.role !== 'admin') { + return { error: "Yetkisiz işlem. Sadece yöneticiler kullanıcı oluşturabilir." } + } + + // 2. Create user using Admin client + const { data: newUser, error: createError } = await supabaseAdmin.auth.admin.createUser({ + email, + password, + email_confirm: true, // Auto confirm + user_metadata: { + full_name: `${firstName} ${lastName}`.trim() + } + }) + + if (createError) { + return { error: createError.message } + } + + if (!newUser.user) { + return { error: "Kullanıcı oluşturulamadı." } + } + + // 3. Create profile entry (if not handled by trigger, but we'll do it manually to be safe/explicit about role) + const { error: profileError } = await supabaseAdmin + .from('profiles') + .insert({ + id: newUser.user.id, + full_name: `${firstName} ${lastName}`.trim(), + role: role + }) + + if (profileError) { + // Optional: delete auth user if profile creation fails? + // For now just return error + return { error: "Kullanıcı oluşturuldu ancak profil kaydedilemedi: " + profileError.message } + } + + revalidatePath("/dashboard/users") + return { success: true } +} + +export async function deleteUser(userId: string) { + const supabase = createClient() + + // Check admin + const { data: { user: currentUser } } = await supabase.auth.getUser() + if (!currentUser) return { error: "Oturum açmanız gerekiyor." } + + const { data: profile } = await supabase.from('profiles').select('role').eq('id', currentUser.id).single() + if (profile?.role !== 'admin') return { error: "Yetkisiz işlem." } + + // Delete user + const { error } = await supabaseAdmin.auth.admin.deleteUser(userId) + + if (error) return { error: error.message } + + revalidatePath("/dashboard/users") + return { success: true } +} + +export async function updateUser(userId: string, data: { firstName: string, lastName: string, email: string, password?: string, role: 'admin' | 'user' }) { + const supabase = createClient() + + // Check admin + const { data: { user: currentUser } } = await supabase.auth.getUser() + if (!currentUser) return { error: "Oturum açmanız gerekiyor." } + + // Check if current user is admin + const { data: profile } = await supabase.from('profiles').select('role').eq('id', currentUser.id).single() + if (profile?.role !== 'admin') return { error: "Yetkisiz işlem." } + + // 1. Update Profile (Role and Name) + const { error: profileError } = await supabaseAdmin + .from('profiles') + .update({ + full_name: `${data.firstName} ${data.lastName}`.trim(), + role: data.role + }) + .eq('id', userId) + + if (profileError) return { error: "Profil güncellenemedi: " + profileError.message } + + // 2. Update Auth (Email and Password) + const authUpdates: any = { + email: data.email, + user_metadata: { + full_name: `${data.firstName} ${data.lastName}`.trim() + } + } + if (data.password && data.password.length >= 6) { + authUpdates.password = data.password + } + + const { error: authError } = await supabaseAdmin.auth.admin.updateUserById(userId, authUpdates) + + if (authError) return { error: "Kullanıcı giriş bilgileri güncellenemedi: " + authError.message } + + revalidatePath("/dashboard/users") + return { success: true } +} diff --git a/app/(dashboard)/dashboard/users/new/page.tsx b/app/(dashboard)/dashboard/users/new/page.tsx new file mode 100644 index 0000000..3650b55 --- /dev/null +++ b/app/(dashboard)/dashboard/users/new/page.tsx @@ -0,0 +1,13 @@ + +import { UserForm } from "@/components/dashboard/user-form" + +export default function NewUserPage() { + return ( +
+
+

Yeni Kullanıcı Ekle

+
+ +
+ ) +} diff --git a/app/(dashboard)/dashboard/users/page.tsx b/app/(dashboard)/dashboard/users/page.tsx new file mode 100644 index 0000000..0290841 --- /dev/null +++ b/app/(dashboard)/dashboard/users/page.tsx @@ -0,0 +1,87 @@ +import { createClient } from "@/lib/supabase-server" +import { Button } from "@/components/ui/button" +import Link from "next/link" +import { Plus } from "lucide-react" +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table" +import { Badge } from "@/components/ui/badge" + +export default async function UsersPage() { + const supabase = createClient() + const { data: { user } } = await supabase.auth.getUser() + + // Protected Route Check (Simple) + const { data: currentUserProfile } = await supabase + .from('profiles') + .select('role') + .eq('id', user?.id) + .single() + + // Only verify if we have profiles, if not (first run), maybe allow? + // But for safety, blocking non-admins. + if (currentUserProfile?.role !== 'admin') { + return ( +
+

Yetkisiz Erişim

+

Bu sayfayı görüntüleme yetkiniz yok.

+
+ ) + } + + const { data: profiles } = await supabase + .from("profiles") + .select("*") + .order("created_at", { ascending: false }) + + return ( +
+
+

Kullanıcı Yönetimi

+
+ + + +
+
+ +
+ + + + Ad Soyad + Rol + Kayıt Tarihi + İşlemler + + + + {profiles?.map((profile) => ( + + {profile.full_name} + + + {profile.role === 'admin' ? 'Yönetici' : 'Kullanıcı'} + + + {new Date(profile.created_at).toLocaleDateString('tr-TR')} + + + + + + + ))} + +
+
+
+ ) +} diff --git a/app/(public)/login/page.tsx b/app/(public)/login/page.tsx index 0d037ea..bc053b9 100644 --- a/app/(public)/login/page.tsx +++ b/app/(public)/login/page.tsx @@ -3,7 +3,7 @@ import { useState } from "react" import { useRouter } from "next/navigation" import Link from "next/link" -import { supabase } from "@/lib/supabase" +import { createClient } from "@/lib/supabase-browser" import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" import { Label } from "@/components/ui/label" @@ -11,6 +11,7 @@ import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } import { AlertCircle, Loader2 } from "lucide-react" export default function LoginPage() { + const supabase = createClient() const router = useRouter() const [email, setEmail] = useState("") const [password, setPassword] = useState("") @@ -33,7 +34,7 @@ export default function LoginPage() { return } - router.push("/") + router.push("/dashboard") router.refresh() } catch (err: any) { setError("Bir hata oluştu. Lütfen tekrar deneyin.") diff --git a/app/layout.tsx b/app/layout.tsx index bc34f6f..2d099e5 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -6,10 +6,20 @@ import "./globals.css"; const inter = Inter({ subsets: ["latin"] }); const outfit = Outfit({ subsets: ["latin"], variable: "--font-outfit" }); -export const metadata: Metadata = { - title: "ParaKasa - Premium Çelik Kasalar", - description: "Eviniz ve iş yeriniz için en yüksek güvenlikli çelik kasa ve para sayma çözümleri.", -}; +import { getSiteSettings } from "@/lib/site-settings"; + +export async function generateMetadata() { + const settings = await getSiteSettings(); + + return { + title: settings?.site_title || "ParaKasa - Premium Çelik Kasalar", + description: settings?.site_description || "Eviniz ve iş yeriniz için en yüksek güvenlikli çelik kasa ve para sayma çözümleri.", + }; +} + +import { ThemeProvider } from "@/components/theme-provider" + +// ... imports export default function RootLayout({ children, @@ -17,12 +27,19 @@ export default function RootLayout({ children: React.ReactNode; }>) { return ( - + - {children} - + + {children} + + ); diff --git a/components/dashboard/appearance-form.tsx b/components/dashboard/appearance-form.tsx new file mode 100644 index 0000000..5020a9f --- /dev/null +++ b/components/dashboard/appearance-form.tsx @@ -0,0 +1,57 @@ +"use client" + +import { useTheme } from "next-themes" +import { Card, CardContent, CardTitle, CardHeader } from "@/components/ui/card" +import { Label } from "@/components/ui/label" +import { Switch } from "@/components/ui/switch" +import { useEffect, useState } from "react" + +export function AppearanceForm() { + const { theme, setTheme } = useTheme() + const [mounted, setMounted] = useState(false) + + // Avoid hydration mismatch + useEffect(() => { + setMounted(true) + }, []) + + if (!mounted) { + return ( + + + Görünüm + + +
+ + +
+
+
+ ) + } + + return ( + + + Görünüm + + +
+ + setTheme(checked ? 'dark' : 'light')} + /> +
+
+
+ ) +} diff --git a/components/dashboard/category-form.tsx b/components/dashboard/category-form.tsx new file mode 100644 index 0000000..4bc5b76 --- /dev/null +++ b/components/dashboard/category-form.tsx @@ -0,0 +1,186 @@ +"use client" + +import { useState } from "react" +import { useForm } from "react-hook-form" +import { zodResolver } from "@hookform/resolvers/zod" +import * as z from "zod" +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import { Textarea } from "@/components/ui/textarea" +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form" +import { toast } from "sonner" +import { useRouter } from "next/navigation" +import { createCategory, updateCategory, deleteCategory } from "@/app/(dashboard)/dashboard/categories/actions" +import { Trash } from "lucide-react" +import { AlertModal } from "@/components/modals/alert-modal" + +const formSchema = z.object({ + name: z.string().min(2, "Kategori adı en az 2 karakter olmalıdır."), + description: z.string().optional(), + image_url: z.string().optional(), +}) + +type CategoryFormValues = z.infer + +interface CategoryFormProps { + initialData?: { + id: string + name: string + description?: string + image_url?: string + } | null +} + +export function CategoryForm({ initialData }: CategoryFormProps) { + const router = useRouter() + const [open, setOpen] = useState(false) + const [loading, setLoading] = useState(false) + + const title = initialData ? "Kategoriyi Düzenle" : "Yeni Kategori" + const description = initialData ? "Kategori detaylarını düzenleyin." : "Yeni bir kategori ekleyin." + const toastMessage = initialData ? "Kategori güncellendi." : "Kategori oluşturuldu." + const action = initialData ? "Kaydet" : "Oluştur" + + const form = useForm({ + resolver: zodResolver(formSchema), + defaultValues: initialData || { + name: "", + description: "", + image_url: "", + }, + }) + + const onSubmit = async (data: CategoryFormValues) => { + setLoading(true) + try { + if (initialData) { + const result = await updateCategory(initialData.id, data) + if ((result as any).error) { + toast.error((result as any).error) + } else { + toast.success(toastMessage) + router.push(`/dashboard/categories`) + router.refresh() + } + } else { + const result = await createCategory(data) + if ((result as any).error) { + toast.error((result as any).error) + } else { + toast.success(toastMessage) + router.push(`/dashboard/categories`) + router.refresh() + } + } + } catch (error) { + toast.error("Bir hata oluştu.") + } finally { + setLoading(false) + } + } + + const onDelete = async () => { + setLoading(true) + try { + const result = await deleteCategory(initialData!.id) + if ((result as any).error) { + toast.error((result as any).error) + } else { + toast.success("Kategori silindi.") + router.push(`/dashboard/categories`) + router.refresh() + } + } catch (error) { + toast.error("Silme işlemi başarısız.") + } finally { + setLoading(false) + setOpen(false) + } + } + + return ( + <> + setOpen(false)} + onConfirm={onDelete} + loading={loading} + /> +
+
+

{title}

+

{description}

+
+ {initialData && ( + + )} +
+
+
+ +
+ ( + + Başlık + + + + + + )} + /> + ( + + Görsel URL (Opsiyonel) + + + + + + )} + /> +
+ ( + + Açıklama + +