create-nextjs-stack 0.1.1 → 0.1.4
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.
- package/README.md +502 -31
- package/package.json +6 -1
- package/templates/admin/app/(dashboard)/[resource]/[id]/page.tsx +6 -5
- package/templates/admin/app/(dashboard)/[resource]/new/page.tsx +11 -12
- package/templates/admin/app/(dashboard)/[resource]/page.tsx +4 -3
- package/templates/admin/app/actions/upload.ts +0 -7
- package/templates/admin/app/globals.css +112 -14
- package/templates/admin/app/layout.tsx +4 -1
- package/templates/admin/components/admin/Sidebar.tsx +2 -5
- package/templates/admin/components.json +22 -0
- package/templates/admin/hooks/useResource.ts +3 -0
- package/templates/admin/lib/services/resource.service.ts +2 -7
- package/templates/admin/lib/supabase/client.ts +8 -4
- package/templates/admin/lib/supabase/server.ts +1 -1
- package/templates/admin/lib/utils.ts +6 -0
- package/templates/admin/middleware.ts +1 -3
- package/templates/admin/next.config.ts +10 -2
- package/templates/admin/package-lock.json +13 -1
- package/templates/admin/package.json +11 -5
- package/templates/admin/src/lib/providers/StoreProvider.tsx +12 -0
- package/templates/admin/src/store/actions/index.ts +2 -0
- package/templates/admin/src/store/hooks.ts +11 -0
- package/templates/admin/src/store/index.ts +17 -0
- package/templates/admin/src/store/reducers/index.ts +11 -0
- package/templates/admin/src/store/types/index.ts +2 -0
- package/templates/admin/tsconfig.json +1 -1
- package/templates/web/.env.example +5 -1
- package/templates/web/package-lock.json +49 -18
- package/templates/web/package.json +3 -4
- package/templates/web/postcss.config.mjs +3 -1
- package/templates/web/src/app/api/revalidate/route.ts +46 -87
- package/templates/web/src/app/globals.css +1 -13
- package/templates/web/src/app/layout.tsx +4 -46
- package/templates/web/src/app/robots.ts +1 -1
- package/templates/web/src/app/sitemap.ts +27 -31
- package/templates/web/src/lib/seo/metadata.ts +5 -5
- package/templates/web/src/lib/seo/seo.config.ts +55 -59
- package/templates/web/src/lib/seo/seo.types.ts +1 -7
- package/templates/web/src/lib/services/categories.service.ts +3 -3
- package/templates/web/src/lib/services/clients.service.ts +2 -2
- package/templates/web/src/lib/services/products.service.ts +3 -3
- package/templates/web/src/lib/services/projects.service.ts +3 -3
- package/templates/web/src/lib/services/users.service.ts +2 -2
- package/templates/web/src/lib/supabase/client.ts +1 -1
- package/templates/web/src/lib/supabase/server.ts +1 -1
- package/templates/web/src/store/hooks.ts +11 -0
- package/templates/web/src/store/index.ts +11 -7
- package/templates/web/src/store/reducers/index.ts +1 -3
- package/templates/admin/app/(dashboard)/categories/[id]/page.tsx +0 -22
- package/templates/admin/app/(dashboard)/categories/new/page.tsx +0 -5
- package/templates/admin/app/(dashboard)/categories/page.tsx +0 -33
- package/templates/admin/app/(dashboard)/clients/[id]/page.tsx +0 -22
- package/templates/admin/app/(dashboard)/clients/new/page.tsx +0 -5
- package/templates/admin/app/(dashboard)/clients/page.tsx +0 -33
- package/templates/admin/app/(dashboard)/products/[id]/page.tsx +0 -22
- package/templates/admin/app/(dashboard)/products/new/page.tsx +0 -5
- package/templates/admin/app/(dashboard)/products/page.tsx +0 -33
- package/templates/admin/app/(dashboard)/projects/[id]/page.tsx +0 -22
- package/templates/admin/app/(dashboard)/projects/new/page.tsx +0 -5
- package/templates/admin/app/(dashboard)/projects/page.tsx +0 -33
- package/templates/admin/app/(dashboard)/users/[id]/page.tsx +0 -22
- package/templates/admin/app/(dashboard)/users/new/page.tsx +0 -5
- package/templates/admin/app/(dashboard)/users/page.tsx +0 -33
- package/templates/admin/components/categories/CategoryForm.tsx +0 -24
- package/templates/admin/components/categories/CategoryList.tsx +0 -113
- package/templates/admin/components/clients/ClientForm.tsx +0 -24
- package/templates/admin/components/clients/ClientList.tsx +0 -113
- package/templates/admin/components/products/ProductForm.tsx +0 -24
- package/templates/admin/components/products/ProductList.tsx +0 -117
- package/templates/admin/components/projects/ProjectForm.tsx +0 -24
- package/templates/admin/components/projects/ProjectList.tsx +0 -121
- package/templates/admin/components/users/UserForm.tsx +0 -39
- package/templates/admin/components/users/UserList.tsx +0 -101
- package/templates/web/src/lib/services/categoryService.ts +0 -251
- package/templates/web/src/lib/services/clientService.ts +0 -132
- package/templates/web/src/lib/services/productService.ts +0 -261
- package/templates/web/src/lib/services/projectService.ts +0 -234
- package/templates/web/src/lib/utils/cache.ts +0 -98
- package/templates/web/src/lib/utils/rate-limiter.ts +0 -102
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { getAdminClient } from "@/lib/supabase/server";
|
|
2
2
|
|
|
3
3
|
export class ProjectService {
|
|
4
4
|
private static table = "projects";
|
|
5
5
|
|
|
6
6
|
static async getAll() {
|
|
7
|
-
const supabase = await
|
|
7
|
+
const supabase = await getAdminClient();
|
|
8
8
|
const { data, error } = await supabase
|
|
9
9
|
.from(this.table)
|
|
10
10
|
.select("*, clients(name, logo_url)")
|
|
@@ -20,7 +20,7 @@ export class ProjectService {
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
static async getBySlug(slug: string) {
|
|
23
|
-
const supabase = await
|
|
23
|
+
const supabase = await getAdminClient();
|
|
24
24
|
const { data, error } = await supabase
|
|
25
25
|
.from(this.table)
|
|
26
26
|
.select("*, clients(name, logo_url)")
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { getAdminClient } from "@/lib/supabase/server";
|
|
2
2
|
|
|
3
3
|
export class UserService {
|
|
4
4
|
private static table = "users";
|
|
5
5
|
|
|
6
6
|
static async getAll() {
|
|
7
|
-
const supabase = await
|
|
7
|
+
const supabase = await getAdminClient();
|
|
8
8
|
const { data, error } = await supabase
|
|
9
9
|
.from(this.table)
|
|
10
10
|
.select("*")
|
|
@@ -6,7 +6,7 @@ import type { Database } from './types';
|
|
|
6
6
|
* Get Supabase client for server-side operations
|
|
7
7
|
* Uses SERVICE_ROLE_KEY for full database access
|
|
8
8
|
*/
|
|
9
|
-
export function
|
|
9
|
+
export function getAdminClient() {
|
|
10
10
|
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
|
|
11
11
|
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY;
|
|
12
12
|
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { useDispatch, useSelector } from 'react-redux';
|
|
2
|
+
import type { AppDispatch, RootState } from '.';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Typed hook wrappers — use these instead of plain useDispatch/useSelector.
|
|
6
|
+
* @example const dispatch = useAppDispatch();
|
|
7
|
+
* @example const value = useAppSelector((state) => state.someSlice.value);
|
|
8
|
+
*/
|
|
9
|
+
export const useAppDispatch = () => useDispatch<AppDispatch>();
|
|
10
|
+
export const useAppSelector = <T>(selector: (state: RootState) => T): T =>
|
|
11
|
+
useSelector(selector);
|
|
@@ -1,13 +1,17 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { thunk } from 'redux-thunk';
|
|
1
|
+
import { configureStore } from '@reduxjs/toolkit';
|
|
3
2
|
import logger from 'redux-logger';
|
|
4
3
|
import rootReducer from './reducers';
|
|
5
4
|
|
|
6
|
-
const
|
|
5
|
+
const store = configureStore({
|
|
6
|
+
reducer: rootReducer,
|
|
7
|
+
middleware: (getDefaultMiddleware) =>
|
|
8
|
+
process.env.NODE_ENV !== 'production'
|
|
9
|
+
? getDefaultMiddleware().concat(logger)
|
|
10
|
+
: getDefaultMiddleware(),
|
|
11
|
+
});
|
|
7
12
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
);
|
|
13
|
+
// Inferred types — export and use via store/hooks.ts
|
|
14
|
+
export type RootState = ReturnType<typeof store.getState>;
|
|
15
|
+
export type AppDispatch = typeof store.dispatch;
|
|
12
16
|
|
|
13
17
|
export default store;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { combineReducers } from '
|
|
1
|
+
import { combineReducers } from '@reduxjs/toolkit';
|
|
2
2
|
// Import your reducers here
|
|
3
3
|
// import exampleReducer from './exampleReducer';
|
|
4
4
|
|
|
@@ -8,6 +8,4 @@ const rootReducer = combineReducers({
|
|
|
8
8
|
_placeholder: (state = {}) => state
|
|
9
9
|
});
|
|
10
10
|
|
|
11
|
-
export type RootState = ReturnType<typeof rootReducer>;
|
|
12
|
-
|
|
13
11
|
export default rootReducer;
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import { getService } from "@/lib/services";
|
|
2
|
-
import { notFound } from "next/navigation";
|
|
3
|
-
import CategoryForm from "@/components/categories/CategoryForm";
|
|
4
|
-
|
|
5
|
-
export default async function EditCategoryPage({ params }: { params: Promise<{ id: string }> }) {
|
|
6
|
-
const { id } = await params;
|
|
7
|
-
const service = getService("categories");
|
|
8
|
-
|
|
9
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
10
|
-
let item: any = null;
|
|
11
|
-
try {
|
|
12
|
-
item = await service.getById(id);
|
|
13
|
-
} catch (error) {
|
|
14
|
-
console.error(error);
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
if (!item) {
|
|
18
|
-
return notFound();
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
return <CategoryForm mode="update" initialData={item} id={id} />;
|
|
22
|
-
}
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import Link from "next/link";
|
|
2
|
-
import { getService } from "@/lib/services";
|
|
3
|
-
import CategoryList from "@/components/categories/CategoryList";
|
|
4
|
-
import { resources } from "@/config/resources";
|
|
5
|
-
|
|
6
|
-
export default async function CategoriesPage() {
|
|
7
|
-
const service = getService("categories");
|
|
8
|
-
const config = resources.find((r) => r.name === "categories")!;
|
|
9
|
-
|
|
10
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
11
|
-
let items: any[] = [];
|
|
12
|
-
try {
|
|
13
|
-
items = await service.getAll();
|
|
14
|
-
} catch (error) {
|
|
15
|
-
console.error(error);
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
return (
|
|
19
|
-
<div>
|
|
20
|
-
<div className="flex justify-between items-center mb-6">
|
|
21
|
-
<h1 className="text-2xl font-bold">Categories</h1>
|
|
22
|
-
<Link
|
|
23
|
-
href="/categories/new"
|
|
24
|
-
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition"
|
|
25
|
-
>
|
|
26
|
-
Add Category
|
|
27
|
-
</Link>
|
|
28
|
-
</div>
|
|
29
|
-
|
|
30
|
-
<CategoryList items={items} config={config} />
|
|
31
|
-
</div>
|
|
32
|
-
);
|
|
33
|
-
}
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import { getService } from "@/lib/services";
|
|
2
|
-
import { notFound } from "next/navigation";
|
|
3
|
-
import ClientForm from "@/components/clients/ClientForm";
|
|
4
|
-
|
|
5
|
-
export default async function EditClientPage({ params }: { params: Promise<{ id: string }> }) {
|
|
6
|
-
const { id } = await params;
|
|
7
|
-
const service = getService("clients");
|
|
8
|
-
|
|
9
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
10
|
-
let item: any = null;
|
|
11
|
-
try {
|
|
12
|
-
item = await service.getById(id);
|
|
13
|
-
} catch (error) {
|
|
14
|
-
console.error(error);
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
if (!item) {
|
|
18
|
-
return notFound();
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
return <ClientForm mode="update" initialData={item} id={id} />;
|
|
22
|
-
}
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import Link from "next/link";
|
|
2
|
-
import { getService } from "@/lib/services";
|
|
3
|
-
import ClientList from "@/components/clients/ClientList";
|
|
4
|
-
import { resources } from "@/config/resources";
|
|
5
|
-
|
|
6
|
-
export default async function ClientsPage() {
|
|
7
|
-
const service = getService("clients");
|
|
8
|
-
const config = resources.find((r) => r.name === "clients")!;
|
|
9
|
-
|
|
10
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
11
|
-
let items: any[] = [];
|
|
12
|
-
try {
|
|
13
|
-
items = await service.getAll();
|
|
14
|
-
} catch (error) {
|
|
15
|
-
console.error(error);
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
return (
|
|
19
|
-
<div>
|
|
20
|
-
<div className="flex justify-between items-center mb-6">
|
|
21
|
-
<h1 className="text-2xl font-bold">Clients</h1>
|
|
22
|
-
<Link
|
|
23
|
-
href="/clients/new"
|
|
24
|
-
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition"
|
|
25
|
-
>
|
|
26
|
-
Add Client
|
|
27
|
-
</Link>
|
|
28
|
-
</div>
|
|
29
|
-
|
|
30
|
-
<ClientList items={items} config={config} />
|
|
31
|
-
</div>
|
|
32
|
-
);
|
|
33
|
-
}
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import { getService } from "@/lib/services";
|
|
2
|
-
import { notFound } from "next/navigation";
|
|
3
|
-
import ProductForm from "@/components/products/ProductForm";
|
|
4
|
-
|
|
5
|
-
export default async function EditProductPage({ params }: { params: Promise<{ id: string }> }) {
|
|
6
|
-
const { id } = await params;
|
|
7
|
-
const service = getService("products");
|
|
8
|
-
|
|
9
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
10
|
-
let item: any = null;
|
|
11
|
-
try {
|
|
12
|
-
item = await service.getById(id);
|
|
13
|
-
} catch (error) {
|
|
14
|
-
console.error(error);
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
if (!item) {
|
|
18
|
-
return notFound();
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
return <ProductForm mode="update" initialData={item} id={id} />;
|
|
22
|
-
}
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import Link from "next/link";
|
|
2
|
-
import { getService } from "@/lib/services";
|
|
3
|
-
import ProductList from "@/components/products/ProductList";
|
|
4
|
-
import { resources } from "@/config/resources";
|
|
5
|
-
|
|
6
|
-
export default async function ProductsPage() {
|
|
7
|
-
const service = getService("products");
|
|
8
|
-
const config = resources.find((r) => r.name === "products")!;
|
|
9
|
-
|
|
10
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
11
|
-
let items: any[] = [];
|
|
12
|
-
try {
|
|
13
|
-
items = await service.getAll();
|
|
14
|
-
} catch (error) {
|
|
15
|
-
console.error(error);
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
return (
|
|
19
|
-
<div>
|
|
20
|
-
<div className="flex justify-between items-center mb-6">
|
|
21
|
-
<h1 className="text-2xl font-bold">Products</h1>
|
|
22
|
-
<Link
|
|
23
|
-
href="/products/new"
|
|
24
|
-
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition"
|
|
25
|
-
>
|
|
26
|
-
Add Product
|
|
27
|
-
</Link>
|
|
28
|
-
</div>
|
|
29
|
-
|
|
30
|
-
<ProductList items={items} config={config} />
|
|
31
|
-
</div>
|
|
32
|
-
);
|
|
33
|
-
}
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import { getService } from "@/lib/services";
|
|
2
|
-
import { notFound } from "next/navigation";
|
|
3
|
-
import ProjectForm from "@/components/projects/ProjectForm";
|
|
4
|
-
|
|
5
|
-
export default async function EditProjectPage({ params }: { params: Promise<{ id: string }> }) {
|
|
6
|
-
const { id } = await params;
|
|
7
|
-
const service = getService("projects");
|
|
8
|
-
|
|
9
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
10
|
-
let item: any = null;
|
|
11
|
-
try {
|
|
12
|
-
item = await service.getById(id);
|
|
13
|
-
} catch (error) {
|
|
14
|
-
console.error(error);
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
if (!item) {
|
|
18
|
-
return notFound();
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
return <ProjectForm mode="update" initialData={item} id={id} />;
|
|
22
|
-
}
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import Link from "next/link";
|
|
2
|
-
import { getService } from "@/lib/services";
|
|
3
|
-
import ProjectList from "@/components/projects/ProjectList";
|
|
4
|
-
import { resources } from "@/config/resources";
|
|
5
|
-
|
|
6
|
-
export default async function ProjectsPage() {
|
|
7
|
-
const service = getService("projects");
|
|
8
|
-
const config = resources.find((r) => r.name === "projects")!;
|
|
9
|
-
|
|
10
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
11
|
-
let items: any[] = [];
|
|
12
|
-
try {
|
|
13
|
-
items = await service.getAll();
|
|
14
|
-
} catch (error) {
|
|
15
|
-
console.error(error);
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
return (
|
|
19
|
-
<div>
|
|
20
|
-
<div className="flex justify-between items-center mb-6">
|
|
21
|
-
<h1 className="text-2xl font-bold">Projects</h1>
|
|
22
|
-
<Link
|
|
23
|
-
href="/projects/new"
|
|
24
|
-
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition"
|
|
25
|
-
>
|
|
26
|
-
Add Project
|
|
27
|
-
</Link>
|
|
28
|
-
</div>
|
|
29
|
-
|
|
30
|
-
<ProjectList items={items} config={config} />
|
|
31
|
-
</div>
|
|
32
|
-
);
|
|
33
|
-
}
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import { getService } from "@/lib/services";
|
|
2
|
-
import { notFound } from "next/navigation";
|
|
3
|
-
import UserForm from "@/components/users/UserForm";
|
|
4
|
-
|
|
5
|
-
export default async function EditUserPage({ params }: { params: Promise<{ id: string }> }) {
|
|
6
|
-
const { id } = await params;
|
|
7
|
-
const service = getService("users");
|
|
8
|
-
|
|
9
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
10
|
-
let item: any = null;
|
|
11
|
-
try {
|
|
12
|
-
item = await service.getById(id);
|
|
13
|
-
} catch (error) {
|
|
14
|
-
console.error(error);
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
if (!item) {
|
|
18
|
-
return notFound();
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
return <UserForm mode="update" initialData={item} id={id} />;
|
|
22
|
-
}
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import Link from "next/link";
|
|
2
|
-
import { getService } from "@/lib/services";
|
|
3
|
-
import UserList from "@/components/users/UserList";
|
|
4
|
-
import { resources } from "@/config/resources";
|
|
5
|
-
|
|
6
|
-
export default async function UsersPage() {
|
|
7
|
-
const service = getService("users");
|
|
8
|
-
const config = resources.find((r) => r.name === "users")!;
|
|
9
|
-
|
|
10
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
11
|
-
let items: any[] = [];
|
|
12
|
-
try {
|
|
13
|
-
items = await service.getAll();
|
|
14
|
-
} catch (error) {
|
|
15
|
-
console.error(error);
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
return (
|
|
19
|
-
<div>
|
|
20
|
-
<div className="flex justify-between items-center mb-6">
|
|
21
|
-
<h1 className="text-2xl font-bold">Users</h1>
|
|
22
|
-
<Link
|
|
23
|
-
href="/users/new"
|
|
24
|
-
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition"
|
|
25
|
-
>
|
|
26
|
-
Add User
|
|
27
|
-
</Link>
|
|
28
|
-
</div>
|
|
29
|
-
|
|
30
|
-
<UserList items={items} config={config} />
|
|
31
|
-
</div>
|
|
32
|
-
);
|
|
33
|
-
}
|
|
@@ -1,24 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,113 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import ResourceFormClient from "@/components/admin/ResourceFormClient";
|
|
4
|
-
import { resources } from "@/config/resources";
|
|
5
|
-
|
|
6
|
-
interface ClientFormProps {
|
|
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 ClientForm({ mode, initialData, id }: ClientFormProps) {
|
|
14
|
-
const config = resources.find((r) => r.name === "clients")!;
|
|
15
|
-
|
|
16
|
-
return (
|
|
17
|
-
<ResourceFormClient
|
|
18
|
-
config={config}
|
|
19
|
-
mode={mode}
|
|
20
|
-
initialData={initialData}
|
|
21
|
-
id={id}
|
|
22
|
-
/>
|
|
23
|
-
);
|
|
24
|
-
}
|