Changes
This commit is contained in:
@@ -1,17 +1,33 @@
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Layout } from '@/components/Layout';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { ArrowRight, BookOpen, Video, Users } from 'lucide-react';
|
||||
import { useBranding } from '@/hooks/useBranding';
|
||||
import { ArrowRight, BookOpen, Video, Users, Star, Award, Target, Zap, Heart, Shield, Rocket } from 'lucide-react';
|
||||
|
||||
const iconMap: Record<string, React.ComponentType<{ className?: string }>> = {
|
||||
Users,
|
||||
Video,
|
||||
BookOpen,
|
||||
Star,
|
||||
Award,
|
||||
Target,
|
||||
Zap,
|
||||
Heart,
|
||||
Shield,
|
||||
Rocket,
|
||||
};
|
||||
|
||||
export default function Index() {
|
||||
const branding = useBranding();
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<section className="container mx-auto px-4 py-16 text-center">
|
||||
<h1 className="text-5xl md:text-6xl font-bold mb-6">
|
||||
Learn. Grow. Succeed.
|
||||
{branding.homepage_headline}
|
||||
</h1>
|
||||
<p className="text-xl text-muted-foreground max-w-2xl mx-auto mb-8">
|
||||
Access premium consulting, live webinars, and intensive bootcamps to accelerate your career.
|
||||
{branding.homepage_description}
|
||||
</p>
|
||||
<div className="flex gap-4 justify-center">
|
||||
<Link to="/products">
|
||||
@@ -30,27 +46,18 @@ export default function Index() {
|
||||
|
||||
<section className="container mx-auto px-4 py-16">
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
|
||||
<div className="border-2 border-border p-8 shadow-sm">
|
||||
<Users className="w-12 h-12 mb-4" />
|
||||
<h3 className="text-2xl font-bold mb-2">Consulting</h3>
|
||||
<p className="text-muted-foreground">
|
||||
One-on-one sessions with industry experts to solve your specific challenges.
|
||||
</p>
|
||||
</div>
|
||||
<div className="border-2 border-border p-8 shadow-sm">
|
||||
<Video className="w-12 h-12 mb-4" />
|
||||
<h3 className="text-2xl font-bold mb-2">Webinars</h3>
|
||||
<p className="text-muted-foreground">
|
||||
Live and recorded sessions covering the latest trends and techniques.
|
||||
</p>
|
||||
</div>
|
||||
<div className="border-2 border-border p-8 shadow-sm">
|
||||
<BookOpen className="w-12 h-12 mb-4" />
|
||||
<h3 className="text-2xl font-bold mb-2">Bootcamps</h3>
|
||||
<p className="text-muted-foreground">
|
||||
Intensive programs to master new skills in weeks, not months.
|
||||
</p>
|
||||
</div>
|
||||
{branding.homepage_features.map((feature, index) => {
|
||||
const IconComponent = iconMap[feature.icon] || Users;
|
||||
return (
|
||||
<div key={index} className="border-2 border-border p-8 shadow-sm">
|
||||
<IconComponent className="w-12 h-12 mb-4" />
|
||||
<h3 className="text-2xl font-bold mb-2">{feature.title}</h3>
|
||||
<p className="text-muted-foreground">
|
||||
{feature.description}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
</Layout>
|
||||
|
||||
@@ -86,6 +86,13 @@ export default function Products() {
|
||||
}
|
||||
};
|
||||
|
||||
// Strip HTML tags for preview, but keep first 100 chars
|
||||
const stripHtml = (html: string) => {
|
||||
const tmp = document.createElement('div');
|
||||
tmp.innerHTML = html;
|
||||
return tmp.textContent || tmp.innerText || '';
|
||||
};
|
||||
|
||||
return (
|
||||
<AppLayout>
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
@@ -149,7 +156,10 @@ export default function Products() {
|
||||
<CardTitle className="text-xl">{product.title}</CardTitle>
|
||||
<Badge className="bg-secondary">{getTypeLabel(product.type)}</Badge>
|
||||
</div>
|
||||
<CardDescription className="line-clamp-2">{product.description}</CardDescription>
|
||||
<CardDescription
|
||||
className="line-clamp-2"
|
||||
dangerouslySetInnerHTML={{ __html: product.description }}
|
||||
/>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useNavigate, Link } from "react-router-dom";
|
||||
import { AppLayout } from "@/components/AppLayout";
|
||||
import { useAuth } from "@/hooks/useAuth";
|
||||
import { supabase } from "@/integrations/supabase/client";
|
||||
@@ -7,6 +7,7 @@ import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { formatIDR, formatDate } from "@/lib/format";
|
||||
import { ChevronRight } from "lucide-react";
|
||||
|
||||
interface Order {
|
||||
id: string;
|
||||
@@ -96,25 +97,28 @@ export default function MemberOrders() {
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{orders.map((order) => (
|
||||
<Card key={order.id} className="border-2 border-border">
|
||||
<CardContent className="py-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="font-mono text-sm text-muted-foreground">{order.id.slice(0, 8)}</p>
|
||||
<p className="text-sm text-muted-foreground">{formatDate(order.created_at)}</p>
|
||||
{order.payment_method && (
|
||||
<p className="text-xs text-muted-foreground uppercase">{order.payment_method}</p>
|
||||
)}
|
||||
<Link key={order.id} to={`/orders/${order.id}`}>
|
||||
<Card className="border-2 border-border hover:border-primary transition-colors cursor-pointer">
|
||||
<CardContent className="py-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="font-mono text-sm text-muted-foreground">{order.id.slice(0, 8)}</p>
|
||||
<p className="text-sm text-muted-foreground">{formatDate(order.created_at)}</p>
|
||||
{order.payment_method && (
|
||||
<p className="text-xs text-muted-foreground uppercase">{order.payment_method}</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<Badge className={getStatusColor(order.payment_status || order.status)}>
|
||||
{getPaymentStatusLabel(order.payment_status || order.status)}
|
||||
</Badge>
|
||||
<span className="font-bold">{formatIDR(order.total_amount)}</span>
|
||||
<ChevronRight className="w-5 h-5 text-muted-foreground" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<Badge className={getStatusColor(order.payment_status || order.status)}>
|
||||
{getPaymentStatusLabel(order.payment_status || order.status)}
|
||||
</Badge>
|
||||
<span className="font-bold">{formatIDR(order.total_amount)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
258
src/pages/member/OrderDetail.tsx
Normal file
258
src/pages/member/OrderDetail.tsx
Normal file
@@ -0,0 +1,258 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useNavigate, useParams, Link } from "react-router-dom";
|
||||
import { AppLayout } from "@/components/AppLayout";
|
||||
import { useAuth } from "@/hooks/useAuth";
|
||||
import { supabase } from "@/integrations/supabase/client";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { formatIDR, formatDate } from "@/lib/format";
|
||||
import { ArrowLeft, Package, CreditCard, Calendar, Clock } from "lucide-react";
|
||||
|
||||
interface OrderItem {
|
||||
id: string;
|
||||
product_id: string;
|
||||
quantity: number;
|
||||
price: number;
|
||||
products: {
|
||||
title: string;
|
||||
type: string;
|
||||
slug: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface Order {
|
||||
id: string;
|
||||
total_amount: number;
|
||||
status: string;
|
||||
payment_status: string | null;
|
||||
payment_method: string | null;
|
||||
payment_provider: string | null;
|
||||
payment_url: string | null;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
order_items: OrderItem[];
|
||||
}
|
||||
|
||||
export default function OrderDetail() {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const { user, loading: authLoading } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
const [order, setOrder] = useState<Order | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
if (!authLoading && !user) navigate("/auth");
|
||||
else if (user && id) fetchOrder();
|
||||
}, [user, authLoading, id]);
|
||||
|
||||
const fetchOrder = async () => {
|
||||
const { data, error } = await supabase
|
||||
.from("orders")
|
||||
.select(`
|
||||
*,
|
||||
order_items (
|
||||
id,
|
||||
product_id,
|
||||
quantity,
|
||||
price,
|
||||
products (title, type, slug)
|
||||
)
|
||||
`)
|
||||
.eq("id", id)
|
||||
.eq("user_id", user!.id)
|
||||
.single();
|
||||
|
||||
if (error || !data) {
|
||||
navigate("/orders");
|
||||
return;
|
||||
}
|
||||
|
||||
setOrder(data as Order);
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case "paid":
|
||||
return "bg-accent text-primary";
|
||||
case "pending":
|
||||
return "bg-secondary text-primary";
|
||||
case "cancelled":
|
||||
case "failed":
|
||||
return "bg-destructive";
|
||||
default:
|
||||
return "bg-secondary text-primary";
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusLabel = (status: string | null) => {
|
||||
switch (status) {
|
||||
case "paid":
|
||||
return "Lunas";
|
||||
case "pending":
|
||||
return "Menunggu Pembayaran";
|
||||
case "failed":
|
||||
return "Gagal";
|
||||
case "cancelled":
|
||||
return "Dibatalkan";
|
||||
default:
|
||||
return status || "Pending";
|
||||
}
|
||||
};
|
||||
|
||||
const getTypeLabel = (type: string) => {
|
||||
switch (type) {
|
||||
case "consulting":
|
||||
return "Konsultasi";
|
||||
case "webinar":
|
||||
return "Webinar";
|
||||
case "bootcamp":
|
||||
return "Bootcamp";
|
||||
default:
|
||||
return type;
|
||||
}
|
||||
};
|
||||
|
||||
if (authLoading || loading) {
|
||||
return (
|
||||
<AppLayout>
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
<Skeleton className="h-10 w-1/3 mb-8" />
|
||||
<Skeleton className="h-64 w-full" />
|
||||
</div>
|
||||
</AppLayout>
|
||||
);
|
||||
}
|
||||
|
||||
if (!order) return null;
|
||||
|
||||
return (
|
||||
<AppLayout>
|
||||
<div className="container mx-auto px-4 py-8 max-w-3xl">
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => navigate("/orders")}
|
||||
className="mb-4"
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4 mr-2" />
|
||||
Kembali ke Riwayat Order
|
||||
</Button>
|
||||
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold">Detail Order</h1>
|
||||
<p className="text-muted-foreground font-mono">#{order.id.slice(0, 8)}</p>
|
||||
</div>
|
||||
<Badge className={getStatusColor(order.payment_status || order.status)}>
|
||||
{getStatusLabel(order.payment_status || order.status)}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
{/* Order Info */}
|
||||
<Card className="border-2 border-border mb-6">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg flex items-center gap-2">
|
||||
<Calendar className="w-5 h-5" />
|
||||
Informasi Order
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4 text-sm">
|
||||
<div>
|
||||
<p className="text-muted-foreground">Tanggal Order</p>
|
||||
<p className="font-medium">{formatDate(order.created_at)}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-muted-foreground">Terakhir Update</p>
|
||||
<p className="font-medium">{formatDate(order.updated_at)}</p>
|
||||
</div>
|
||||
{order.payment_method && (
|
||||
<div>
|
||||
<p className="text-muted-foreground">Metode Pembayaran</p>
|
||||
<p className="font-medium uppercase">{order.payment_method}</p>
|
||||
</div>
|
||||
)}
|
||||
{order.payment_provider && (
|
||||
<div>
|
||||
<p className="text-muted-foreground">Provider</p>
|
||||
<p className="font-medium capitalize">{order.payment_provider}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{order.payment_status === "pending" && order.payment_url && (
|
||||
<div className="pt-4">
|
||||
<Button asChild className="w-full shadow-sm">
|
||||
<a href={order.payment_url} target="_blank" rel="noopener noreferrer">
|
||||
<CreditCard className="w-4 h-4 mr-2" />
|
||||
Lanjutkan Pembayaran
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Order Items */}
|
||||
<Card className="border-2 border-border mb-6">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg flex items-center gap-2">
|
||||
<Package className="w-5 h-5" />
|
||||
Item Pesanan
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
{order.order_items.map((item) => (
|
||||
<div key={item.id} className="flex items-center justify-between py-2">
|
||||
<div className="flex-1">
|
||||
<Link
|
||||
to={`/products/${item.products.slug}`}
|
||||
className="font-medium hover:underline"
|
||||
>
|
||||
{item.products.title}
|
||||
</Link>
|
||||
<div className="flex items-center gap-2 mt-1">
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{getTypeLabel(item.products.type)}
|
||||
</Badge>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
x{item.quantity}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<p className="font-medium">{formatIDR(item.price)}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<Separator className="my-4" />
|
||||
|
||||
<div className="flex items-center justify-between text-lg font-bold">
|
||||
<span>Total</span>
|
||||
<span>{formatIDR(order.total_amount)}</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Access Info */}
|
||||
{order.payment_status === "paid" && (
|
||||
<Card className="border-2 border-primary bg-primary/5">
|
||||
<CardContent className="py-4">
|
||||
<p className="text-sm">
|
||||
Pembayaran berhasil! Akses produk Anda tersedia di halaman{" "}
|
||||
<Link to="/access" className="font-medium underline">
|
||||
Akses Saya
|
||||
</Link>
|
||||
.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
</AppLayout>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user