diff --git a/src/controllers/index.ts b/src/controllers/index.ts index 8bd19dd..082442f 100644 --- a/src/controllers/index.ts +++ b/src/controllers/index.ts @@ -4,3 +4,4 @@ export { ServicesController } from "./services.controller"; export { ProjectsController } from "./projects.controller"; export { BlogsController } from "./blogs.controller"; export { ClientInformationController } from "./client-information.controller"; +export { TestimonialsController } from "./testimonials.controller"; diff --git a/src/controllers/testimonials.controller.ts b/src/controllers/testimonials.controller.ts new file mode 100644 index 0000000..348db97 --- /dev/null +++ b/src/controllers/testimonials.controller.ts @@ -0,0 +1,94 @@ +import type { Context } from "hono"; +import { TestimonialsService } from "../services"; +import type { CreateTestimonialRequest } from "../types"; + +export class TestimonialsController { + static async getAllTestimonials(c: Context) { + try { + const testimonials = await TestimonialsService.getAllTestimonials(); + return c.json({ success: true, data: testimonials }); + } catch (error) { + return c.json( + { + success: false, + error: error instanceof Error ? error.message : "Unknown error", + }, + 500, + ); + } + } + + static async getTestimonialById(c: Context) { + try { + const id = parseInt(c.req.param("id"), 10); + const testimonial = await TestimonialsService.getTestimonialById(id); + if (!testimonial) { + return c.json({ success: false, error: "Testimonial not found" }, 404); + } + return c.json({ success: true, data: testimonial }); + } catch (error) { + return c.json( + { + success: false, + error: error instanceof Error ? error.message : "Unknown error", + }, + 500, + ); + } + } + + static async createTestimonial(c: Context) { + try { + const data = (await c.req.json()) as CreateTestimonialRequest; + const testimonial = await TestimonialsService.createTestimonial(data); + return c.json({ success: true, data: testimonial }, 201); + } catch (error) { + return c.json( + { + success: false, + error: error instanceof Error ? error.message : "Unknown error", + }, + 400, + ); + } + } + + static async updateTestimonial(c: Context) { + try { + const id = parseInt(c.req.param("id"), 10); + const data = (await c.req.json()) as Partial; + const testimonial = await TestimonialsService.updateTestimonial(id, data); + if (!testimonial) { + return c.json({ success: false, error: "Testimonial not found" }, 404); + } + return c.json({ success: true, data: testimonial }); + } catch (error) { + return c.json( + { + success: false, + error: error instanceof Error ? error.message : "Unknown error", + }, + 400, + ); + } + } + + static async deleteTestimonial(c: Context) { + try { + const id = parseInt(c.req.param("id"), 10); + const deleted = await TestimonialsService.deleteTestimonial(id); + if (!deleted) { + return c.json({ success: false, error: "Testimonial not found" }, 404); + } + return c.json({ success: true, message: "Testimonial deleted successfully" }); + } catch (error) { + return c.json( + { + success: false, + error: error instanceof Error ? error.message : "Unknown error", + }, + 500, + ); + } + } +} diff --git a/src/db/schema.ts b/src/db/schema.ts index 9fbdca4..da0f3d6 100644 --- a/src/db/schema.ts +++ b/src/db/schema.ts @@ -118,6 +118,18 @@ export async function createTables(): Promise { ) `; + // Create testimonials table + await sql` + CREATE TABLE IF NOT EXISTS testimonials ( + id SERIAL PRIMARY KEY, + full_name VARCHAR(100) NOT NULL, + role VARCHAR(100) NOT NULL, + description VARCHAR(500) NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) + `; + // Create indexes for better performance await sql`CREATE INDEX IF NOT EXISTS idx_projects_tag_id ON projects(tag_id)`; await sql`CREATE INDEX IF NOT EXISTS idx_project_tech_stack_project_id ON project_tech_stack(project_id)`; @@ -136,6 +148,7 @@ export async function createTables(): Promise { export async function dropTables(): Promise { try { + await sql`DROP TABLE IF EXISTS testimonials CASCADE`; await sql`DROP TABLE IF EXISTS client_information CASCADE`; await sql`DROP TABLE IF EXISTS blogs CASCADE`; await sql`DROP TABLE IF EXISTS project_tech_stack CASCADE`; diff --git a/src/index.ts b/src/index.ts index 3651023..e4eaad1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,6 +9,7 @@ import { projectsRoutes, blogsRoutes, clientInformationRoutes, + testimonialsRoutes, } from "./routes"; const app = new Hono(); @@ -54,6 +55,7 @@ app.route("/api/services", servicesRoutes); app.route("/api/projects", projectsRoutes); app.route("/api/blogs", blogsRoutes); app.route("/api/client-information", clientInformationRoutes); +app.route("/api/testimonials", testimonialsRoutes); // Initialize database on startup async function initializeDatabase() { diff --git a/src/repositories/index.ts b/src/repositories/index.ts index 008f2c5..24323c5 100644 --- a/src/repositories/index.ts +++ b/src/repositories/index.ts @@ -4,3 +4,4 @@ export { ServicesRepository } from "./services.repository"; export { ProjectsRepository } from "./projects.repository"; export { BlogsRepository } from "./blogs.repository"; export { ClientInformationRepository } from "./client-information.repository"; +export { TestimonialsRepository } from "./testimonials.repository"; diff --git a/src/repositories/testimonials.repository.ts b/src/repositories/testimonials.repository.ts new file mode 100644 index 0000000..51ba56b --- /dev/null +++ b/src/repositories/testimonials.repository.ts @@ -0,0 +1,43 @@ +import { sql } from "../db"; +import type { Testimonial, CreateTestimonialRequest } from "../types"; + +export class TestimonialsRepository { + static async findAll(): Promise { + return await sql`SELECT * FROM testimonials ORDER BY created_at DESC`; + } + + static async findById(id: number): Promise { + const result = await sql`SELECT * FROM testimonials WHERE id = ${id}`; + return result[0] || null; + } + + static async create(data: CreateTestimonialRequest): Promise { + const result = await sql` + INSERT INTO testimonials (full_name, role, description) + VALUES (${data.full_name}, ${data.role}, ${data.description}) + RETURNING * + `; + return result[0]; + } + + static async update( + id: number, + data: Partial, + ): Promise { + const result = await sql` + UPDATE testimonials + SET full_name = COALESCE(${data.full_name ?? null}, full_name), + role = COALESCE(${data.role ?? null}, role), + description = COALESCE(${data.description ?? null}, description), + updated_at = CURRENT_TIMESTAMP + WHERE id = ${id} + RETURNING * + `; + return result[0] || null; + } + + static async delete(id: number): Promise { + const result = await sql`DELETE FROM testimonials WHERE id = ${id}`; + return result.count > 0; + } +} diff --git a/src/routes/index.ts b/src/routes/index.ts index c432103..46cdb36 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -4,3 +4,4 @@ export { servicesRoutes } from "./services.routes"; export { projectsRoutes } from "./projects.routes"; export { blogsRoutes } from "./blogs.routes"; export { clientInformationRoutes } from "./client-information.routes"; +export { testimonialsRoutes } from "./testimonials.routes"; diff --git a/src/routes/testimonials.routes.ts b/src/routes/testimonials.routes.ts new file mode 100644 index 0000000..192b8b1 --- /dev/null +++ b/src/routes/testimonials.routes.ts @@ -0,0 +1,9 @@ +import { Hono } from "hono"; +import { TestimonialsController } from "../controllers"; + +export const testimonialsRoutes = new Hono() + .get("/", TestimonialsController.getAllTestimonials) + .get("/:id", TestimonialsController.getTestimonialById) + .post("/", TestimonialsController.createTestimonial) + .put("/:id", TestimonialsController.updateTestimonial) + .delete("/:id", TestimonialsController.deleteTestimonial); diff --git a/src/services/index.ts b/src/services/index.ts index ce30d64..fb72743 100644 --- a/src/services/index.ts +++ b/src/services/index.ts @@ -4,3 +4,4 @@ export { ServicesService } from "./services.service"; export { ProjectsService } from "./projects.service"; export { BlogsService } from "./blogs.service"; export { ClientInformationService } from "./client-information.service"; +export { TestimonialsService } from "./testimonials.service"; diff --git a/src/services/testimonials.service.ts b/src/services/testimonials.service.ts new file mode 100644 index 0000000..0d6bd5a --- /dev/null +++ b/src/services/testimonials.service.ts @@ -0,0 +1,63 @@ +import { TestimonialsRepository } from "../repositories"; +import type { Testimonial, CreateTestimonialRequest } from "../types"; + +export class TestimonialsService { + static async getAllTestimonials(): Promise { + return await TestimonialsRepository.findAll(); + } + + static async getTestimonialById(id: number): Promise { + if (!id || id <= 0) { + throw new Error("Invalid testimonial ID"); + } + return await TestimonialsRepository.findById(id); + } + + static async createTestimonial(data: CreateTestimonialRequest): Promise { + if (!data.full_name || data.full_name.trim().length === 0) { + throw new Error("Full name is required"); + } + if (data.full_name.length > 100) { + throw new Error("Full name cannot exceed 100 characters"); + } + if (!data.role || data.role.trim().length === 0) { + throw new Error("Role is required"); + } + if (data.role.length > 100) { + throw new Error("Role cannot exceed 100 characters"); + } + if (!data.description || data.description.trim().length === 0) { + throw new Error("Description is required"); + } + if (data.description.length > 500) { + throw new Error("Description cannot exceed 500 characters"); + } + return await TestimonialsRepository.create(data); + } + + static async updateTestimonial( + id: number, + data: Partial, + ): Promise { + if (!id || id <= 0) { + throw new Error("Invalid testimonial ID"); + } + if (data.full_name !== undefined && data.full_name.length > 100) { + throw new Error("Full name cannot exceed 100 characters"); + } + if (data.role !== undefined && data.role.length > 100) { + throw new Error("Role cannot exceed 100 characters"); + } + if (data.description !== undefined && data.description.length > 500) { + throw new Error("Description cannot exceed 500 characters"); + } + return await TestimonialsRepository.update(id, data); + } + + static async deleteTestimonial(id: number): Promise { + if (!id || id <= 0) { + throw new Error("Invalid testimonial ID"); + } + return await TestimonialsRepository.delete(id); + } +} diff --git a/src/types/index.ts b/src/types/index.ts index 4d8258e..cd4d9af 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -138,6 +138,21 @@ export interface CreateServiceRequest { service_description: string; } +export interface Testimonial { + id: number; + full_name: string; + role: string; + description: string; + created_at: Date; + updated_at: Date; +} + +export interface CreateTestimonialRequest { + full_name: string; + role: string; + description: string; +} + export interface CreateClientInformationRequest { nama_lengkap: string; email: string;