add: settings page locales

This commit is contained in:
Mahyar Khanbolooki
2025-08-12 03:09:22 +03:30
parent 67f7ac35bf
commit 81bb8e9c14
9 changed files with 421 additions and 173 deletions
+102 -145
View File
@@ -7,12 +7,23 @@ import { TabHeader } from '@/components/tab/TabHeader'
import { Switch } from '@/components/ui/switch'; import { Switch } from '@/components/ui/switch';
import Accordion from '@/components/utils/Accordion'; import Accordion from '@/components/utils/Accordion';
import Seperator from '@/components/utils/Seperator'; import Seperator from '@/components/utils/Seperator';
import { Key, KeySquare, Notification1, ArrowLeft, MedalStar, TickCircle } from 'iconsax-react' import { SmartIcon } from '@/components/utils/SmartIcon';
import { getI18nObject } from '@/lib/i18n';
import { SettingsData } from '@/types/i18n/settings';
import { Key, KeySquare, Notification1, ArrowLeft, TickCircle } from 'iconsax-react'
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import React from 'react' import React, { Fragment } from 'react'
import { useTranslation } from 'react-i18next';
function UserSettingsIndex() { function UserSettingsIndex() {
const router = useRouter(); const router = useRouter();
const { t } = useTranslation('settings');
const data: SettingsData = getI18nObject(t, "data");
const onSwitched = (e: React.MouseEvent) => {
const target = e.target as HTMLElement;
console.log('Save changed setting?', target.id);
}
const notifsTab = () => { const notifsTab = () => {
@@ -22,106 +33,46 @@ function UserSettingsIndex() {
className="bg-container rounded-container shadow-container p-4"> className="bg-container rounded-container shadow-container p-4">
<div className="flex justify-between items-center pb-[25px]"> <div className="flex justify-between items-center pb-[25px]">
<h2 className='text-sm leading-5'>تنظیمات نوتیفیکیشن</h2> <h2 className='text-sm leading-5'>{data.TabNotifications.Heading}</h2>
</div> </div>
<div className='p-2 pt-0'> <div className='p-2 pt-0'>
{data.TabNotifications.Accordions.map((accordion, i) => {
return (
<Fragment key={i}>
<Accordion <Accordion
title='حساب' title={accordion.Heading}
group='1' group={'accordions'}
icon={accordion.Icon}
> >
<div <div className='grid gap-4 mb-3'>
{accordion.Items.map((item, j) => {
return (
<label
key={j}
htmlFor={item.HtmlName}
className="flex w-full h-11 items-center justify-between gap-2 px-3 py-4 relative bg-white/29 rounded-xl border border-solid border-neutral-200 focus:outline-none focus:ring-2 focus:ring-black" className="flex w-full h-11 items-center justify-between gap-2 px-3 py-4 relative bg-white/29 rounded-xl border border-solid border-neutral-200 focus:outline-none focus:ring-2 focus:ring-black"
> >
<span className="inline-flex items-center gap-2.5 text-sm2">
<MedalStar size="16" color="#333333" />
ورود به حساب
</span>
<Switch className="w-12 h-6 scale-[0.9]" />
</div>
<div
className="flex w-full my-3 h-11 items-center justify-between gap-2 px-3 py-4 relative bg-white/29 rounded-xl border border-solid border-neutral-200 focus:outline-none focus:ring-2 focus:ring-black"
>
<span className="inline-flex items-center gap-2.5 text-sm2"> <span className="inline-flex items-center gap-2.5 text-sm2">
<MedalStar size="16" color="#333333" /> <SmartIcon icon={item.Icon} />
تغییر رمز عبور {item.Label}
</span> </span>
<Switch className="w-12 h-6 scale-[0.9]" /> <Switch
</div> onClick={onSwitched}
</Accordion> id={item.HtmlName}
<Seperator /> name={item.HtmlName}
className="w-12 h-6 scale-[0.9]"
<Accordion />
title='کیف پول' </label>
group='1' )
> })}
<div
className="flex w-full h-11 items-center justify-between gap-2 px-3 py-4 relative bg-white/29 rounded-xl border border-solid border-neutral-200 focus:outline-none focus:ring-2 focus:ring-black"
>
<span className="inline-flex items-center gap-2.5 text-sm2">
<MedalStar size="16" color="#333333" />
افزایش شارژ
</span>
<Switch className="w-12 h-6 scale-[0.9]" />
</div>
<div
className="flex w-full my-3 h-11 items-center justify-between gap-2 px-3 py-4 relative bg-white/29 rounded-xl border border-solid border-neutral-200 focus:outline-none focus:ring-2 focus:ring-black"
>
<span className="inline-flex items-center gap-2.5 text-sm2">
<MedalStar size="16" color="#333333" />
برداشت از کیف
</span>
<Switch className="w-12 h-6 scale-[0.9]" />
</div>
<div
className="flex w-full my-3 h-11 items-center justify-between gap-2 px-3 py-4 relative bg-white/29 rounded-xl border border-solid border-neutral-200 focus:outline-none focus:ring-2 focus:ring-black"
>
<span className="inline-flex items-center gap-2.5 text-sm2">
<MedalStar size="16" color="#333333" />
استفاده از تخفیف
</span>
<Switch className="w-12 h-6 scale-[0.9]" />
</div>
</Accordion>
<Seperator />
<Accordion
title='سفارش'
group='1'
>
<div
className="flex w-full h-11 items-center justify-between gap-2 px-3 py-4 relative bg-white/29 rounded-xl border border-solid border-neutral-200 focus:outline-none focus:ring-2 focus:ring-black"
>
<span className="inline-flex items-center gap-2.5 text-sm2">
<MedalStar size="16" color="#333333" />
<span className='mt-0.5'>وضعیت سفارش</span>
</span>
<Switch className="w-12 h-6 scale-[0.9]" />
</div>
<div
className="flex w-full my-3 h-11 items-center justify-between gap-2 px-3 py-4 relative bg-white/29 rounded-xl border border-solid border-neutral-200 focus:outline-none focus:ring-2 focus:ring-black"
>
<span className="inline-flex items-center gap-2.5 text-sm2">
<MedalStar size="16" color="#333333" />
<span className='mt-0.5'>تحویل</span>
</span>
<Switch className="w-12 h-6 scale-[0.9]" />
</div>
<div
className="flex w-full my-3 h-11 items-center justify-between gap-2 px-3 py-4 relative bg-white/29 rounded-xl border border-solid border-neutral-200 focus:outline-none focus:ring-2 focus:ring-black"
>
<span className="inline-flex items-center gap-2.5 text-sm2">
<MedalStar size="16" color="#333333" />
<span className='mt-0.5'>کنسلی</span>
</span>
<Switch className="w-12 h-6 scale-[0.9]" />
</div> </div>
</Accordion> </Accordion>
<Seperator className='last:hidden' />
</Fragment>
)
})}
</div> </div>
</section> </section>
@@ -138,45 +89,49 @@ function UserSettingsIndex() {
className="bg-container rounded-container shadow-container p-4"> className="bg-container rounded-container shadow-container p-4">
<div className="flex justify-between items-center pb-[25px]"> <div className="flex justify-between items-center pb-[25px]">
<h2 className='text-sm leading-5'>تغییر رمز عبور</h2> <h2 className='text-sm leading-5'>{data.TabPassword.Heading}</h2>
</div> </div>
<form <form
onSubmit={(e) => e.preventDefault()} onSubmit={(e) => e.preventDefault()}
className='grid gap-6 p-2' className='grid gap-6 p-2'
> >
{data.TabPassword.Inputs.map((input, i) => {
return (
<PasswordField <PasswordField
htmlFor='oldPassword' labelText={input.Label}
placeholder='رمز عبور فعلی' className='bg-container'
autoComplete='current-password' key={i}
htmlFor={input.HtmlName ?? ''}
placeholder={input.Placeholder}
autoComplete={input.AutoComplete}
/> />
)
})
<PasswordField }
htmlFor='newPassowrd'
placeholder='رمز عبور جدید'
autoComplete='new-password'
/>
<PasswordField {data.TabPassword.Rules.map((rule, i) => {
htmlFor='passwordRepeat' return (
placeholder='تکرار رمز عبور' <section
autoComplete='new-password' key={i}
/> className='bg-background rounded-normal p-4 text-xs flex flex-col gap-2 font-light'
>
<section className='bg-background rounded-normal p-4 text-xs flex flex-col gap-2 font-light'> <h6 className='text-sm mb-2'>{rule.Heading}</h6>
<h6 className='text-sm'>رمز عبور میبایست:</h6> {rule.Items.map((item, i) => {
<li className='mt-2'>حداقل 8 کاراکتر باشد</li> return (
<li>ترکیبی از حروف موچک و بزرگ باشد</li> <li key={i}>{item}</li>
<li>شامل اعداد باشد</li> )
<li>شامل کاراکتر های خاص (نماد ها) باشد</li> })}
</section> </section>
)
})}
<Button className='mt-4'> <Button className='mt-4'>
<div className='flex items-center justify-center gap-1'> <div className='flex items-center justify-center gap-1'>
<TickCircle className='stroke-white' size={20} /> <TickCircle className='stroke-white' size={20} />
<span className='mt-0.5'> <span className='mt-0.5'>
ذخیره تغییرات {data.TabPassword.ButtonSubmit}
</span> </span>
</div> </div>
</Button> </Button>
@@ -194,8 +149,8 @@ function UserSettingsIndex() {
className="bg-container rounded-container shadow-container p-4"> className="bg-container rounded-container shadow-container p-4">
<div className="pb-4"> <div className="pb-4">
<h2 className='text-sm leading-5'>راهاندازی احراز هویت دو مرحلهای</h2> <h2 className='text-sm leading-5'>{data.TabAuthenticator.Heading}</h2>
<p className='text-xs mt-3 text-disabled-text font-light'>احراز هویت دو مرحلهای امنیت اکانت شما را افزایش میدهد</p> <p className='text-xs mt-3 text-disabled-text font-light'>{data.TabAuthenticator.Description}</p>
</div> </div>
<Seperator /> <Seperator />
@@ -205,7 +160,7 @@ function UserSettingsIndex() {
<Key size={32} className='stroke-blue-500' /> <Key size={32} className='stroke-blue-500' />
</div> </div>
<p className='text-sm2 mt-4 font-light place-self-center'> <p className='text-sm2 mt-4 font-light place-self-center'>
برای شروع فرآیند راهاندازی احراز هویت دو مرحلهای، روی دکمه زیر کلیک کنید {data.TabAuthenticator.Tip}
</p> </p>
<div className='place-self-center mt-2'> <div className='place-self-center mt-2'>
@@ -213,44 +168,46 @@ function UserSettingsIndex() {
<div className='flex items-center justify-center gap-1'> <div className='flex items-center justify-center gap-1'>
<TickCircle className='stroke-white' size={20} /> <TickCircle className='stroke-white' size={20} />
<span className='mt-0.5'> <span className='mt-0.5'>
ذخیره تغییرات {data.TabAuthenticator.ButtonSubmit}
</span> </span>
</div> </div>
</Button> </Button>
</div> </div>
<section className='bg-background rounded-normal mt-14 p-4 text-xs flex flex-col gap-2 font-light'> {data.TabAuthenticator.Guides.map((guide, i) => {
<h6 className='text-sm'>راهنمای احراز هویت دو مرحلهای</h6> return (
<p className='text-disabled-text'>این قابلیت امنیت حساب شما را به شدت افزایش میدهد</p> <section
<div> key={i}
className='bg-background rounded-normal mt-14 p-4 text-xs flex flex-col gap-2 font-light'
>
<h6 className='text-sm'>{guide.Heading}</h6>
<p className='text-disabled-text'>{guide.Description}</p>
{guide.Items.map((item, i) => {
return (
<div key={i}>
<div className='ml-2 mt-2 bg-blue-100 inline-block size-6 text-center pt-[5px] pr-[1px] text-blue-500 rounded-full'> <div className='ml-2 mt-2 bg-blue-100 inline-block size-6 text-center pt-[5px] pr-[1px] text-blue-500 rounded-full'>
1 {item.Mark}
</div> </div>
<span> <span>
یک برنامه Authenticator مانند Google Authenticator نصب کنید {item.Label}
</span>
</div>
<div>
<div className='ml-2 mt-2 bg-blue-100 inline-block size-6 text-center pt-[5px] pr-[1px] text-blue-500 rounded-full'>
2
</div>
<span>
QR Code را اسکن کنید یا کد را به صورت دستی وارد کنید
</span>
</div>
<div>
<div className='ml-2 mt-2 bg-blue-100 inline-block size-6 text-center pt-[5px] pr-[1px] text-blue-500 rounded-full'>
3
</div>
<span>
کد 6 رقمی تولید شده توسط برنامه را وارد کنید
</span> </span>
</div> </div>
)
})}
<div className='mt-4 p-3 text-yellow-800 rounded-lg bg-yellow-50 border border-solid border-yellow-200'> {guide.Notes.map((note, i) => {
توجه: کد های پشتیبان خود را در مکانی امن نگهداری کنید return (
<div
key={i}
className='mt-4 p-3 text-yellow-800 rounded-lg bg-yellow-50 border border-solid border-yellow-200'
>
{note}
</div> </div>
)
})}
</section> </section>
)
})}
</div> </div>
</div> </div>
@@ -262,7 +219,7 @@ function UserSettingsIndex() {
<div className='overflow-y-auto h-full noscrollbar flex flex-col gap-6'> <div className='overflow-y-auto h-full noscrollbar flex flex-col gap-6'>
<div className='grid grid-cols-3 items-center'> <div className='grid grid-cols-3 items-center'>
<span></span> <span></span>
<h1 className='text-sm2 place-self-center font-medium'>تنظیمات</h1> <h1 className='text-sm2 place-self-center font-medium'>{data.Heading}</h1>
<ArrowLeft <ArrowLeft
className='cursor-pointer place-self-end' className='cursor-pointer place-self-end'
size='24' size='24'
@@ -273,17 +230,17 @@ function UserSettingsIndex() {
<TabContainer> <TabContainer>
<TabHeader <TabHeader
viewRenderer={notifsTab()} viewRenderer={notifsTab()}
title='نوتیفیکیشن' title={data.TabNotifications.Title}
icon={<Notification1 size={24} />} icon={<Notification1 size={24} />}
/> />
<TabHeader <TabHeader
viewRenderer={passwordTab()} viewRenderer={passwordTab()}
title='رمز عبور' title={data.TabPassword.Title}
icon={<KeySquare size={24} />} icon={<KeySquare size={24} />}
/> />
<TabHeader <TabHeader
viewRenderer={authenticatorTab()} viewRenderer={authenticatorTab()}
title='دو مرحله ای' title={data.TabAuthenticator.Title}
icon={<Key size={24} />} icon={<Key size={24} />}
/> />
</TabContainer> </TabContainer>
+1 -1
View File
@@ -16,7 +16,7 @@ export const viewport: Viewport = {
themeColor: '#F4F5F9', themeColor: '#F4F5F9',
} }
const i18nNamespaces = ['common', 'menu', 'orders', 'auth', "rating", "chat", "notifications", 'parallels']; const i18nNamespaces = ['settings', 'common', 'menu', 'orders', 'auth', "rating", "chat", "notifications", 'parallels'];
export default async function RootLayout({ export default async function RootLayout({
children, children,
+8 -8
View File
@@ -1,21 +1,22 @@
'use client'; 'use client';
import useToggle from '@/hooks/helpers/useToggle'; import useToggle from '@/hooks/helpers/useToggle';
import { renderIcon } from '@/lib/helpers/smartIcon';
import { IconType } from '@/types/iconType';
import clsx from 'clsx' import clsx from 'clsx'
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import { Icon } from 'iconsax-react'
import { ChevronDown } from 'lucide-react'; import { ChevronDown } from 'lucide-react';
import React, { useEffect } from 'react' import React, { useEffect } from 'react'
interface DropdownProps extends React.HTMLAttributes<HTMLDivElement> { interface AccordionProps extends React.HTMLAttributes<HTMLDivElement> {
initial?: boolean, initial?: boolean
children?: React.ReactNode children?: React.ReactNode
title?: string title?: string
icon?: Icon icon?: IconType;
group?: string group?: string
}; };
function Accordion({ initial, group, title, icon: Icon, children, className, ...rest }: DropdownProps) { function Accordion({ initial, group, title, icon, children, className, ...rest }: AccordionProps) {
const { state, toggle, set } = useToggle(initial); const { state, toggle, set } = useToggle(initial);
useEffect(() => { useEffect(() => {
@@ -37,6 +38,7 @@ function Accordion({ initial, group, title, icon: Icon, children, className, ...
}, [group, set, state]); }, [group, set, state]);
return ( return (
<div <div
className={clsx( className={clsx(
@@ -56,9 +58,7 @@ function Accordion({ initial, group, title, icon: Icon, children, className, ...
)} )}
> >
<div className='flex items-center gap-2'> <div className='flex items-center gap-2'>
{Icon && {renderIcon(icon)}
<Icon size={16} className='stroke-foreground mb-0.5' />
}
<span className='text-sm2'>{title ?? ''}</span> <span className='text-sm2'>{title ?? ''}</span>
</div> </div>
<ChevronDown <ChevronDown
+49
View File
@@ -0,0 +1,49 @@
'use client';
import React from "react";
import * as Iconsax from "iconsax-react";
import { SmartIconType } from "@/types/iconType";
type IconsaxIconProps = {
size?: number;
color?: string;
className?: string;
variant?: string;
};
type IconComponentType = React.FC<IconsaxIconProps>;
export interface SmartIconProps {
icon?: SmartIconType;
name?: string;
size?: number;
color?: string;
className?: string;
variant?: string;
}
export const SmartIcon: React.FC<SmartIconProps> = ({
icon,
name,
size,
color,
className,
variant,
}) => {
const iconName = (icon?.Name || name || "").charAt(0).toUpperCase() + (icon?.Name || name || "").slice(1);
if (!iconName) return null;
const IconComponent = Iconsax[iconName as keyof typeof Iconsax] as IconComponentType | undefined;
if (!IconComponent) return null;
return (
<IconComponent
variant={icon?.Variant ?? variant ?? 'lineat'}
size={icon?.Size ?? size ?? 20}
color={icon?.Color ?? color ?? "currentColor"}
className={icon?.ClassName ?? className}
/>
);
};
+31
View File
@@ -0,0 +1,31 @@
import React from 'react';
import { IconType, SmartIconType } from "@/types/iconType";
import { SmartIcon } from '@/components/utils/SmartIcon';
export function isSmartIconType(icon: unknown): icon is SmartIconType {
return (
typeof icon === 'object' &&
icon !== null &&
'Name' in icon &&
typeof (icon as SmartIconType).Name === 'string'
);
}
export function renderIcon(icon: IconType) {
if (React.isValidElement(icon)) {
return icon; // It's a React node
} else if (typeof icon === 'function') {
const IconComponent = icon; // It's a functional component
// Create a ref to pass to the forwardRef component
const ref = React.createRef<SVGSVGElement>(); // Adjust the type based on your icon component
return <IconComponent ref={ref} size={20} className='stroke-foreground mb-0.5' />;
} else if (isSmartIconType(icon)) {
// Handle SmartIconType
return (
<SmartIcon icon={icon} />
);
}
return null; // No valid icon provided
}
+4
View File
@@ -3,6 +3,10 @@ import { initReactI18next } from 'react-i18next/initReactI18next';
import resourcesToBackend from 'i18next-resources-to-backend'; import resourcesToBackend from 'i18next-resources-to-backend';
import i18nConfig from '../../i18nConfig'; import i18nConfig from '../../i18nConfig';
export const getI18nObject = (t, x) => {
return t(x, { returnObjects: true })
}
export default async function initTranslations( export default async function initTranslations(
locale, locale,
namespaces, namespaces,
+126
View File
@@ -0,0 +1,126 @@
{
"data": {
"Heading": "تنظیمات",
"TabNotifications": {
"Title": "نوتیفیکیشن",
"Heading": "تنظیمات نوتیفیکیشن",
"Description": "",
"Accordions": [
{
"Heading": "حساب",
"Items": [
{
"Label": "ورود به حساب",
"HtmlName": "login-notif"
},
{
"Label": "تغییر رمز عبور",
"HtmlName": "password-notif"
}
]
},
{
"Heading": "کیف پول",
"Items": [
{
"Label": "افزایش شارژ",
"HtmlName": "wallet-recharge"
},
{
"Label": "برداشت از کیف",
"HtmlName": "wallet-withdrawal"
},
{
"Label": "استفاده از تخفیف",
"HtmlName": "redeem-discount"
}
]
},
{
"Heading": "سفارش",
"Items": [
{
"Label": "وضعیت سفارش",
"HtmlName": "order-state"
},
{
"Label": "تحویل",
"HtmlName": "order-deliver"
},
{
"Label": "کنسلی",
"HtmlName": "order-canceled"
}
]
}
]
},
"TabPassword": {
"Title": "رمز عبور",
"Heading": "تغییر رمز عبور",
"Description": "",
"Inputs": [
{
"Label": "",
"Placeholder": "رمز عبور فعلی",
"HtmlName": "oldPassword",
"AutoComplete": "old-password"
},
{
"Label": "",
"Placeholder": "رمز عبور جدید",
"HtmlName": "newPassword",
"AutoComplete": "off"
},
{
"Label": "",
"Placeholder": "تکرار رمز عبور",
"HtmlName": "passwordRepeat",
"AutoComplete": "off"
}
],
"Rules": [
{
"Heading": "رمز عبور میبایست:",
"Items": [
"حداقل 8 کاراکتر باشد",
"ترکیبی از حروف موچک و بزرگ باشد",
"شامل اعداد باشد",
"شامل کاراکتر های خاص (نماد ها) باشد"
]
}
],
"ButtonSubmit": "ذخیره تغییرات"
},
"TabAuthenticator": {
"Title": "دو مرحله ای",
"Heading": "راه‌اندازی احراز هویت دو مرحله‌ای",
"Description": "احراز هویت دو مرحله‌ای امنیت اکانت شما را افزایش می‌دهد",
"Tip": "برای شروع فرآیند راه‌اندازی احراز هویت دو مرحله‌ای، روی دکمه زیر کلیک کنید",
"Guides": [
{
"Heading": "راهنمای احراز هویت دو مرحله‌ای",
"Description": "این قابلیت امنیت حساب شما را به شدت افزایش می‌دهد",
"Items": [
{
"Label": "یک برنامه Authenticator مانند Google Authenticator نصب کنید",
"Mark": "1"
},
{
"Label": "QR Code را اسکن کنید یا کد را به صورت دستی وارد کنید",
"Mark": "2"
},
{
"Label": "کد 6 رقمی تولید شده توسط برنامه را وارد کنید",
"Mark": "3"
}
],
"Notes": [
"توجه: کد های پشتیبان خود را در مکانی امن نگهداری کنید"
]
}
],
"ButtonSubmit": "راه‌اندازی کد دو مرحله‌ای"
}
}
}
+69
View File
@@ -0,0 +1,69 @@
import { SmartIconType } from "../iconType";
export interface Item {
Label: string;
HtmlName: string;
Icon?: SmartIconType;
}
export interface Accordion {
Heading: string;
Icon?: SmartIconType;
Items: Item[];
}
export interface TabNotifications {
Title: string;
Heading: string;
Description?: string;
Accordions: Accordion[];
}
export interface InputField {
Label: string;
Placeholder?: string;
HtmlName?: string;
AutoComplete?: string;
}
export interface RuleGroup {
Heading: string;
Items: string[];
}
export interface TabPassword {
Title: string;
Heading: string;
Description?: string;
Inputs: InputField[];
Rules: RuleGroup[];
ButtonSubmit: string;
}
export interface GuideItem {
Label: string;
Mark: string;
}
export interface Guide {
Heading: string;
Description: string;
Items: GuideItem[];
Notes: string[];
}
export interface TabAuthenticator {
Title: string;
Heading: string;
Description?: string;
Tip: string;
Guides: Guide[];
ButtonSubmit: string;
}
export interface SettingsData {
Heading: string;
TabNotifications: TabNotifications;
TabPassword: TabPassword;
TabAuthenticator: TabAuthenticator;
}
+12
View File
@@ -0,0 +1,12 @@
export interface SmartIconType {
Name: string;
Size?: number;
Color?: string;
ClassName?: string;
Variant?: string;
}
export type IconType =
React.ElementType |
React.ReactNode |
SmartIconType;