ws-02 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AGENTS.md ADDED
@@ -0,0 +1,5 @@
1
+ <!-- BEGIN:nextjs-agent-rules -->
2
+ # This is NOT the Next.js you know
3
+
4
+ This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` before writing any code. Heed deprecation notices.
5
+ <!-- END:nextjs-agent-rules -->
package/CLAUDE.md ADDED
@@ -0,0 +1 @@
1
+ @AGENTS.md
package/README.md ADDED
@@ -0,0 +1,36 @@
1
+ This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
2
+
3
+ ## Getting Started
4
+
5
+ First, run the development server:
6
+
7
+ ```bash
8
+ npm run dev
9
+ # or
10
+ yarn dev
11
+ # or
12
+ pnpm dev
13
+ # or
14
+ bun dev
15
+ ```
16
+
17
+ Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
18
+
19
+ You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
20
+
21
+ This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
22
+
23
+ ## Learn More
24
+
25
+ To learn more about Next.js, take a look at the following resources:
26
+
27
+ - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
28
+ - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
29
+
30
+ You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
31
+
32
+ ## Deploy on Vercel
33
+
34
+ The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
35
+
36
+ Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
@@ -0,0 +1,88 @@
1
+ "use client";
2
+ import Image from "next/image";
3
+ import { useCart } from "@/app/context/CartContext";
4
+
5
+ export default function CartPage() {
6
+ const { cart, increaseQuantity, decreaseQuantity, removeFromCart } = useCart();
7
+
8
+ const total = cart.reduce(
9
+ (sum, item) => sum + item.price * item.quantity,
10
+ 0
11
+ );
12
+
13
+ if (cart.length === 0) {
14
+ return (
15
+ <div className="p-4">
16
+ <h1 className="text-3xl font-bold mb-4">Votre Panier</h1>
17
+ <p className="text-lg">
18
+ Vous n&apos;avez encore ajouté aucun produit à votre panier.
19
+ </p>
20
+ </div>
21
+ );
22
+ }
23
+
24
+ return (
25
+ <div className="max-w-screen-lg mx-auto p-6">
26
+ <h1 className="text-3xl font-bold mb-4">Votre Panier</h1>
27
+
28
+ <ul className="space-y-4">
29
+ {cart.map((item) => (
30
+ <li
31
+ key={item.id}
32
+ className="flex justify-between items-center border-b pb-4 gap-4"
33
+ >
34
+ <div className="flex items-center space-x-4 min-w-0">
35
+ <div className="relative w-16 h-16 shrink-0 bg-gray-50">
36
+ <Image
37
+ src={item.image}
38
+ alt={item.title}
39
+ fill
40
+ className="object-contain"
41
+ />
42
+ </div>
43
+ <div className="min-w-0">
44
+ <h2 className="font-bold truncate">{item.title}</h2>
45
+ <p className="text-gray-600">{item.price.toFixed(2)} €</p>
46
+ </div>
47
+ </div>
48
+
49
+ <div className="flex items-center gap-4 shrink-0">
50
+ <div className="flex items-center gap-2">
51
+ <button
52
+ onClick={() => decreaseQuantity(item.id)}
53
+ aria-label="Diminuer la quantité"
54
+ className="w-8 h-8 flex items-center justify-center rounded border border-gray-300 hover:bg-gray-100"
55
+ >
56
+
57
+ </button>
58
+ <span className="w-6 text-center">{item.quantity}</span>
59
+ <button
60
+ onClick={() => increaseQuantity(item.id)}
61
+ aria-label="Augmenter la quantité"
62
+ className="w-8 h-8 flex items-center justify-center rounded border border-gray-300 hover:bg-gray-100"
63
+ >
64
+ +
65
+ </button>
66
+ </div>
67
+
68
+ <span className="w-20 text-right font-semibold">
69
+ {(item.price * item.quantity).toFixed(2)} €
70
+ </span>
71
+
72
+ <button
73
+ onClick={() => removeFromCart(item.id)}
74
+ className="text-red-600 hover:text-red-800 hover:underline"
75
+ >
76
+ Supprimer
77
+ </button>
78
+ </div>
79
+ </li>
80
+ ))}
81
+ </ul>
82
+
83
+ <div className="mt-6 text-right text-xl font-bold">
84
+ Total : {total.toFixed(2)} €
85
+ </div>
86
+ </div>
87
+ );
88
+ }
@@ -0,0 +1,39 @@
1
+ "use client";
2
+ import { useState, useEffect } from 'react';
3
+
4
+ const DarkLightToggle = () => {
5
+ const [isDarkMode, setIsDarkMode] = useState(false);
6
+
7
+ useEffect(() => {
8
+ if (localStorage.getItem('theme') === 'dark') {
9
+ setIsDarkMode(true);
10
+ document.documentElement.classList.add('dark');
11
+ } else {
12
+ setIsDarkMode(false);
13
+ document.documentElement.classList.remove('dark');
14
+ }
15
+ }, []);
16
+
17
+ const toggleTheme = () => {
18
+ if (isDarkMode) {
19
+ document.documentElement.classList.remove('dark');
20
+ localStorage.setItem('theme', 'light');
21
+ } else {
22
+ document.documentElement.classList.add('dark');
23
+ localStorage.setItem('theme', 'dark');
24
+ }
25
+ setIsDarkMode(!isDarkMode);
26
+ };
27
+
28
+ return (
29
+ <button
30
+ onClick={toggleTheme}
31
+ aria-label="Basculer entre le mode clair et le mode sombre"
32
+ className="p-2 rounded-full bg-gray-200 dark:bg-gray-700 text-gray-900 dark:text-white"
33
+ >
34
+ {isDarkMode ? <span>🌙</span> : <span>🌞</span>}
35
+ </button>
36
+ );
37
+ };
38
+
39
+ export default DarkLightToggle;
@@ -0,0 +1,78 @@
1
+ "use client";
2
+ import { useState } from "react";
3
+ import { useRouter } from "next/navigation";
4
+ import { useAuth } from "@/app/context/AuthContext";
5
+
6
+ export default function LoginForm() {
7
+ const { login, error } = useAuth();
8
+ const router = useRouter();
9
+
10
+ const [username, setUsername] = useState("");
11
+ const [password, setPassword] = useState("");
12
+ const [submitting, setSubmitting] = useState(false);
13
+
14
+ const handleSubmit = async (e) => {
15
+ e.preventDefault();
16
+ setSubmitting(true);
17
+ const success = await login(username, password);
18
+ setSubmitting(false);
19
+
20
+ if (success) {
21
+ router.push("/profile");
22
+ }
23
+ };
24
+
25
+ return (
26
+ <form
27
+ onSubmit={handleSubmit}
28
+ className="w-full max-w-sm mx-auto bg-white p-6 rounded-2xl shadow space-y-4"
29
+ >
30
+ <h1 className="text-2xl font-bold text-gray-800">Connexion</h1>
31
+
32
+ {error && (
33
+ <p className="bg-red-100 text-red-700 text-sm rounded p-2">{error}</p>
34
+ )}
35
+
36
+ <div className="flex flex-col gap-1">
37
+ <label htmlFor="username" className="text-sm font-medium text-gray-700">
38
+ Identifiant
39
+ </label>
40
+ <input
41
+ id="username"
42
+ type="text"
43
+ value={username}
44
+ onChange={(e) => setUsername(e.target.value)}
45
+ required
46
+ className="border border-gray-300 rounded px-3 py-2 text-gray-900 focus:outline-none focus:ring-2 focus:ring-blue-500"
47
+ />
48
+ </div>
49
+
50
+ <div className="flex flex-col gap-1">
51
+ <label htmlFor="password" className="text-sm font-medium text-gray-700">
52
+ Mot de passe
53
+ </label>
54
+ <input
55
+ id="password"
56
+ type="password"
57
+ value={password}
58
+ onChange={(e) => setPassword(e.target.value)}
59
+ required
60
+ className="border border-gray-300 rounded px-3 py-2 text-gray-900 focus:outline-none focus:ring-2 focus:ring-blue-500"
61
+ />
62
+ </div>
63
+
64
+ <button
65
+ type="submit"
66
+ disabled={submitting}
67
+ className="w-full bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600 transition-colors disabled:opacity-60"
68
+ >
69
+ {submitting ? "Connexion..." : "Se connecter"}
70
+ </button>
71
+
72
+ <p className="text-xs text-gray-500">
73
+ Démo FakeStoreAPI — identifiant&nbsp;: <code>mor_2314</code>, mot de
74
+ passe&nbsp;: <code>83r5^_</code>
75
+ </p>
76
+ </form>
77
+ );
78
+ }
@@ -0,0 +1,58 @@
1
+ "use client";
2
+ import Link from "next/link";
3
+ import { useRouter } from "next/navigation";
4
+ import { AiOutlineShoppingCart } from 'react-icons/ai';
5
+ import { useCart } from "@/app/context/CartContext";
6
+ import { useAuth } from "@/app/context/AuthContext";
7
+ import DarkLightToggle from "@/app/components/DarkLightToggle";
8
+
9
+ export default function Navbar() {
10
+ const { cart } = useCart();
11
+ const { accessToken, logout } = useAuth();
12
+ const router = useRouter();
13
+
14
+ const itemCount = cart.reduce((sum, item) => sum + item.quantity, 0);
15
+
16
+ const handleLogout = () => {
17
+ logout();
18
+ router.push("/");
19
+ };
20
+
21
+ return (
22
+ <nav className="bg-gray-800 text-white p-4">
23
+ <div className="container mx-auto flex justify-between items-center">
24
+ <Link href="/">
25
+ <span className="text-2xl font-bold cursor-pointer">Fake Store</span>
26
+ </Link>
27
+
28
+ <div className="flex items-center gap-6">
29
+ <DarkLightToggle />
30
+
31
+ {accessToken ? (
32
+ <>
33
+ <Link href="/profile" className="hover:underline">
34
+ Profil
35
+ </Link>
36
+ <button onClick={handleLogout} className="hover:underline">
37
+ Déconnexion
38
+ </button>
39
+ </>
40
+ ) : (
41
+ <Link href="/login" className="hover:underline">
42
+ Connexion
43
+ </Link>
44
+ )}
45
+
46
+ <Link href="/cart" className="relative" aria-label="Voir le panier">
47
+ <AiOutlineShoppingCart className="text-2xl cursor-pointer" />
48
+ {itemCount > 0 && (
49
+ <span className="absolute -top-2 -right-2 bg-red-500 text-white text-xs font-bold rounded-full h-5 min-w-5 px-1 flex items-center justify-center">
50
+ {itemCount}
51
+ </span>
52
+ )}
53
+ </Link>
54
+ </div>
55
+ </div>
56
+ </nav>
57
+ );
58
+ }
@@ -0,0 +1,50 @@
1
+ "use client";
2
+ import Link from "next/link";
3
+ import Image from "next/image";
4
+ import { useCart } from "@/app/context/CartContext";
5
+
6
+ export default function ProductCard({ product }) {
7
+ const { addToCart } = useCart();
8
+
9
+ const handleAddToCart = () => {
10
+ addToCart(product);
11
+ };
12
+
13
+ return (
14
+ <div className="flex flex-col bg-white dark:bg-gray-800 rounded-2xl shadow hover:shadow-lg transition-shadow duration-200 overflow-hidden h-full">
15
+ <Link href={`/products/${product.id}`} className="flex flex-col flex-1 cursor-pointer">
16
+ <div className="relative w-full h-48 bg-gray-50 dark:bg-gray-700">
17
+ <Image
18
+ src={product.image}
19
+ alt={product.title}
20
+ fill
21
+ className="object-contain p-4"
22
+ />
23
+ </div>
24
+
25
+ <div className="flex flex-col flex-1 p-4 gap-2">
26
+ <span className="text-xs font-medium text-indigo-600 dark:text-indigo-400 uppercase tracking-wide">
27
+ {product.category}
28
+ </span>
29
+
30
+ <h2 className="text-sm font-semibold text-gray-800 dark:text-gray-100 line-clamp-2 flex-1">
31
+ {product.title}
32
+ </h2>
33
+
34
+ <span className="text-lg font-bold text-gray-900 dark:text-white mt-2">
35
+ ${product.price.toFixed(2)}
36
+ </span>
37
+ </div>
38
+ </Link>
39
+
40
+ <div className="p-4 pt-0">
41
+ <button
42
+ onClick={handleAddToCart}
43
+ className="w-full bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600 transition-colors"
44
+ >
45
+ Ajouter au panier
46
+ </button>
47
+ </div>
48
+ </div>
49
+ );
50
+ }
@@ -0,0 +1,46 @@
1
+ "use client";
2
+ import { createContext, useContext, useState } from "react";
3
+ import { loginUser } from "@/app/utils/api";
4
+
5
+ const AuthContext = createContext();
6
+
7
+ export const AuthProvider = ({ children }) => {
8
+ const [accessToken, setAccessToken] = useState(null);
9
+ const [error, setError] = useState(null);
10
+
11
+ const login = async (username, password) => {
12
+ setError(null);
13
+ try {
14
+ const data = await loginUser(username, password);
15
+ const { token } = data;
16
+
17
+ if (token) {
18
+ setAccessToken(token);
19
+ return true;
20
+ }
21
+
22
+ setError("Connexion impossible : aucun token reçu.");
23
+ return false;
24
+ } catch (error) {
25
+ const apiMessage =
26
+ typeof error.response?.data === "string"
27
+ ? error.response.data
28
+ : error.response?.data?.message;
29
+ setError(apiMessage || "Identifiant ou mot de passe incorrect.");
30
+ return false;
31
+ }
32
+ };
33
+
34
+ const logout = () => {
35
+ setAccessToken(null);
36
+ setError(null);
37
+ };
38
+
39
+ return (
40
+ <AuthContext.Provider value={{ accessToken, error, login, logout }}>
41
+ {children}
42
+ </AuthContext.Provider>
43
+ );
44
+ };
45
+
46
+ export const useAuth = () => useContext(AuthContext);
@@ -0,0 +1,62 @@
1
+ "use client";
2
+ import { createContext, useContext, useState } from "react";
3
+
4
+ const CartContext = createContext();
5
+
6
+ export const CartProvider = ({ children }) => {
7
+ const [cart, setCart] = useState([]);
8
+
9
+ const addToCart = (product) => {
10
+ setCart((prevCart) => {
11
+ const existing = prevCart.find((item) => item.id === product.id);
12
+ if (existing) {
13
+ return prevCart.map((item) =>
14
+ item.id === product.id
15
+ ? { ...item, quantity: item.quantity + 1 }
16
+ : item
17
+ );
18
+ }
19
+ return [...prevCart, { ...product, quantity: 1 }];
20
+ });
21
+ };
22
+
23
+ const increaseQuantity = (id) => {
24
+ setCart((prevCart) =>
25
+ prevCart.map((item) =>
26
+ item.id === id ? { ...item, quantity: item.quantity + 1 } : item
27
+ )
28
+ );
29
+ };
30
+
31
+ const decreaseQuantity = (id) => {
32
+ setCart((prevCart) =>
33
+ prevCart
34
+ .map((item) =>
35
+ item.id === id
36
+ ? { ...item, quantity: item.quantity - 1 }
37
+ : item
38
+ )
39
+ .filter((item) => item.quantity > 0)
40
+ );
41
+ };
42
+
43
+ const removeFromCart = (id) => {
44
+ setCart((prevCart) => prevCart.filter((item) => item.id !== id));
45
+ };
46
+
47
+ return (
48
+ <CartContext.Provider
49
+ value={{
50
+ cart,
51
+ addToCart,
52
+ increaseQuantity,
53
+ decreaseQuantity,
54
+ removeFromCart,
55
+ }}
56
+ >
57
+ {children}
58
+ </CartContext.Provider>
59
+ );
60
+ };
61
+
62
+ export const useCart = () => useContext(CartContext);
Binary file
@@ -0,0 +1,29 @@
1
+ @import "tailwindcss";
2
+
3
+ /* Tailwind v4 : pilote la variante `dark:` via la classe `.dark`
4
+ (au lieu de prefers-color-scheme), pour que le bouton de bascule
5
+ contrôle réellement le thème. */
6
+ @custom-variant dark (&:where(.dark, .dark *));
7
+
8
+ :root {
9
+ --background: #ffffff;
10
+ --foreground: #171717;
11
+ }
12
+
13
+ .dark {
14
+ --background: #0a0a0a;
15
+ --foreground: #ededed;
16
+ }
17
+
18
+ @theme inline {
19
+ --color-background: var(--background);
20
+ --color-foreground: var(--foreground);
21
+ --font-sans: var(--font-geist-sans);
22
+ --font-mono: var(--font-geist-mono);
23
+ }
24
+
25
+ body {
26
+ background: var(--background);
27
+ color: var(--foreground);
28
+ font-family: Arial, Helvetica, sans-serif;
29
+ }
package/app/layout.tsx ADDED
@@ -0,0 +1,44 @@
1
+ import type { Metadata } from "next";
2
+ import { Geist, Geist_Mono } from "next/font/google";
3
+ import "./globals.css";
4
+
5
+ import Navbar from '@/app/components/Navbar.js';
6
+ import { CartProvider } from '@/app/context/CartContext';
7
+ import { AuthProvider } from '@/app/context/AuthContext';
8
+
9
+ const geistSans = Geist({
10
+ variable: "--font-geist-sans",
11
+ subsets: ["latin"],
12
+ });
13
+
14
+ const geistMono = Geist_Mono({
15
+ variable: "--font-geist-mono",
16
+ subsets: ["latin"],
17
+ });
18
+
19
+ export const metadata: Metadata = {
20
+ title: "Create Next App",
21
+ description: "Generated by create next app",
22
+ };
23
+
24
+ export default function RootLayout({
25
+ children,
26
+ }: Readonly<{
27
+ children: React.ReactNode;
28
+ }>) {
29
+ return (
30
+ <html
31
+ lang="en"
32
+ className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`}
33
+ >
34
+ <body className="min-h-full flex flex-col">
35
+ <AuthProvider>
36
+ <CartProvider>
37
+ <Navbar />
38
+ {children}
39
+ </CartProvider>
40
+ </AuthProvider>
41
+ </body>
42
+ </html>
43
+ );
44
+ }
@@ -0,0 +1,9 @@
1
+ import LoginForm from "@/app/components/LoginForm";
2
+
3
+ export default function LoginPage() {
4
+ return (
5
+ <div className="container mx-auto p-4 flex justify-center">
6
+ <LoginForm />
7
+ </div>
8
+ );
9
+ }
package/app/page.tsx ADDED
@@ -0,0 +1,39 @@
1
+ 'use client';
2
+ import Image from "next/image";
3
+ import ProductCard from "@/app/components/ProductCard.js";
4
+ import { useEffect, useState } from 'react';
5
+ import { getProducts } from '@/app/utils/api.js';
6
+
7
+ interface Product {
8
+ id: number;
9
+ title: string;
10
+ price: number;
11
+ description: string;
12
+ category: string;
13
+ image: string;
14
+ rating: { rate: number; count: number };
15
+ }
16
+
17
+ export default function Home() {
18
+ const [products, setProducts] = useState<Product[]>([]);
19
+
20
+ useEffect(() => {
21
+ const fetchProducts = async () => {
22
+ const data = await getProducts();
23
+ setProducts(data);
24
+ };
25
+
26
+ fetchProducts();
27
+ }, []);
28
+
29
+ return (
30
+ <div className="container mx-auto p-4">
31
+ <h1 className="text-2xl font-bold text-gray-800 dark:text-gray-100 mb-4">Nos produits</h1>
32
+ <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
33
+ {products.map((product) => (
34
+ <ProductCard key={product.id} product={product} />
35
+ ))}
36
+ </div>
37
+ </div>
38
+ );
39
+ }
@@ -0,0 +1,23 @@
1
+ import Image from 'next/image';
2
+ import { getProductById } from '@/app/utils/api';
3
+
4
+ export default async function ProductDetails({ params }) {
5
+ const { id } = await params;
6
+
7
+ const product = await getProductById(id);
8
+
9
+ return (
10
+ <div className="max-w-3xl mx-auto p-6">
11
+ <div className="relative w-full h-80 bg-gray-50">
12
+ <Image src={product.image} alt={product.title} fill className="object-contain p-4" />
13
+ </div>
14
+ <h1 className="text-2xl font-bold mt-4">{product.title}</h1>
15
+ <p className="text-gray-600 mt-2">{product.category}</p>
16
+ <p className="text-xl font-semibold mt-2">{product.price} €</p>
17
+ <p className="mt-4">{product.description}</p>
18
+ <p className="mt-2 text-sm">
19
+ Note : {product.rating?.rate} / 5 ({product.rating?.count} avis)
20
+ </p>
21
+ </div>
22
+ );
23
+ }
@@ -0,0 +1,49 @@
1
+ "use client";
2
+ import { useEffect } from "react";
3
+ import { useRouter } from "next/navigation";
4
+ import { useAuth } from "@/app/context/AuthContext";
5
+
6
+ export default function ProfilePage() {
7
+ const { accessToken, logout } = useAuth();
8
+ const router = useRouter();
9
+
10
+ useEffect(() => {
11
+ if (!accessToken) {
12
+ router.replace("/login");
13
+ }
14
+ }, [accessToken, router]);
15
+
16
+ if (!accessToken) {
17
+ return null;
18
+ }
19
+
20
+ const handleLogout = () => {
21
+ logout();
22
+ router.push("/login");
23
+ };
24
+
25
+ return (
26
+ <div className="max-w-screen-lg mx-auto p-6">
27
+ <h1 className="text-3xl font-bold mb-4">Mon Profil</h1>
28
+ <p className="text-gray-700 mb-4">
29
+ Bienvenue ! Vous êtes connecté et pouvez accéder à cette page protégée.
30
+ </p>
31
+
32
+ <div className="bg-gray-50 border border-gray-200 rounded-lg p-4 mb-6">
33
+ <p className="text-sm font-medium text-gray-600 mb-1">
34
+ Token d&apos;accès
35
+ </p>
36
+ <p className="text-xs font-mono text-gray-800 break-all">
37
+ {accessToken}
38
+ </p>
39
+ </div>
40
+
41
+ <button
42
+ onClick={handleLogout}
43
+ className="bg-red-500 text-white px-4 py-2 rounded hover:bg-red-600 transition-colors"
44
+ >
45
+ Se déconnecter
46
+ </button>
47
+ </div>
48
+ );
49
+ }
@@ -0,0 +1,39 @@
1
+ import axios from "axios";
2
+
3
+ const apiClient = axios.create({
4
+ baseURL: "https://fakestoreapi.com",
5
+ timeout: 5000,
6
+ });
7
+
8
+ export const getProducts = async () => {
9
+ try {
10
+ const reponse = await apiClient.get("/products");
11
+ return reponse.data;
12
+ } catch (error) {
13
+ console.error("Erreur lors de la récupération des produits :", error);
14
+ throw error;
15
+ }
16
+ }
17
+
18
+ export const getProductById = async (id) => {
19
+ try {
20
+ const reponse = await apiClient.get(`/products/${id}`);
21
+ return reponse.data;
22
+ } catch (error) {
23
+ console.error("Erreur lors de la récupération du produit :", error);
24
+ throw error;
25
+ }
26
+ }
27
+
28
+ export const loginUser = async (username, password) => {
29
+ try {
30
+ const response = await apiClient.post("/auth/login", {
31
+ username,
32
+ password,
33
+ });
34
+ return response.data; // Renvoie le token ou d'autres données
35
+ } catch (error) {
36
+ console.error("Erreur lors de la connexion :", error);
37
+ throw error;
38
+ }
39
+ };
@@ -0,0 +1,18 @@
1
+ import { defineConfig, globalIgnores } from "eslint/config";
2
+ import nextVitals from "eslint-config-next/core-web-vitals";
3
+ import nextTs from "eslint-config-next/typescript";
4
+
5
+ const eslintConfig = defineConfig([
6
+ ...nextVitals,
7
+ ...nextTs,
8
+ // Override default ignores of eslint-config-next.
9
+ globalIgnores([
10
+ // Default ignores of eslint-config-next:
11
+ ".next/**",
12
+ "out/**",
13
+ "build/**",
14
+ "next-env.d.ts",
15
+ ]),
16
+ ]);
17
+
18
+ export default eslintConfig;
package/next.config.ts ADDED
@@ -0,0 +1,16 @@
1
+ import type { NextConfig } from "next";
2
+
3
+ const nextConfig: NextConfig = {
4
+ images: {
5
+ remotePatterns: [
6
+ {
7
+ protocol: 'https',
8
+ hostname: 'fakestoreapi.com',
9
+ port: '',
10
+ pathname: '/img/**',
11
+ },
12
+ ],
13
+ },
14
+ };
15
+
16
+ export default nextConfig;
package/package.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "ws-02",
3
+ "version": "0.1.0",
4
+ "private": false,
5
+ "scripts": {
6
+ "dev": "next dev",
7
+ "build": "next build",
8
+ "start": "next start",
9
+ "lint": "eslint"
10
+ },
11
+ "dependencies": {
12
+ "axios": "^1.17.0",
13
+ "next": "16.2.7",
14
+ "react": "19.2.4",
15
+ "react-dom": "19.2.4",
16
+ "react-icons": "^5.6.0"
17
+ },
18
+ "devDependencies": {
19
+ "@tailwindcss/postcss": "^4",
20
+ "@types/node": "^20",
21
+ "@types/react": "^19",
22
+ "@types/react-dom": "^19",
23
+ "eslint": "^9",
24
+ "eslint-config-next": "16.2.7",
25
+ "tailwindcss": "^4",
26
+ "typescript": "^5"
27
+ }
28
+ }
@@ -0,0 +1,7 @@
1
+ const config = {
2
+ plugins: {
3
+ "@tailwindcss/postcss": {},
4
+ },
5
+ };
6
+
7
+ export default config;
@@ -0,0 +1 @@
1
+ <svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>
@@ -0,0 +1 @@
1
+ <svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>
@@ -0,0 +1 @@
1
+ <svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>
@@ -0,0 +1 @@
1
+ <svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>
package/tsconfig.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2017",
4
+ "lib": ["dom", "dom.iterable", "esnext"],
5
+ "allowJs": true,
6
+ "skipLibCheck": true,
7
+ "strict": true,
8
+ "noEmit": true,
9
+ "esModuleInterop": true,
10
+ "module": "esnext",
11
+ "moduleResolution": "bundler",
12
+ "resolveJsonModule": true,
13
+ "isolatedModules": true,
14
+ "jsx": "react-jsx",
15
+ "incremental": true,
16
+ "plugins": [
17
+ {
18
+ "name": "next"
19
+ }
20
+ ],
21
+ "paths": {
22
+ "@/*": ["./*"]
23
+ }
24
+ },
25
+ "include": [
26
+ "next-env.d.ts",
27
+ "**/*.ts",
28
+ "**/*.tsx",
29
+ ".next/types/**/*.ts",
30
+ ".next/dev/types/**/*.ts",
31
+ "**/*.mts"
32
+ ],
33
+ "exclude": ["node_modules"]
34
+ }