create-nextjs-stack 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +60 -0
  3. package/bin/cli.js +187 -0
  4. package/package.json +48 -0
  5. package/templates/admin/.env.example +11 -0
  6. package/templates/admin/README.md +82 -0
  7. package/templates/admin/app/(auth)/login/page.tsx +84 -0
  8. package/templates/admin/app/(dashboard)/[resource]/[id]/page.tsx +45 -0
  9. package/templates/admin/app/(dashboard)/[resource]/new/page.tsx +32 -0
  10. package/templates/admin/app/(dashboard)/[resource]/page.tsx +131 -0
  11. package/templates/admin/app/(dashboard)/categories/[id]/page.tsx +22 -0
  12. package/templates/admin/app/(dashboard)/categories/new/page.tsx +5 -0
  13. package/templates/admin/app/(dashboard)/categories/page.tsx +33 -0
  14. package/templates/admin/app/(dashboard)/clients/[id]/page.tsx +22 -0
  15. package/templates/admin/app/(dashboard)/clients/new/page.tsx +5 -0
  16. package/templates/admin/app/(dashboard)/clients/page.tsx +33 -0
  17. package/templates/admin/app/(dashboard)/dashboard/page.tsx +45 -0
  18. package/templates/admin/app/(dashboard)/layout.tsx +13 -0
  19. package/templates/admin/app/(dashboard)/products/[id]/page.tsx +22 -0
  20. package/templates/admin/app/(dashboard)/products/new/page.tsx +5 -0
  21. package/templates/admin/app/(dashboard)/products/page.tsx +33 -0
  22. package/templates/admin/app/(dashboard)/projects/[id]/page.tsx +22 -0
  23. package/templates/admin/app/(dashboard)/projects/new/page.tsx +5 -0
  24. package/templates/admin/app/(dashboard)/projects/page.tsx +33 -0
  25. package/templates/admin/app/(dashboard)/users/[id]/page.tsx +22 -0
  26. package/templates/admin/app/(dashboard)/users/new/page.tsx +5 -0
  27. package/templates/admin/app/(dashboard)/users/page.tsx +33 -0
  28. package/templates/admin/app/actions/resources.ts +46 -0
  29. package/templates/admin/app/actions/upload.ts +58 -0
  30. package/templates/admin/app/favicon.ico +0 -0
  31. package/templates/admin/app/globals.css +23 -0
  32. package/templates/admin/app/layout.tsx +23 -0
  33. package/templates/admin/app/page.tsx +5 -0
  34. package/templates/admin/components/admin/AdminLayoutClient.tsx +22 -0
  35. package/templates/admin/components/admin/DeleteModal.tsx +90 -0
  36. package/templates/admin/components/admin/FormLayout.tsx +113 -0
  37. package/templates/admin/components/admin/ImageUpload.tsx +137 -0
  38. package/templates/admin/components/admin/ResourceFormClient.tsx +62 -0
  39. package/templates/admin/components/admin/Sidebar.tsx +74 -0
  40. package/templates/admin/components/admin/SubmitButton.tsx +34 -0
  41. package/templates/admin/components/admin/ToastProvider.tsx +8 -0
  42. package/templates/admin/components/categories/CategoryForm.tsx +24 -0
  43. package/templates/admin/components/categories/CategoryList.tsx +113 -0
  44. package/templates/admin/components/clients/ClientForm.tsx +24 -0
  45. package/templates/admin/components/clients/ClientList.tsx +113 -0
  46. package/templates/admin/components/products/ProductForm.tsx +24 -0
  47. package/templates/admin/components/products/ProductList.tsx +117 -0
  48. package/templates/admin/components/projects/ProjectForm.tsx +24 -0
  49. package/templates/admin/components/projects/ProjectList.tsx +121 -0
  50. package/templates/admin/components/users/UserForm.tsx +39 -0
  51. package/templates/admin/components/users/UserList.tsx +101 -0
  52. package/templates/admin/config/resources.ts +123 -0
  53. package/templates/admin/eslint.config.mjs +18 -0
  54. package/templates/admin/hooks/useResource.ts +86 -0
  55. package/templates/admin/lib/services/base.service.ts +106 -0
  56. package/templates/admin/lib/services/categories.service.ts +7 -0
  57. package/templates/admin/lib/services/clients.service.ts +7 -0
  58. package/templates/admin/lib/services/index.ts +27 -0
  59. package/templates/admin/lib/services/products.service.ts +9 -0
  60. package/templates/admin/lib/services/projects.service.ts +22 -0
  61. package/templates/admin/lib/services/resource.service.ts +26 -0
  62. package/templates/admin/lib/services/users.service.ts +9 -0
  63. package/templates/admin/lib/supabase/client.ts +9 -0
  64. package/templates/admin/lib/supabase/middleware.ts +57 -0
  65. package/templates/admin/lib/supabase/server.ts +29 -0
  66. package/templates/admin/middleware.ts +15 -0
  67. package/templates/admin/next.config.ts +10 -0
  68. package/templates/admin/package-lock.json +6768 -0
  69. package/templates/admin/package.json +33 -0
  70. package/templates/admin/postcss.config.mjs +7 -0
  71. package/templates/admin/public/file.svg +1 -0
  72. package/templates/admin/public/globe.svg +1 -0
  73. package/templates/admin/public/next.svg +1 -0
  74. package/templates/admin/public/vercel.svg +1 -0
  75. package/templates/admin/public/window.svg +1 -0
  76. package/templates/admin/supabase_mock_data.sql +57 -0
  77. package/templates/admin/supabase_schema.sql +93 -0
  78. package/templates/admin/tsconfig.json +34 -0
  79. package/templates/web/.env.example +21 -0
  80. package/templates/web/README.md +129 -0
  81. package/templates/web/components.json +22 -0
  82. package/templates/web/eslint.config.mjs +25 -0
  83. package/templates/web/next.config.ts +25 -0
  84. package/templates/web/package-lock.json +6778 -0
  85. package/templates/web/package.json +45 -0
  86. package/templates/web/postcss.config.mjs +5 -0
  87. package/templates/web/src/app/api/contact/route.ts +181 -0
  88. package/templates/web/src/app/api/revalidate/route.ts +95 -0
  89. package/templates/web/src/app/error.tsx +28 -0
  90. package/templates/web/src/app/globals.css +838 -0
  91. package/templates/web/src/app/layout.tsx +126 -0
  92. package/templates/web/src/app/loading.tsx +60 -0
  93. package/templates/web/src/app/not-found.tsx +68 -0
  94. package/templates/web/src/app/page.tsx +106 -0
  95. package/templates/web/src/app/robots.ts +12 -0
  96. package/templates/web/src/app/sitemap.ts +66 -0
  97. package/templates/web/src/components/home/StatsGrid.tsx +89 -0
  98. package/templates/web/src/hooks/useIntersectionObserver.ts +39 -0
  99. package/templates/web/src/lib/providers/StoreProvider.tsx +12 -0
  100. package/templates/web/src/lib/seo/index.ts +4 -0
  101. package/templates/web/src/lib/seo/metadata.ts +103 -0
  102. package/templates/web/src/lib/seo/seo.config.ts +161 -0
  103. package/templates/web/src/lib/seo/seo.types.ts +76 -0
  104. package/templates/web/src/lib/services/categories.service.ts +38 -0
  105. package/templates/web/src/lib/services/categoryService.ts +251 -0
  106. package/templates/web/src/lib/services/clientService.ts +132 -0
  107. package/templates/web/src/lib/services/clients.service.ts +20 -0
  108. package/templates/web/src/lib/services/productService.ts +261 -0
  109. package/templates/web/src/lib/services/products.service.ts +38 -0
  110. package/templates/web/src/lib/services/projectService.ts +234 -0
  111. package/templates/web/src/lib/services/projects.service.ts +38 -0
  112. package/templates/web/src/lib/services/users.service.ts +20 -0
  113. package/templates/web/src/lib/supabase/client.ts +42 -0
  114. package/templates/web/src/lib/supabase/constants.ts +25 -0
  115. package/templates/web/src/lib/supabase/server.ts +29 -0
  116. package/templates/web/src/lib/supabase/types.ts +112 -0
  117. package/templates/web/src/lib/utils/cache.ts +98 -0
  118. package/templates/web/src/lib/utils/rate-limiter.ts +102 -0
  119. package/templates/web/src/store/actions/index.ts +2 -0
  120. package/templates/web/src/store/index.ts +13 -0
  121. package/templates/web/src/store/reducers/index.ts +13 -0
  122. package/templates/web/src/store/types/index.ts +2 -0
  123. package/templates/web/tsconfig.json +41 -0
@@ -0,0 +1,22 @@
1
+ "use client";
2
+
3
+ import Sidebar from "./Sidebar";
4
+ import ToastProvider from "./ToastProvider";
5
+
6
+ export default function AdminLayoutClient({
7
+ children,
8
+ }: {
9
+ children: React.ReactNode;
10
+ }) {
11
+ return (
12
+ <div className="flex min-h-screen bg-gray-100">
13
+ <Sidebar />
14
+ <main className="flex-1 p-8 overflow-y-auto h-screen">
15
+ <div className="max-w-7xl mx-auto">
16
+ {children}
17
+ </div>
18
+ </main>
19
+ <ToastProvider />
20
+ </div>
21
+ );
22
+ }
@@ -0,0 +1,90 @@
1
+ "use client";
2
+
3
+ import { useEffect, useState } from "react";
4
+ import { AlertTriangle, X } from "lucide-react";
5
+
6
+ interface DeleteModalProps {
7
+ isOpen: boolean;
8
+ onClose: () => void;
9
+ onConfirm: () => void;
10
+ title?: string;
11
+ description?: string;
12
+ isLoading?: boolean;
13
+ }
14
+
15
+ export default function DeleteModal({
16
+ isOpen,
17
+ onClose,
18
+ onConfirm,
19
+ title = "Delete Resource",
20
+ description = "Are you sure you want to delete this resource? This action cannot be undone.",
21
+ isLoading = false,
22
+ }: DeleteModalProps) {
23
+ const [show, setShow] = useState(isOpen);
24
+
25
+ useEffect(() => {
26
+ setShow(isOpen);
27
+ }, [isOpen]);
28
+
29
+ if (!isOpen) return null;
30
+
31
+ return (
32
+ <div className="fixed inset-0 z-50 flex items-center justify-center p-4 sm:p-0">
33
+ {/* Backdrop */}
34
+ <div
35
+ className="fixed inset-0 bg-black/40 backdrop-blur-sm transition-opacity"
36
+ onClick={!isLoading ? onClose : undefined}
37
+ />
38
+
39
+ {/* Modal Panel */}
40
+ <div className="relative bg-white rounded-lg shadow-xl w-full max-w-md overflow-hidden transform transition-all animate-in fade-in zoom-in-95 duration-200">
41
+ <div className="p-6">
42
+ <div className="flex items-start gap-4">
43
+ <div className="flex-shrink-0 w-10 h-10 rounded-full bg-red-100 flex items-center justify-center">
44
+ <AlertTriangle className="w-5 h-5 text-red-600" />
45
+ </div>
46
+ <div className="flex-1">
47
+ <h3 className="text-lg font-semibold text-gray-900 mb-2">
48
+ {title}
49
+ </h3>
50
+ <p className="text-sm text-gray-500 leading-relaxed">
51
+ {description}
52
+ </p>
53
+ </div>
54
+ <button
55
+ onClick={onClose}
56
+ disabled={isLoading}
57
+ className="text-gray-400 hover:text-gray-500 transition-colors"
58
+ >
59
+ <X className="w-5 h-5" />
60
+ </button>
61
+ </div>
62
+ </div>
63
+
64
+ <div className="bg-gray-50 px-6 py-4 flex justify-end gap-3">
65
+ <button
66
+ onClick={onClose}
67
+ disabled={isLoading}
68
+ className="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-200 transition-colors disabled:opacity-50"
69
+ >
70
+ Cancel
71
+ </button>
72
+ <button
73
+ onClick={onConfirm}
74
+ disabled={isLoading}
75
+ className="px-4 py-2 text-sm font-medium text-white bg-red-600 border border-transparent rounded-lg hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 transition-colors disabled:opacity-50 flex items-center gap-2"
76
+ >
77
+ {isLoading ? (
78
+ <>
79
+ <div className="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin" />
80
+ Deleting...
81
+ </>
82
+ ) : (
83
+ "Delete"
84
+ )}
85
+ </button>
86
+ </div>
87
+ </div>
88
+ </div>
89
+ );
90
+ }
@@ -0,0 +1,113 @@
1
+ "use client";
2
+
3
+ import { useFormContext } from "react-hook-form";
4
+ import SubmitButton from "./SubmitButton";
5
+ import { getRelationOptions } from "@/app/actions/resources";
6
+ import { useEffect, useState } from "react";
7
+ import ImageUpload from "./ImageUpload";
8
+
9
+ interface FormLayoutProps {
10
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
11
+ fields: any[];
12
+ title: string;
13
+ isSubmitting: boolean;
14
+ }
15
+
16
+ export default function FormLayout({ fields, title, isSubmitting }: FormLayoutProps) {
17
+ const { register, formState: { errors }, control, watch } = useFormContext();
18
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
19
+ const [relationOptions, setRelationOptions] = useState<Record<string, any[]>>({});
20
+
21
+ useEffect(() => {
22
+ // Fetch relation options
23
+ fields.forEach(async (field) => {
24
+ if (field.type === 'select' && field.relation) {
25
+ try {
26
+ const data = await getRelationOptions(
27
+ field.relation.table,
28
+ field.relation.display
29
+ );
30
+ setRelationOptions(prev => ({ ...prev, [field.name]: data }));
31
+ } catch (error) {
32
+ console.error('Failed to fetch options', error);
33
+ }
34
+ }
35
+ });
36
+ }, [fields]);
37
+
38
+ return (
39
+ <div className="bg-white rounded-lg shadow p-6">
40
+ <h2 className="text-xl font-semibold mb-6">{title}</h2>
41
+ <div className="space-y-6">
42
+ {fields.map((field) => (
43
+ <div key={field.name}>
44
+ <label className="block text-sm font-medium text-gray-700 mb-1">
45
+ {field.label}
46
+ {field.required && <span className="text-red-500 ml-1">*</span>}
47
+ </label>
48
+
49
+ {field.type === 'textarea' ? (
50
+ <textarea
51
+ {...register(field.name)}
52
+ rows={4}
53
+ className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 bg-white text-gray-900"
54
+ />
55
+ ) : field.type === 'boolean' ? (
56
+ <input
57
+ type="checkbox"
58
+ {...register(field.name)}
59
+ className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
60
+ />
61
+ ) : field.type === 'select' ? (
62
+ <select
63
+ {...register(field.name)}
64
+ className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 bg-white text-gray-900"
65
+ >
66
+ <option value="">Select...</option>
67
+ {field.options ? (
68
+ field.options.map((opt: { value: string; label: string }) => (
69
+ <option key={opt.value} value={opt.value}>
70
+ {opt.label}
71
+ </option>
72
+ ))
73
+ ) : (
74
+ relationOptions[field.name]?.map((opt: { id: string; [key: string]: string }) => (
75
+ <option key={opt.id} value={opt.id}>
76
+ {opt[field.relation!.display]}
77
+ </option>
78
+ ))
79
+ )}
80
+ </select>
81
+ ) : field.type === 'image' ? (
82
+ <ImageUpload
83
+ name={field.name}
84
+ control={control}
85
+ defaultValue={watch(field.name)}
86
+ />
87
+ ) : (
88
+ <input
89
+ type={field.type}
90
+ {...register(field.name)}
91
+ className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 bg-white text-gray-900"
92
+ />
93
+ )}
94
+
95
+ {errors[field.name] && (
96
+ <p className="text-red-500 text-sm mt-1">
97
+ {errors[field.name]?.message as string}
98
+ </p>
99
+ )}
100
+ </div>
101
+ ))}
102
+
103
+ <div className="pt-4">
104
+ <SubmitButton
105
+ isSubmitting={isSubmitting}
106
+ label="Save"
107
+ submittingLabel="Saving..."
108
+ />
109
+ </div>
110
+ </div>
111
+ </div>
112
+ );
113
+ }
@@ -0,0 +1,137 @@
1
+ "use client";
2
+
3
+ import { Control, Controller } from "react-hook-form";
4
+ import { Image as ImageIcon, Upload, X, Loader2 } from "lucide-react";
5
+ import Image from "next/image";
6
+ import { uploadImage } from "@/app/actions/upload";
7
+ import { useState, useRef, useEffect } from "react";
8
+ import { toast } from "react-toastify";
9
+
10
+ interface ImageUploadProps {
11
+ name: string;
12
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
13
+ control: Control<any>;
14
+ defaultValue?: string;
15
+ }
16
+
17
+ export default function ImageUpload({ name, control, defaultValue }: ImageUploadProps) {
18
+ const [isUploading, setIsUploading] = useState(false);
19
+ const fileInputRef = useRef<HTMLInputElement>(null);
20
+ const isMounted = useRef(false);
21
+
22
+ useEffect(() => {
23
+ isMounted.current = true;
24
+ return () => {
25
+ isMounted.current = false;
26
+ };
27
+ }, []);
28
+
29
+ const handleUpload = async (file: File, onChange: (value: string) => void) => {
30
+ setIsUploading(true);
31
+ const formData = new FormData();
32
+ formData.append("file", file);
33
+
34
+ try {
35
+ const result = await uploadImage(formData);
36
+
37
+ // Check if mounted before updating state
38
+ if (!isMounted.current) return;
39
+
40
+ if (result.error) {
41
+ toast.error(result.error);
42
+ } else if (result.url) {
43
+ onChange(result.url);
44
+ toast.success("Image uploaded successfully");
45
+ }
46
+ } catch (error) {
47
+ console.error(error);
48
+ if (isMounted.current) toast.error("Upload failed");
49
+ } finally {
50
+ if (isMounted.current) setIsUploading(false);
51
+ }
52
+ };
53
+
54
+ const onFileInputChange = (e: React.ChangeEvent<HTMLInputElement>, onChange: (value: string) => void) => {
55
+ if (e.target.files && e.target.files[0]) {
56
+ handleUpload(e.target.files[0], onChange);
57
+ }
58
+ };
59
+
60
+ return (
61
+ <Controller
62
+ name={name}
63
+ control={control}
64
+ render={({ field: { onChange, value } }) => {
65
+ const currentImage = value || defaultValue;
66
+
67
+ return (
68
+ <div className="space-y-4">
69
+ {!currentImage ? (
70
+ <div
71
+ onClick={() => fileInputRef.current?.click()}
72
+ className="border-2 border-dashed border-gray-300 rounded-lg p-6 flex flex-col items-center justify-center cursor-pointer hover:border-blue-500 transition-colors bg-gray-50 hover:bg-white"
73
+ >
74
+ <input
75
+ ref={fileInputRef}
76
+ type="file"
77
+ accept="image/*"
78
+ className="hidden"
79
+ onChange={(e) => onFileInputChange(e, onChange)}
80
+ />
81
+
82
+ {isUploading ? (
83
+ <div className="flex flex-col items-center">
84
+ <Loader2 className="w-8 h-8 text-blue-500 animate-spin mb-2" />
85
+ <p className="text-sm text-gray-500">Uploading...</p>
86
+ </div>
87
+ ) : (
88
+ <>
89
+ <Upload className="w-8 h-8 text-gray-400 mb-2" />
90
+ <p className="text-sm text-gray-500 font-medium">
91
+ Click to upload image
92
+ </p>
93
+ <p className="text-xs text-gray-400 mt-1">
94
+ SVG, PNG, JPG or GIF (max 5MB)
95
+ </p>
96
+ </>
97
+ )}
98
+ </div>
99
+ ) : (
100
+ <div className="relative w-full h-64 rounded-lg overflow-hidden border border-gray-200 bg-gray-100 group">
101
+ <Image
102
+ src={currentImage}
103
+ alt="Upload preview"
104
+ fill
105
+ className="object-contain"
106
+ />
107
+ <div className="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity">
108
+ <button
109
+ type="button"
110
+ onClick={() => onChange("")}
111
+ className="bg-red-500 text-white p-1 rounded-full hover:bg-red-600 transition-colors shadow-lg"
112
+ >
113
+ <X className="w-4 h-4" />
114
+ </button>
115
+ </div>
116
+ </div>
117
+ )}
118
+
119
+ {/* Fallback Input for manual URL entry (optional, hidden by default but good for debugging or external URLs) */}
120
+ <div className="mt-2 text-xs text-gray-400 flex justify-end">
121
+ <button
122
+ type="button"
123
+ onClick={() => {
124
+ const url = prompt("Enter image URL manually:", currentImage);
125
+ if (url !== null) onChange(url);
126
+ }}
127
+ className="hover:underline"
128
+ >
129
+ Enter URL manually
130
+ </button>
131
+ </div>
132
+ </div>
133
+ );
134
+ }}
135
+ />
136
+ );
137
+ }
@@ -0,0 +1,62 @@
1
+ "use client";
2
+
3
+ import { useForm, FormProvider } from "react-hook-form";
4
+ import FormLayout from "./FormLayout";
5
+ import { ResourceConfig } from "@/config/resources";
6
+ import { Trash2 } from "lucide-react";
7
+ import { useResource } from "@/hooks/useResource";
8
+
9
+ interface ResourceFormClientProps {
10
+ config: ResourceConfig;
11
+ mode: "create" | "update";
12
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
13
+ initialData?: any;
14
+ id?: string;
15
+ }
16
+
17
+ export default function ResourceFormClient({
18
+ config,
19
+ mode,
20
+ initialData,
21
+ id,
22
+ }: ResourceFormClientProps) {
23
+ const { create, update, remove, isSubmitting, isDeleting } = useResource(config);
24
+
25
+ const methods = useForm({
26
+ defaultValues: initialData || {},
27
+ });
28
+
29
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
30
+ const onSubmit = async (data: any) => {
31
+ if (mode === "create") {
32
+ await create(data);
33
+ } else {
34
+ await update(id!, data);
35
+ }
36
+ };
37
+
38
+ return (
39
+ <FormProvider {...methods}>
40
+ <form onSubmit={methods.handleSubmit(onSubmit)}>
41
+ {mode === 'update' && (
42
+ <div className="flex justify-end mb-4">
43
+ <button
44
+ type="button"
45
+ onClick={() => remove(id!)}
46
+ disabled={isDeleting}
47
+ className="flex items-center text-red-600 hover:text-red-800 px-3 py-2 rounded bg-red-50 hover:bg-red-100 transition-colors"
48
+ >
49
+ <Trash2 className="w-4 h-4 mr-2" />
50
+ {isDeleting ? "Deleting..." : "Delete Record"}
51
+ </button>
52
+ </div>
53
+ )}
54
+ <FormLayout
55
+ fields={config.fields}
56
+ title={`${mode === "create" ? "New" : "Edit"} ${config.singular}`}
57
+ isSubmitting={isSubmitting}
58
+ />
59
+ </form>
60
+ </FormProvider>
61
+ );
62
+ }
@@ -0,0 +1,74 @@
1
+ "use client";
2
+
3
+ import Link from "next/link";
4
+ import { usePathname } from "next/navigation";
5
+ import { resources } from "@/config/resources";
6
+ import * as Icons from "lucide-react";
7
+ import { LogOut } from "lucide-react";
8
+ import { createBrowserClient } from "@supabase/ssr";
9
+ import { useRouter } from "next/navigation";
10
+
11
+ export default function Sidebar() {
12
+ const pathname = usePathname();
13
+ const router = useRouter();
14
+
15
+ const handleLogout = async () => {
16
+ const supabase = createBrowserClient(
17
+ process.env.NEXT_PUBLIC_SUPABASE_URL!,
18
+ process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
19
+ );
20
+ await supabase.auth.signOut();
21
+ router.push("/login");
22
+ router.refresh();
23
+ };
24
+
25
+ return (
26
+ <div className="flex flex-col w-64 bg-gray-900 h-screen text-white sticky top-0">
27
+ <div className="p-6 border-b border-gray-800">
28
+ <h1 className="text-2xl font-bold">Admin Panel</h1>
29
+ </div>
30
+ <nav className="flex-1 p-4 space-y-2 overflow-y-auto">
31
+ <Link
32
+ href="/dashboard"
33
+ className={`flex items-center px-4 py-3 rounded-lg transition-colors ${
34
+ pathname === "/dashboard"
35
+ ? "bg-blue-600 text-white"
36
+ : "text-gray-400 hover:bg-gray-800 hover:text-white"
37
+ }`}
38
+ >
39
+ <Icons.LayoutDashboard className="w-5 h-5 mr-3" />
40
+ Dashboard
41
+ </Link>
42
+
43
+ {resources.map((resource) => {
44
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
45
+ const Icon = (Icons as any)[resource.icon] || Icons.Box;
46
+ const isActive = pathname.startsWith(resource.path);
47
+ return (
48
+ <Link
49
+ key={resource.name}
50
+ href={resource.path}
51
+ className={`flex items-center px-4 py-3 rounded-lg transition-colors ${
52
+ isActive
53
+ ? "bg-blue-600 text-white"
54
+ : "text-gray-400 hover:bg-gray-800 hover:text-white"
55
+ }`}
56
+ >
57
+ <Icon className="w-5 h-5 mr-3" />
58
+ {resource.plural}
59
+ </Link>
60
+ );
61
+ })}
62
+ </nav>
63
+ <div className="p-4 border-t border-gray-800">
64
+ <button
65
+ onClick={handleLogout}
66
+ className="w-full flex items-center px-4 py-2 text-sm text-gray-400 hover:text-white hover:bg-gray-800 rounded-lg transition-colors"
67
+ >
68
+ <LogOut className="w-4 h-4 mr-2" />
69
+ Logout
70
+ </button>
71
+ </div>
72
+ </div>
73
+ );
74
+ }
@@ -0,0 +1,34 @@
1
+ "use client";
2
+
3
+ import { Loader2 } from "lucide-react";
4
+
5
+ interface SubmitButtonProps {
6
+ isSubmitting: boolean;
7
+ label: string;
8
+ submittingLabel: string;
9
+ className?: string;
10
+ }
11
+
12
+ export default function SubmitButton({
13
+ isSubmitting,
14
+ label,
15
+ submittingLabel,
16
+ className = "",
17
+ }: SubmitButtonProps) {
18
+ return (
19
+ <button
20
+ type="submit"
21
+ disabled={isSubmitting}
22
+ className={`flex items-center justify-center w-full sm:w-auto px-6 py-2 bg-blue-600 hover:bg-blue-700 text-white font-semibold rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed ${className}`}
23
+ >
24
+ {isSubmitting ? (
25
+ <>
26
+ <Loader2 className="w-4 h-4 mr-2 animate-spin" />
27
+ {submittingLabel}
28
+ </>
29
+ ) : (
30
+ label
31
+ )}
32
+ </button>
33
+ );
34
+ }
@@ -0,0 +1,8 @@
1
+ "use client";
2
+
3
+ import { ToastContainer } from "react-toastify";
4
+ import "react-toastify/dist/ReactToastify.css";
5
+
6
+ export default function ToastProvider() {
7
+ return <ToastContainer position="bottom-right" theme="colored" />;
8
+ }
@@ -0,0 +1,24 @@
1
+ "use client";
2
+
3
+ import ResourceFormClient from "@/components/admin/ResourceFormClient";
4
+ import { resources } from "@/config/resources";
5
+
6
+ interface CategoryFormProps {
7
+ mode: "create" | "update";
8
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
9
+ initialData?: any;
10
+ id?: string;
11
+ }
12
+
13
+ export default function CategoryForm({ mode, initialData, id }: CategoryFormProps) {
14
+ const config = resources.find((r) => r.name === "categories")!;
15
+
16
+ return (
17
+ <ResourceFormClient
18
+ config={config}
19
+ mode={mode}
20
+ initialData={initialData}
21
+ id={id}
22
+ />
23
+ );
24
+ }
@@ -0,0 +1,113 @@
1
+ "use client";
2
+
3
+ import Link from "next/link";
4
+ import { ResourceConfig } from "@/config/resources";
5
+ import { useResource } from "@/hooks/useResource";
6
+ import { Pencil, Trash2 } from "lucide-react";
7
+
8
+ interface CategoryListProps {
9
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
10
+ items: any[];
11
+ config: ResourceConfig;
12
+ }
13
+
14
+ import { useState } from "react";
15
+ import DeleteModal from "@/components/admin/DeleteModal";
16
+
17
+ export default function CategoryList({ items, config }: CategoryListProps) {
18
+ const { remove, isDeleting } = useResource(config);
19
+ const [deleteId, setDeleteId] = useState<string | null>(null);
20
+
21
+ const handleDelete = async () => {
22
+ if (deleteId) {
23
+ const success = await remove(deleteId);
24
+ if (success) {
25
+ setDeleteId(null);
26
+ }
27
+ }
28
+ };
29
+
30
+ return (
31
+ <>
32
+ <div className="bg-white rounded-lg shadow overflow-hidden">
33
+ <div className="overflow-x-auto">
34
+ <table className="w-full text-left">
35
+ <thead className="bg-gray-50 text-gray-500 uppercase text-xs font-semibold">
36
+ <tr>
37
+ <th className="px-6 py-3">Title</th>
38
+ <th className="px-6 py-3">Slug</th>
39
+ <th className="px-6 py-3">Featured</th>
40
+ <th className="px-6 py-3">Published</th>
41
+ <th className="px-6 py-3 text-right">Actions</th>
42
+ </tr>
43
+ </thead>
44
+ <tbody className="divide-y divide-gray-200">
45
+ {items?.map((item) => (
46
+ <tr key={item.id} className="hover:bg-gray-50 transition">
47
+ <td className="px-6 py-4 text-gray-900 font-medium">{item.title}</td>
48
+ <td className="px-6 py-4 text-gray-500">{item.slug}</td>
49
+ <td className="px-6 py-4">
50
+ <span
51
+ className={`px-2 py-1 text-xs rounded-full ${
52
+ item.featured
53
+ ? "bg-yellow-100 text-yellow-700"
54
+ : "bg-gray-100 text-gray-600"
55
+ }`}
56
+ >
57
+ {item.featured ? "Yes" : "No"}
58
+ </span>
59
+ </td>
60
+ <td className="px-6 py-4">
61
+ <span
62
+ className={`px-2 py-1 text-xs rounded-full ${
63
+ item.published
64
+ ? "bg-green-100 text-green-700"
65
+ : "bg-gray-100 text-gray-600"
66
+ }`}
67
+ >
68
+ {item.published ? "Published" : "Draft"}
69
+ </span>
70
+ </td>
71
+ <td className="px-6 py-4 text-right">
72
+ <div className="flex justify-end gap-2">
73
+ <Link
74
+ href={`/categories/${item.id}`}
75
+ className="flex items-center text-indigo-600 hover:text-indigo-900 bg-indigo-50 hover:bg-indigo-100 px-3 py-1.5 rounded-md transition-colors duration-200"
76
+ title="Edit"
77
+ >
78
+ <Pencil className="w-4 h-4" />
79
+ </Link>
80
+ <button
81
+ onClick={() => setDeleteId(item.id)}
82
+ className="flex items-center text-red-600 hover:text-red-900 bg-red-50 hover:bg-red-100 px-3 py-1.5 rounded-md transition-colors duration-200"
83
+ title="Delete"
84
+ >
85
+ <Trash2 className="w-4 h-4" />
86
+ </button>
87
+ </div>
88
+ </td>
89
+ </tr>
90
+ ))}
91
+ {items?.length === 0 && (
92
+ <tr>
93
+ <td colSpan={5} className="px-6 py-8 text-center text-gray-500">
94
+ No categories found.
95
+ </td>
96
+ </tr>
97
+ )}
98
+ </tbody>
99
+ </table>
100
+ </div>
101
+ </div>
102
+
103
+ <DeleteModal
104
+ isOpen={!!deleteId}
105
+ onClose={() => setDeleteId(null)}
106
+ onConfirm={handleDelete}
107
+ isLoading={isDeleting}
108
+ title="Delete Category"
109
+ description="Are you sure you want to delete this category? This action cannot be undone."
110
+ />
111
+ </>
112
+ );
113
+ }