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

330 lines
15 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({
name: setting?.name || '',
description: setting?.description || '',
address: setting?.address || '',
phone: setting?.phone || '',
logo: null as File | null,
icon: null as File | null,
});
const logoInputRef = useRef<HTMLInputElement>(null);
const iconInputRef = useRef<HTMLInputElement>(null);
const [logoPreview, setLogoPreview] = useState<string | null>(
setting?.logo_url || null,
);
const [iconPreview, setIconPreview] = useState<string | null>(
setting?.icon_url || null,
);
const handleLogoChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) {
return;
}
setData('logo', file);
const reader = new FileReader();
reader.onloadend = () => setLogoPreview(reader.result as string);
reader.readAsDataURL(file);
};
const removeLogo = () => {
setData('logo', null);
setLogoPreview(null);
if (logoInputRef.current) {
logoInputRef.current.value = '';
}
};
const handleIconChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) {
return;
}
setData('icon', file);
const reader = new FileReader();
reader.onloadend = () => setIconPreview(reader.result as string);
reader.readAsDataURL(file);
};
const removeIcon = () => {
setData('icon', null);
setIconPreview(null);
if (iconInputRef.current) {
iconInputRef.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="phone" required>
No. Telepon
</Label>
<Input
id="phone"
value={data.phone || ''}
onChange={(e) =>
setData('phone', e.target.value)
}
placeholder="0812xxxx"
/>
<FieldError
error={errors.phone}
label="No. Telepon"
className="text-xs"
/>
</Field>
<Field>
<Label htmlFor="name" required>
Nama Aplikasi
</Label>
<Input
id="name"
name="name"
value={data.name}
onChange={(e) =>
setData('name', e.target.value)
}
autoComplete="off"
placeholder="Contoh: VN Grup Dress"
maxLength={100}
/>
<FieldError
error={errors.name}
label="Nama Aplikasi"
className="text-xs"
/>
</Field>
</div>
<Field>
<Label htmlFor="description" required>
Deskripsi
</Label>
<Textarea
id="description"
value={data.description || ''}
onChange={(e) =>
setData(
'description',
e.target.value,
)
}
placeholder="Deskripsi singkat aplikasi"
rows={4}
/>
<FieldError
error={errors.description}
label="Deskripsi"
className="mt-1 text-xs"
/>
</Field>
<Field>
<Label htmlFor="address" required>
Alamat
</Label>
<Textarea
id="address"
value={data.address || ''}
onChange={(e) =>
setData('address', e.target.value)
}
placeholder="Alamat lengkap toko"
rows={3}
/>
<FieldError
error={errors.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>
{logoPreview ? (
<div className="relative inline-block w-full">
<img
src={logoPreview}
alt="Logo preview"
className="w-full rounded-xl border object-cover shadow"
/>
<button
type="button"
onClick={removeLogo}
className="absolute -top-2 -right-2 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={() =>
logoInputRef.current?.click()
}
className="group flex h-40 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="mb-2 h-10 w-10 text-muted-foreground transition-colors group-hover:text-primary" />
<span className="text-sm font-medium text-muted-foreground transition-colors group-hover:text-primary">
Klik untuk upload logo
</span>
<span className="mt-1 text-xs text-muted-foreground">
PNG, JPG, WEBP maks. 2MB
</span>
</button>
)}
<input
ref={logoInputRef}
type="file"
accept="image/*"
className="hidden"
onChange={handleLogoChange}
/>
<FieldError
error={errors.logo}
label="Logo Aplikasi"
className="mt-2 text-xs"
/>
</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>
{iconPreview ? (
<div className="relative flex inline-block w-full justify-center">
<div className="relative">
<img
src={iconPreview}
alt="Icon preview"
className="size-24 rounded-xl border object-contain p-2 shadow"
/>
<button
type="button"
onClick={removeIcon}
className="absolute -top-2 -right-2 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>
</div>
) : (
<button
type="button"
onClick={() =>
iconInputRef.current?.click()
}
className="group flex h-32 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="mb-2 h-8 w-8 text-muted-foreground transition-colors group-hover:text-primary" />
<span className="text-sm font-medium text-muted-foreground transition-colors group-hover:text-primary">
Klik untuk upload icon
</span>
<span className="mt-1 text-xs text-muted-foreground">
ICO, PNG maks. 1MB
</span>
</button>
)}
<input
ref={iconInputRef}
type="file"
accept="image/*"
className="hidden"
onChange={handleIconChange}
/>
<FieldError
error={errors.icon}
label="Favicon / Icon"
className="mt-2 text-xs"
/>
</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',
},
],
};