feat: Implement Phase 1 Shopify-inspired settings (Store, Payments, Shipping)
✨ Features: - Store Details page with live currency preview - Payments page with visual provider cards and test mode - Shipping & Delivery page with zone cards and local pickup - Shared components: SettingsLayout, SettingsCard, SettingsSection, ToggleField 🎨 UI/UX: - Card-based layouts (not boring forms) - Generous whitespace and visual hierarchy - Toast notifications using sonner (reused from Orders) - Sticky save button at top - Mobile-responsive design 🔧 Technical: - Installed ESLint with TypeScript support - Fixed all lint errors (0 errors) - Phase 1 files have zero warnings - Used existing toast from sonner (not reinvented) - Updated routes in App.tsx 📝 Files Created: - Store.tsx (currency preview, address, timezone) - Payments.tsx (provider cards, manual methods) - Shipping.tsx (zone cards, rates, local pickup) - SettingsLayout.tsx, SettingsCard.tsx, SettingsSection.tsx, ToggleField.tsx Phase 1 complete: 18-24 hours estimated work
This commit is contained in:
23
admin-spa/src/routes/Settings/components/SettingsCard.tsx
Normal file
23
admin-spa/src/routes/Settings/components/SettingsCard.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import React from 'react';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
|
||||
interface SettingsCardProps {
|
||||
title: string;
|
||||
description?: string;
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function SettingsCard({ title, description, children, className = '' }: SettingsCardProps) {
|
||||
return (
|
||||
<Card className={className}>
|
||||
<CardHeader>
|
||||
<CardTitle>{title}</CardTitle>
|
||||
{description && <CardDescription>{description}</CardDescription>}
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{children}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
82
admin-spa/src/routes/Settings/components/SettingsLayout.tsx
Normal file
82
admin-spa/src/routes/Settings/components/SettingsLayout.tsx
Normal file
@@ -0,0 +1,82 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Loader2 } from 'lucide-react';
|
||||
|
||||
interface SettingsLayoutProps {
|
||||
title: string;
|
||||
description?: string;
|
||||
children: React.ReactNode;
|
||||
onSave?: () => Promise<void>;
|
||||
saveLabel?: string;
|
||||
isLoading?: boolean;
|
||||
}
|
||||
|
||||
export function SettingsLayout({
|
||||
title,
|
||||
description,
|
||||
children,
|
||||
onSave,
|
||||
saveLabel = 'Save changes',
|
||||
isLoading = false,
|
||||
}: SettingsLayoutProps) {
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!onSave) return;
|
||||
setIsSaving(true);
|
||||
try {
|
||||
await onSave();
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
{/* Sticky Header with Save Button */}
|
||||
{onSave && (
|
||||
<div className="sticky top-0 z-10 border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
|
||||
<div className="container max-w-5xl mx-auto px-4 py-3 flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-lg font-semibold">{title}</h1>
|
||||
</div>
|
||||
<Button
|
||||
onClick={handleSave}
|
||||
disabled={isSaving || isLoading}
|
||||
size="sm"
|
||||
>
|
||||
{isSaving ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
Saving...
|
||||
</>
|
||||
) : (
|
||||
saveLabel
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Content */}
|
||||
<div className="container max-w-5xl mx-auto px-4 py-8">
|
||||
{!onSave && (
|
||||
<div className="mb-8">
|
||||
<h1 className="text-2xl font-bold tracking-tight">{title}</h1>
|
||||
{description && (
|
||||
<p className="text-muted-foreground mt-2">{description}</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-6">{children}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
31
admin-spa/src/routes/Settings/components/SettingsSection.tsx
Normal file
31
admin-spa/src/routes/Settings/components/SettingsSection.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import React from 'react';
|
||||
import { Label } from '@/components/ui/label';
|
||||
|
||||
interface SettingsSectionProps {
|
||||
label: string;
|
||||
description?: string;
|
||||
required?: boolean;
|
||||
children: React.ReactNode;
|
||||
htmlFor?: string;
|
||||
}
|
||||
|
||||
export function SettingsSection({
|
||||
label,
|
||||
description,
|
||||
required = false,
|
||||
children,
|
||||
htmlFor,
|
||||
}: SettingsSectionProps) {
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor={htmlFor} className="text-sm font-medium">
|
||||
{label}
|
||||
{required && <span className="text-destructive ml-1">*</span>}
|
||||
</Label>
|
||||
{description && (
|
||||
<p className="text-sm text-muted-foreground">{description}</p>
|
||||
)}
|
||||
<div>{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
40
admin-spa/src/routes/Settings/components/ToggleField.tsx
Normal file
40
admin-spa/src/routes/Settings/components/ToggleField.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import React from 'react';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { Label } from '@/components/ui/label';
|
||||
|
||||
interface ToggleFieldProps {
|
||||
id: string;
|
||||
label: string;
|
||||
description?: string;
|
||||
checked: boolean;
|
||||
onCheckedChange: (checked: boolean) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export function ToggleField({
|
||||
id,
|
||||
label,
|
||||
description,
|
||||
checked,
|
||||
onCheckedChange,
|
||||
disabled = false,
|
||||
}: ToggleFieldProps) {
|
||||
return (
|
||||
<div className="flex items-start justify-between space-x-4 py-2">
|
||||
<div className="flex-1 space-y-1">
|
||||
<Label htmlFor={id} className="text-sm font-medium cursor-pointer">
|
||||
{label}
|
||||
</Label>
|
||||
{description && (
|
||||
<p className="text-sm text-muted-foreground">{description}</p>
|
||||
)}
|
||||
</div>
|
||||
<Switch
|
||||
id={id}
|
||||
checked={checked}
|
||||
onCheckedChange={onCheckedChange}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user