feat: implement events and testimonials base pages

This commit is contained in:
arraysid
2025-05-23 01:14:49 +07:00
parent 08b4c39c03
commit e13e5b4de2
4 changed files with 862 additions and 42 deletions
@@ -1,3 +1,194 @@
export default function Page() {
return <></>;
'use client';
import events from '@/data/events.json';
import { buttonVariants } from '@components/atoms';
import { cn } from '@utils/ui';
import Image from 'next/image';
import { HiCalendar, HiClock, HiLocationMarker } from 'react-icons/hi';
const formatDate = (dateString: string) => {
const date = new Date(dateString);
return date.toLocaleDateString('id-ID', {
day: 'numeric',
month: 'long',
year: 'numeric',
});
};
const formatTime = (dateString: string) => {
const date = new Date(dateString);
return date.toLocaleTimeString('id-ID', {
hour: '2-digit',
minute: '2-digit',
timeZone: 'Asia/Jakarta',
});
};
const getEventStatus = (endDate: string) => {
const now = new Date();
const end = new Date(endDate);
return end > now ? 'upcoming' : 'past';
};
export default function EventsPage() {
const sortedEvents = [...events].sort(
(a, b) =>
new Date(b.start_date).getTime() - new Date(a.start_date).getTime()
);
return (
<div className="min-h-screen bg-background">
<div className="max-w-7xl mx-auto px-4 py-12">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{/* Featured Card */}
<div className="col-span-full">
<div className="rounded-xl shadow-sm hover:shadow-md transition-shadow duration-200 overflow-hidden bg-card">
<div className="grid md:grid-cols-2">
<div className="min-h-96 bg-muted relative">
<Image
src={sortedEvents[0].thumbnail}
alt={sortedEvents[0].name}
fill
className="object-cover object-top"
sizes="(max-width: 768px) 100vw, 50vw"
priority
unoptimized
/>
</div>
<div className="p-8 flex flex-col">
<div className="flex items-center gap-2 mb-4">
<span className="bg-primary/20 text-primary text-xs px-2.5 py-1 rounded-full">
Event Terbaru
</span>
<span
className={cn(
'px-2 py-1 rounded-full text-xs',
getEventStatus(sortedEvents[0].end_date) === 'upcoming'
? 'bg-primary/20 text-primary'
: 'bg-muted text-muted-foreground'
)}
>
{getEventStatus(sortedEvents[0].end_date) === 'upcoming'
? 'Upcoming'
: 'Selesai'}
</span>
</div>
<h2 className="text-2xl font-bold mb-4 text-foreground">
{sortedEvents[0].name}
</h2>
<div className="space-y-3 mb-6 text-sm text-muted-foreground">
<div className="flex items-center gap-2">
<HiCalendar className="w-4 h-4" />
<span>{formatDate(sortedEvents[0].start_date)}</span>
</div>
<div className="flex items-center gap-2">
<HiClock className="w-4 h-4" />
<span>
{formatTime(sortedEvents[0].start_date)} -{' '}
{formatTime(sortedEvents[0].end_date)} WIB
</span>
</div>
<div className="flex items-center gap-2">
<HiLocationMarker className="w-4 h-4" />
<span>
{sortedEvents[0].type === 'online'
? 'Online'
: sortedEvents[0].location}
</span>
</div>
{sortedEvents[0].price > 0 && (
<div className="mt-1 font-medium">
Rp {sortedEvents[0].price.toLocaleString('id-ID')}
</div>
)}
</div>
<p className="text-muted-foreground mb-6 line-clamp-4">
{sortedEvents[0].description}
</p>
<a
href={sortedEvents[0].detail_link}
target="_blank"
className={cn(
buttonVariants({ variant: 'default' }),
'mt-auto w-full md:w-fit'
)}
>
Lihat Detail
</a>
</div>
</div>
</div>
</div>
{/* Regular Cards */}
{sortedEvents.slice(1).map((event) => (
<div
key={event.name}
className="rounded-xl shadow-sm hover:shadow-md transition-shadow duration-200 bg-card"
>
<div className="h-48 bg-muted relative">
<Image
src={event.thumbnail}
alt={event.name}
fill
className="object-cover object-top rounded-t-xl"
sizes="(max-width: 768px) 100vw, 33vw"
unoptimized
/>
</div>
<div className="p-6">
<div className="flex justify-between items-start mb-4">
<span
className={cn(
'px-2 py-1 rounded-full text-xs',
getEventStatus(event.end_date) === 'upcoming'
? 'bg-primary/20 text-primary'
: 'bg-muted text-muted-foreground'
)}
>
{getEventStatus(event.end_date) === 'upcoming'
? 'Upcoming'
: 'Selesai'}
</span>
</div>
<h3 className="text-lg font-semibold mb-3 text-foreground">
{event.name}
</h3>
<div className="space-y-2 mb-4 text-sm text-muted-foreground">
<div className="flex items-center gap-2">
<HiCalendar className="w-4 h-4" />
<span>{formatDate(event.start_date)}</span>
</div>
<div className="flex items-center gap-2">
<HiLocationMarker className="w-4 h-4" />
<span>
{event.type === 'online' ? 'Online' : event.location}
</span>
</div>
{event.price > 0 && (
<div className="font-medium">
Rp {event.price.toLocaleString('id-ID')}
</div>
)}
</div>
<p className="text-muted-foreground text-sm mb-4 line-clamp-3">
{event.description}
</p>
<a
href={event.detail_link}
target="_blank"
className={cn(
buttonVariants({ variant: 'outline' }),
'w-full text-sm'
)}
>
Lihat Detail
</a>
</div>
</div>
))}
</div>
</div>
</div>
);
}
@@ -1,3 +1,114 @@
export default function Page() {
return <></>;
import {
Button,
Input,
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@components/atoms';
import Link from 'next/link';
import { BsChatLeftQuote } from 'react-icons/bs';
const testimonials = [
{
id: 1,
name: 'User 1',
date: 'April 2025',
text: 'Bergabung dengan komunitas ini memberikan saya banyak inspirasi. Saya belajar banyak hal baru dan bertemu dengan orang-orang yang luar biasa.',
},
{
id: 2,
name: 'User 2',
date: 'Maret 2025',
text: 'Pengalaman yang sangat berharga. Saya mendapatkan banyak wawasan baru dan dapat mengembangkan keterampilan menulis saya dengan lebih baik.',
},
{
id: 3,
name: 'User 3',
date: 'Maret 2025',
text: 'Komunitas ini sangat mendukung dan memotivasi. Saya senang bisa menjadi bagian dari perjalanan ini dan berbagi pengalaman dengan sesama anggota.',
},
];
export default function TestimonialPage() {
return (
<div className="min-h-screen">
{/* Hero Section */}
<div className="py-16 px-4 text-center border-b border-border">
<BsChatLeftQuote className="size-14 mx-auto my-4 text-foreground" />
<h1 className="text-4xl font-bold mb-4 text-foreground">Testimonial</h1>
<p className="max-w-2xl mx-auto text-lg mb-8 text-balance text-muted-foreground">
Menampilkan pengalaman dan cerita para member yang telah join ke dalam
komunitas kami
</p>
<Link href="/testimonial/submit">
<Button>Tulis Testimonialmu</Button>
</Link>
</div>
{/* Search and Filter */}
<div className="max-w-7xl mx-auto px-4 py-6 border-b border-border">
<div className="flex flex-col md:flex-row gap-4 justify-between">
<div className="relative w-full md:w-96">
<Input
type="text"
placeholder="Cari berdasarkan topik..."
className="w-full bg-background"
/>
</div>
<div className="flex gap-4">
<Select defaultValue="all-time">
<SelectTrigger className="w-[180px] bg-background">
<SelectValue placeholder="Semua Periode" />
</SelectTrigger>
<SelectContent className="bg-popover text-popover-foreground">
<SelectItem value="all-time">Semua Periode</SelectItem>
<SelectItem value="this-month">Bulan Ini</SelectItem>
<SelectItem value="last-month">Bulan Lalu</SelectItem>
<SelectItem value="this-year">Tahun Ini</SelectItem>
</SelectContent>
</Select>
<Select defaultValue="all-topics">
<SelectTrigger className="w-[180px] bg-background">
<SelectValue placeholder="Semua Topik" />
</SelectTrigger>
<SelectContent className="bg-popover text-popover-foreground">
<SelectItem value="all-topics">Semua Topik</SelectItem>
<SelectItem value="writing">Menulis</SelectItem>
<SelectItem value="community">Komunitas</SelectItem>
<SelectItem value="learning">Pembelajaran</SelectItem>
</SelectContent>
</Select>
</div>
</div>
</div>
{/* Testimonial Cards */}
<div className="max-w-7xl mx-auto px-4 py-12">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{testimonials.map((testimonial) => (
<div
key={testimonial.id}
className="border rounded-lg p-6 shadow-sm bg-card border-border"
>
<div className="flex items-center mb-4">
<div>
<h3 className="font-semibold text-foreground">
{testimonial.name}
</h3>
<p className="text-sm text-muted-foreground">
Lulus: {testimonial.date}
</p>
</div>
</div>
<p className="text-foreground/80 italic">
&quot;{testimonial.text}&quot;
</p>
</div>
))}
</div>
</div>
</div>
);
}
File diff suppressed because one or more lines are too long
+510 -38
View File
@@ -116,6 +116,166 @@ export interface paths {
patch?: never;
trace?: never;
};
"/v1/gacha/claims/create": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
post: operations["post_create_gacha_claim"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/v1/gacha/claims/detail/{id}": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get: operations["get_detail_gacha_claim"];
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/v1/gacha/items": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get: operations["get_gacha_item_list"];
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/v1/gacha/items/create": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
post: operations["post_create_gacha_item"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/v1/gacha/items/delete/{id}": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
post?: never;
delete: operations["delete_gacha_item"];
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/v1/gacha/items/detail/{id}": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get: operations["get_gacha_item_by_id"];
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/v1/gacha/items/update/{id}": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put: operations["put_update_gacha_item"];
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/v1/gacha/rolls/create": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
post: operations["post_create_gacha_roll"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/v1/gacha/rolls/detail/{id}": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get: operations["get_detail_gacha_roll"];
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/v1/gacha/rolls/execute": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
post: operations["post_execute_gacha_roll"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/v1/permissions": {
parameters: {
query?: never;
@@ -414,7 +574,7 @@ export interface components {
};
AuthLoginResponsetDto: {
token: components["schemas"]["TokenDto"];
user: components["schemas"]["AuthUserItemDto"];
user: components["schemas"]["UsersDetailItemDto"];
};
AuthNewPasswordRequestDto: {
password: string;
@@ -428,29 +588,56 @@ export interface components {
fullname: string;
password: string;
phone_number: string;
referral_code?: string | null;
referred_by?: string | null;
student_type: string;
};
AuthResendOtpRequestDto: {
email: string;
};
AuthUserItemDto: {
avatar?: string | null;
birthdate?: string | null;
email: string;
fullname: string;
gender?: string | null;
id: string;
is_active: boolean;
phone_number: string;
role: components["schemas"]["RolesItemDto"];
};
AuthVerifyEmailRequestDto: {
email: string;
/** Format: int32 */
otp: number;
};
GachaClaimItemDto: {
created_at?: string | null;
id: string;
is_deleted: boolean;
item: components["schemas"]["GachaItemDto"];
updated_at?: string | null;
user: components["schemas"]["UsersDetailItemDto"];
};
GachaClaimRequestDto: {
item_id: string;
user_id: string;
};
GachaItemDto: {
created_at?: string | null;
id: string;
is_deleted: boolean;
name: string;
updated_at?: string | null;
};
GachaItemRequestDto: {
image_url: string;
name: string;
};
GachaRollItemDto: {
created_at?: string | null;
id: string;
is_deleted: boolean;
item: components["schemas"]["GachaItemDto"];
/** Format: int32 */
quantity: number;
updated_at?: string | null;
/** Format: float */
weight: number;
};
GachaRollRequestDto: {
item_id: string;
/** Format: int32 */
quantity: number;
/** Format: float */
weight: number;
};
MessageResponseDto: {
message: string;
version: string;
@@ -483,6 +670,16 @@ export interface components {
PermissionsRequestDto: {
name: string;
};
ResponseListSuccessDto_Vec_GachaItemDto: {
data: {
created_at?: string | null;
id: string;
is_deleted: boolean;
name: string;
updated_at?: string | null;
}[];
meta?: null | components["schemas"]["MetaResponseDto"];
};
ResponseListSuccessDto_Vec_PermissionsItemDto: {
data: {
created_at?: string | null;
@@ -492,12 +689,12 @@ export interface components {
}[];
meta?: null | components["schemas"]["MetaResponseDto"];
};
ResponseListSuccessDto_Vec_RolesItemDto: {
ResponseListSuccessDto_Vec_RolesListItemDto: {
data: {
created_at?: string | null;
id: string;
name: string;
permissions: components["schemas"]["PermissionsItemDto"][];
permissions_count: number;
updated_at?: string | null;
}[];
meta?: null | components["schemas"]["MetaResponseDto"];
@@ -519,7 +716,39 @@ export interface components {
ResponseSuccessDto_AuthLoginResponsetDto: {
data: {
token: components["schemas"]["TokenDto"];
user: components["schemas"]["AuthUserItemDto"];
user: components["schemas"]["UsersDetailItemDto"];
};
};
ResponseSuccessDto_GachaClaimItemDto: {
data: {
created_at?: string | null;
id: string;
is_deleted: boolean;
item: components["schemas"]["GachaItemDto"];
updated_at?: string | null;
user: components["schemas"]["UsersDetailItemDto"];
};
};
ResponseSuccessDto_GachaItemDto: {
data: {
created_at?: string | null;
id: string;
is_deleted: boolean;
name: string;
updated_at?: string | null;
};
};
ResponseSuccessDto_GachaRollItemDto: {
data: {
created_at?: string | null;
id: string;
is_deleted: boolean;
item: components["schemas"]["GachaItemDto"];
/** Format: int32 */
quantity: number;
updated_at?: string | null;
/** Format: float */
weight: number;
};
};
ResponseSuccessDto_PermissionsItemDto: {
@@ -530,10 +759,11 @@ export interface components {
updated_at?: string | null;
};
};
ResponseSuccessDto_RolesItemDto: {
ResponseSuccessDto_RolesDetailItemDto: {
data: {
created_at?: string | null;
id: string;
is_deleted: boolean;
name: string;
permissions: components["schemas"]["PermissionsItemDto"][];
updated_at?: string | null;
@@ -549,20 +779,30 @@ export interface components {
data: {
avatar?: string | null;
birthdate?: string | null;
created_at: string;
email: string;
fullname: string;
gender?: string | null;
id: string;
is_active: boolean;
phone_number: string;
role: components["schemas"]["RolesItemDto"];
role: components["schemas"]["RolesDetailItemDto"];
updated_at: string;
};
};
RolesItemDto: {
RolesDetailItemDto: {
created_at?: string | null;
id: string;
is_deleted: boolean;
name: string;
permissions: components["schemas"]["PermissionsItemDto"][];
updated_at?: string | null;
};
RolesListItemDto: {
created_at?: string | null;
id: string;
name: string;
permissions: components["schemas"]["PermissionsItemDto"][];
permissions_count: number;
updated_at?: string | null;
};
RolesRequestCreateDto: {
@@ -571,6 +811,7 @@ export interface components {
};
RolesRequestUpdateDto: {
name?: string | null;
overwrite?: boolean | null;
permissions?: string[] | null;
};
TokenDto: {
@@ -589,17 +830,6 @@ export interface components {
role_id: string;
};
UsersDetailItemDto: {
avatar?: string | null;
birthdate?: string | null;
email: string;
fullname: string;
gender?: string | null;
id: string;
is_active: boolean;
phone_number: string;
role: components["schemas"]["RolesItemDto"];
};
UsersItemDto: {
avatar?: string | null;
birthdate?: string | null;
created_at: string;
@@ -608,10 +838,8 @@ export interface components {
gender?: string | null;
id: string;
is_active: boolean;
is_deleted: boolean;
password: string;
phone_number: string;
role: components["schemas"]["RolesItemDto"];
role: components["schemas"]["RolesDetailItemDto"];
updated_at: string;
};
UsersListItemDto: {
@@ -875,6 +1103,250 @@ export interface operations {
};
};
};
post_create_gacha_claim: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody: {
content: {
"application/json": components["schemas"]["GachaClaimRequestDto"];
};
};
responses: {
/** @description Create new gacha claim */
201: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["MessageResponseDto"];
};
};
};
};
get_detail_gacha_claim: {
parameters: {
query?: never;
header?: never;
path: {
/** @description Gacha Claim ID */
id: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Get Gacha Claim by ID */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ResponseSuccessDto_GachaClaimItemDto"];
};
};
};
};
get_gacha_item_list: {
parameters: {
query?: {
/** @description Page number */
page?: number;
/** @description Items per page */
per_page?: number;
/** @description Search keyword */
search?: string;
/** @description Sort by field */
sort_by?: string;
/** @description Order ASC or DESC */
order?: string;
/** @description Filter value */
filter?: string;
/** @description Field to filter by */
filter_by?: string;
};
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Get gacha item list */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ResponseListSuccessDto_Vec_GachaItemDto"];
};
};
};
};
post_create_gacha_item: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody: {
content: {
"application/json": components["schemas"]["GachaItemRequestDto"];
};
};
responses: {
/** @description Create gacha item */
201: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["MessageResponseDto"];
};
};
};
};
delete_gacha_item: {
parameters: {
query?: never;
header?: never;
path: {
id: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Delete gacha item */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["MessageResponseDto"];
};
};
};
};
get_gacha_item_by_id: {
parameters: {
query?: never;
header?: never;
path: {
/** @description Gacha Item ID */
id: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Get gacha item by ID */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ResponseSuccessDto_GachaItemDto"];
};
};
};
};
put_update_gacha_item: {
parameters: {
query?: never;
header?: never;
path: {
id: string;
};
cookie?: never;
};
requestBody: {
content: {
"application/json": components["schemas"]["GachaItemRequestDto"];
};
};
responses: {
/** @description Update gacha item */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["MessageResponseDto"];
};
};
};
};
post_create_gacha_roll: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody: {
content: {
"application/json": components["schemas"]["GachaRollRequestDto"];
};
};
responses: {
/** @description Create new gacha roll */
201: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["MessageResponseDto"];
};
};
};
};
get_detail_gacha_roll: {
parameters: {
query?: never;
header?: never;
path: {
/** @description Gacha Roll ID */
id: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Get Gacha Roll by ID */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ResponseSuccessDto_GachaRollItemDto"];
};
};
};
};
post_execute_gacha_roll: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Execute and get 1 gacha result */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ResponseSuccessDto_GachaRollItemDto"];
};
};
};
};
get_permission_list: {
parameters: {
query?: {
@@ -1035,7 +1507,7 @@ export interface operations {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ResponseListSuccessDto_Vec_RolesItemDto"];
"application/json": components["schemas"]["ResponseListSuccessDto_Vec_RolesListItemDto"];
};
};
};
@@ -1104,7 +1576,7 @@ export interface operations {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ResponseSuccessDto_RolesItemDto"];
"application/json": components["schemas"]["ResponseSuccessDto_RolesDetailItemDto"];
};
};
};