feat(profile): implement account fetching and context provider for user profile
feat(wallet): enhance wallet UI and transaction item display fix(profile): update profile detail card to handle loading and error states style: improve UI consistency across profile and wallet components
This commit is contained in:
parent
cbd0b4bebe
commit
b0ce5ce2f9
15 changed files with 452 additions and 137 deletions
|
|
@ -1,17 +1,28 @@
|
|||
import { requireSession } from '@/lib/auth/session';
|
||||
import React from 'react'
|
||||
import { requireSession } from "@/lib/auth/session";
|
||||
import { getAccount } from "@/lib/api/account";
|
||||
import { AccountProvider } from "@/lib/providers/account-provider";
|
||||
|
||||
interface ProfileLayoutInterface {
|
||||
export default async function DashboardLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
await requireSession("/login");
|
||||
|
||||
let account: Awaited<ReturnType<typeof getAccount>> | null = null;
|
||||
let error: string | null = null;
|
||||
|
||||
try {
|
||||
account = await getAccount();
|
||||
} catch (e: any) {
|
||||
error = e?.message ?? "خطا در دریافت اطلاعات پروفایل";
|
||||
}
|
||||
|
||||
|
||||
const ProfileLayout = async ({ children }: ProfileLayoutInterface) => {
|
||||
await requireSession("/profile");
|
||||
|
||||
return <main className='max-w-5xl mx-auto w-full p-4'>
|
||||
return (
|
||||
<main className="max-w-5xl w-full p-4 mx-auto">
|
||||
<AccountProvider account={account} error={error}>
|
||||
{children}
|
||||
</AccountProvider>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
export default ProfileLayout
|
||||
|
|
@ -15,8 +15,8 @@ export const metadata: Metadata = {
|
|||
|
||||
export default function RootLayout({ children }: LayoutProps<"/">) {
|
||||
return (
|
||||
<html lang="fa" dir="rtl" className={`${vazirmatn.variable} h-full antialiased`}>
|
||||
<body className="min-h-full flex flex-col bg-background text-foreground">
|
||||
<html lang="fa" dir="rtl" className={`${vazirmatn.variable} h-full antialiased`} suppressHydrationWarning>
|
||||
<body className="min-h-full flex flex-col bg-background text-foreground" suppressHydrationWarning>
|
||||
<QueryProvider>{children}</QueryProvider>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
'use client';
|
||||
|
||||
import { ChevronLeft, LogOutIcon } from "lucide-react"
|
||||
import { ChevronLeft, LogOutIcon } from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useTransition } from "react";
|
||||
|
||||
const ProfileExitAccount = () => {
|
||||
|
||||
const router = useRouter();
|
||||
const [isPending, startTransition] = useTransition();
|
||||
|
||||
|
|
@ -17,20 +16,20 @@ const ProfileExitAccount = () => {
|
|||
});
|
||||
}
|
||||
|
||||
return <div
|
||||
className={`
|
||||
flex items-center justify-between w-full py-3 cursor-pointer
|
||||
`}
|
||||
return (
|
||||
<div
|
||||
className="flex items-center justify-between w-full py-3 cursor-pointer"
|
||||
onClick={handleLogout}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="bg-[#FEF2F2] p-2 rounded-lg">
|
||||
<LogOutIcon className="w-4 h-4 text-[#DC2626]" />
|
||||
<div className="bg-error/10 p-2 rounded-lg">
|
||||
<LogOutIcon className="w-4 h-4 text-error" />
|
||||
</div>
|
||||
<span className="text-sm font-semibold text-[#DC2626]">خروج</span>
|
||||
<span className="text-sm font-semibold text-error">خروج</span>
|
||||
</div>
|
||||
<ChevronLeft className="w-5 h-5 text-[#DC2626]" />
|
||||
<ChevronLeft className="w-5 h-5 text-error" />
|
||||
</div>
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default ProfileExitAccount
|
||||
export default ProfileExitAccount;
|
||||
|
|
@ -21,16 +21,16 @@ const ProfileMenuItem = ({
|
|||
href={href}
|
||||
className={`
|
||||
flex items-center justify-between w-full py-3
|
||||
${!isLast ? 'border-b border-gray-200' : ''}
|
||||
${!isLast ? 'border-b border-border' : ''}
|
||||
`}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="bg-[#FAFAFA] p-2 rounded-lg">
|
||||
{icon || <User className="w-4 h-4 text-[#223D5D]" />}
|
||||
<div className="bg-gray-50 p-2 rounded-lg">
|
||||
{icon || <User className="w-4 h-4 text-primary-action" />}
|
||||
</div>
|
||||
<span className="text-sm text-gray-700">{text}</span>
|
||||
</div>
|
||||
<ChevronLeft className="w-5 h-5 text-gray-400" />
|
||||
<ChevronLeft className="w-5 h-5 text-gray-500" />
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -5,19 +5,48 @@ import {
|
|||
MapPin,
|
||||
Wallet,
|
||||
Bell,
|
||||
Headset,
|
||||
CircleQuestionMark
|
||||
CircleQuestionMark,
|
||||
} from 'lucide-react';
|
||||
import ProfileMenuItem from './profile-menu-item';
|
||||
import ProfileExitAccount from './profile-exit-account';
|
||||
|
||||
const menuItems = [
|
||||
{ id: 1, href: '/addresses', text: 'سعادتآباد', icon: <MapPin className="size-5 text-[#223D5D]" /> },
|
||||
{ id: 2, href: '/orders', text: 'سفارشهای من', icon: <ShoppingBag className="size-5 text-[#223D5D]" /> },
|
||||
{ id: 3, href: '/addresses', text: 'آدرسهای من', icon: <MapPin className="size-5 text-[#223D5D]" /> },
|
||||
{ id: 4, href: '/profile/wallet', text: 'کیف پول', icon: <Wallet className="size-5 text-[#223D5D]" /> },
|
||||
{ id: 5, href: '/profile/notifications', text: 'اعلانها', icon: <Bell className="size-5 text-[#223D5D]" /> },
|
||||
{ id: 6, href: '/profile/support', text: 'پشتیبانی', icon: <CircleQuestionMark className="size-5 text-[#223D5D]" /> },
|
||||
{
|
||||
id: 1,
|
||||
href: '/addresses',
|
||||
text: 'سعادتآباد',
|
||||
icon: <MapPin className="size-5 text-primary-action" />,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
href: '/orders',
|
||||
text: 'سفارشهای من',
|
||||
icon: <ShoppingBag className="size-5 text-primary-action" />,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
href: '/addresses',
|
||||
text: 'آدرسهای من',
|
||||
icon: <MapPin className="size-5 text-primary-action" />,
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
href: '/profile/wallet',
|
||||
text: 'کیف پول',
|
||||
icon: <Wallet className="size-5 text-primary-action" />,
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
href: '/profile/notifications',
|
||||
text: 'اعلانها',
|
||||
icon: <Bell className="size-5 text-primary-action" />,
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
href: '/profile/support',
|
||||
text: 'پشتیبانی',
|
||||
icon: <CircleQuestionMark className="size-5 text-primary-action" />,
|
||||
},
|
||||
];
|
||||
|
||||
const ProfileMenu = () => {
|
||||
|
|
@ -32,7 +61,7 @@ const ProfileMenu = () => {
|
|||
isLast={index === menuItems.length - 1}
|
||||
/>
|
||||
))}
|
||||
<div className="w-full h-px bg-gray-200 mt-4" />
|
||||
<div className="w-full h-px bg-border mt-4" />
|
||||
<ProfileExitAccount />
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,22 +1,70 @@
|
|||
'use client';
|
||||
|
||||
import React from 'react'
|
||||
import { useAccount } from "@/lib/providers/account-provider";
|
||||
import { getDisplayNameHelper, getInitialsHelper } from "@/lib/utils/account-utils";
|
||||
|
||||
const ProfilePersonDetailCard = () => {
|
||||
const { data, error, isLoading } = useAccount();
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className='w-full min-h-[128px] rounded-2xl bg-[#E0F4F4] my-6 py-4 px-10 flex items-center justify-center'>
|
||||
<div className="flex flex-row items-center w-full">
|
||||
<div className='size-16 bg-[#223D5D] rounded-full flex items-center justify-center text-lg font-bold text-white'>
|
||||
م.ر
|
||||
<div className="w-full min-h-[128px] rounded-2xl bg-teal-light my-6 py-4 px-10 flex items-center justify-center">
|
||||
<p className="text-sm text-gray-500">در حال بارگذاری...</p>
|
||||
</div>
|
||||
<div className="flex flex-col mr-8 items-start">
|
||||
<p className='text-lg font-bold'>محمد رضایی</p>
|
||||
<p className='text-sm text-[#4B5563] my-2 font-normal'>09121234567</p>
|
||||
<button className='text-sm font-semibold text-[#223D5D] cursor-pointer underline' onClick={() => { alert('latter...') }}>ویرایش پروفایل</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export default ProfilePersonDetailCard
|
||||
if (error) {
|
||||
return (
|
||||
<div className="w-full min-h-[128px] rounded-2xl bg-teal-light my-6 py-4 px-10 flex items-center justify-center">
|
||||
<p className="text-sm text-destructive">{error}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
return (
|
||||
<div className="w-full min-h-[128px] rounded-2xl bg-teal-light my-6 py-4 px-10 flex items-center justify-center">
|
||||
<p className="text-sm text-gray-500">اطلاعات پروفایل یافت نشد</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const displayName = getDisplayNameHelper(data);
|
||||
const initials = getInitialsHelper(data);
|
||||
|
||||
return (
|
||||
<div className="w-full min-h-[128px] rounded-2xl bg-teal-light my-6 py-4 px-10 flex items-center justify-center">
|
||||
<div className="flex flex-row items-center w-full">
|
||||
<div className="size-16 bg-primary-action rounded-full flex items-center justify-center text-lg font-bold text-white overflow-hidden shrink-0">
|
||||
{data.avatar ? (
|
||||
<img
|
||||
src={data.avatar}
|
||||
alt={displayName}
|
||||
className="size-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<span>{initials}</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col mr-6 items-start">
|
||||
<p className="text-lg font-bold">{displayName}</p>
|
||||
|
||||
{data.phone_number && (
|
||||
<p className="text-sm text-gray-700 my-2 font-normal" dir="ltr">
|
||||
{data.phone_number}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<button
|
||||
className="text-sm font-semibold text-primary-action cursor-pointer underline"
|
||||
onClick={() => alert('latter...')}
|
||||
>
|
||||
ویرایش پروفایل
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProfilePersonDetailCard;
|
||||
|
|
@ -27,7 +27,7 @@ const TransactionHistory = () => {
|
|||
key={item.id}
|
||||
amount={item.amount}
|
||||
description={item.description}
|
||||
type={item.type} // حالا نوعش دقیقاً 'deposit' | 'withdraw' هست
|
||||
type={item.type}
|
||||
isLast={index === transactions.length - 1}
|
||||
/>
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import React from 'react';
|
||||
import { ArrowDownCircle, ArrowDownIcon, ArrowUp, ArrowUpCircle } from 'lucide-react';
|
||||
import { ArrowDownIcon, ArrowUp } from 'lucide-react';
|
||||
import { formatPriceWithToman } from '@/lib/utils/priceUtils';
|
||||
|
||||
interface TransactionItemProps {
|
||||
|
|
@ -17,15 +17,16 @@ const TransactionItem = ({
|
|||
}: TransactionItemProps) => {
|
||||
const isDeposit = type === 'deposit';
|
||||
const Icon = isDeposit ? ArrowDownIcon : ArrowUp;
|
||||
const amountColor = isDeposit ? 'text-[#2E7D32]' : 'text-[#D32F2F]';
|
||||
const bgColor = isDeposit ? 'bg-[#2E7D3210]' : 'bg-[#D32F2F10]';
|
||||
const iconColor = isDeposit ? 'text-[#2E7D32]' : 'text-[#D32F2F]';
|
||||
|
||||
const amountColor = isDeposit ? 'text-success' : 'text-error';
|
||||
const bgColor = isDeposit ? 'bg-success/10' : 'bg-error/10';
|
||||
const iconColor = isDeposit ? 'text-success' : 'text-error';
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`
|
||||
flex items-center justify-between w-full py-3
|
||||
${!isLast ? 'border-b border-gray-200' : ''}
|
||||
${!isLast ? 'border-b border-border' : ''}
|
||||
`}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
|
|
|
|||
|
|
@ -1,28 +1,52 @@
|
|||
'use client';
|
||||
|
||||
import { formatPriceWithToman } from '@/lib/utils/priceUtils';
|
||||
import { LogOutIcon, PlusIcon } from 'lucide-react'
|
||||
import React from 'react'
|
||||
import { LogOutIcon, PlusIcon } from 'lucide-react';
|
||||
|
||||
const WalletBlancCard = () => {
|
||||
return (
|
||||
<div className='w-full min-h-12 rounded-2xl my-6 py-6 px-6 flex flex-col justify-start bg-gradient-to-l from-[#004D4D] to-[#223D5D] shadow-[0_12px_24px_#006A6A40] border border-[#006A6A40]'>
|
||||
<h4 className='text-sm font-medium text-gray-200'>موجودی کیف پول</h4>
|
||||
<h1 className='text-2xl font-bold my-4 text-white'>{formatPriceWithToman(35000000)}</h1>
|
||||
<div
|
||||
className="
|
||||
w-full min-h-12 rounded-2xl my-6 py-6 px-6
|
||||
flex flex-col justify-start
|
||||
bg-gradient-to-l from-primary to-primary-action
|
||||
shadow-[0_12px_24px] shadow-primary/25
|
||||
border border-primary/25
|
||||
"
|
||||
>
|
||||
<h4 className="text-sm font-medium text-gray-300">
|
||||
موجودی کیف پول
|
||||
</h4>
|
||||
<h1 className="text-2xl font-bold my-4 text-primary-foreground">
|
||||
{formatPriceWithToman(35000000)}
|
||||
</h1>
|
||||
|
||||
<div className="flex w-full gap-4">
|
||||
<button className='w-full bg-white h-12 rounded-xl cursor-pointer flex items-center justify-center gap-2'
|
||||
onClick={() => { alert('latter...') }}>
|
||||
<PlusIcon className='text-[#223D5D] size-4' />
|
||||
<span className='text-[#223D5D]'>شارژ کیف پول</span>
|
||||
<button
|
||||
className="
|
||||
w-full bg-white h-12 rounded-xl cursor-pointer
|
||||
flex items-center justify-center gap-2
|
||||
"
|
||||
onClick={() => alert('latter...')}
|
||||
>
|
||||
<PlusIcon className="text-primary-action size-4" />
|
||||
<span className="text-primary-action">شارژ کیف پول</span>
|
||||
</button>
|
||||
<button className='w-full border border-white h-12 rounded-xl cursor-pointer flex items-center justify-center gap-2' onClick={() => { alert('latter...') }}>
|
||||
<LogOutIcon className='text-white size-4 -rotate-90' />
|
||||
<span className='text-white'>برداشت</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default WalletBlancCard
|
||||
<button
|
||||
className="
|
||||
w-full h-12 rounded-xl cursor-pointer
|
||||
flex items-center justify-center gap-2
|
||||
border border-primary-foreground
|
||||
"
|
||||
onClick={() => alert('latter...')}
|
||||
>
|
||||
<LogOutIcon className="text-primary-foreground size-4 -rotate-90" />
|
||||
<span className="text-primary-foreground">برداشت</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default WalletBlancCard;
|
||||
8
src/lib/api/account.ts
Normal file
8
src/lib/api/account.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
"use server";
|
||||
|
||||
import { gooyalFetch } from "./gooyal-accounts";
|
||||
import type { Account } from "@/types/api";
|
||||
|
||||
export async function getAccount(): Promise<Account> {
|
||||
return gooyalFetch<Account>("/users/api/account/");
|
||||
}
|
||||
65
src/lib/api/gooyal-accounts.ts
Normal file
65
src/lib/api/gooyal-accounts.ts
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
import "server-only";
|
||||
import { getValidSession } from "@/lib/auth/session";
|
||||
import { ApiError, type WinofyErrorDetails } from "./errors";
|
||||
import { logWinofyRequest, logWinofyResponse } from "./dev-logger";
|
||||
|
||||
const BASE_URL = process.env.GOOYAL_ACCOUNTS_BASE_URL!;
|
||||
|
||||
interface GooyalFetchOptions extends Omit<RequestInit, "body"> {
|
||||
auth?: boolean;
|
||||
params?: Record<string, string | number | boolean | undefined>;
|
||||
body?: unknown;
|
||||
}
|
||||
|
||||
export async function gooyalFetch<T>(path: string, options: GooyalFetchOptions = {}): Promise<T> {
|
||||
const { auth = true, params, headers, body, ...rest } = options;
|
||||
const method = rest.method ?? "GET";
|
||||
|
||||
const url = new URL(BASE_URL.replace(/\/$/, "") + path);
|
||||
if (params) {
|
||||
for (const [key, value] of Object.entries(params)) {
|
||||
if (value !== undefined) url.searchParams.set(key, String(value));
|
||||
}
|
||||
}
|
||||
|
||||
const finalHeaders = new Headers(headers);
|
||||
const hasBody = body !== undefined;
|
||||
if (hasBody && !finalHeaders.has("Content-Type")) {
|
||||
finalHeaders.set("Content-Type", "application/json");
|
||||
}
|
||||
|
||||
if (auth) {
|
||||
const session = await getValidSession();
|
||||
if (session) {
|
||||
finalHeaders.set("Authorization", `Bearer ${session.accessToken}`);
|
||||
}
|
||||
}
|
||||
|
||||
logWinofyRequest?.(method, url.toString(), body);
|
||||
|
||||
const res = await fetch(url.toString(), {
|
||||
...rest,
|
||||
headers: finalHeaders,
|
||||
body: hasBody ? JSON.stringify(body) : undefined,
|
||||
cache: "no-store",
|
||||
});
|
||||
|
||||
if (res.status === 204) {
|
||||
logWinofyResponse?.(method, url.toString(), res.status, null);
|
||||
return undefined as T;
|
||||
}
|
||||
|
||||
const data = await res.json().catch(() => null);
|
||||
logWinofyResponse?.(method, url.toString(), res.status, data);
|
||||
|
||||
if (!res.ok) {
|
||||
const envelope = data as { status_message?: string; details?: WinofyErrorDetails } | null;
|
||||
throw new ApiError(
|
||||
res.status,
|
||||
envelope?.status_message ?? res.statusText,
|
||||
envelope?.details ?? null,
|
||||
);
|
||||
}
|
||||
|
||||
return data as T;
|
||||
}
|
||||
42
src/lib/providers/account-provider.tsx
Normal file
42
src/lib/providers/account-provider.tsx
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
"use client";
|
||||
|
||||
import { createContext, useContext, type ReactNode } from "react";
|
||||
import type { Account } from "@/types/api";
|
||||
|
||||
type AccountContextValue = {
|
||||
data: Account | null;
|
||||
error: string | null;
|
||||
isLoading: boolean;
|
||||
};
|
||||
|
||||
const AccountContext = createContext<AccountContextValue | undefined>(undefined);
|
||||
|
||||
export function AccountProvider({
|
||||
children,
|
||||
account,
|
||||
error = null,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
account: Account | null;
|
||||
error?: string | null;
|
||||
}) {
|
||||
return (
|
||||
<AccountContext.Provider
|
||||
value={{
|
||||
data: account,
|
||||
error,
|
||||
isLoading: false,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</AccountContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useAccount() {
|
||||
const ctx = useContext(AccountContext);
|
||||
if (ctx === undefined) {
|
||||
throw new Error("useAccount must be used within AccountProvider");
|
||||
}
|
||||
return ctx;
|
||||
}
|
||||
49
src/lib/utils/account-utils.ts
Normal file
49
src/lib/utils/account-utils.ts
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
'use client';
|
||||
|
||||
import { useAccount } from '@/lib/providers/account-provider';
|
||||
import React from 'react';
|
||||
|
||||
export function getInitialsHelper(account: {
|
||||
name?: string | null;
|
||||
first_name?: string | null;
|
||||
last_name?: string | null;
|
||||
username?: string | null;
|
||||
}): string {
|
||||
if (account.name?.trim()) {
|
||||
const parts = account.name.trim().split(/\s+/);
|
||||
if (parts.length >= 2) {
|
||||
return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();
|
||||
}
|
||||
return parts[0].slice(0, 2).toUpperCase();
|
||||
}
|
||||
|
||||
if (account.first_name || account.last_name) {
|
||||
const first = account.first_name?.[0] ?? '';
|
||||
const last = account.last_name?.[0] ?? '';
|
||||
return (first + last).toUpperCase() || '؟';
|
||||
}
|
||||
|
||||
if (account.username) {
|
||||
return account.username.slice(0, 2).toUpperCase();
|
||||
}
|
||||
|
||||
return '؟';
|
||||
}
|
||||
|
||||
export function getDisplayNameHelper(account: {
|
||||
name?: string | null;
|
||||
first_name?: string | null;
|
||||
last_name?: string | null;
|
||||
username?: string | null;
|
||||
}): string {
|
||||
if (account.name?.trim()) return account.name.trim();
|
||||
|
||||
const fullName = [account.first_name, account.last_name]
|
||||
.filter(Boolean)
|
||||
.join(' ')
|
||||
.trim();
|
||||
|
||||
if (fullName) return fullName;
|
||||
|
||||
return account.username || 'کاربر';
|
||||
}
|
||||
|
|
@ -310,3 +310,42 @@ export interface SellerAnalytics {
|
|||
chart: SalesChartPoint[];
|
||||
top_products: TopProduct[];
|
||||
}
|
||||
|
||||
|
||||
// ---- Account / Profile ----------------------------------------------------
|
||||
|
||||
export type Gender = "m" | "f" | "other" | null;
|
||||
export type EducationDegree =
|
||||
| "primary"
|
||||
| "secondary"
|
||||
| "diploma"
|
||||
| "bachelor"
|
||||
| "master"
|
||||
| "phd"
|
||||
| string
|
||||
| null;
|
||||
|
||||
export interface Account {
|
||||
uuid: string;
|
||||
username: string;
|
||||
email: string | null;
|
||||
phone_number: string | null;
|
||||
first_name: string | null;
|
||||
last_name: string | null;
|
||||
name: string | null;
|
||||
avatar: string | null;
|
||||
gender: Gender;
|
||||
education_degree: EducationDegree;
|
||||
education_field: string | null;
|
||||
birthdate: string | null; // "YYYY-MM-DD"
|
||||
iban: string | null;
|
||||
iban_verified: boolean;
|
||||
province: number | null;
|
||||
province_name: string | null;
|
||||
city: number | null;
|
||||
city_name: string | null;
|
||||
postal_code: string | null;
|
||||
address: string | null;
|
||||
date_joined: string;
|
||||
last_update: string;
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue