dress/resources/js/pages/admin/system/settings/index.tsx

458 lines
24 KiB
TypeScript

import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Field, FieldError } from '@/components/ui/field';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea';
import systemRoutes from '@/routes/system';
import type { GeneralSetting } from '@/types';
import { Head, useForm } from '@inertiajs/react';
import { ImagePlus, Loader, Save, X } from 'lucide-react';
import React, { useRef, useState } from 'react';
import { toast } from 'sonner';
export default function SettingIndex({
setting,
}: {
setting: GeneralSetting | null;
}) {
const { data, setData, post, processing, errors } = useForm({
site_name: setting?.site_name || '',
site_description: setting?.site_description || '',
site_address: setting?.site_address || '',
site_phone: setting?.site_phone || '',
logo_light: null as File | null,
logo_dark: null as File | null,
icon_light: null as File | null,
icon_dark: null as File | null,
});
const logoLightInputRef = useRef<HTMLInputElement>(null);
const logoDarkInputRef = useRef<HTMLInputElement>(null);
const iconLightInputRef = useRef<HTMLInputElement>(null);
const iconDarkInputRef = useRef<HTMLInputElement>(null);
const [logoLightPreview, setLogoLightPreview] = useState<string | null>(
setting?.logo_light_url || null,
);
const [logoDarkPreview, setLogoDarkPreview] = useState<string | null>(
setting?.logo_dark_url || null,
);
const [iconLightPreview, setIconLightPreview] = useState<string | null>(
setting?.icon_light_url || null,
);
const [iconDarkPreview, setIconDarkPreview] = useState<string | null>(
setting?.icon_dark_url || null,
);
const handleFileChange = (
e: React.ChangeEvent<HTMLInputElement>,
field: 'logo_light' | 'logo_dark' | 'icon_light' | 'icon_dark',
setPreview: React.Dispatch<React.SetStateAction<string | null>>,
) => {
const file = e.target.files?.[0];
if (!file) {
return;
}
setData(field, file);
const reader = new FileReader();
reader.onloadend = () => setPreview(reader.result as string);
reader.readAsDataURL(file);
};
const removeFile = (
field: 'logo_light' | 'logo_dark' | 'icon_light' | 'icon_dark',
setPreview: React.Dispatch<React.SetStateAction<string | null>>,
inputRef: React.RefObject<HTMLInputElement | null>,
) => {
setData(field, null);
setPreview(null);
if (inputRef.current) {
inputRef.current.value = '';
}
};
const onSubmit = (e: React.FormEvent) => {
e.preventDefault();
post(systemRoutes.settings.update().url, {
forceFormData: true,
onSuccess: (response: any) => {
toast.success(response.props.flash.success);
},
});
};
return (
<div className="flex flex-col gap-6 p-6">
<Head title="Pengaturan Umum" />
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold tracking-tight">
Pengaturan Umum
</h1>
</div>
</div>
<form onSubmit={onSubmit} className="space-y-6">
<div className="grid grid-cols-1 gap-6 lg:grid-cols-3">
<div className="space-y-6 lg:col-span-2">
<Card className="overflow-hidden border-none bg-card/50 shadow-lg backdrop-blur-sm">
<CardHeader className="border-b">
<CardTitle>Informasi Aplikasi</CardTitle>
</CardHeader>
<CardContent className="space-y-6">
<div className="grid grid-cols-1 gap-6 md:grid-cols-2">
<Field>
<Label htmlFor="site_phone" required>
No. Telepon
</Label>
<Input
id="site_phone"
value={data.site_phone || ''}
onChange={(e) =>
setData('site_phone', e.target.value)
}
placeholder="0812xxxx"
/>
<FieldError
error={errors.site_phone}
label="No. Telepon"
className="text-xs"
/>
</Field>
<Field>
<Label htmlFor="site_name" required>
Nama Aplikasi
</Label>
<Input
id="site_name"
name="site_name"
value={data.site_name}
onChange={(e) =>
setData('site_name', e.target.value)
}
autoComplete="off"
placeholder="Contoh: VN Grup Dress"
maxLength={100}
/>
<FieldError
error={errors.site_name}
label="Nama Aplikasi"
className="text-xs"
/>
</Field>
</div>
<Field>
<Label htmlFor="site_description" required>
Deskripsi
</Label>
<Textarea
id="site_description"
value={data.site_description || ''}
onChange={(e) =>
setData(
'site_description',
e.target.value,
)
}
placeholder="Deskripsi singkat aplikasi"
rows={4}
/>
<FieldError
error={errors.site_description}
label="Deskripsi"
className="mt-1 text-xs"
/>
</Field>
<Field>
<Label htmlFor="site_address" required>
Alamat
</Label>
<Textarea
id="site_address"
value={data.site_address || ''}
onChange={(e) =>
setData('site_address', e.target.value)
}
placeholder="Alamat lengkap toko"
rows={3}
/>
<FieldError
error={errors.site_address}
label="Alamat"
className="mt-1 text-xs"
/>
</Field>
</CardContent>
</Card>
</div>
<div className="space-y-6">
<Card className="overflow-hidden border-none bg-card/50 shadow-lg backdrop-blur-sm">
<CardHeader className="border-b">
<CardTitle>Logo Aplikasi</CardTitle>
</CardHeader>
<CardContent className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label className="text-xs">
Logo Terang
</Label>
{logoLightPreview ? (
<div className="relative h-20 w-full overflow-hidden rounded-xl border bg-white shadow-sm">
<img
src={logoLightPreview}
alt="Logo Light preview"
className="h-full w-full object-contain p-2"
/>
<button
type="button"
onClick={() =>
removeFile(
'logo_light',
setLogoLightPreview,
logoLightInputRef,
)
}
className="absolute top-1 right-1 flex h-6 w-6 items-center justify-center rounded-full bg-red-500 text-white shadow-md transition-colors hover:bg-red-600"
>
<X className="h-3.5 w-3.5" />
</button>
</div>
) : (
<button
type="button"
onClick={() =>
logoLightInputRef.current?.click()
}
className="group flex h-20 w-full cursor-pointer flex-col items-center justify-center rounded-xl border-2 border-dashed border-border transition-all hover:border-primary hover:bg-primary/5"
>
<ImagePlus className="h-5 w-5 text-muted-foreground transition-colors group-hover:text-primary" />
</button>
)}
<input
ref={logoLightInputRef}
type="file"
accept="image/*"
className="hidden"
onChange={(e) =>
handleFileChange(
e,
'logo_light',
setLogoLightPreview,
)
}
/>
<FieldError
error={errors.logo_light}
label="Logo Terang"
className="mt-1 text-[10px]"
/>
</div>
<div className="space-y-2">
<Label className="text-xs">
Logo Gelap
</Label>
{logoDarkPreview ? (
<div className="relative h-20 w-full overflow-hidden rounded-xl border bg-slate-900 shadow-sm">
<img
src={logoDarkPreview}
alt="Logo Dark preview"
className="h-full w-full object-contain p-2"
/>
<button
type="button"
onClick={() =>
removeFile(
'logo_dark',
setLogoDarkPreview,
logoDarkInputRef,
)
}
className="absolute top-1 right-1 flex h-6 w-6 items-center justify-center rounded-full bg-red-500 text-white shadow-md transition-colors hover:bg-red-600"
>
<X className="h-3.5 w-3.5" />
</button>
</div>
) : (
<button
type="button"
onClick={() =>
logoDarkInputRef.current?.click()
}
className="group flex h-20 w-full cursor-pointer flex-col items-center justify-center rounded-xl border-2 border-dashed border-border transition-all hover:border-primary hover:bg-primary/5"
>
<ImagePlus className="h-5 w-5 text-muted-foreground transition-colors group-hover:text-primary" />
</button>
)}
<input
ref={logoDarkInputRef}
type="file"
accept="image/*"
className="hidden"
onChange={(e) =>
handleFileChange(
e,
'logo_dark',
setLogoDarkPreview,
)
}
/>
<FieldError
error={errors.logo_dark}
label="Logo Gelap"
className="mt-1 text-[10px]"
/>
</div>
</CardContent>
</Card>
<Card className="overflow-hidden border-none bg-card/50 shadow-lg backdrop-blur-sm">
<CardHeader className="border-b">
<CardTitle>Favicon / Icon</CardTitle>
</CardHeader>
<CardContent className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label className="text-xs">
Icon Terang
</Label>
{iconLightPreview ? (
<div className="relative h-20 w-full overflow-hidden rounded-xl border bg-white shadow-sm">
<img
src={iconLightPreview}
alt="Icon Light preview"
className="h-full w-full object-contain p-3"
/>
<button
type="button"
onClick={() =>
removeFile(
'icon_light',
setIconLightPreview,
iconLightInputRef,
)
}
className="absolute top-1 right-1 flex h-6 w-6 items-center justify-center rounded-full bg-red-500 text-white shadow-md transition-colors hover:bg-red-600"
>
<X className="h-3.5 w-3.5" />
</button>
</div>
) : (
<button
type="button"
onClick={() =>
iconLightInputRef.current?.click()
}
className="group flex h-20 w-full cursor-pointer flex-col items-center justify-center rounded-xl border-2 border-dashed border-border transition-all hover:border-primary hover:bg-primary/5"
>
<ImagePlus className="h-5 w-5 text-muted-foreground transition-colors group-hover:text-primary" />
</button>
)}
<input
ref={iconLightInputRef}
type="file"
accept="image/*"
className="hidden"
onChange={(e) =>
handleFileChange(
e,
'icon_light',
setIconLightPreview,
)
}
/>
<FieldError
error={errors.icon_light}
label="Icon Terang"
className="mt-1 text-[10px]"
/>
</div>
<div className="space-y-2">
<Label className="text-xs">
Icon Gelap
</Label>
{iconDarkPreview ? (
<div className="relative h-20 w-full overflow-hidden rounded-xl border bg-slate-900 shadow-sm">
<img
src={iconDarkPreview}
alt="Icon Dark preview"
className="h-full w-full object-contain p-3"
/>
<button
type="button"
onClick={() =>
removeFile(
'icon_dark',
setIconDarkPreview,
iconDarkInputRef,
)
}
className="absolute top-1 right-1 flex h-6 w-6 items-center justify-center rounded-full bg-red-500 text-white shadow-md transition-colors hover:bg-red-600"
>
<X className="h-3.5 w-3.5" />
</button>
</div>
) : (
<button
type="button"
onClick={() =>
iconDarkInputRef.current?.click()
}
className="group flex h-20 w-full cursor-pointer flex-col items-center justify-center rounded-xl border-2 border-dashed border-border transition-all hover:border-primary hover:bg-primary/5"
>
<ImagePlus className="h-5 w-5 text-muted-foreground transition-colors group-hover:text-primary" />
</button>
)}
<input
ref={iconDarkInputRef}
type="file"
accept="image/*"
className="hidden"
onChange={(e) =>
handleFileChange(
e,
'icon_dark',
setIconDarkPreview,
)
}
/>
<FieldError
error={errors.icon_dark}
label="Icon Gelap"
className="mt-1 text-[10px]"
/>
</div>
</CardContent>
</Card>
</div>
</div>
<Button type="submit" disabled={processing}>
{processing ? (
<Loader className="size-4 animate-spin" />
) : (
<Save className="size-4" />
)}
{processing ? 'Menyimpan...' : 'Simpan'}
</Button>
</form>
</div>
);
}
SettingIndex.layout = {
breadcrumbs: [
{
title: 'Sistem',
},
],
};