Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/controllers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
94 changes: 94 additions & 0 deletions src/controllers/testimonials.controller.ts
Original file line number Diff line number Diff line change
@@ -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<CreateTestimonialRequest>;
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,
);
}
}
}
13 changes: 13 additions & 0 deletions src/db/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,18 @@ export async function createTables(): Promise<void> {
)
`;

// 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)`;
Expand All @@ -136,6 +148,7 @@ export async function createTables(): Promise<void> {

export async function dropTables(): Promise<void> {
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`;
Expand Down
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
projectsRoutes,
blogsRoutes,
clientInformationRoutes,
testimonialsRoutes,
} from "./routes";

const app = new Hono();
Expand Down Expand Up @@ -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() {
Expand Down
1 change: 1 addition & 0 deletions src/repositories/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
43 changes: 43 additions & 0 deletions src/repositories/testimonials.repository.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { sql } from "../db";
import type { Testimonial, CreateTestimonialRequest } from "../types";

export class TestimonialsRepository {
static async findAll(): Promise<Testimonial[]> {
return await sql<Testimonial[]>`SELECT * FROM testimonials ORDER BY created_at DESC`;
}

static async findById(id: number): Promise<Testimonial | null> {
const result = await sql<Testimonial[]>`SELECT * FROM testimonials WHERE id = ${id}`;
return result[0] || null;
}

static async create(data: CreateTestimonialRequest): Promise<Testimonial> {
const result = await sql<Testimonial[]>`
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<CreateTestimonialRequest>,
): Promise<Testimonial | null> {
const result = await sql<Testimonial[]>`
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<boolean> {
const result = await sql`DELETE FROM testimonials WHERE id = ${id}`;
return result.count > 0;
}
}
1 change: 1 addition & 0 deletions src/routes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
9 changes: 9 additions & 0 deletions src/routes/testimonials.routes.ts
Original file line number Diff line number Diff line change
@@ -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);
1 change: 1 addition & 0 deletions src/services/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
63 changes: 63 additions & 0 deletions src/services/testimonials.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { TestimonialsRepository } from "../repositories";
import type { Testimonial, CreateTestimonialRequest } from "../types";

export class TestimonialsService {
static async getAllTestimonials(): Promise<Testimonial[]> {
return await TestimonialsRepository.findAll();
}

static async getTestimonialById(id: number): Promise<Testimonial | null> {
if (!id || id <= 0) {
throw new Error("Invalid testimonial ID");
}
return await TestimonialsRepository.findById(id);
}

static async createTestimonial(data: CreateTestimonialRequest): Promise<Testimonial> {
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<CreateTestimonialRequest>,
): Promise<Testimonial | null> {
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<boolean> {
if (!id || id <= 0) {
throw new Error("Invalid testimonial ID");
}
return await TestimonialsRepository.delete(id);
}
}
15 changes: 15 additions & 0 deletions src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down