fix ui style
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
import { Download } from "lucide-react";
|
||||
|
||||
interface ExportCsvLinkProps {
|
||||
/** URL endpoint export, mis. `/api/admin/export/refunds`. */
|
||||
href: string;
|
||||
@@ -22,7 +24,7 @@ export function ExportCsvLink({
|
||||
className="inline-flex items-center gap-1.5 rounded-xl border border-neutral-200 bg-white px-3 py-1.5 text-xs font-semibold text-neutral-700 hover:bg-neutral-50"
|
||||
download
|
||||
>
|
||||
<span aria-hidden>⬇️</span>
|
||||
<Download size={16} strokeWidth={2} aria-hidden />
|
||||
<span>{label}</span>
|
||||
</a>
|
||||
);
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Lock } from "lucide-react";
|
||||
import { manualOverrideVerificationAction } from "@/features/organizer/actions";
|
||||
|
||||
interface ManualVerifyButtonProps {
|
||||
@@ -49,9 +50,10 @@ export function ManualVerifyButton({
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setOpen(true)}
|
||||
className="rounded-xl border border-secondary-300 bg-white px-4 py-2 text-sm font-bold text-secondary-700 hover:bg-secondary-50"
|
||||
className="inline-flex items-center gap-1.5 rounded-xl border border-secondary-300 bg-white px-4 py-2 text-sm font-bold text-secondary-700 hover:bg-secondary-50"
|
||||
>
|
||||
🔒 Manual verify (tanpa KYC)
|
||||
<Lock size={18} strokeWidth={2} aria-hidden />
|
||||
Manual verify (tanpa KYC)
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Check } from "lucide-react";
|
||||
import { adminReconcileMidtransAction } from "@/features/booking/actions";
|
||||
|
||||
interface AdminReconcileButtonProps {
|
||||
@@ -45,8 +46,9 @@ export function AdminReconcileButton({
|
||||
{loading ? "Reconciling..." : "Reconcile Midtrans"}
|
||||
</button>
|
||||
{status && (
|
||||
<span className="text-[11px] font-medium text-emerald-700">
|
||||
✓ {reconcileOutcomeLabel(status)}
|
||||
<span className="inline-flex items-center gap-1 text-[11px] font-medium text-emerald-700">
|
||||
<Check size={12} strokeWidth={2.5} aria-hidden />
|
||||
{reconcileOutcomeLabel(status)}
|
||||
</span>
|
||||
)}
|
||||
{error && (
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { CircleAlert } from "lucide-react";
|
||||
import { cancelBookingWithRefundAction } from "@/features/booking/actions";
|
||||
import { formatRupiah } from "@/lib/utils";
|
||||
|
||||
@@ -112,9 +113,17 @@ export function CancelBookingButton({ tripId, preview }: CancelBookingButtonProp
|
||||
Tier: {preview.tierLabel}
|
||||
</p>
|
||||
{noRefund ? (
|
||||
<p className="mt-2 text-xs text-red-700">
|
||||
⚠️ Di luar window refund — uang tidak dikembalikan. Booking akan
|
||||
di-cancel langsung.
|
||||
<p className="mt-2 flex items-start gap-1.5 text-xs text-red-700">
|
||||
<CircleAlert
|
||||
size={14}
|
||||
strokeWidth={2}
|
||||
aria-hidden
|
||||
className="mt-0.5 shrink-0"
|
||||
/>
|
||||
<span>
|
||||
Di luar window refund — uang tidak dikembalikan. Booking akan
|
||||
di-cancel langsung.
|
||||
</span>
|
||||
</p>
|
||||
) : (
|
||||
<p className="mt-2 text-xs text-neutral-600">
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Check, Copy } from "lucide-react";
|
||||
|
||||
interface CopyButtonProps {
|
||||
value: string;
|
||||
@@ -24,9 +25,19 @@ export function CopyButton({ value, label = "Salin" }: CopyButtonProps) {
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleClick}
|
||||
className="rounded-lg border border-neutral-200 bg-white px-2.5 py-1 text-xs font-medium text-neutral-600 transition-colors hover:bg-neutral-50"
|
||||
className="inline-flex items-center gap-1 rounded-lg border border-neutral-200 bg-white px-2.5 py-1 text-xs font-medium text-neutral-600 transition-colors hover:bg-neutral-50"
|
||||
>
|
||||
{copied ? "✓ Tersalin" : label}
|
||||
{copied ? (
|
||||
<>
|
||||
<Check size={13} strokeWidth={2.5} aria-hidden className="text-emerald-600" />
|
||||
Tersalin
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Copy size={13} strokeWidth={1.75} aria-hidden />
|
||||
{label}
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Check } from "lucide-react";
|
||||
import { retryEmailJobAction, resendEmailAction } from "@/features/email/actions";
|
||||
|
||||
const BTN_CLS =
|
||||
@@ -85,9 +86,18 @@ export function ResendEmailButton({
|
||||
type="button"
|
||||
onClick={handleResend}
|
||||
disabled={loading || done}
|
||||
className={BTN_CLS}
|
||||
className={`${BTN_CLS} inline-flex items-center gap-1`}
|
||||
>
|
||||
{loading ? "Mengirim…" : done ? "✓ Terkirim" : "Resend"}
|
||||
{loading ? (
|
||||
"Mengirim…"
|
||||
) : done ? (
|
||||
<>
|
||||
<Check size={12} strokeWidth={2.5} aria-hidden />
|
||||
Terkirim
|
||||
</>
|
||||
) : (
|
||||
"Resend"
|
||||
)}
|
||||
</button>
|
||||
{error && (
|
||||
<p className="mt-1 max-w-[200px] text-[10px] text-red-600">{error}</p>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { CircleCheck, CircleX, RefreshCw } from "lucide-react";
|
||||
import {
|
||||
reopenVerificationAction,
|
||||
requestReuploadAction,
|
||||
@@ -180,9 +181,10 @@ export function ReviewCard({ verification }: { verification: Verification }) {
|
||||
type="button"
|
||||
onClick={() => setShowReopen(true)}
|
||||
disabled={loading}
|
||||
className="rounded-xl border border-amber-300 bg-white px-4 py-2 text-sm font-bold text-amber-700 hover:bg-amber-50 disabled:opacity-50"
|
||||
className="inline-flex items-center gap-1.5 rounded-xl border border-amber-300 bg-white px-4 py-2 text-sm font-bold text-amber-700 hover:bg-amber-50 disabled:opacity-50"
|
||||
>
|
||||
🔄 Buka kembali ke PENDING
|
||||
<RefreshCw size={18} strokeWidth={2} aria-hidden />
|
||||
Buka kembali ke PENDING
|
||||
</button>
|
||||
) : (
|
||||
<div className="space-y-2 rounded-xl border border-amber-200 bg-amber-50/60 p-3">
|
||||
@@ -336,25 +338,28 @@ export function ReviewCard({ verification }: { verification: Verification }) {
|
||||
type="button"
|
||||
onClick={() => decide("APPROVED")}
|
||||
disabled={loading}
|
||||
className="rounded-xl bg-primary-600 px-4 py-2 text-sm font-bold text-white hover:bg-primary-700 disabled:opacity-50"
|
||||
className="inline-flex items-center gap-1.5 rounded-xl bg-primary-600 px-4 py-2 text-sm font-bold text-white hover:bg-primary-700 disabled:opacity-50"
|
||||
>
|
||||
✅ Setujui
|
||||
<CircleCheck size={18} strokeWidth={2} aria-hidden />
|
||||
Setujui
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowReupload(true)}
|
||||
disabled={loading}
|
||||
className="rounded-xl border border-amber-300 bg-white px-4 py-2 text-sm font-bold text-amber-700 hover:bg-amber-50 disabled:opacity-50"
|
||||
className="inline-flex items-center gap-1.5 rounded-xl border border-amber-300 bg-white px-4 py-2 text-sm font-bold text-amber-700 hover:bg-amber-50 disabled:opacity-50"
|
||||
>
|
||||
🔄 Minta re-upload
|
||||
<RefreshCw size={18} strokeWidth={2} aria-hidden />
|
||||
Minta re-upload
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowReject(true)}
|
||||
disabled={loading}
|
||||
className="rounded-xl border border-red-200 bg-white px-4 py-2 text-sm font-bold text-red-600 hover:bg-red-50 disabled:opacity-50"
|
||||
className="inline-flex items-center gap-1.5 rounded-xl border border-red-200 bg-white px-4 py-2 text-sm font-bold text-red-600 hover:bg-red-50 disabled:opacity-50"
|
||||
>
|
||||
❌ Tolak
|
||||
<CircleX size={18} strokeWidth={2} aria-hidden />
|
||||
Tolak
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { IdCard, Image as ImageIcon, Landmark, Check } from "lucide-react";
|
||||
import { submitVerificationAction } from "@/features/organizer/actions";
|
||||
import { DateField } from "@/components/shared/date-picker";
|
||||
|
||||
@@ -79,7 +80,15 @@ export function VerifyForm({ initial }: { initial: Initial }) {
|
||||
)}
|
||||
|
||||
<section>
|
||||
<h2 className="mb-3 text-base font-bold text-neutral-900">📇 Data KTP</h2>
|
||||
<h2 className="mb-3 flex items-center gap-2 text-base font-bold text-neutral-900">
|
||||
<IdCard
|
||||
size={18}
|
||||
strokeWidth={1.75}
|
||||
aria-hidden
|
||||
className="text-primary-600"
|
||||
/>
|
||||
Data KTP
|
||||
</h2>
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<div className="sm:col-span-2">
|
||||
<label className="mb-1.5 block text-sm font-semibold text-neutral-700">
|
||||
@@ -145,7 +154,15 @@ export function VerifyForm({ initial }: { initial: Initial }) {
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="mb-3 text-base font-bold text-neutral-900">🖼️ Foto</h2>
|
||||
<h2 className="mb-3 flex items-center gap-2 text-base font-bold text-neutral-900">
|
||||
<ImageIcon
|
||||
size={18}
|
||||
strokeWidth={1.75}
|
||||
aria-hidden
|
||||
className="text-primary-600"
|
||||
/>
|
||||
Foto
|
||||
</h2>
|
||||
<p className="mb-3 text-xs text-neutral-500">
|
||||
Foto disimpan terenkripsi di server SeTrip dan hanya bisa dilihat oleh
|
||||
tim admin saat review. Maks 5MB, JPG/PNG/WebP.
|
||||
@@ -178,7 +195,15 @@ export function VerifyForm({ initial }: { initial: Initial }) {
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="mb-3 text-base font-bold text-neutral-900">🏦 Rekening Bank</h2>
|
||||
<h2 className="mb-3 flex items-center gap-2 text-base font-bold text-neutral-900">
|
||||
<Landmark
|
||||
size={18}
|
||||
strokeWidth={1.75}
|
||||
aria-hidden
|
||||
className="text-primary-600"
|
||||
/>
|
||||
Rekening Bank
|
||||
</h2>
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<div>
|
||||
<label className="mb-1.5 block text-sm font-semibold text-neutral-700">
|
||||
@@ -312,7 +337,10 @@ function FileUpload({
|
||||
/>
|
||||
</label>
|
||||
{value && !busy && (
|
||||
<span className="text-xs text-neutral-500">✓ Terunggah</span>
|
||||
<span className="inline-flex items-center gap-1 text-xs text-emerald-600">
|
||||
<Check size={13} strokeWidth={2.5} aria-hidden />
|
||||
Terunggah
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{previewUrl && (
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { ArrowRight, Banknote, CircleAlert } from "lucide-react";
|
||||
import { markPayoutPaidAction } from "@/features/payout/actions";
|
||||
import { formatRupiah } from "@/lib/utils";
|
||||
|
||||
@@ -95,9 +96,10 @@ export function PayoutReviewCard({ payout }: { payout: PayoutCardData }) {
|
||||
</p>
|
||||
<Link
|
||||
href={`/admin/bookings/${payout.booking.id}`}
|
||||
className="mt-1 inline-block text-[11px] font-semibold text-secondary-700 hover:text-secondary-900"
|
||||
className="mt-1 inline-flex items-center gap-1 text-[11px] font-semibold text-secondary-700 hover:text-secondary-900"
|
||||
>
|
||||
→ Lihat timeline booking
|
||||
<ArrowRight size={14} strokeWidth={2} aria-hidden />
|
||||
Lihat timeline booking
|
||||
</Link>
|
||||
</div>
|
||||
<StatusPill status={payout.status} />
|
||||
@@ -142,9 +144,18 @@ export function PayoutReviewCard({ payout }: { payout: PayoutCardData }) {
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-amber-700">
|
||||
⚠️ Organizer belum menyelesaikan verifikasi (KYC) — tidak ada rekening
|
||||
snapshot. Hubungi organizer untuk konfirmasi rekening sebelum transfer.
|
||||
<p className="flex gap-1.5 text-amber-700">
|
||||
<CircleAlert
|
||||
size={16}
|
||||
strokeWidth={1.75}
|
||||
aria-hidden
|
||||
className="mt-0.5 shrink-0"
|
||||
/>
|
||||
<span>
|
||||
Organizer belum menyelesaikan verifikasi (KYC) — tidak ada
|
||||
rekening snapshot. Hubungi organizer untuk konfirmasi rekening
|
||||
sebelum transfer.
|
||||
</span>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
@@ -212,9 +223,10 @@ export function PayoutReviewCard({ payout }: { payout: PayoutCardData }) {
|
||||
type="button"
|
||||
onClick={() => setOpen(true)}
|
||||
disabled={loading}
|
||||
className="rounded-xl bg-primary-600 px-4 py-2 text-sm font-bold text-white hover:bg-primary-700 disabled:opacity-50"
|
||||
className="inline-flex items-center gap-1.5 rounded-xl bg-primary-600 px-4 py-2 text-sm font-bold text-white hover:bg-primary-700 disabled:opacity-50"
|
||||
>
|
||||
💸 Tandai sudah ditransfer ke organizer
|
||||
<Banknote size={18} strokeWidth={2} aria-hidden />
|
||||
Tandai sudah ditransfer ke organizer
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { BadgeCheck, Star } from "lucide-react";
|
||||
import type { OrganizerTrust } from "@/server/services/trust.service";
|
||||
|
||||
interface OrganizerStatsPanelProps {
|
||||
@@ -47,7 +48,8 @@ export function OrganizerStatsPanel({ trust }: OrganizerStatsPanelProps) {
|
||||
className="inline-flex items-center gap-1 rounded-full bg-primary-100 px-2.5 py-0.5 text-[11px] font-bold uppercase tracking-wide text-primary-800"
|
||||
title="Identitas organizer telah diverifikasi (KTP & rekening)"
|
||||
>
|
||||
✅ Verified Organizer
|
||||
<BadgeCheck size={12} strokeWidth={2} aria-hidden />
|
||||
Verified Organizer
|
||||
</span>
|
||||
)}
|
||||
{isTripLeader && (
|
||||
@@ -83,7 +85,21 @@ export function OrganizerStatsPanel({ trust }: OrganizerStatsPanelProps) {
|
||||
/>
|
||||
<Stat
|
||||
label="Rating"
|
||||
value={avgRating != null ? `${avgRating} ★` : "—"}
|
||||
value={
|
||||
avgRating != null ? (
|
||||
<span className="inline-flex items-center gap-1">
|
||||
{avgRating}
|
||||
<Star
|
||||
size={14}
|
||||
strokeWidth={2}
|
||||
fill="currentColor"
|
||||
aria-hidden
|
||||
/>
|
||||
</span>
|
||||
) : (
|
||||
"—"
|
||||
)
|
||||
}
|
||||
subtitle={
|
||||
reviewCount > 0
|
||||
? `${reviewCount} ulasan`
|
||||
@@ -107,8 +123,15 @@ export function OrganizerStatsPanel({ trust }: OrganizerStatsPanelProps) {
|
||||
key={star}
|
||||
className="flex items-center gap-2 text-xs"
|
||||
>
|
||||
<span className="w-8 shrink-0 font-medium text-neutral-600">
|
||||
{star} ★
|
||||
<span className="flex w-8 shrink-0 items-center gap-0.5 font-medium text-neutral-600">
|
||||
{star}
|
||||
<Star
|
||||
size={11}
|
||||
strokeWidth={2}
|
||||
fill="currentColor"
|
||||
aria-hidden
|
||||
className="text-amber-500"
|
||||
/>
|
||||
</span>
|
||||
<div className="h-2 flex-1 overflow-hidden rounded-full bg-neutral-100">
|
||||
<div
|
||||
@@ -138,7 +161,7 @@ const TONE_CLASSES = {
|
||||
|
||||
interface StatProps {
|
||||
label: string;
|
||||
value: string;
|
||||
value: React.ReactNode;
|
||||
subtitle?: string;
|
||||
tone: keyof typeof TONE_CLASSES;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { ArrowUpRight } from "lucide-react";
|
||||
import { updateProfileAction } from "@/features/profile/actions";
|
||||
import { LIMITS } from "@/lib/limits";
|
||||
import { VIBES, vibeMeta } from "@/lib/vibe";
|
||||
@@ -102,9 +103,10 @@ export function ProfileEditor({ userId, initial }: ProfileEditorProps) {
|
||||
href={`/u/${userId}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="rounded-lg border border-neutral-200 px-3 py-1.5 text-xs font-medium text-neutral-600 hover:bg-neutral-50"
|
||||
className="inline-flex items-center gap-1 rounded-lg border border-neutral-200 px-3 py-1.5 text-xs font-medium text-neutral-600 hover:bg-neutral-50"
|
||||
>
|
||||
Lihat publik ↗
|
||||
Lihat publik
|
||||
<ArrowUpRight size={13} strokeWidth={1.75} aria-hidden />
|
||||
</a>
|
||||
<button
|
||||
type="button"
|
||||
@@ -324,9 +326,10 @@ export function ProfileEditor({ userId, initial }: ProfileEditorProps) {
|
||||
href={`/u/${userId}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="rounded-xl border border-neutral-200 px-4 py-2.5 text-sm font-medium text-neutral-700 hover:bg-neutral-50"
|
||||
className="inline-flex items-center gap-1 rounded-xl border border-neutral-200 px-4 py-2.5 text-sm font-medium text-neutral-700 hover:bg-neutral-50"
|
||||
>
|
||||
Lihat publik ↗
|
||||
Lihat publik
|
||||
<ArrowUpRight size={14} strokeWidth={1.75} aria-hidden />
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { MapPin, BadgeCheck } from "lucide-react";
|
||||
import { vibeMeta } from "@/lib/vibe";
|
||||
import type { Vibe } from "@/app/generated/prisma/enums";
|
||||
|
||||
@@ -48,17 +49,19 @@ export function UserCard({
|
||||
{name}
|
||||
</p>
|
||||
{profile?.city && (
|
||||
<p className="truncate text-[11px] text-neutral-500 sm:text-xs">
|
||||
📍 {profile.city}
|
||||
<p className="flex items-center gap-1 truncate text-[11px] text-neutral-500 sm:text-xs">
|
||||
<MapPin size={11} strokeWidth={1.75} aria-hidden className="shrink-0" />
|
||||
{profile.city}
|
||||
</p>
|
||||
)}
|
||||
<div className="mt-1 flex flex-wrap gap-1">
|
||||
{isVerifiedOrganizer && (
|
||||
<span
|
||||
className="inline-flex items-center gap-0.5 rounded-full bg-primary-100 px-1.5 py-0.5 text-[10px] font-bold uppercase tracking-wide text-primary-800"
|
||||
className="inline-flex items-center gap-1 rounded-full bg-primary-100 px-1.5 py-0.5 text-[10px] font-bold uppercase tracking-wide text-primary-800"
|
||||
title="Organizer terverifikasi"
|
||||
>
|
||||
✅ Organizer
|
||||
<BadgeCheck size={11} strokeWidth={2} aria-hidden />
|
||||
Organizer
|
||||
</span>
|
||||
)}
|
||||
{profile?.vibe && (
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { LifeBuoy } from "lucide-react";
|
||||
import { getRefundPolicyTiers } from "@/lib/refund-policy";
|
||||
|
||||
/**
|
||||
@@ -9,7 +10,13 @@ export function RefundPolicySection() {
|
||||
return (
|
||||
<details className="rounded-xl border border-neutral-200 bg-neutral-50/60 p-3 text-xs sm:text-sm">
|
||||
<summary className="cursor-pointer select-none font-semibold text-neutral-700">
|
||||
🛟 Kebijakan refund saat peserta cancel
|
||||
<LifeBuoy
|
||||
size={15}
|
||||
strokeWidth={1.75}
|
||||
aria-hidden
|
||||
className="mr-1.5 inline align-text-bottom"
|
||||
/>
|
||||
Kebijakan refund saat peserta cancel
|
||||
</summary>
|
||||
<div className="mt-2 space-y-2 text-neutral-600">
|
||||
<p className="text-[11px] text-neutral-500 sm:text-xs">
|
||||
|
||||
@@ -3,6 +3,13 @@
|
||||
import { useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import {
|
||||
ArrowRight,
|
||||
Banknote,
|
||||
CircleAlert,
|
||||
CircleCheck,
|
||||
CircleX,
|
||||
} from "lucide-react";
|
||||
import { decideRefundAction } from "@/features/refund/actions";
|
||||
import { formatRupiah } from "@/lib/utils";
|
||||
|
||||
@@ -129,9 +136,10 @@ export function RefundReviewCard({ refund }: { refund: RefundCardData }) {
|
||||
</p>
|
||||
<Link
|
||||
href={`/admin/bookings/${refund.booking.id}`}
|
||||
className="mt-1 inline-block text-[11px] font-semibold text-secondary-700 hover:text-secondary-900"
|
||||
className="mt-1 inline-flex items-center gap-1 text-[11px] font-semibold text-secondary-700 hover:text-secondary-900"
|
||||
>
|
||||
→ Lihat timeline payment & refund
|
||||
<ArrowRight size={14} strokeWidth={2} aria-hidden />
|
||||
Lihat timeline payment & refund
|
||||
</Link>
|
||||
</div>
|
||||
<Field
|
||||
@@ -211,17 +219,19 @@ export function RefundReviewCard({ refund }: { refund: RefundCardData }) {
|
||||
type="button"
|
||||
onClick={() => setOpenAction("APPROVE")}
|
||||
disabled={loading}
|
||||
className="rounded-xl bg-primary-600 px-4 py-2 text-sm font-bold text-white hover:bg-primary-700 disabled:opacity-50"
|
||||
className="inline-flex items-center gap-1.5 rounded-xl bg-primary-600 px-4 py-2 text-sm font-bold text-white hover:bg-primary-700 disabled:opacity-50"
|
||||
>
|
||||
✅ Setujui
|
||||
<CircleCheck size={18} strokeWidth={2} aria-hidden />
|
||||
Setujui
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setOpenAction("REJECT")}
|
||||
disabled={loading}
|
||||
className="rounded-xl border border-red-200 bg-white px-4 py-2 text-sm font-bold text-red-600 hover:bg-red-50 disabled:opacity-50"
|
||||
className="inline-flex items-center gap-1.5 rounded-xl border border-red-200 bg-white px-4 py-2 text-sm font-bold text-red-600 hover:bg-red-50 disabled:opacity-50"
|
||||
>
|
||||
❌ Tolak
|
||||
<CircleX size={18} strokeWidth={2} aria-hidden />
|
||||
Tolak
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
@@ -231,17 +241,19 @@ export function RefundReviewCard({ refund }: { refund: RefundCardData }) {
|
||||
type="button"
|
||||
onClick={() => setOpenAction("SUCCEEDED")}
|
||||
disabled={loading}
|
||||
className="rounded-xl bg-primary-600 px-4 py-2 text-sm font-bold text-white hover:bg-primary-700 disabled:opacity-50"
|
||||
className="inline-flex items-center gap-1.5 rounded-xl bg-primary-600 px-4 py-2 text-sm font-bold text-white hover:bg-primary-700 disabled:opacity-50"
|
||||
>
|
||||
💸 Tandai sudah ditransfer
|
||||
<Banknote size={18} strokeWidth={2} aria-hidden />
|
||||
Tandai sudah ditransfer
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setOpenAction("FAILED")}
|
||||
disabled={loading}
|
||||
className="rounded-xl border border-red-200 bg-white px-4 py-2 text-sm font-bold text-red-600 hover:bg-red-50 disabled:opacity-50"
|
||||
className="inline-flex items-center gap-1.5 rounded-xl border border-red-200 bg-white px-4 py-2 text-sm font-bold text-red-600 hover:bg-red-50 disabled:opacity-50"
|
||||
>
|
||||
⚠️ Tandai gagal
|
||||
<CircleAlert size={18} strokeWidth={2} aria-hidden />
|
||||
Tandai gagal
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import { Star } from "lucide-react";
|
||||
import type { OrganizerReviewItem } from "@/server/services/review.service";
|
||||
|
||||
interface OrganizerReviewsListProps {
|
||||
@@ -62,11 +63,22 @@ export function OrganizerReviewsList({
|
||||
>
|
||||
{r.user.name}
|
||||
</Link>
|
||||
<span className="text-xs font-bold text-amber-600">
|
||||
{"★".repeat(r.rating)}
|
||||
<span className="text-neutral-300">
|
||||
{"★".repeat(5 - r.rating)}
|
||||
</span>
|
||||
<span
|
||||
className="flex shrink-0 items-center gap-0.5"
|
||||
aria-label={`Rating ${r.rating} dari 5`}
|
||||
>
|
||||
{[1, 2, 3, 4, 5].map((n) => (
|
||||
<Star
|
||||
key={n}
|
||||
size={12}
|
||||
strokeWidth={2}
|
||||
fill="currentColor"
|
||||
aria-hidden
|
||||
className={
|
||||
n <= r.rating ? "text-amber-500" : "text-neutral-200"
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { CircleCheck } from "lucide-react";
|
||||
import { adminCancelTripAction } from "@/features/trip/actions";
|
||||
|
||||
interface AdminCancelTripButtonProps {
|
||||
@@ -42,7 +43,10 @@ export function AdminCancelTripButton({ tripId }: AdminCancelTripButtonProps) {
|
||||
if (result) {
|
||||
return (
|
||||
<div className="rounded-xl border border-emerald-200 bg-emerald-50 p-4 text-sm text-emerald-900">
|
||||
<p className="font-bold">✅ Trip berhasil dibatalkan.</p>
|
||||
<p className="flex items-center gap-1.5 font-bold">
|
||||
<CircleCheck size={18} strokeWidth={2} aria-hidden />
|
||||
Trip berhasil dibatalkan.
|
||||
</p>
|
||||
<ul className="mt-2 space-y-0.5 text-xs">
|
||||
<li>• {result.refundCount} booking PAID → refund auto-dibuat</li>
|
||||
<li>
|
||||
|
||||
@@ -2,6 +2,14 @@
|
||||
|
||||
import { useMemo, useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import {
|
||||
ArrowLeft,
|
||||
ArrowRight,
|
||||
Check,
|
||||
X,
|
||||
CircleAlert,
|
||||
Users,
|
||||
} from "lucide-react";
|
||||
import { DateRangeField, TimeField } from "@/components/shared/date-picker";
|
||||
import { createTripAction } from "@/features/trip/actions";
|
||||
import { ImageUrlInput } from "@/features/trip/components/image-url-input";
|
||||
@@ -367,9 +375,10 @@ export function CreateTripForm({ isVerifiedOrganizer }: CreateTripFormProps) {
|
||||
type="button"
|
||||
onClick={goBack}
|
||||
disabled={step === 1 || loading}
|
||||
className="rounded-xl border border-neutral-200 bg-white px-4 py-2.5 text-sm font-semibold text-neutral-700 transition-colors hover:bg-neutral-50 disabled:cursor-not-allowed disabled:opacity-40"
|
||||
className="inline-flex items-center gap-1 rounded-xl border border-neutral-200 bg-white px-4 py-2.5 text-sm font-semibold text-neutral-700 transition-colors hover:bg-neutral-50 disabled:cursor-not-allowed disabled:opacity-40"
|
||||
>
|
||||
← Kembali
|
||||
<ArrowLeft size={15} strokeWidth={2} aria-hidden />
|
||||
Kembali
|
||||
</button>
|
||||
|
||||
{isLastStep ? (
|
||||
@@ -388,9 +397,10 @@ export function CreateTripForm({ isVerifiedOrganizer }: CreateTripFormProps) {
|
||||
<button
|
||||
type="button"
|
||||
onClick={goNext}
|
||||
className="rounded-xl bg-primary-600 px-5 py-2.5 text-sm font-bold text-white shadow-lg shadow-primary-600/20 transition-colors hover:bg-primary-700"
|
||||
className="inline-flex items-center gap-1 rounded-xl bg-primary-600 px-5 py-2.5 text-sm font-bold text-white shadow-lg shadow-primary-600/20 transition-colors hover:bg-primary-700"
|
||||
>
|
||||
Lanjut →
|
||||
Lanjut
|
||||
<ArrowRight size={15} strokeWidth={2} aria-hidden />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
@@ -442,7 +452,11 @@ function Stepper({
|
||||
: "cursor-not-allowed"
|
||||
}`}
|
||||
>
|
||||
{isCompleted ? "✓" : s.id}
|
||||
{isCompleted ? (
|
||||
<Check size={14} strokeWidth={3} aria-hidden />
|
||||
) : (
|
||||
s.id
|
||||
)}
|
||||
</button>
|
||||
<span
|
||||
className={`ml-2 hidden text-xs font-semibold sm:inline ${
|
||||
@@ -942,7 +956,7 @@ function ItineraryBuilder({
|
||||
aria-label="Hapus aktivitas"
|
||||
className="self-end rounded-lg px-2 py-1.5 text-neutral-400 hover:bg-red-50 hover:text-red-500 sm:self-center"
|
||||
>
|
||||
✕
|
||||
<X size={16} strokeWidth={2} aria-hidden />
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
@@ -1032,14 +1046,7 @@ function StepSchedule({
|
||||
</label>
|
||||
<div className="relative">
|
||||
<span className="absolute left-3 top-1/2 -translate-y-1/2 text-neutral-400">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
className="h-4 w-4"
|
||||
>
|
||||
<path d="M7 8a3 3 0 100-6 3 3 0 000 6zM14.5 9a2.5 2.5 0 100-5 2.5 2.5 0 000 5zM1.615 16.428a1.224 1.224 0 01-.569-1.175 6.002 6.002 0 0111.908 0c.058.467-.172.92-.57 1.174A9.953 9.953 0 017 18a9.953 9.953 0 01-5.385-1.572zM14.5 16h-.106c.07-.297.088-.611.048-.933a7.47 7.47 0 00-1.588-3.755 4.502 4.502 0 015.874 2.636.818.818 0 01-.36.98A7.465 7.465 0 0114.5 16z" />
|
||||
</svg>
|
||||
<Users size={16} strokeWidth={1.75} aria-hidden />
|
||||
</span>
|
||||
<input
|
||||
id="maxParticipants"
|
||||
@@ -1084,8 +1091,9 @@ function StepSchedule({
|
||||
/>
|
||||
</div>
|
||||
{blockedByVerification && (
|
||||
<p className="mt-2 text-xs font-medium text-amber-700">
|
||||
⚠️ Trip berbayar butuh verifikasi organizer terlebih dahulu.
|
||||
<p className="mt-2 flex items-center gap-1.5 text-xs font-medium text-amber-700">
|
||||
<CircleAlert size={14} strokeWidth={2} aria-hidden />
|
||||
Trip berbayar butuh verifikasi organizer terlebih dahulu.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import Image from "next/image";
|
||||
import { useState } from "react";
|
||||
import { Mountain } from "lucide-react";
|
||||
|
||||
interface TripImage {
|
||||
id: string;
|
||||
@@ -14,8 +15,13 @@ export function ImageGallery({ images }: { images: TripImage[] }) {
|
||||
|
||||
if (images.length === 0) {
|
||||
return (
|
||||
<div className="flex h-44 items-center justify-center bg-linear-to-br from-primary-800 to-secondary-900 sm:h-56 lg:h-72">
|
||||
<span className="text-5xl sm:text-6xl">🏔️</span>
|
||||
<div className="flex h-44 items-center justify-center bg-neutral-100 sm:h-56 lg:h-72">
|
||||
<Mountain
|
||||
size={56}
|
||||
strokeWidth={1.5}
|
||||
aria-hidden
|
||||
className="text-neutral-300"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { Plus, X } from "lucide-react";
|
||||
import { LIMITS } from "@/lib/limits";
|
||||
|
||||
interface ImageUrlInputProps {
|
||||
@@ -59,7 +60,7 @@ export function ImageUrlInput({ value, onChange }: ImageUrlInputProps) {
|
||||
aria-label={`Hapus foto ${i + 1}`}
|
||||
className="flex h-10 w-10 shrink-0 items-center justify-center rounded-xl border border-neutral-200 text-neutral-400 hover:bg-red-50 hover:text-red-500"
|
||||
>
|
||||
✕
|
||||
<X size={16} strokeWidth={2} aria-hidden />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
@@ -71,7 +72,8 @@ export function ImageUrlInput({ value, onChange }: ImageUrlInputProps) {
|
||||
onClick={addField}
|
||||
className="mt-2 flex items-center gap-1 rounded-lg px-2 py-1 text-sm font-medium text-secondary-600 hover:bg-secondary-50"
|
||||
>
|
||||
+ Tambah foto
|
||||
<Plus size={15} strokeWidth={2} aria-hidden />
|
||||
Tambah foto
|
||||
</button>
|
||||
)}
|
||||
<p className="mt-1.5 text-xs text-neutral-400">
|
||||
|
||||
@@ -143,7 +143,7 @@ export function JoinTripButton({
|
||||
Kamu sudah{" "}
|
||||
<span className="font-semibold">terkonfirmasi</span> sebagai peserta
|
||||
trip ini
|
||||
{isFree && <span> — trip gratis, tidak ada pembayaran 🎉</span>}.
|
||||
{isFree && <span> — trip gratis, tidak ada pembayaran</span>}.
|
||||
</div>
|
||||
)}
|
||||
{needsPayment && (
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import Image from "next/image";
|
||||
import { BadgeCheck, Star } from "lucide-react";
|
||||
import type { OrganizerTrust } from "@/server/services/trust.service";
|
||||
|
||||
interface OrganizerTrustPanelProps {
|
||||
@@ -13,7 +14,7 @@ export function OrganizerTrustPanel({
|
||||
trust,
|
||||
}: OrganizerTrustPanelProps) {
|
||||
return (
|
||||
<div className="rounded-xl border border-neutral-200 bg-linear-to-br from-white to-neutral-50 p-4 sm:p-5">
|
||||
<div className="rounded-xl border border-neutral-200 bg-white p-4 sm:p-5">
|
||||
<h2 className="mb-3 text-xs font-bold text-neutral-700 sm:text-sm">
|
||||
Organizer & kepercayaan
|
||||
</h2>
|
||||
@@ -42,7 +43,8 @@ export function OrganizerTrustPanel({
|
||||
className="inline-flex items-center gap-1 rounded-full bg-primary-100 px-2 py-0.5 text-[10px] font-bold uppercase tracking-wide text-primary-800 sm:text-xs"
|
||||
title="Identitas organizer telah diverifikasi (KTP & rekening)"
|
||||
>
|
||||
✅ Verified Organizer
|
||||
<BadgeCheck size={12} strokeWidth={2} aria-hidden />
|
||||
Verified Organizer
|
||||
</span>
|
||||
)}
|
||||
{trust.isTripLeader && (
|
||||
@@ -79,8 +81,20 @@ export function OrganizerTrustPanel({
|
||||
<p className="text-[10px] font-medium text-neutral-500 sm:text-xs">
|
||||
Rating organizer
|
||||
</p>
|
||||
<p className="text-lg font-bold text-amber-700">
|
||||
{trust.avgRating != null ? `${trust.avgRating} ★` : "—"}
|
||||
<p className="flex items-center gap-1 text-lg font-bold text-amber-700">
|
||||
{trust.avgRating != null ? (
|
||||
<>
|
||||
{trust.avgRating}
|
||||
<Star
|
||||
size={15}
|
||||
strokeWidth={2}
|
||||
fill="currentColor"
|
||||
aria-hidden
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
"—"
|
||||
)}
|
||||
</p>
|
||||
{trust.reviewCount > 0 && (
|
||||
<p className="text-[10px] text-neutral-400">
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import {
|
||||
MapPin,
|
||||
CalendarDays,
|
||||
UserRound,
|
||||
BadgeCheck,
|
||||
Sparkles,
|
||||
} from "lucide-react";
|
||||
import { formatRupiah } from "@/lib/utils";
|
||||
import { formatTripCalendarDateRangeLong } from "@/lib/trip-dates";
|
||||
import { categoryMeta } from "@/lib/activity-category";
|
||||
@@ -132,21 +139,38 @@ export function TripCard({
|
||||
|
||||
<div className="mt-3 space-y-1 text-sm text-neutral-600">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="text-xs text-secondary-500">📍</span> {location}
|
||||
<MapPin
|
||||
size={14}
|
||||
strokeWidth={1.75}
|
||||
aria-hidden
|
||||
className="shrink-0 text-neutral-400"
|
||||
/>
|
||||
<span className="truncate">{location}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="text-xs text-secondary-500">📅</span>{" "}
|
||||
{formatTripCalendarDateRangeLong(date, endDate)}
|
||||
<CalendarDays
|
||||
size={14}
|
||||
strokeWidth={1.75}
|
||||
aria-hidden
|
||||
className="shrink-0 text-neutral-400"
|
||||
/>
|
||||
<span>{formatTripCalendarDateRangeLong(date, endDate)}</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-1.5">
|
||||
<span className="text-xs text-secondary-500">👤</span>{" "}
|
||||
<UserRound
|
||||
size={14}
|
||||
strokeWidth={1.75}
|
||||
aria-hidden
|
||||
className="shrink-0 text-neutral-400"
|
||||
/>
|
||||
<span className="truncate">{organizerName}</span>
|
||||
{isVerifiedOrganizer && (
|
||||
<span
|
||||
className="inline-flex items-center gap-0.5 rounded-full bg-primary-100 px-1.5 py-0.5 text-[10px] font-bold uppercase tracking-wide text-primary-800"
|
||||
className="inline-flex items-center gap-1 rounded-full bg-primary-100 px-1.5 py-0.5 text-[10px] font-bold uppercase tracking-wide text-primary-800"
|
||||
title="Identitas organizer telah diverifikasi (KTP & rekening)"
|
||||
>
|
||||
✅ Verified
|
||||
<BadgeCheck size={11} strokeWidth={2} aria-hidden />
|
||||
Verified
|
||||
</span>
|
||||
)}
|
||||
{isSmallGroup && (
|
||||
@@ -193,10 +217,11 @@ export function TripCard({
|
||||
)}
|
||||
{overlapCount > 0 && (
|
||||
<span
|
||||
className="rounded-full bg-secondary-50 px-2 py-0.5 text-[11px] font-semibold text-secondary-700"
|
||||
className="inline-flex items-center gap-1 rounded-full bg-secondary-50 px-2 py-0.5 text-[11px] font-semibold text-secondary-700"
|
||||
title="Peserta dengan minimal 1 minat sama dengan kamu"
|
||||
>
|
||||
✨ {overlapCount} peserta sama minat
|
||||
<Sparkles size={11} strokeWidth={2} aria-hidden />
|
||||
{overlapCount} peserta sama minat
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
import { Search } from "lucide-react";
|
||||
import { DateRangeField } from "@/components/shared/date-picker";
|
||||
import {
|
||||
formatLocalCalendarYmd,
|
||||
@@ -263,18 +264,7 @@ export function TripFilter() {
|
||||
</label>
|
||||
<div className="relative">
|
||||
<span className="absolute left-3 top-1/2 -translate-y-1/2 text-neutral-400">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
className="h-4 w-4"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M9 3.5a5.5 5.5 0 100 11 5.5 5.5 0 000-11zM2 9a7 7 0 1112.452 4.391l3.328 3.329a.75.75 0 11-1.06 1.06l-3.329-3.328A7 7 0 012 9z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
<Search size={16} strokeWidth={1.75} aria-hidden />
|
||||
</span>
|
||||
<input
|
||||
type="text"
|
||||
|
||||
Reference in New Issue
Block a user