diff --git a/.gitignore b/.gitignore index 2309cc8..0c31be1 100644 --- a/.gitignore +++ b/.gitignore @@ -136,3 +136,4 @@ dist .yarn/install-state.gz .pnp.* +data \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..b09ab45 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,46 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Next.js: debug server-side", + "type": "node-terminal", + "request": "launch", + "command": "npm run dev", + "skipFiles": [ + "/**" + ], + "console": "integratedTerminal" + }, + { + "name": "Next.js: debug client-side", + "type": "chrome", + "request": "launch", + "url": "http://localhost:3000", + "webRoot": "${workspaceFolder}", + "sourceMapPathOverrides": { + "webpack://_N_E/*": "${webRoot}/*" + } + }, + { + "name": "Next.js: debug full stack", + "type": "node-terminal", + "request": "launch", + "command": "npm run dev", + "serverReadyAction": { + "pattern": "started server on .+, url: (https?://.+)", + "uriFormat": "%s", + "action": "debugWithChrome" + }, + "console": "integratedTerminal" + }, + { + "name": "Next.js: attach to server", + "type": "node", + "request": "attach", + "port": 9229, + "skipFiles": [ + "/**" + ] + } + ] +} \ No newline at end of file diff --git a/doc/prompts/8. Add Customer CRUD b/doc/prompts/8. Add Customer CRUD new file mode 100644 index 0000000..7208a29 --- /dev/null +++ b/doc/prompts/8. Add Customer CRUD @@ -0,0 +1,7 @@ +Now I want to add something like customer management to this cms. + +Please generate following table and make CRUD operation into admin console. + +Tablename : Customer +Params: +id, name, url, email, modifiedAt, createdAt diff --git a/next-env.d.ts b/next-env.d.ts new file mode 100644 index 0000000..1b3be08 --- /dev/null +++ b/next-env.d.ts @@ -0,0 +1,5 @@ +/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/src/app/(admin)/admin/customers/DeleteButton.tsx b/src/app/(admin)/admin/customers/DeleteButton.tsx new file mode 100644 index 0000000..af5fdf3 --- /dev/null +++ b/src/app/(admin)/admin/customers/DeleteButton.tsx @@ -0,0 +1,57 @@ +'use client'; + +import { useState } from 'react'; +import { useRouter } from 'next/navigation'; + +interface DeleteButtonProps { + customerId: string; + customerName: string; +} + +export default function DeleteButton({ customerId, customerName }: DeleteButtonProps) { + const [isDeleting, setIsDeleting] = useState(false); + const [error, setError] = useState(null); + const router = useRouter(); + + const handleDelete = async () => { + if (!confirm(`Are you sure you want to delete customer "${customerName}"?`)) { + return; + } + + setIsDeleting(true); + setError(null); + + try { + const response = await fetch(`/api/customers/${customerId}`, { + method: 'DELETE', + }); + + if (!response.ok) { + const data = await response.json(); + throw new Error(data.error || 'Failed to delete customer'); + } + + // Refresh the page to show updated customer list + router.refresh(); + } catch (err) { + setError(err instanceof Error ? err.message : 'An error occurred'); + } finally { + setIsDeleting(false); + } + }; + + return ( + <> + + {error && ( +
{error}
+ )} + + ); +} diff --git a/src/app/(admin)/admin/customers/components/EditCustomer.tsx b/src/app/(admin)/admin/customers/components/EditCustomer.tsx new file mode 100644 index 0000000..c8d9a8b --- /dev/null +++ b/src/app/(admin)/admin/customers/components/EditCustomer.tsx @@ -0,0 +1,206 @@ +'use client'; + +import { useState, useEffect, FormEvent, ChangeEvent } from 'react'; +import { useRouter } from 'next/navigation'; +import Link from 'next/link'; + +interface Customer { + id: string; + name: string; + url: string; + email: string; + createdAt: string; + modifiedAt: string; +} + +interface EditCustomerProps { + id?: string; // Optional for new customer +} + +export default function EditCustomer({ id }: EditCustomerProps) { + const router = useRouter(); + const [formData, setFormData] = useState({ + name: '', + url: '', + email: '', + }); + const [isLoading, setIsLoading] = useState(!!id); // Only loading if editing + const [isSubmitting, setIsSubmitting] = useState(false); + const [error, setError] = useState(null); + + // Fetch customer data if editing + useEffect(() => { + const fetchCustomer = async () => { + if (!id) { + setIsLoading(false); + return; + } + + try { + const response = await fetch(`/api/customers/${id}`); + + if (!response.ok) { + throw new Error('Failed to fetch customer'); + } + + const customer: Customer = await response.json(); + + setFormData({ + name: customer.name, + url: customer.url || '', + email: customer.email, + }); + + setIsLoading(false); + } catch (err) { + setError('Failed to load customer data'); + setIsLoading(false); + } + }; + + fetchCustomer(); + }, [id]); + + const handleChange = (e: ChangeEvent) => { + const { name, value } = e.target; + setFormData((prev) => ({ ...prev, [name]: value })); + }; + + const handleSubmit = async (e: FormEvent) => { + e.preventDefault(); + setIsSubmitting(true); + setError(null); + + try { + // Validate form + if (!formData.name || !formData.email) { + throw new Error('Name and email are required'); + } + + // Determine if creating or updating + const url = id ? `/api/customers/${id}` : '/api/customers'; + const method = id ? 'PUT' : 'POST'; + + // Submit the form + const response = await fetch(url, { + method, + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(formData), + }); + + if (!response.ok) { + const data = await response.json(); + throw new Error(data.error || `Failed to ${id ? 'update' : 'create'} customer`); + } + + // Redirect to customers list on success + router.push('/admin/customers'); + router.refresh(); + } catch (err) { + setError(err instanceof Error ? err.message : 'An error occurred'); + setIsSubmitting(false); + } + }; + + if (isLoading) { + return
Loading...
; + } + + return ( +
+
+

+ {id ? 'Edit Customer' : 'Add New Customer'} +

+ + Back to Customers + +
+ + {error && ( +
+
+
+ + + +
+
+

{error}

+
+
+
+ )} + +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + + Cancel + +
+
+
+ ); +} diff --git a/src/app/(admin)/admin/customers/edit/[id]/page.tsx b/src/app/(admin)/admin/customers/edit/[id]/page.tsx new file mode 100644 index 0000000..2970ef5 --- /dev/null +++ b/src/app/(admin)/admin/customers/edit/[id]/page.tsx @@ -0,0 +1,6 @@ +import EditCustomer from '../../components/EditCustomer'; + +export default async function EditCustomerPage(props: { params: Promise<{ id: string }> }) { + const { id } = await props.params; + return ; +} diff --git a/src/app/(admin)/admin/customers/new/page.tsx b/src/app/(admin)/admin/customers/new/page.tsx new file mode 100644 index 0000000..7813eb8 --- /dev/null +++ b/src/app/(admin)/admin/customers/new/page.tsx @@ -0,0 +1,5 @@ +import EditCustomer from '../components/EditCustomer'; + +export default function NewCustomerPage() { + return ; +} diff --git a/src/app/(admin)/admin/customers/page.tsx b/src/app/(admin)/admin/customers/page.tsx new file mode 100644 index 0000000..b782d64 --- /dev/null +++ b/src/app/(admin)/admin/customers/page.tsx @@ -0,0 +1,129 @@ +import Link from 'next/link'; +import { getDataSource, Customer } from '@/lib/database'; +import DeleteButton from './DeleteButton'; + +export default async function AdminCustomers() { + // Fetch customers from the database + const dataSource = await getDataSource(); + const customerRepository = dataSource.getRepository(Customer); + + const customers = await customerRepository.find({ + order: { createdAt: 'DESC' } + }); + + return ( +
+
+

Customers

+ + Add New Customer + +
+ +
+
+
+
+ + + + + + + + + + + + + {customers.length > 0 ? ( + customers.map((customer) => ( + + + + + + + + + )) + ) : ( + + + + )} + +
+ Name + + URL + + Email + + Created + + Modified + + Actions +
+ {customer.name} + + {customer.url ? ( + + {customer.url} + + ) : ( + - + )} + + + {customer.email} + + + {new Date(customer.createdAt).toLocaleDateString()} + + {new Date(customer.modifiedAt).toLocaleDateString()} + + + Edit + + +
+ No customers found. Create your first customer! +
+
+
+
+
+
+ ); +} diff --git a/src/app/(admin)/admin/layout.tsx b/src/app/(admin)/admin/layout.tsx index 8540574..e4c385c 100644 --- a/src/app/(admin)/admin/layout.tsx +++ b/src/app/(admin)/admin/layout.tsx @@ -100,6 +100,12 @@ export default function RootLayout({ > Users + + Customers +
diff --git a/src/app/api/customers/[id]/route.ts b/src/app/api/customers/[id]/route.ts new file mode 100644 index 0000000..bb6e1d7 --- /dev/null +++ b/src/app/api/customers/[id]/route.ts @@ -0,0 +1,119 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { getDataSource, Customer } from '@/lib/database'; + +// GET /api/customers/[id] - Get a specific customer +export async function GET( + request: NextRequest, + props: { params: Promise<{ id: string }> } +) { + try { + const { id } = await props.params; + const dataSource = await getDataSource(); + const customerRepository = dataSource.getRepository(Customer); + + const customer = await customerRepository.findOne({ + where: { id: id } + }); + + if (!customer) { + return NextResponse.json( + { error: 'Customer not found' }, + { status: 404 } + ); + } + + return NextResponse.json(customer); + } catch (error) { + console.error('Error fetching customer:', error); + return NextResponse.json( + { error: 'Failed to fetch customer' }, + { status: 500 } + ); + } +} + +// PUT /api/customers/[id] - Update a customer +export async function PUT( + request: NextRequest, + props: { params: Promise<{ id: string }> } +) { + try { + const { id } = await props.params; + const dataSource = await getDataSource(); + const customerRepository = dataSource.getRepository(Customer); + + // Find the customer to update + const customer = await customerRepository.findOne({ + where: { id: id } + }); + + if (!customer) { + return NextResponse.json( + { error: 'Customer not found' }, + { status: 404 } + ); + } + + const data = await request.json(); + const { name, url, email } = data; + + // Validate required fields + if (!name || !email) { + return NextResponse.json( + { error: 'Name and email are required' }, + { status: 400 } + ); + } + + // Update customer fields + customer.name = name; + customer.url = url || ''; + customer.email = email; + + // Save the updated customer + const updatedCustomer = await customerRepository.save(customer); + + return NextResponse.json(updatedCustomer); + } catch (error) { + console.error('Error updating customer:', error); + return NextResponse.json( + { error: 'Failed to update customer' }, + { status: 500 } + ); + } +} + +// DELETE /api/customers/[id] - Delete a customer +export async function DELETE( + request: NextRequest, + props: { params: Promise<{ id: string }> } +) { + try { + const { id } = await props.params; + const dataSource = await getDataSource(); + const customerRepository = dataSource.getRepository(Customer); + + // Find the customer to delete + const customer = await customerRepository.findOne({ + where: { id: id } + }); + + if (!customer) { + return NextResponse.json( + { error: 'Customer not found' }, + { status: 404 } + ); + } + + // Delete the customer + await customerRepository.remove(customer); + + return NextResponse.json({ success: true }); + } catch (error) { + console.error('Error deleting customer:', error); + return NextResponse.json( + { error: 'Failed to delete customer' }, + { status: 500 } + ); + } +} diff --git a/src/app/api/customers/route.ts b/src/app/api/customers/route.ts new file mode 100644 index 0000000..c489f4b --- /dev/null +++ b/src/app/api/customers/route.ts @@ -0,0 +1,57 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { getDataSource, Customer } from '@/lib/database'; + +// GET /api/customers - Get all customers +export async function GET(request: NextRequest) { + try { + const dataSource = await getDataSource(); + const customerRepository = dataSource.getRepository(Customer); + + const customers = await customerRepository.find({ + order: { createdAt: 'DESC' } + }); + + return NextResponse.json(customers); + } catch (error) { + console.error('Error fetching customers:', error); + return NextResponse.json( + { error: 'Failed to fetch customers' }, + { status: 500 } + ); + } +} + +// POST /api/customers - Create a new customer +export async function POST(request: NextRequest) { + try { + const dataSource = await getDataSource(); + const customerRepository = dataSource.getRepository(Customer); + + const data = await request.json(); + const { name, url, email } = data; + + // Validate required fields + if (!name || !email) { + return NextResponse.json( + { error: 'Name and email are required' }, + { status: 400 } + ); + } + + // Create and save the new customer + const customer = new Customer(); + customer.name = name; + customer.url = url || ''; + customer.email = email; + + const savedCustomer = await customerRepository.save(customer); + + return NextResponse.json(savedCustomer, { status: 201 }); + } catch (error) { + console.error('Error creating customer:', error); + return NextResponse.json( + { error: 'Failed to create customer' }, + { status: 500 } + ); + } +} diff --git a/src/lib/database/config.ts b/src/lib/database/config.ts index b28b592..8d9d649 100644 --- a/src/lib/database/config.ts +++ b/src/lib/database/config.ts @@ -1,13 +1,14 @@ import { DataSource, DataSourceOptions } from 'typeorm'; import { User } from './entities/User'; import { Post } from './entities/Post'; +import { Customer } from './entities/Customer'; import path from 'path'; // Default configuration for SQLite (development/testing) const sqliteConfig: DataSourceOptions = { type: 'sqlite', database: path.join(process.cwd(), 'data', 'database.sqlite'), - entities: [User, Post], + entities: [User, Post, Customer], synchronize: true, // Set to false in production logging: process.env.NODE_ENV === 'development', }; @@ -20,7 +21,7 @@ const mysqlConfig: DataSourceOptions = { username: process.env.DB_USERNAME || 'root', password: process.env.DB_PASSWORD || '', database: process.env.DB_DATABASE || 'kantancms', - entities: [User, Post], + entities: [User, Post, Customer], synchronize: false, // Always false in production logging: process.env.NODE_ENV === 'development', }; @@ -33,7 +34,7 @@ const postgresConfig: DataSourceOptions = { username: process.env.DB_USERNAME || 'postgres', password: process.env.DB_PASSWORD || '', database: process.env.DB_DATABASE || 'kantancms', - entities: [User, Post], + entities: [User, Post, Customer], synchronize: false, // Always false in production logging: process.env.NODE_ENV === 'development', }; diff --git a/src/lib/database/entities/Customer.ts b/src/lib/database/entities/Customer.ts new file mode 100644 index 0000000..466877a --- /dev/null +++ b/src/lib/database/entities/Customer.ts @@ -0,0 +1,22 @@ +import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from 'typeorm'; + +@Entity('customers') +export class Customer { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column() + name: string; + + @Column() + url: string; + + @Column() + email: string; + + @CreateDateColumn() + createdAt: Date; + + @UpdateDateColumn() + modifiedAt: Date; +} diff --git a/src/lib/database/index.ts b/src/lib/database/index.ts index f6f9164..6f7f8bb 100644 --- a/src/lib/database/index.ts +++ b/src/lib/database/index.ts @@ -34,3 +34,4 @@ export const getDataSource = async () => { // Export entities - Post must be exported after User to resolve circular dependency export * from './entities/User'; export * from './entities/Post'; +export * from './entities/Customer';