feat: components
This commit is contained in:
parent
34a1c9ab19
commit
028487d708
|
|
@ -4,6 +4,9 @@ import { useState } from 'react'
|
||||||
import { useLogin } from '@/hooks/queries/auth'
|
import { useLogin } from '@/hooks/queries/auth'
|
||||||
import { useRouter } from 'next/navigation'
|
import { useRouter } from 'next/navigation'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
|
import Button from '@/components/common/Button'
|
||||||
|
import Input from '@/components/common/Input'
|
||||||
|
import Card from '@/components/common/Card'
|
||||||
|
|
||||||
export default function LoginPage() {
|
export default function LoginPage() {
|
||||||
const [email, setEmail] = useState('')
|
const [email, setEmail] = useState('')
|
||||||
|
|
@ -21,58 +24,56 @@ export default function LoginPage() {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-screen items-center justify-center bg-gray-50 px-4 py-12 sm:px-6 lg:px-8">
|
<div className="flex min-h-screen items-center justify-center bg-gray-50 dark:bg-neutral-950 px-4 py-12 sm:px-6 lg:px-8">
|
||||||
<div className="w-full max-w-md space-y-8">
|
<div className="w-full max-w-md space-y-8">
|
||||||
<div>
|
<div className="text-center">
|
||||||
<h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900">
|
<h2 className="text-3xl font-extrabold text-gray-900 dark:text-white">
|
||||||
Connectez-vous à votre compte
|
Connectez-vous
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
<form className="mt-8 space-y-6" onSubmit={handleSubmit}>
|
|
||||||
<div className="-space-y-px rounded-md shadow-sm">
|
<Card>
|
||||||
<div>
|
<form className="space-y-6" onSubmit={handleSubmit}>
|
||||||
<input
|
<div className="space-y-4">
|
||||||
|
<Input
|
||||||
|
label="Adresse email"
|
||||||
type="email"
|
type="email"
|
||||||
required
|
required
|
||||||
className="relative block w-full appearance-none rounded-none rounded-t-md border border-gray-300 px-3 py-2 text-gray-900 placeholder-gray-500 focus:z-10 focus:border-indigo-500 focus:outline-none focus:ring-indigo-500 sm:text-sm"
|
placeholder="email@example.com"
|
||||||
placeholder="Adresse email"
|
|
||||||
value={email}
|
value={email}
|
||||||
onChange={(e) => setEmail(e.target.value)}
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
<Input
|
||||||
<div>
|
label="Mot de passe"
|
||||||
<input
|
|
||||||
type="password"
|
type="password"
|
||||||
required
|
required
|
||||||
className="relative block w-full appearance-none rounded-none rounded-b-md border border-gray-300 px-3 py-2 text-gray-900 placeholder-gray-500 focus:z-10 focus:border-indigo-500 focus:outline-none focus:ring-indigo-500 sm:text-sm"
|
placeholder="••••••••"
|
||||||
placeholder="Mot de passe"
|
|
||||||
value={password}
|
value={password}
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{login.isError && (
|
{login.isError && (
|
||||||
<div className="text-red-500 text-sm text-center">
|
<p className="text-red-500 text-sm text-center">
|
||||||
{(login.error as any).message}
|
{(login.error as any).message}
|
||||||
</div>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div>
|
<Button
|
||||||
<button
|
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={login.isPending}
|
disabled={login.isPending}
|
||||||
className="group relative flex w-full justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-sm font-medium text-white hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 disabled:opacity-50"
|
className="w-full"
|
||||||
>
|
>
|
||||||
{login.isPending ? 'Connexion...' : 'Se connecter'}
|
{login.isPending ? 'Connexion...' : 'Se connecter'}
|
||||||
</button>
|
</Button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div className="mt-6 text-center">
|
||||||
|
<Link href="/register" className="text-sm font-medium text-black hover:underline dark:text-white">
|
||||||
|
Pas encore de compte ? S'inscrire
|
||||||
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</Card>
|
||||||
<div className="text-center">
|
|
||||||
<Link href="/register" className="text-indigo-600 hover:text-indigo-500 text-sm">
|
|
||||||
Pas encore de compte ? S'inscrire
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
103
src/app/page.tsx
103
src/app/page.tsx
|
|
@ -2,69 +2,78 @@
|
||||||
|
|
||||||
import { useUser, useLogout } from '@/hooks/queries/auth'
|
import { useUser, useLogout } from '@/hooks/queries/auth'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
|
import Button from '@/components/common/Button'
|
||||||
|
import Card from '@/components/common/Card'
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
|
const [mounted, setMounted] = useState(false)
|
||||||
const { data: user, isLoading } = useUser()
|
const { data: user, isLoading } = useUser()
|
||||||
const logout = useLogout()
|
const logout = useLogout()
|
||||||
|
|
||||||
if (isLoading) {
|
useEffect(() => {
|
||||||
|
setMounted(true)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
if (!mounted || isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-screen items-center justify-center">
|
<div className="flex min-h-screen items-center justify-center bg-gray-50 dark:bg-neutral-950">
|
||||||
<p className="text-lg">Chargement...</p>
|
<p className="text-lg text-gray-600 dark:text-neutral-400">Chargement...</p>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-screen flex-col items-center justify-center bg-gray-50 p-4">
|
<div className="flex min-h-screen flex-col items-center justify-center bg-gray-50 dark:bg-neutral-950 p-4">
|
||||||
<main className="w-full max-w-2xl bg-white p-8 rounded-lg shadow-md text-center">
|
<main className="w-full max-w-2xl text-center">
|
||||||
<h1 className="text-4xl font-bold text-gray-900 mb-8">
|
<h1 className="text-4xl font-bold text-gray-900 dark:text-white mb-8">
|
||||||
Bienvenue sur Next Starter
|
Bienvenue sur Next Starter
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
{user ? (
|
<Card className="mx-auto">
|
||||||
<div className="space-y-6">
|
{user ? (
|
||||||
<div className="flex flex-col items-center">
|
<div className="space-y-6">
|
||||||
{user.avatar_url && (
|
<div className="flex flex-col items-center">
|
||||||
<img
|
{user.avatarUrl && (
|
||||||
src={user.avatar_url.startsWith('http') ? user.avatar_url : `http://localhost:8003/storage/${user.avatar_url}`}
|
<img
|
||||||
alt={user.name}
|
src={user?.avatarUrl}
|
||||||
className="w-24 h-24 rounded-full object-cover mb-4 border-2 border-indigo-500"
|
alt={user.name}
|
||||||
/>
|
className="w-24 h-24 rounded-full object-cover mb-4 border-2 border-black dark:border-white"
|
||||||
)}
|
/>
|
||||||
<p className="text-xl font-medium">Bonjour, {user.name} !</p>
|
)}
|
||||||
<p className="text-gray-600">{user.email}</p>
|
<p className="text-xl font-medium text-gray-900 dark:text-white">Bonjour, {user.name} !</p>
|
||||||
</div>
|
<p className="text-gray-600 dark:text-neutral-400">{user.email}</p>
|
||||||
|
</div>
|
||||||
<button
|
|
||||||
onClick={() => logout.mutate()}
|
<Button
|
||||||
disabled={logout.isPending}
|
variant="secondary"
|
||||||
className="px-6 py-2 bg-red-600 text-white rounded-md hover:bg-red-700 transition-colors disabled:opacity-50"
|
onClick={() => logout.mutate()}
|
||||||
>
|
disabled={logout.isPending}
|
||||||
{logout.isPending ? 'Déconnexion...' : 'Se déconnecter'}
|
className="text-red-600 hover:bg-red-50 dark:text-red-400 dark:hover:bg-red-900/20"
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="space-y-4">
|
|
||||||
<p className="text-lg text-gray-600 mb-6">
|
|
||||||
Connectez-vous pour accéder à toutes les fonctionnalités.
|
|
||||||
</p>
|
|
||||||
<div className="flex justify-center space-x-4">
|
|
||||||
<Link
|
|
||||||
href="/login"
|
|
||||||
className="px-6 py-2 bg-indigo-600 text-white rounded-md hover:bg-indigo-700 transition-colors"
|
|
||||||
>
|
>
|
||||||
Se connecter
|
{logout.isPending ? 'Déconnexion...' : 'Se déconnecter'}
|
||||||
</Link>
|
</Button>
|
||||||
<Link
|
|
||||||
href="/register"
|
|
||||||
className="px-6 py-2 border border-indigo-600 text-indigo-600 rounded-md hover:bg-indigo-50 transition-colors"
|
|
||||||
>
|
|
||||||
S'inscrire
|
|
||||||
</Link>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
) : (
|
||||||
)}
|
<div className="space-y-6">
|
||||||
|
<p className="text-lg text-gray-600 dark:text-neutral-400">
|
||||||
|
Connectez-vous pour accéder à toutes les fonctionnalités.
|
||||||
|
</p>
|
||||||
|
<div className="flex justify-center gap-4">
|
||||||
|
<Link href="/login">
|
||||||
|
<Button variant="primary">
|
||||||
|
Se connecter
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
<Link href="/register">
|
||||||
|
<Button variant="secondary">
|
||||||
|
S'inscrire
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,9 @@ import { useState } from 'react'
|
||||||
import { useRegister } from '@/hooks/queries/auth'
|
import { useRegister } from '@/hooks/queries/auth'
|
||||||
import { useRouter } from 'next/navigation'
|
import { useRouter } from 'next/navigation'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
|
import Button from '@/components/common/Button'
|
||||||
|
import Input from '@/components/common/Input'
|
||||||
|
import Card from '@/components/common/Card'
|
||||||
|
|
||||||
export default function RegisterPage() {
|
export default function RegisterPage() {
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
|
|
@ -26,88 +29,82 @@ export default function RegisterPage() {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-screen items-center justify-center bg-gray-50 px-4 py-12 sm:px-6 lg:px-8">
|
<div className="flex min-h-screen items-center justify-center bg-gray-50 dark:bg-neutral-950 px-4 py-12 sm:px-6 lg:px-8">
|
||||||
<div className="w-full max-w-md space-y-8">
|
<div className="w-full max-w-md space-y-8">
|
||||||
<div>
|
<div className="text-center">
|
||||||
<h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900">
|
<h2 className="text-3xl font-extrabold text-gray-900 dark:text-white">
|
||||||
Créez votre compte
|
Créez votre compte
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
<form className="mt-8 space-y-6" onSubmit={handleSubmit}>
|
|
||||||
<div className="-space-y-px rounded-md shadow-sm">
|
<Card>
|
||||||
<div>
|
<form className="space-y-6" onSubmit={handleSubmit}>
|
||||||
<input
|
<div className="space-y-4">
|
||||||
|
<Input
|
||||||
|
label="Nom complet"
|
||||||
type="text"
|
type="text"
|
||||||
required
|
required
|
||||||
className="relative block w-full appearance-none rounded-none rounded-t-md border border-gray-300 px-3 py-2 text-gray-900 placeholder-gray-500 focus:z-10 focus:border-indigo-500 focus:outline-none focus:ring-indigo-500 sm:text-sm"
|
placeholder="John Doe"
|
||||||
placeholder="Nom complet"
|
|
||||||
value={formData.name}
|
value={formData.name}
|
||||||
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||||
/>
|
/>
|
||||||
</div>
|
<Input
|
||||||
<div>
|
label="Adresse email"
|
||||||
<input
|
|
||||||
type="email"
|
type="email"
|
||||||
required
|
required
|
||||||
className="relative block w-full appearance-none rounded-none border-x border-gray-300 px-3 py-2 text-gray-900 placeholder-gray-500 focus:z-10 focus:border-indigo-500 focus:outline-none focus:ring-indigo-500 sm:text-sm"
|
placeholder="email@example.com"
|
||||||
placeholder="Adresse email"
|
|
||||||
value={formData.email}
|
value={formData.email}
|
||||||
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
|
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
|
||||||
/>
|
/>
|
||||||
</div>
|
<Input
|
||||||
<div>
|
label="Mot de passe"
|
||||||
<input
|
|
||||||
type="password"
|
type="password"
|
||||||
required
|
required
|
||||||
className="relative block w-full appearance-none rounded-none border border-gray-300 px-3 py-2 text-gray-900 placeholder-gray-500 focus:z-10 focus:border-indigo-500 focus:outline-none focus:ring-indigo-500 sm:text-sm"
|
placeholder="••••••••"
|
||||||
placeholder="Mot de passe"
|
|
||||||
value={formData.password}
|
value={formData.password}
|
||||||
onChange={(e) => setFormData({ ...formData, password: e.target.value })}
|
onChange={(e) => setFormData({ ...formData, password: e.target.value })}
|
||||||
/>
|
/>
|
||||||
</div>
|
<Input
|
||||||
<div>
|
label="Confirmer le mot de passe"
|
||||||
<input
|
|
||||||
type="password"
|
type="password"
|
||||||
required
|
required
|
||||||
className="relative block w-full appearance-none rounded-none rounded-b-md border-x border-b border-gray-300 px-3 py-2 text-gray-900 placeholder-gray-500 focus:z-10 focus:border-indigo-500 focus:outline-none focus:ring-indigo-500 sm:text-sm"
|
placeholder="••••••••"
|
||||||
placeholder="Confirmer le mot de passe"
|
|
||||||
value={formData.password_confirmation}
|
value={formData.password_confirmation}
|
||||||
onChange={(e) => setFormData({ ...formData, password_confirmation: e.target.value })}
|
onChange={(e) => setFormData({ ...formData, password_confirmation: e.target.value })}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<div className="space-y-1">
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-neutral-300">Avatar (Optionnel)</label>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
accept="image/*"
|
||||||
|
className="block w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-4 file:rounded-md file:border-0 file:text-sm file:font-semibold file:bg-gray-100 file:text-gray-700 hover:file:bg-gray-200 dark:file:bg-neutral-800 dark:file:text-neutral-300 dark:hover:file:bg-neutral-700 cursor-pointer"
|
||||||
|
onChange={(e) => setFormData({ ...formData, avatar: e.target.files?.[0] || null })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
{register.isError && (
|
||||||
<label className="block text-sm font-medium text-gray-700">Avatar (Optionnel)</label>
|
<p className="text-red-500 text-sm text-center">
|
||||||
<input
|
{(register.error as any).message}
|
||||||
type="file"
|
</p>
|
||||||
accept="image/*"
|
)}
|
||||||
className="mt-1 block w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-4 file:rounded-md file:border-0 file:text-sm file:font-semibold file:bg-indigo-50 file:text-indigo-700 hover:file:bg-indigo-100"
|
|
||||||
onChange={(e) => setFormData({ ...formData, avatar: e.target.files?.[0] || null })}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{register.isError && (
|
<Button
|
||||||
<div className="text-red-500 text-sm text-center">
|
|
||||||
{(register.error as any).message}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<button
|
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={register.isPending}
|
disabled={register.isPending}
|
||||||
className="group relative flex w-full justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-sm font-medium text-white hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 disabled:opacity-50"
|
className="w-full"
|
||||||
>
|
>
|
||||||
{register.isPending ? 'Inscription...' : "S'inscrire"}
|
{register.isPending ? 'Inscription...' : "S'inscrire"}
|
||||||
</button>
|
</Button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div className="mt-6 text-center">
|
||||||
|
<Link href="/login" className="text-sm font-medium text-black hover:underline dark:text-white">
|
||||||
|
Déjà un compte ? Se connecter
|
||||||
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</Card>
|
||||||
<div className="text-center">
|
|
||||||
<Link href="/login" className="text-indigo-600 hover:text-indigo-500 text-sm">
|
|
||||||
Déjà un compte ? Se connecter
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||||
|
variant?: 'primary' | 'secondary';
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Button({
|
||||||
|
variant = 'primary',
|
||||||
|
children,
|
||||||
|
className = '',
|
||||||
|
...props
|
||||||
|
}: ButtonProps) {
|
||||||
|
const baseClasses = "px-4 py-2 transition-all cursor-pointer rounded-md text-sm font-medium disabled:opacity-50 disabled:cursor-not-allowed";
|
||||||
|
|
||||||
|
const variants = {
|
||||||
|
primary: "bg-black text-white hover:bg-black/90 dark:bg-white dark:text-black dark:hover:bg-white/90 shadow-sm",
|
||||||
|
secondary: "bg-gray-100 text-gray-900 hover:bg-gray-200 dark:bg-neutral-800 dark:text-white dark:hover:bg-neutral-700 border border-transparent shadow-sm"
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
className={`${baseClasses} ${variants[variant]} ${className}`}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
interface CardProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
|
title?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Card({
|
||||||
|
children,
|
||||||
|
className = '',
|
||||||
|
title
|
||||||
|
}: CardProps) {
|
||||||
|
return (
|
||||||
|
<div className={`rounded-xl border border-gray-100 bg-white p-6 shadow-sm dark:border-neutral-800 dark:bg-black ${className}`}>
|
||||||
|
{title && (
|
||||||
|
<h3 className="mb-4 text-lg font-semibold text-gray-900 dark:text-white">
|
||||||
|
{title}
|
||||||
|
</h3>
|
||||||
|
)}
|
||||||
|
<div className="text-sm text-gray-600 dark:text-neutral-400">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
||||||
|
label?: string;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Input({
|
||||||
|
label,
|
||||||
|
error,
|
||||||
|
className = '',
|
||||||
|
...props
|
||||||
|
}: InputProps) {
|
||||||
|
const baseClasses = "block w-full px-4 py-2 text-sm border rounded-md transition-colors duration-200 outline-none focus:ring-2 focus:ring-black/5 dark:focus:ring-white/10";
|
||||||
|
const stateClasses = error
|
||||||
|
? "border-red-500 text-red-900 placeholder-red-300"
|
||||||
|
: "border-gray-200 focus:border-black dark:border-neutral-800 dark:focus:border-white bg-transparent";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-1">
|
||||||
|
{label && (
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-neutral-300">
|
||||||
|
{label}
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
|
<input
|
||||||
|
className={`${baseClasses} ${stateClasses} ${className}`}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
{error && (
|
||||||
|
<p className="text-xs text-red-500">{error}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -2,7 +2,7 @@ export interface User {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
email: string;
|
email: string;
|
||||||
avatar_url: string | null;
|
avatarUrl: string | null;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue