Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 3 additions & 13 deletions src/app/[country]/[locale]/(checkout)/checkout/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { usePathname, useRouter, useSearchParams } from "next/navigation";
import { useTranslations } from "next-intl";
import { Suspense, use, useCallback, useEffect, useRef, useState } from "react";
import { AddressSection } from "@/components/checkout/AddressSection";
import { CheckoutPageSkeleton } from "@/components/checkout/CheckoutPageSkeleton";
import { CouponCode } from "@/components/checkout/CouponCode";
import { DeliveryMethodSection } from "@/components/checkout/DeliveryMethodSection";
import {
Expand Down Expand Up @@ -543,19 +544,8 @@ function CheckoutPageContent({ params }: CheckoutPageProps) {
// PaymentSection handles setProcessing(false) on error internally
};

// Loading state
if (loading || authLoading) {
return (
<div className="animate-pulse space-y-6">
<div className="h-8 bg-gray-200 rounded w-1/3" />
<div className="h-4 bg-gray-200 rounded w-1/4" />
<div className="space-y-4 mt-8">
<div className="h-12 bg-gray-200 rounded" />
<div className="h-12 bg-gray-200 rounded" />
<div className="h-12 bg-gray-200 rounded" />
</div>
</div>
);
return <CheckoutPageSkeleton />;
}

// Error state (no cart loaded)
Expand Down Expand Up @@ -613,7 +603,7 @@ function CheckoutPageContent({ params }: CheckoutPageProps) {
countries={countries}
savedAddresses={savedAddresses}
isAuthenticated={isAuthenticated}
signInUrl={`${basePath}/account?redirect=${encodeURIComponent(pathname)}`}
signInUrl={`${basePath}/account/login?redirect=${encodeURIComponent(pathname)}`}
fetchStates={fetchStates}
onEmailBlur={handleEmailBlur}
onAutoSave={handleAutoSave}
Expand Down
10 changes: 2 additions & 8 deletions src/app/[country]/[locale]/(checkout)/order-placed/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { usePathname } from "next/navigation";
import { useTranslations } from "next-intl";
import { use, useEffect, useRef, useState } from "react";
import { AddressBlock } from "@/components/order/AddressBlock";
import { OrderPlacedSkeleton } from "@/components/order/OrderPlacedSkeleton";
import { OrderTotals } from "@/components/order/OrderTotals";
import { PaymentInfo } from "@/components/order/PaymentInfo";
import { Button } from "@/components/ui/button";
Expand Down Expand Up @@ -90,14 +91,7 @@ export default function OrderPlacedPage({ params }: OrderPlacedPageProps) {
}, [cartId]);

if (loading) {
return (
<div className="animate-pulse space-y-6 py-12">
<div className="h-12 w-12 bg-gray-200 rounded-lg mx-auto" />
<div className="h-8 bg-gray-200 rounded w-1/2 mx-auto" />
<div className="h-4 bg-gray-200 rounded w-1/3 mx-auto" />
<div className="h-64 bg-gray-200 rounded mt-8" />
</div>
);
return <OrderPlacedSkeleton />;
}

if (error || !order) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { AddressesSkeleton } from "@/components/account/AddressesSkeleton";

export default function AddressesLoading() {
return <AddressesSkeleton />;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { CreditCardsSkeleton } from "@/components/account/CreditCardsSkeleton";

export default function CreditCardsLoading() {
return <CreditCardsSkeleton />;
}
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ export default function ForgotPasswordPage() {

<CardFooter className="justify-center">
<Link
href={`${basePath}/account`}
href={`${basePath}/account/login`}
className="text-sm text-primary hover:text-primary/70 font-medium"
>
{t("backToSignIn")}
Expand Down Expand Up @@ -145,7 +145,7 @@ export default function ForgotPasswordPage() {

<CardFooter className="justify-center">
<Link
href={`${basePath}/account`}
href={`${basePath}/account/login`}
className="text-sm text-primary hover:text-primary/70 font-medium"
>
{t("backToSignIn")}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { GiftCardsSkeleton } from "@/components/account/GiftCardsSkeleton";

export default function GiftCardsLoading() {
return <GiftCardsSkeleton />;
}
84 changes: 43 additions & 41 deletions src/app/[country]/[locale]/(storefront)/account/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,17 @@ import Link from "next/link";
import { usePathname, useRouter } from "next/navigation";
import { useTranslations } from "next-intl";
import { useEffect } from "react";
import { AccountDashboardSkeleton } from "@/components/account/AccountDashboardSkeleton";
import { AddressesSkeleton } from "@/components/account/AddressesSkeleton";
import { AuthFallbackSkeleton } from "@/components/account/AuthFallbackSkeleton";
import { ContentSkeleton } from "@/components/account/ContentSkeleton";
import { CreditCardsSkeleton } from "@/components/account/CreditCardsSkeleton";
import { GiftCardsSkeleton } from "@/components/account/GiftCardsSkeleton";
import { LoginFormSkeleton } from "@/components/account/LoginFormSkeleton";
import { OrdersListSkeleton } from "@/components/account/OrdersListSkeleton";
import { ProfileSkeleton } from "@/components/account/ProfileSkeleton";
import { RegisterFormSkeleton } from "@/components/account/RegisterFormSkeleton";
import { SidebarUserInfoSkeleton } from "@/components/account/SidebarUserInfoSkeleton";
import { Button } from "@/components/ui/button";
import { useAuth } from "@/contexts/AuthContext";
import { extractBasePath } from "@/lib/utils/path";
Expand All @@ -37,17 +48,6 @@ function getNavItems(t: ReturnType<typeof useTranslations<"account">>): {
];
}

function ContentSkeleton() {
return (
<div className="animate-pulse space-y-4">
<div className="h-8 bg-gray-200 rounded w-1/3" />
<div className="h-4 bg-gray-200 rounded w-2/3" />
<div className="h-32 bg-gray-200 rounded" />
<div className="h-32 bg-gray-200 rounded" />
</div>
);
}

interface AccountShellProps {
children: React.ReactNode;
basePath: string;
Expand Down Expand Up @@ -80,10 +80,7 @@ function AccountShell({
{/* User Info */}
<div className="p-4 border-b border-gray-200">
{isLoading ? (
<div className="animate-pulse space-y-2">
<div className="h-4 bg-gray-200 rounded w-24" />
<div className="h-3 bg-gray-200 rounded w-32" />
</div>
<SidebarUserInfoSkeleton />
) : (
<>
<p className="font-medium text-gray-900">
Expand Down Expand Up @@ -155,49 +152,54 @@ export default function AccountLayout({

// Pages that don't require authentication
const authPagePaths = new Set([
`${basePath}/account/login`,
`${basePath}/account/register`,
`${basePath}/account/forgot-password`,
`${basePath}/account/reset-password`,
]);
const isAuthPage = authPagePaths.has(pathname);
const isMainAccountPage = pathname === `${basePath}/account`;

// Redirect to login if not authenticated and trying to access protected sub-pages
// Redirect to login if not authenticated and trying to access protected pages
useEffect(() => {
if (!loading && !isAuthenticated && !isAuthPage && !isMainAccountPage) {
router.replace(`${basePath}/account`);
if (!loading && !isAuthenticated && !isAuthPage) {
router.replace(`${basePath}/account/login`);
}
}, [
loading,
isAuthenticated,
isAuthPage,
isMainAccountPage,
basePath,
router,
]);
}, [loading, isAuthenticated, isAuthPage, basePath, router]);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

// Show loading or redirect-in-progress skeleton
if (loading || (!isAuthenticated && !isAuthPage && !isMainAccountPage)) {
if (isAuthPage || isMainAccountPage) {
return (
<div className="max-w-md mx-auto px-4 sm:px-6 lg:px-8 py-16">
<div className="animate-pulse space-y-4">
<div className="h-8 bg-gray-200 rounded w-1/2 mx-auto" />
<div className="h-4 bg-gray-200 rounded w-3/4 mx-auto" />
<div className="h-48 bg-gray-200 rounded" />
</div>
</div>
);
if (loading || (!isAuthenticated && !isAuthPage)) {
if (pathname === `${basePath}/account/login`) {
return <LoginFormSkeleton />;
}
if (pathname === `${basePath}/account/register`) {
return <RegisterFormSkeleton />;
}
if (isAuthPage) {
// forgot-password / reset-password — generic fallback
return <AuthFallbackSkeleton />;
}
const isDashboardPage = pathname === `${basePath}/account`;
const isProfilePage = pathname === `${basePath}/account/profile`;
const isOrdersPage = pathname === `${basePath}/account/orders`;
const isGiftCardsPage = pathname === `${basePath}/account/gift-cards`;
const isCreditCardsPage = pathname === `${basePath}/account/credit-cards`;
const isAddressesPage = pathname === `${basePath}/account/addresses`;
let content: React.ReactNode = <ContentSkeleton />;
if (isDashboardPage) content = <AccountDashboardSkeleton />;
else if (isProfilePage) content = <ProfileSkeleton />;
else if (isOrdersPage) content = <OrdersListSkeleton />;
else if (isGiftCardsPage) content = <GiftCardsSkeleton />;
else if (isCreditCardsPage) content = <CreditCardsSkeleton />;
else if (isAddressesPage) content = <AddressesSkeleton />;
return (
<AccountShell basePath={basePath} pathname={pathname} isLoading={true}>
<ContentSkeleton />
{content}
</AccountShell>
);
}

// Don't show nav for login/register pages
if (isAuthPage || !isAuthenticated) {
// Don't show nav for login/register/forgot/reset pages
if (isAuthPage) {
return <>{children}</>;
}

Expand Down
167 changes: 167 additions & 0 deletions src/app/[country]/[locale]/(storefront)/account/login/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
"use client";

import { CircleAlert, Eye, EyeOff } from "lucide-react";
import Link from "next/link";
import { usePathname, useRouter, useSearchParams } from "next/navigation";
import { useTranslations } from "next-intl";
import { useEffect, useState } from "react";
import { Alert, AlertDescription } from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Field, FieldLabel } from "@/components/ui/field";
import { Input } from "@/components/ui/input";
import { useAuth } from "@/contexts/AuthContext";
import { extractBasePath } from "@/lib/utils/path";

export default function LoginPage() {
const router = useRouter();
const pathname = usePathname();
const searchParams = useSearchParams();
const basePath = extractBasePath(pathname);
const t = useTranslations("account");
const { login, isAuthenticated, loading: authLoading } = useAuth();

// Get redirect URL from query params (e.g., from checkout)
const redirectUrl = searchParams.get("redirect");
Comment thread
coderabbitai[bot] marked this conversation as resolved.

const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [showPassword, setShowPassword] = useState(false);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);

// Redirect if already authenticated
// useEffect is needed here to prevent rendering issues.
useEffect(() => {
if (!authLoading && isAuthenticated) {
router.push(redirectUrl ?? `${basePath}/account`);
}
}, [authLoading, isAuthenticated, redirectUrl, router, basePath]);
if (authLoading || isAuthenticated) {
return null;
}

const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError(null);
setLoading(true);

const result = await login(email, password);
if (result.success) {
// Redirect to the specified URL or go to account dashboard
router.push(redirectUrl ?? `${basePath}/account`);
} else {
setError(result.error || t("invalidCredentials"));
}
setLoading(false);
};
Comment thread
coderabbitai[bot] marked this conversation as resolved.

return (
<div className="max-w-md mx-auto px-4 sm:px-6 lg:px-8 py-16">
<Card>
<CardHeader className="text-center">
<CardTitle>{t("myAccount")}</CardTitle>
<CardDescription>{t("signInDescription")}</CardDescription>
</CardHeader>

<CardContent>
<form onSubmit={handleSubmit} className="space-y-4">
{error && (
<Alert variant="destructive">
<CircleAlert />
<AlertDescription>{error}</AlertDescription>
</Alert>
)}

<Field>
<FieldLabel htmlFor="email">{t("email")}</FieldLabel>
<Input
type="email"
id="email"
name="email"
autoComplete="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
placeholder="you@example.com"
/>
</Field>

<Field>
<FieldLabel htmlFor="password">{t("password")}</FieldLabel>
<div className="relative">
<Input
type={showPassword ? "text" : "password"}
id="password"
name="current-password"
autoComplete="current-password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
placeholder="••••••••"
className="pr-10"
/>
<div className="absolute right-1 top-1/2 -translate-y-1/2">
<Button
type="button"
variant="ghost"
size="icon-sm"
onClick={() => setShowPassword(!showPassword)}
aria-label={
showPassword ? t("hidePassword") : t("showPassword")
}
>
{showPassword ? (
<EyeOff className="w-5 h-5" />
) : (
<Eye className="w-5 h-5" />
)}
</Button>
</div>
</div>
</Field>

<div className="flex justify-end">
<Link
href={`${basePath}/account/forgot-password`}
className="text-sm text-primary hover:text-primary/70 font-medium"
>
{t("forgotPassword")}
</Link>
</div>

<div className="w-full">
<Button
type="submit"
disabled={loading}
size="lg"
className="w-full"
>
{loading ? t("signingIn") : t("signIn")}
</Button>
</div>
</form>
</CardContent>

<CardFooter className="justify-center">
<p className="text-sm text-muted-foreground">
{t("dontHaveAccount")}{" "}
<Link
href={`${basePath}/account/register`}
className="text-primary hover:text-primary/70 font-medium"
>
{t("signUp")}
</Link>
</p>
</CardFooter>
</Card>
</div>
);
}
Loading