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:
dwindown
2025-11-05 18:54:41 +07:00
parent f8247faf22
commit e49a0d1e3d
19 changed files with 4264 additions and 68 deletions

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}