feat: complete i18n support for all components (6 languages)

Add full internationalization across 127+ components and pages.
790+ translation keys in en, tr, kmr, ckb, ar, fa locales.
Remove duplicate keys and delete unused .json locale files.
This commit is contained in:
2026-02-22 04:48:20 +03:00
parent 5b26cc8907
commit 4f683538d3
129 changed files with 22442 additions and 4186 deletions
+28 -26
View File
@@ -1,5 +1,6 @@
import { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Button } from '@/components/ui/button';
@@ -35,6 +36,7 @@ import { Gavel } from 'lucide-react';
export default function AdminPanel() {
const navigate = useNavigate();
const { t } = useTranslation();
const [users, setUsers] = useState<Array<Record<string, unknown>>>([]);
const [adminRoles, setAdminRoles] = useState<Array<Record<string, unknown>>>([]);
const [loading, setLoading] = useState(true);
@@ -137,7 +139,7 @@ export default function AdminPanel() {
};
if (loading) {
return <div className="flex justify-center items-center h-screen">Loading...</div>;
return <div className="flex justify-center items-center h-screen">{t('admin.loading')}</div>;
}
return (
@@ -148,17 +150,17 @@ export default function AdminPanel() {
>
<ArrowLeft className="w-5 h-5" />
</button>
<h1 className="text-3xl font-bold mb-8">Admin Panel</h1>
<h1 className="text-3xl font-bold mb-8">{t('admin.title')}</h1>
<Tabs defaultValue="setup" className="space-y-4">
<TabsList className="grid w-full grid-cols-11 h-auto">
<TabsTrigger value="setup" className="flex-col h-auto py-3">
<Shield className="h-4 w-4 mb-1" />
<span className="text-xs leading-tight">Commission<br/>Setup</span>
<span className="text-xs leading-tight">{t('admin.commissionSetup')}</span>
</TabsTrigger>
<TabsTrigger value="kyc" className="flex-col h-auto py-3">
<Users className="h-4 w-4 mb-1" />
<span className="text-xs leading-tight">KYC<br/>Approvals</span>
<span className="text-xs leading-tight">{t('admin.kycApprovals')}</span>
</TabsTrigger>
<TabsTrigger value="voting" className="flex-col h-auto py-3">
<Activity className="h-4 w-4 mb-1" />
@@ -213,17 +215,17 @@ export default function AdminPanel() {
<TabsContent value="users">
<Card>
<CardHeader>
<CardTitle>User Management</CardTitle>
<CardTitle>{t('admin.userManagement')}</CardTitle>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead>Username</TableHead>
<TableHead>Email</TableHead>
<TableHead>Verified</TableHead>
<TableHead>Role</TableHead>
<TableHead>Actions</TableHead>
<TableHead>{t('admin.username')}</TableHead>
<TableHead>{t('admin.email')}</TableHead>
<TableHead>{t('admin.verified')}</TableHead>
<TableHead>{t('admin.roleUser')}</TableHead>
<TableHead>{t('admin.actions')}</TableHead>
</TableRow>
</TableHeader>
<TableBody>
@@ -233,7 +235,7 @@ export default function AdminPanel() {
<TableCell>{user.email}</TableCell>
<TableCell>
<Badge variant={user.email_verified ? 'default' : 'secondary'}>
{user.email_verified ? 'Verified' : 'Unverified'}
{user.email_verified ? t('admin.verified') : t('admin.unverified')}
</Badge>
</TableCell>
<TableCell>
@@ -245,10 +247,10 @@ export default function AdminPanel() {
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="none">User</SelectItem>
<SelectItem value="moderator">Moderator</SelectItem>
<SelectItem value="admin">Admin</SelectItem>
<SelectItem value="super_admin">Super Admin</SelectItem>
<SelectItem value="none">{t('admin.roleUser')}</SelectItem>
<SelectItem value="moderator">{t('admin.roleModerator')}</SelectItem>
<SelectItem value="admin">{t('admin.roleAdmin')}</SelectItem>
<SelectItem value="super_admin">{t('admin.roleSuperAdmin')}</SelectItem>
</SelectContent>
</Select>
</TableCell>
@@ -259,7 +261,7 @@ export default function AdminPanel() {
onClick={() => sendNotification(user.id)}
>
<Bell className="h-4 w-4 mr-1" />
Notify
{t('admin.notify')}
</Button>
</TableCell>
</TableRow>
@@ -273,7 +275,7 @@ export default function AdminPanel() {
<TabsContent value="roles">
<Card>
<CardHeader>
<CardTitle>Admin Roles</CardTitle>
<CardTitle>{t('admin.adminRoles')}</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-4">
@@ -309,7 +311,7 @@ export default function AdminPanel() {
<TabsContent value="activity">
<Card>
<CardHeader>
<CardTitle>Recent Activity</CardTitle>
<CardTitle>{t('admin.recentActivity')}</CardTitle>
</CardHeader>
<CardContent>
<p className="text-muted-foreground">Activity logs will be displayed here</p>
@@ -319,32 +321,32 @@ export default function AdminPanel() {
<TabsContent value="settings">
<Card>
<CardHeader>
<CardTitle>System Settings</CardTitle>
<CardTitle>{t('admin.systemSettings')}</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div>
<Label>Maintenance Mode</Label>
<Label>{t('admin.maintenanceMode')}</Label>
<Select defaultValue="off">
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="off">Off</SelectItem>
<SelectItem value="on">On</SelectItem>
<SelectItem value="off">{t('admin.off')}</SelectItem>
<SelectItem value="on">{t('admin.on')}</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<Label>Registration</Label>
<Label>{t('admin.registration')}</Label>
<Select defaultValue="open">
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="open">Open</SelectItem>
<SelectItem value="closed">Closed</SelectItem>
<SelectItem value="invite">Invite Only</SelectItem>
<SelectItem value="open">{t('admin.on')}</SelectItem>
<SelectItem value="closed">{t('admin.closed')}</SelectItem>
<SelectItem value="invite">{t('admin.inviteOnly')}</SelectItem>
</SelectContent>
</Select>
</div>
+39 -39
View File
@@ -1,5 +1,6 @@
import React, { useState, useEffect } from 'react';
import { useNavigate, useSearchParams } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { CitizenshipModal } from '@/components/citizenship/CitizenshipModal';
@@ -12,6 +13,7 @@ const BeCitizen: React.FC = () => {
const [isModalOpen, setIsModalOpen] = useState(false);
const [isInviteModalOpen, setIsInviteModalOpen] = useState(false);
const [referrerAddress, setReferrerAddress] = useState<string | null>(null);
const { t } = useTranslation();
// Check for referral parameter on mount
useEffect(() => {
@@ -34,7 +36,7 @@ const BeCitizen: React.FC = () => {
className="w-full sm:w-auto bg-red-600 hover:bg-red-700 border-yellow-400 border-2 text-white font-semibold shadow-lg"
>
<ArrowLeft className="mr-2 h-4 w-4" />
Back to Home
{t('beCitizen.backToHome')}
</Button>
<Button
@@ -42,20 +44,20 @@ const BeCitizen: React.FC = () => {
className="w-full sm:w-auto bg-green-600 hover:bg-green-700 border-yellow-400 border-2 text-white font-semibold shadow-lg"
>
<UserPlus className="mr-2 h-4 w-4" />
Invite Friend
{t('beCitizen.inviteFriend')}
</Button>
</div>
{/* Hero Section */}
<div className="text-center mb-16">
<h1 className="text-5xl md:text-6xl font-bold text-red-700 mb-6 drop-shadow-lg">
🏛 Digital Kurdistan
🏛 {t('beCitizen.heroTitle')}
</h1>
<h2 className="text-3xl md:text-4xl font-semibold text-green-700 mb-4 drop-shadow-lg">
Bibe Welati / Be a Citizen
{t('beCitizen.heroSubtitle')}
</h2>
<p className="text-xl text-gray-800 font-semibold max-w-3xl mx-auto drop-shadow-md">
Join the Digital Kurdistan State as a sovereign citizen. Receive your Welati Tiki NFT and unlock governance, trust scoring, and community benefits.
{t('beCitizen.heroDesc')}
</p>
</div>
@@ -64,9 +66,9 @@ const BeCitizen: React.FC = () => {
<Card className="bg-red-50/90 backdrop-blur-md border-red-600/50 hover:border-red-600 transition-all shadow-lg">
<CardHeader>
<Shield className="h-12 w-12 text-red-600 mb-3" />
<CardTitle className="text-red-700 font-bold">Privacy Protected</CardTitle>
<CardTitle className="text-red-700 font-bold">{t('beCitizen.privacyTitle')}</CardTitle>
<CardDescription className="text-gray-700 font-medium">
Your data is encrypted with ZK-proofs. Only hashes are stored on-chain.
{t('beCitizen.privacyDesc')}
</CardDescription>
</CardHeader>
</Card>
@@ -74,9 +76,9 @@ const BeCitizen: React.FC = () => {
<Card className="bg-yellow-50/90 backdrop-blur-md border-yellow-600/50 hover:border-yellow-600 transition-all shadow-lg">
<CardHeader>
<Award className="h-12 w-12 text-yellow-700 mb-3" />
<CardTitle className="text-yellow-800 font-bold">Welati Tiki NFT</CardTitle>
<CardTitle className="text-yellow-800 font-bold">{t('beCitizen.nftTitle')}</CardTitle>
<CardDescription className="text-gray-700 font-medium">
Receive your unique soulbound citizenship NFT after KYC approval.
{t('beCitizen.nftDesc')}
</CardDescription>
</CardHeader>
</Card>
@@ -84,9 +86,9 @@ const BeCitizen: React.FC = () => {
<Card className="bg-green-50/90 backdrop-blur-md border-green-600/50 hover:border-green-600 transition-all shadow-lg">
<CardHeader>
<Users className="h-12 w-12 text-green-600 mb-3" />
<CardTitle className="text-green-700 font-bold">Trust Scoring</CardTitle>
<CardTitle className="text-green-700 font-bold">{t('beCitizen.trustTitle')}</CardTitle>
<CardDescription className="text-gray-700 font-medium">
Build trust through referrals, staking, and community contributions.
{t('beCitizen.trustDesc')}
</CardDescription>
</CardHeader>
</Card>
@@ -94,9 +96,9 @@ const BeCitizen: React.FC = () => {
<Card className="bg-red-50/90 backdrop-blur-md border-red-600/50 hover:border-red-600 transition-all shadow-lg">
<CardHeader>
<Globe className="h-12 w-12 text-red-600 mb-3" />
<CardTitle className="text-red-700 font-bold">Governance Access</CardTitle>
<CardTitle className="text-red-700 font-bold">{t('beCitizen.govTitle')}</CardTitle>
<CardDescription className="text-gray-700 font-medium">
Participate in on-chain governance and shape the future of Digital Kurdistan.
{t('beCitizen.govDesc')}
</CardDescription>
</CardHeader>
</Card>
@@ -108,9 +110,9 @@ const BeCitizen: React.FC = () => {
<CardContent className="pt-8 pb-8">
<div className="text-center space-y-6">
<div>
<h3 className="text-2xl font-bold text-red-700 mb-3">Ready to Join?</h3>
<h3 className="text-2xl font-bold text-red-700 mb-3">{t('beCitizen.readyToJoin')}</h3>
<p className="text-gray-800 font-medium mb-6">
Whether you&apos;re already a citizen or want to become one, start your journey here.
{t('beCitizen.readyToJoinDesc')}
</p>
</div>
@@ -119,24 +121,24 @@ const BeCitizen: React.FC = () => {
size="lg"
className="bg-gradient-to-r from-red-600 to-green-700 hover:from-red-700 hover:to-green-800 text-white font-bold px-8 py-6 text-lg group shadow-xl border-2 border-yellow-300"
>
<span>Start Citizenship Process</span>
<span>{t('beCitizen.startProcess')}</span>
<ChevronRight className="ml-2 h-5 w-5 group-hover:translate-x-1 transition-transform" />
</Button>
<div className="flex flex-col md:flex-row gap-4 justify-center items-center text-sm text-gray-700 font-medium pt-4">
<div className="flex items-center gap-2">
<Shield className="h-4 w-4 text-green-700" />
<span>Secure ZK-Proof Authentication</span>
<span>{t('beCitizen.zkAuth')}</span>
</div>
<div className="hidden md:block text-red-600"></div>
<div className="flex items-center gap-2">
<Award className="h-4 w-4 text-red-600" />
<span>Soulbound NFT Citizenship</span>
<span>{t('beCitizen.soulboundNft')}</span>
</div>
<div className="hidden md:block text-red-600"></div>
<div className="flex items-center gap-2">
<Globe className="h-4 w-4 text-green-700" />
<span>Decentralized Identity</span>
<span>{t('beCitizen.decentralizedId')}</span>
</div>
</div>
</div>
@@ -146,7 +148,7 @@ const BeCitizen: React.FC = () => {
{/* Process Overview */}
<div className="mt-16 max-w-5xl mx-auto">
<h3 className="text-3xl font-bold text-red-700 text-center mb-8 drop-shadow-lg">How It Works</h3>
<h3 className="text-3xl font-bold text-red-700 text-center mb-8 drop-shadow-lg">{t('beCitizen.howItWorks')}</h3>
<div className="grid md:grid-cols-3 gap-8">
{/* Existing Citizens */}
@@ -155,13 +157,13 @@ const BeCitizen: React.FC = () => {
<div className="bg-red-600 w-12 h-12 rounded-full flex items-center justify-center mb-4">
<span className="text-2xl font-bold text-white">1</span>
</div>
<CardTitle className="text-red-700 font-bold">Already a Citizen?</CardTitle>
<CardTitle className="text-red-700 font-bold">{t('beCitizen.existingTitle')}</CardTitle>
</CardHeader>
<CardContent className="text-gray-700 font-medium space-y-2 text-sm">
<p> Enter your Welati Tiki NFT number</p>
<p> Verify NFT ownership on-chain</p>
<p> Sign authentication challenge</p>
<p> Access your citizen dashboard</p>
<p> {t('beCitizen.existing1')}</p>
<p> {t('beCitizen.existing2')}</p>
<p> {t('beCitizen.existing3')}</p>
<p> {t('beCitizen.existing4')}</p>
</CardContent>
</Card>
@@ -171,13 +173,13 @@ const BeCitizen: React.FC = () => {
<div className="bg-yellow-600 w-12 h-12 rounded-full flex items-center justify-center mb-4">
<span className="text-2xl font-bold text-white">2</span>
</div>
<CardTitle className="text-yellow-800 font-bold">New to Citizenship?</CardTitle>
<CardTitle className="text-yellow-800 font-bold">{t('beCitizen.newTitle')}</CardTitle>
</CardHeader>
<CardContent className="text-gray-700 font-medium space-y-2 text-sm">
<p> Fill detailed KYC application</p>
<p> Data encrypted with ZK-proofs</p>
<p> Your referrer approves your identity</p>
<p> Confirm and receive your Welati Tiki NFT</p>
<p> {t('beCitizen.new1')}</p>
<p> {t('beCitizen.new2')}</p>
<p> {t('beCitizen.new3')}</p>
<p> {t('beCitizen.new4')}</p>
</CardContent>
</Card>
@@ -187,13 +189,13 @@ const BeCitizen: React.FC = () => {
<div className="bg-green-600 w-12 h-12 rounded-full flex items-center justify-center mb-4">
<span className="text-2xl font-bold text-white">3</span>
</div>
<CardTitle className="text-green-700 font-bold">Citizen Benefits</CardTitle>
<CardTitle className="text-green-700 font-bold">{t('beCitizen.benefitsTitle')}</CardTitle>
</CardHeader>
<CardContent className="text-gray-700 font-medium space-y-2 text-sm">
<p> Trust score calculation enabled</p>
<p> Governance voting rights</p>
<p> Referral tree participation</p>
<p> Staking multiplier bonuses</p>
<p> {t('beCitizen.benefit1')}</p>
<p> {t('beCitizen.benefit2')}</p>
<p> {t('beCitizen.benefit3')}</p>
<p> {t('beCitizen.benefit4')}</p>
</CardContent>
</Card>
</div>
@@ -206,11 +208,9 @@ const BeCitizen: React.FC = () => {
<div className="flex items-start gap-3">
<Shield className="h-6 w-6 text-yellow-700 mt-1 flex-shrink-0" />
<div className="text-sm text-gray-700">
<p className="font-bold text-yellow-800 mb-2">Privacy & Security</p>
<p className="font-bold text-yellow-800 mb-2">{t('beCitizen.securityTitle')}</p>
<p className="font-medium">
Your personal data is encrypted using AES-GCM with your wallet-derived keys.
Only commitment hashes are stored on the blockchain. Encrypted data is stored
on IPFS and locally on your device. No personal information is ever publicly visible.
{t('beCitizen.securityDesc')}
</p>
</div>
</div>
+58 -70
View File
@@ -1,5 +1,6 @@
import { useEffect, useState, useRef } from 'react';
import { useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { Card, CardContent } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
@@ -47,6 +48,7 @@ export default function Citizens() {
const { selectedAccount } = usePezkuwi();
const navigate = useNavigate();
const { toast } = useToast();
const { t } = useTranslation();
const { nftDetails, citizenNumber, loading } = useDashboard();
const [currentAnnouncementIndex, setCurrentAnnouncementIndex] = useState(0);
const [uploadingPhoto, setUploadingPhoto] = useState(false);
@@ -99,8 +101,8 @@ export default function Citizens() {
localStorage.setItem(storageKey, JSON.stringify(data));
setCitizenProfile(data);
toast({
title: "Profîl hat tomarkirin (Profile saved)",
description: "Zanyariyên we bi serkeftî hatin tomarkirin (Your information has been saved successfully)"
title: t('citizens.profileSaved'),
description: t('citizens.profileSavedDesc')
});
}
};
@@ -112,8 +114,8 @@ export default function Citizens() {
if (!selectedAccount?.address) {
toast({
title: "Cüzdan bağlı değil (Wallet not connected)",
description: "Ji kerema xwe wallet-ê xwe girêbide (Please connect your wallet)",
title: t('citizens.walletNotConnected'),
description: t('citizens.connectWallet'),
variant: "destructive"
});
return;
@@ -124,8 +126,8 @@ export default function Citizens() {
// Validate file type
if (!file.type.startsWith('image/')) {
toast({
title: "Dosya hatası (File error)",
description: "Lütfen resim dosyası yükleyin (Please upload an image file)",
title: t('citizens.fileError'),
description: t('citizens.uploadImageOnly'),
variant: "destructive"
});
return;
@@ -134,8 +136,8 @@ export default function Citizens() {
// Validate file size (max 2MB for localStorage)
if (file.size > 2 * 1024 * 1024) {
toast({
title: "Dosya çok büyük (File too large)",
description: "Maksimum dosya boyutu 2MB (Maximum file size is 2MB)",
title: t('citizens.fileTooLarge'),
description: t('citizens.maxFileSize'),
variant: "destructive"
});
return;
@@ -163,14 +165,14 @@ export default function Citizens() {
saveCitizenProfile(updatedProfile);
toast({
title: "Wêne hat barkirin (Photo uploaded)",
description: "Wêneyê we bi serkeftî hat tomarkirin (Your photo has been saved successfully)"
title: t('citizens.photoUploaded'),
description: t('citizens.photoUploadedDesc')
});
} catch (error) {
console.error('Photo upload error:', error);
toast({
title: "Xeletiya barkirinê (Upload error)",
description: error instanceof Error ? error.message : "Wêne nehat barkirin (Could not upload photo)",
title: t('citizens.uploadError'),
description: error instanceof Error ? error.message : t('citizens.couldNotUpload'),
variant: "destructive"
});
} finally {
@@ -185,14 +187,13 @@ export default function Citizens() {
<Card className="bg-white/90 backdrop-blur max-w-md">
<CardContent className="pt-6">
<div className="text-center space-y-4">
<p className="text-xl font-bold text-red-700">Ji kerema xwe re Wallet-ê xwe girêbide</p>
<p className="text-lg text-gray-700">(Please connect your wallet)</p>
<p className="text-xl font-bold text-red-700">{t('citizens.connectWalletMsg')}</p>
<Button
onClick={() => navigate('/')}
className="bg-red-600 hover:bg-red-700 text-white"
>
<Home className="mr-2 h-4 w-4" />
Vegere Malê (Back to Home)
{t('citizens.backToHome')}
</Button>
</div>
</CardContent>
@@ -205,8 +206,8 @@ export default function Citizens() {
// Check if user has Tiki NFT
if (!nftDetails.citizenNFT) {
toast({
title: "Mafê Te Tuneye (No Access)",
description: "Divê hûn xwedîyê Tiki NFT bin ku vê rûpelê bigihînin (You must own a Tiki NFT to access this page)",
title: t('citizens.noAccess'),
description: t('citizens.noAccessDesc'),
variant: "destructive"
});
return;
@@ -221,8 +222,8 @@ export default function Citizens() {
// Check if user has Tiki NFT
if (!nftDetails.citizenNFT) {
toast({
title: "Mafê Te Tuneye (No Access)",
description: "Divê hûn xwedîyê Tiki NFT bin ku vê rûpelê bigihînin (You must own a Tiki NFT to access this page)",
title: t('citizens.noAccess'),
description: t('citizens.noAccessDesc'),
variant: "destructive"
});
return;
@@ -250,21 +251,21 @@ export default function Citizens() {
if (dialogType === 'gov') {
toast({
title: "✅ Girêdayî Destûrdar (Authentication Successful)",
description: "Hûn dikarin dest bi hikûmetê bikin (You can now access government portal)",
title: t('citizens.authSuccess'),
description: t('citizens.govAuthDesc'),
});
navigate('/citizens/government');
} else {
toast({
title: "✅ Girêdayî Destûrdar (Authentication Successful)",
description: "Hûn dikarin dest bi karên welatiyên bikin (You can now access citizens issues)",
title: t('citizens.authSuccess'),
description: t('citizens.citizenAuthDesc'),
});
navigate('/citizens/issues');
}
} else {
toast({
title: "❌ Hejmara Welatî Şaş e (Wrong Citizen Number)",
description: "Hejmara welatîbûna ku hûn nivîsandiye rast nine (The citizen number you entered is incorrect)",
title: t('citizens.wrongNumber'),
description: t('citizens.wrongNumberDesc'),
variant: "destructive"
});
}
@@ -287,7 +288,7 @@ export default function Citizens() {
<CardContent className="pt-6">
<div className="flex items-center space-x-4">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-red-600"></div>
<p className="text-gray-700 font-medium">Portala Welatiyên barkirin... (Loading Citizens Portal...)</p>
<p className="text-gray-700 font-medium">{t('citizens.loading')}</p>
</div>
</CardContent>
</Card>
@@ -317,17 +318,17 @@ export default function Citizens() {
className="bg-red-600 hover:bg-red-700 border-yellow-400 border-2 text-white font-semibold shadow-lg"
>
<Home className="mr-2 h-4 w-4" />
Vegere Malê (Back to Home)
{t('citizens.backToHome')}
</Button>
</div>
{/* Title Section */}
<div className="text-center mb-8">
<h1 className="text-5xl md:text-6xl font-bold text-red-700 mb-3 drop-shadow-lg">
Portala Welatiyên (Citizen Portal)
{t('citizens.portalTitle')}
</h1>
<p className="text-xl text-gray-800 font-semibold drop-shadow-md">
Kurdistana Dijîtal (Digital Kurdistan)
{t('citizens.digitalKurdistan')}
</p>
</div>
@@ -347,7 +348,7 @@ export default function Citizens() {
<div className="flex-1 text-center px-6">
<div className="flex items-center justify-center mb-3">
<Bell className="h-7 w-7 text-white mr-3 animate-pulse" />
<h3 className="text-3xl font-bold text-white">Daxuyanî</h3>
<h3 className="text-3xl font-bold text-white">{t('citizens.announcements')}</h3>
</div>
<h4 className="text-xl font-bold text-white mb-3">{currentAnnouncement.title}</h4>
<p className="text-white/90 text-base leading-relaxed max-w-2xl mx-auto">{currentAnnouncement.description}</p>
@@ -394,11 +395,8 @@ export default function Citizens() {
</div>
</div>
<h2 className="text-3xl font-bold text-purple-700 mb-2 text-center">
Karên Welatiyên
{t('citizens.citizenIssues')}
</h2>
<p className="text-lg text-gray-600 font-medium text-center">
(Citizens Issues)
</p>
</CardContent>
</Card>
@@ -414,11 +412,8 @@ export default function Citizens() {
</div>
</div>
<h2 className="text-3xl font-bold text-green-700 mb-2 text-center">
Deriyê Hikûmetê
{t('citizens.govEntrance')}
</h2>
<p className="text-lg text-gray-600 font-medium text-center">
(Gov. Entrance)
</p>
</CardContent>
</Card>
</div>
@@ -501,7 +496,7 @@ export default function Citizens() {
) : (
<>
<Upload className="h-4 w-4 mr-1" />
Upload
{t('citizens.upload')}
</>
)}
</Button>
@@ -525,27 +520,27 @@ export default function Citizens() {
setShowEditModal(true);
}}
className="absolute -top-2 right-0 bg-white/90 hover:bg-white rounded-full p-1.5 shadow-md transition-colors"
title="Biguherîne (Edit)"
title={t('citizens.editProfile')}
>
<Pencil className="h-3 w-3 text-gray-600" />
</button>
{/* Name */}
<div className="bg-white/80 backdrop-blur-sm rounded-lg px-3 py-2 border-l-4 border-green-600 shadow-sm">
<div className="text-[10px] text-gray-500 uppercase tracking-wider font-medium">Nav / Name</div>
<div className="text-sm font-bold text-gray-800 truncate">{citizenProfile.fullName || 'Biguherîne...'}</div>
<div className="text-[10px] text-gray-500 uppercase tracking-wider font-medium">{t('citizens.nameLabel')}</div>
<div className="text-sm font-bold text-gray-800 truncate">{citizenProfile.fullName || '...'}</div>
</div>
{/* Father's Name */}
<div className="bg-white/80 backdrop-blur-sm rounded-lg px-3 py-2 border-l-4 border-yellow-500 shadow-sm">
<div className="text-[10px] text-gray-500 uppercase tracking-wider font-medium">Navê Bav / Father&apos;s Name</div>
<div className="text-sm font-bold text-gray-800 truncate">{citizenProfile.fatherName || 'Biguherîne...'}</div>
<div className="text-[10px] text-gray-500 uppercase tracking-wider font-medium">{t('citizens.fatherNameLabel')}</div>
<div className="text-sm font-bold text-gray-800 truncate">{citizenProfile.fatherName || '...'}</div>
</div>
{/* Location */}
<div className="bg-white/80 backdrop-blur-sm rounded-lg px-3 py-2 border-l-4 border-red-600 shadow-sm">
<div className="text-[10px] text-gray-500 uppercase tracking-wider font-medium">Cih / Location</div>
<div className="text-sm font-bold text-gray-800 truncate">{citizenProfile.location || 'Biguherîne...'}</div>
<div className="text-[10px] text-gray-500 uppercase tracking-wider font-medium">{t('citizens.locationLabel')}</div>
<div className="text-sm font-bold text-gray-800 truncate">{citizenProfile.location || '...'}</div>
</div>
</div>
@@ -607,18 +602,16 @@ export default function Citizens() {
<div className="flex items-center justify-center mb-4">
<Sun className="h-16 w-16 text-yellow-500 animate-spin" style={{ animationDuration: '8s' }} />
</div>
Deriyê Hikûmetê (Government Entrance)
{t('citizens.govDialogTitle')}
</DialogTitle>
<DialogDescription className="text-center text-gray-700 font-medium">
Ji kerema xwe hejmara welatîbûna xwe binivîse
<br />
<span className="text-sm italic">(Please enter your citizen number)</span>
{t('citizens.enterCitizenNumber')}
</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-4">
<div className="space-y-2">
<label className="text-sm font-semibold text-gray-800">
Hejmara Welatîbûnê (Citizen Number)
{t('citizens.citizenNumberLabel')}
</label>
<Input
type="text"
@@ -639,7 +632,7 @@ export default function Citizens() {
className="w-full bg-green-600 hover:bg-green-700 text-white font-bold py-3"
>
<ShieldCheck className="mr-2 h-5 w-5" />
{isVerifying ? 'Kontrolkirina... (Verifying...)' : 'Daxelbûn (Enter)'}
{isVerifying ? t('citizens.verifying') : t('citizens.enter')}
</Button>
</div>
</DialogContent>
@@ -653,18 +646,16 @@ export default function Citizens() {
<div className="flex items-center justify-center mb-4">
<Sun className="h-16 w-16 text-yellow-500 animate-spin" style={{ animationDuration: '8s' }} />
</div>
Karên Welatiyên (Citizens Issues)
{t('citizens.citizenDialogTitle')}
</DialogTitle>
<DialogDescription className="text-center text-gray-700 font-medium">
Ji kerema xwe hejmara welatîbûna xwe binivîse
<br />
<span className="text-sm italic">(Please enter your citizen number)</span>
{t('citizens.enterCitizenNumber')}
</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-4">
<div className="space-y-2">
<label className="text-sm font-semibold text-gray-800">
Hejmara Welatîbûnê (Citizen Number)
{t('citizens.citizenNumberLabel')}
</label>
<Input
type="text"
@@ -685,7 +676,7 @@ export default function Citizens() {
className="w-full bg-purple-600 hover:bg-purple-700 text-white font-bold py-3"
>
<ShieldCheck className="mr-2 h-5 w-5" />
{isVerifying ? 'Kontrolkirina... (Verifying...)' : 'Daxelbûn (Enter)'}
{isVerifying ? t('citizens.verifying') : t('citizens.enter')}
</Button>
</div>
</DialogContent>
@@ -696,21 +687,18 @@ export default function Citizens() {
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle className="text-center text-xl font-bold text-gray-800">
Zanyariyên Kesane Biguherîne
{t('citizens.editProfile')}
</DialogTitle>
<DialogDescription className="text-center text-gray-600">
Edit Your Personal Information
</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-4">
<div className="space-y-2">
<Label htmlFor="edit-name" className="text-sm font-medium">
Nav / Name
{t('citizens.nameLabel')}
</Label>
<Input
id="edit-name"
type="text"
placeholder="Navê xwe binivîse..."
placeholder={t('citizens.namePlaceholder')}
value={editForm.fullName}
onChange={(e) => setEditForm({ ...editForm, fullName: e.target.value })}
className="border-2"
@@ -719,12 +707,12 @@ export default function Citizens() {
<div className="space-y-2">
<Label htmlFor="edit-father" className="text-sm font-medium">
Navê Bav / Father&apos;s Name
{t('citizens.fatherNameLabel')}
</Label>
<Input
id="edit-father"
type="text"
placeholder="Navê bavê xwe binivîse..."
placeholder={t('citizens.fatherNamePlaceholder')}
value={editForm.fatherName}
onChange={(e) => setEditForm({ ...editForm, fatherName: e.target.value })}
className="border-2"
@@ -733,12 +721,12 @@ export default function Citizens() {
<div className="space-y-2">
<Label htmlFor="edit-location" className="text-sm font-medium">
Cih / Location
{t('citizens.locationLabel')}
</Label>
<Input
id="edit-location"
type="text"
placeholder="Cihê xwe binivîse..."
placeholder={t('citizens.locationPlaceholder')}
value={editForm.location}
onChange={(e) => setEditForm({ ...editForm, location: e.target.value })}
className="border-2"
@@ -751,7 +739,7 @@ export default function Citizens() {
onClick={() => setShowEditModal(false)}
className="flex-1"
>
Betal (Cancel)
{t('common.cancel')}
</Button>
<Button
onClick={() => {
@@ -760,7 +748,7 @@ export default function Citizens() {
}}
className="flex-1 bg-green-600 hover:bg-green-700"
>
Tomar bike (Save)
{t('citizens.save')}
</Button>
</div>
</div>
+131 -135
View File
@@ -1,4 +1,5 @@
import { useEffect, useState, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
@@ -18,6 +19,7 @@ import { ReferralDashboard } from '@/components/referral/ReferralDashboard';
// import { CommissionProposalsCard } from '@/components/dashboard/CommissionProposalsCard';
export default function Dashboard() {
const { t } = useTranslation();
const { user } = useAuth();
const { api, isApiReady, peopleApi, isPeopleReady, selectedAccount } = usePezkuwi();
const navigate = useNavigate();
@@ -146,8 +148,8 @@ export default function Dashboard() {
const handleStartScoreTracking = async () => {
if (!peopleApi || !selectedAccount) {
toast({
title: "Hata",
description: "Lütfen önce cüzdanınızı bağlayın",
title: t('common.error'),
description: t('dashboard.connectWalletError'),
variant: "destructive"
});
return;
@@ -161,23 +163,23 @@ export default function Dashboard() {
if (result.success) {
toast({
title: "Başarılı",
description: "Score tracking başlatıldı! Staking score'unuz artık hesaplanacak."
title: t('common.success'),
description: t('dashboard.scoreTrackingStarted')
});
// Refresh scores after starting tracking
fetchScoresAndTikis();
} else {
toast({
title: "Hata",
description: result.error || "Score tracking başlatılamadı",
title: t('common.error'),
description: result.error || t('dashboard.scoreTrackingFailed'),
variant: "destructive"
});
}
} catch (error) {
if (import.meta.env.DEV) console.error('Error starting score tracking:', error);
toast({
title: "Hata",
description: error instanceof Error ? error.message : "Score tracking başlatılamadı",
title: t('common.error'),
description: error instanceof Error ? error.message : t('dashboard.scoreTrackingFailed'),
variant: "destructive"
});
} finally {
@@ -194,13 +196,13 @@ export default function Dashboard() {
const result = await recordTrustScore(peopleApi, selectedAccount.address, injector.signer);
if (result.success) {
toast({ title: "Success", description: "Trust score recorded for this epoch." });
toast({ title: t('common.success'), description: t('dashboard.trustScoreRecorded') });
fetchScoresAndTikis();
} else {
toast({ title: "Error", description: result.error || "Failed to record trust score", variant: "destructive" });
toast({ title: t('common.error'), description: result.error || t('dashboard.trustScoreRecordFailed'), variant: "destructive" });
}
} catch (error) {
toast({ title: "Error", description: error instanceof Error ? error.message : "Failed to record trust score", variant: "destructive" });
toast({ title: t('common.error'), description: error instanceof Error ? error.message : t('dashboard.trustScoreRecordFailed'), variant: "destructive" });
} finally {
setIsRecordingScore(false);
}
@@ -216,13 +218,13 @@ export default function Dashboard() {
if (result.success) {
const rewardInfo = pezRewards?.claimableRewards.find(r => r.epoch === epochIndex);
toast({ title: "Success", description: `${rewardInfo?.amount || '0'} PEZ reward claimed!` });
toast({ title: t('common.success'), description: t('dashboard.rewardClaimed', { amount: rewardInfo?.amount || '0' }) });
fetchScoresAndTikis();
} else {
toast({ title: "Error", description: result.error || "Failed to claim reward", variant: "destructive" });
toast({ title: t('common.error'), description: result.error || t('dashboard.rewardClaimFailed'), variant: "destructive" });
}
} catch (error) {
toast({ title: "Error", description: error instanceof Error ? error.message : "Failed to claim reward", variant: "destructive" });
toast({ title: t('common.error'), description: error instanceof Error ? error.message : t('dashboard.rewardClaimFailed'), variant: "destructive" });
} finally {
setIsClaimingReward(false);
}
@@ -238,8 +240,8 @@ export default function Dashboard() {
const sendVerificationEmail = async () => {
if (!user?.email) {
toast({
title: "Error",
description: "No email address found",
title: t('common.error'),
description: t('dashboard.noEmailFound'),
variant: "destructive"
});
return;
@@ -276,25 +278,25 @@ export default function Dashboard() {
}
toast({
title: "Verification Email Sent",
description: "Please check your email inbox and spam folder",
title: t('dashboard.verificationEmailSent'),
description: t('dashboard.checkInboxAndSpam'),
});
} catch (error) {
if (import.meta.env.DEV) console.error('Error sending verification email:', error);
// Provide more detailed error message
let errorMessage = "Failed to send verification email";
let errorMessage = t('dashboard.failedToSendEmail');
if (error.message?.includes('Email rate limit exceeded')) {
errorMessage = "Too many requests. Please wait a few minutes and try again.";
errorMessage = t('dashboard.rateLimitExceeded');
} else if (error.message?.includes('User not found')) {
errorMessage = "Account not found. Please sign up first.";
errorMessage = t('dashboard.accountNotFound');
} else if (error.message) {
errorMessage = error.message;
}
toast({
title: "Error",
title: t('common.error'),
description: errorMessage,
variant: "destructive"
});
@@ -304,8 +306,8 @@ export default function Dashboard() {
const handleRenounceCitizenship = async () => {
if (!api || !selectedAccount) {
toast({
title: "Error",
description: "Please connect your wallet first",
title: t('common.error'),
description: t('dashboard.connectWalletError'),
variant: "destructive"
});
return;
@@ -313,21 +315,15 @@ export default function Dashboard() {
if (kycStatus !== 'Approved') {
toast({
title: "Error",
description: "Only citizens can renounce citizenship",
title: t('common.error'),
description: t('dashboard.renounceOnlyCitizens'),
variant: "destructive"
});
return;
}
// Confirm action
const confirmed = window.confirm(
'Are you sure you want to renounce your citizenship? This will:\n' +
'• Burn your Citizen (Welati) NFT\n' +
'• Reset your KYC status to NotStarted\n' +
'• Remove all associated citizen privileges\n\n' +
'You can always reapply later if you change your mind.'
);
const confirmed = window.confirm(t('dashboard.renounceConfirmMsg'));
if (!confirmed) return;
@@ -351,7 +347,7 @@ export default function Dashboard() {
}
if (import.meta.env.DEV) console.error(errorMessage);
toast({
title: "Renunciation Failed",
title: t('dashboard.renounceFailed'),
description: errorMessage,
variant: "destructive"
});
@@ -367,8 +363,8 @@ export default function Dashboard() {
if (event.section === 'identityKyc' && event.method === 'CitizenshipRenounced') {
if (import.meta.env.DEV) console.log('📢 CitizenshipRenounced event detected');
toast({
title: "Citizenship Renounced",
description: "Your citizenship has been successfully renounced. You can reapply anytime."
title: t('dashboard.citizenshipRenounced'),
description: t('dashboard.renounceSuccess')
});
// Refresh data after a short delay
@@ -388,7 +384,7 @@ export default function Dashboard() {
if (import.meta.env.DEV) console.error('Renunciation error:', err);
const errorMsg = err instanceof Error ? err.message : 'Failed to renounce citizenship';
toast({
title: "Error",
title: t('common.error'),
description: errorMsg,
variant: "destructive"
});
@@ -397,7 +393,7 @@ export default function Dashboard() {
};
const getRoleDisplay = (): string => {
if (loadingScores) return 'Loading...';
if (loadingScores) return t('dashboard.loading');
if (!selectedAccount) return 'Member';
if (tikis.length === 0) return 'Member';
@@ -411,7 +407,7 @@ export default function Dashboard() {
};
if (loading) {
return <div className="flex justify-center items-center h-screen">Loading...</div>;
return <div className="flex justify-center items-center h-screen">{t('dashboard.loading')}</div>;
}
return (
@@ -422,33 +418,33 @@ export default function Dashboard() {
>
<ArrowLeft className="w-5 h-5" />
</button>
<h1 className="text-3xl font-bold mb-6">User Dashboard</h1>
<h1 className="text-3xl font-bold mb-6">{t('dashboard.title')}</h1>
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-4 mb-6">
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Account Status</CardTitle>
<CardTitle className="text-sm font-medium">{t('dashboard.accountStatus')}</CardTitle>
<Shield className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">
{user?.email_confirmed_at || profile?.email_verified ? (
<Badge className="bg-green-500">Verified</Badge>
<Badge className="bg-green-500">{t('dashboard.verified')}</Badge>
) : (
<Badge variant="destructive">Unverified</Badge>
<Badge variant="destructive">{t('dashboard.unverified')}</Badge>
)}
</div>
<p className="text-xs text-muted-foreground">
{user?.email_confirmed_at
? `Verified on ${new Date(user.email_confirmed_at).toLocaleDateString()}`
: 'Email verification status'}
? t('dashboard.verifiedOn', { date: new Date(user.email_confirmed_at).toLocaleDateString() })
: t('dashboard.emailVerificationStatus')}
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Member Since</CardTitle>
<CardTitle className="text-sm font-medium">{t('dashboard.memberSince')}</CardTitle>
<Calendar className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
@@ -456,14 +452,14 @@ export default function Dashboard() {
{new Date(profile?.joined_at || user?.created_at).toLocaleDateString()}
</div>
<p className="text-xs text-muted-foreground">
Registration date
{t('dashboard.registrationDate')}
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Role</CardTitle>
<CardTitle className="text-sm font-medium">{t('dashboard.role')}</CardTitle>
<User className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
@@ -471,14 +467,14 @@ export default function Dashboard() {
{getRoleDisplay()}
</div>
<p className="text-xs text-muted-foreground">
{selectedAccount ? 'From Tiki NFTs' : 'Connect wallet for roles'}
{selectedAccount ? t('dashboard.fromTikiNfts') : t('dashboard.connectWalletForRoles')}
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Total Score</CardTitle>
<CardTitle className="text-sm font-medium">{t('dashboard.totalScore')}</CardTitle>
<Award className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
@@ -486,7 +482,7 @@ export default function Dashboard() {
{loadingScores ? '...' : scores.totalScore}
</div>
<p className="text-xs text-muted-foreground">
Combined from all score types
{t('dashboard.combinedScores')}
</p>
</CardContent>
</Card>
@@ -495,7 +491,7 @@ export default function Dashboard() {
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-4 mb-6">
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Trust Score</CardTitle>
<CardTitle className="text-sm font-medium">{t('dashboard.trustScore')}</CardTitle>
<Shield className="h-4 w-4 text-purple-500" />
</CardHeader>
<CardContent>
@@ -503,14 +499,14 @@ export default function Dashboard() {
{loadingScores ? '...' : scores.trustScore}
</div>
<p className="text-xs text-muted-foreground">
Frontend calculation
{t('dashboard.frontendCalc')}
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Referral Score</CardTitle>
<CardTitle className="text-sm font-medium">{t('dashboard.referralScore')}</CardTitle>
<Users className="h-4 w-4 text-cyan-500" />
</CardHeader>
<CardContent>
@@ -518,14 +514,14 @@ export default function Dashboard() {
{loadingScores ? '...' : scores.referralScore}
</div>
<p className="text-xs text-muted-foreground">
From referral system
{t('dashboard.fromReferral')}
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Staking Score</CardTitle>
<CardTitle className="text-sm font-medium">{t('dashboard.stakingScore')}</CardTitle>
<TrendingUp className="h-4 w-4 text-green-500" />
</CardHeader>
<CardContent>
@@ -534,7 +530,7 @@ export default function Dashboard() {
</div>
{stakingStatus?.isTracking ? (
<p className="text-xs text-muted-foreground">
Tracking: {formatDuration(stakingStatus.durationBlocks)}
{t('dashboard.tracking', { duration: formatDuration(stakingStatus.durationBlocks) })}
</p>
) : selectedAccount ? (
<Button
@@ -547,18 +543,18 @@ export default function Dashboard() {
{startingScoreTracking ? (
<>
<Loader2 className="h-3 w-3 mr-1 animate-spin" />
Starting...
{t('dashboard.starting')}
</>
) : (
<>
<Play className="h-3 w-3 mr-1" />
Start Tracking
{t('dashboard.startTracking')}
</>
)}
</Button>
) : (
<p className="text-xs text-muted-foreground">
Connect wallet to track
{t('dashboard.connectToTrack')}
</p>
)}
</CardContent>
@@ -566,7 +562,7 @@ export default function Dashboard() {
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Tiki Score</CardTitle>
<CardTitle className="text-sm font-medium">{t('dashboard.tikiScore')}</CardTitle>
<Award className="h-4 w-4 text-pink-500" />
</CardHeader>
<CardContent>
@@ -574,7 +570,7 @@ export default function Dashboard() {
{loadingScores ? '...' : scores.tikiScore}
</div>
<p className="text-xs text-muted-foreground">
{tikis.length} {tikis.length === 1 ? 'role' : 'roles'} assigned
{t('dashboard.rolesAssigned', { count: tikis.length })}
</p>
</CardContent>
</Card>
@@ -584,7 +580,7 @@ export default function Dashboard() {
{selectedAccount && pezRewards && (
<Card className="mb-6">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">PEZ Rewards</CardTitle>
<CardTitle className="text-sm font-medium">{t('dashboard.pezRewards')}</CardTitle>
<div className="flex items-center gap-2">
<Badge className={
pezRewards.epochStatus === 'Open'
@@ -593,21 +589,21 @@ export default function Dashboard() {
? 'bg-orange-500'
: 'bg-gray-500'
}>
{pezRewards.epochStatus === 'Open' ? 'Open' : pezRewards.epochStatus === 'ClaimPeriod' ? 'Claim Period' : 'Closed'}
{pezRewards.epochStatus === 'Open' ? t('dashboard.epochOpen') : pezRewards.epochStatus === 'ClaimPeriod' ? t('dashboard.epochClaim') : t('dashboard.epochClosed')}
</Badge>
<Coins className="h-4 w-4 text-orange-500" />
</div>
</CardHeader>
<CardContent>
<div className="space-y-3">
<p className="text-xs text-muted-foreground">Epoch {pezRewards.currentEpoch}</p>
<p className="text-xs text-muted-foreground">{t('dashboard.epoch', { number: pezRewards.currentEpoch })}</p>
{/* Open epoch: Record score or show recorded score */}
{pezRewards.epochStatus === 'Open' && (
pezRewards.hasRecordedThisEpoch ? (
<div className="flex items-center gap-2">
<div className="text-lg font-bold text-green-600">Score: {pezRewards.userScoreCurrentEpoch}</div>
<Badge variant="outline" className="text-green-600 border-green-300">Recorded</Badge>
<Badge variant="outline" className="text-green-600 border-green-300">{t('dashboard.recorded')}</Badge>
</div>
) : (
<Button
@@ -619,12 +615,12 @@ export default function Dashboard() {
{isRecordingScore ? (
<>
<Loader2 className="h-3 w-3 mr-1 animate-spin" />
Recording...
{t('dashboard.recording')}
</>
) : (
<>
<Play className="h-3 w-3 mr-1" />
Record Trust Score
{t('dashboard.recordTrustScore')}
</>
)}
</Button>
@@ -639,7 +635,7 @@ export default function Dashboard() {
</div>
{pezRewards.claimableRewards.map((reward) => (
<div key={reward.epoch} className="flex items-center justify-between">
<span className="text-xs text-muted-foreground">Epoch {reward.epoch}: {reward.amount} PEZ</span>
<span className="text-xs text-muted-foreground">{t('dashboard.epoch', { number: reward.epoch })}: {reward.amount} PEZ</span>
<Button
size="sm"
variant="outline"
@@ -647,7 +643,7 @@ export default function Dashboard() {
disabled={isClaimingReward}
className="h-6 text-xs px-2"
>
{isClaimingReward ? '...' : 'Claim'}
{isClaimingReward ? '...' : t('dashboard.claim')}
</Button>
</div>
))}
@@ -664,63 +660,63 @@ export default function Dashboard() {
<Tabs defaultValue="profile" className="space-y-4">
<TabsList className="flex flex-wrap gap-1">
<TabsTrigger value="profile" className="text-xs sm:text-sm px-2 sm:px-3">Profile</TabsTrigger>
<TabsTrigger value="roles" className="text-xs sm:text-sm px-2 sm:px-3">Roles & Tikis</TabsTrigger>
<TabsTrigger value="referrals" className="text-xs sm:text-sm px-2 sm:px-3">Referrals</TabsTrigger>
<TabsTrigger value="security" className="text-xs sm:text-sm px-2 sm:px-3">Security</TabsTrigger>
<TabsTrigger value="activity" className="text-xs sm:text-sm px-2 sm:px-3">Activity</TabsTrigger>
<TabsTrigger value="profile" className="text-xs sm:text-sm px-2 sm:px-3">{t('dashboard.profileTab')}</TabsTrigger>
<TabsTrigger value="roles" className="text-xs sm:text-sm px-2 sm:px-3">{t('dashboard.rolesTab')}</TabsTrigger>
<TabsTrigger value="referrals" className="text-xs sm:text-sm px-2 sm:px-3">{t('dashboard.referralsTab')}</TabsTrigger>
<TabsTrigger value="security" className="text-xs sm:text-sm px-2 sm:px-3">{t('dashboard.securityTab')}</TabsTrigger>
<TabsTrigger value="activity" className="text-xs sm:text-sm px-2 sm:px-3">{t('dashboard.activityTab')}</TabsTrigger>
</TabsList>
<TabsContent value="profile" className="space-y-4">
<Card>
<CardHeader>
<CardTitle>Profile Information</CardTitle>
<CardDescription>Your personal details and contact information</CardDescription>
<CardTitle>{t('dashboard.profileInfo')}</CardTitle>
<CardDescription>{t('dashboard.personalDetails')}</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid gap-4">
<div className="flex items-center gap-2">
<User className="h-4 w-4 text-muted-foreground" />
<span className="font-medium">Full Name:</span>
<span>{profile?.full_name || 'Not set'}</span>
<span className="font-medium">{t('dashboard.fullName')}</span>
<span>{profile?.full_name || t('dashboard.notSet')}</span>
</div>
<div className="flex items-center gap-2">
<Mail className="h-4 w-4 text-muted-foreground" />
<span className="font-medium">Email:</span>
<span className="font-medium">{t('dashboard.emailLabel')}</span>
<span>{user?.email}</span>
{user?.email_confirmed_at || profile?.email_verified ? (
<Badge className="bg-green-500">Verified</Badge>
<Badge className="bg-green-500">{t('dashboard.verified')}</Badge>
) : (
<Button size="sm" variant="outline" onClick={sendVerificationEmail}>
Verify Email
{t('dashboard.verifyEmail')}
</Button>
)}
</div>
<div className="flex items-center gap-2">
<Mail className="h-4 w-4 text-muted-foreground" />
<span className="font-medium">Recovery Email:</span>
<span>{profile?.recovery_email || 'Not set'}</span>
<span className="font-medium">{t('dashboard.recoveryEmail')}</span>
<span>{profile?.recovery_email || t('dashboard.notSet')}</span>
{profile?.recovery_email_verified && profile?.recovery_email && (
<Badge className="bg-green-500">Verified</Badge>
<Badge className="bg-green-500">{t('dashboard.verified')}</Badge>
)}
</div>
<div className="flex items-center gap-2">
<Phone className="h-4 w-4 text-muted-foreground" />
<span className="font-medium">Phone:</span>
<span>{profile?.phone_number || 'Not set'}</span>
<span className="font-medium">{t('dashboard.phone')}</span>
<span>{profile?.phone_number || t('dashboard.notSet')}</span>
</div>
<div className="flex items-center gap-2">
<Globe className="h-4 w-4 text-muted-foreground" />
<span className="font-medium">Website:</span>
<span>{profile?.website || 'Not set'}</span>
<span className="font-medium">{t('dashboard.website')}</span>
<span>{profile?.website || t('dashboard.notSet')}</span>
</div>
<div className="flex items-center gap-2">
<MapPin className="h-4 w-4 text-muted-foreground" />
<span className="font-medium">Location:</span>
<span>{profile?.location || 'Not set'}</span>
<span className="font-medium">{t('dashboard.location')}</span>
<span>{profile?.location || t('dashboard.notSet')}</span>
</div>
</div>
<Button onClick={() => navigate('/profile/settings')}>Edit Profile</Button>
<Button onClick={() => navigate('/profile/settings')}>{t('dashboard.editProfile')}</Button>
</CardContent>
</Card>
</TabsContent>
@@ -728,11 +724,11 @@ export default function Dashboard() {
<TabsContent value="roles" className="space-y-4">
<Card>
<CardHeader>
<CardTitle>Roles & Tikis</CardTitle>
<CardTitle>{t('dashboard.rolesTitle')}</CardTitle>
<CardDescription>
{selectedAccount
? 'Your roles from the blockchain (Pallet-Tiki)'
: 'Connect your wallet to view your roles'}
? t('dashboard.rolesFromBlockchain')
: t('dashboard.connectWalletToView')}
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
@@ -740,17 +736,17 @@ export default function Dashboard() {
<div className="text-center py-8">
<Shield className="h-12 w-12 text-muted-foreground mx-auto mb-4" />
<p className="text-muted-foreground mb-4">
Connect your Pezkuwi wallet to view your on-chain roles
{t('dashboard.connectWalletMsg')}
</p>
<Button onClick={() => navigate('/')}>
Go to Home to Connect Wallet
{t('dashboard.goHomeToConnect')}
</Button>
</div>
)}
{selectedAccount && loadingScores && (
<div className="text-center py-8">
<p className="text-muted-foreground">Loading roles from blockchain...</p>
<p className="text-muted-foreground">{t('dashboard.loadingRoles')}</p>
</div>
)}
@@ -758,10 +754,10 @@ export default function Dashboard() {
<div className="text-center py-8">
<Award className="h-12 w-12 text-muted-foreground mx-auto mb-4" />
<p className="text-muted-foreground mb-2">
No roles assigned yet
{t('dashboard.noRolesYet')}
</p>
<p className="text-sm text-muted-foreground">
Complete KYC to become a Citizen (Welati)
{t('dashboard.completeKyc')}
</p>
</div>
)}
@@ -770,23 +766,23 @@ export default function Dashboard() {
<div className="space-y-4">
<div className="grid gap-2">
<div className="flex items-center gap-2">
<span className="font-medium">Primary Role:</span>
<span className="font-medium">{t('dashboard.primaryRole')}</span>
<Badge className="text-lg">
{getTikiEmoji(getPrimaryRole(tikis))} {getTikiDisplayName(getPrimaryRole(tikis))}
</Badge>
</div>
<div className="flex items-center gap-2">
<span className="font-medium">Total Score:</span>
<span className="font-medium">{t('dashboard.totalScore')}:</span>
<span className="text-lg font-bold text-purple-600">{scores.totalScore}</span>
</div>
<div className="flex items-center gap-2">
<span className="font-medium">Categories:</span>
<span className="font-medium">{t('dashboard.categories')}</span>
<span className="text-muted-foreground">{getRoleCategories().join(', ')}</span>
</div>
</div>
<div className="border-t pt-4">
<h4 className="font-medium mb-3">All Roles ({tikis.length})</h4>
<h4 className="font-medium mb-3">{t('dashboard.allRoles', { count: tikis.length })}</h4>
<div className="flex flex-wrap gap-2">
{tikis.map((tiki, index) => (
<Badge
@@ -802,16 +798,16 @@ export default function Dashboard() {
{nftDetails.totalNFTs > 0 && (
<div className="border-t pt-4">
<h4 className="font-medium mb-3">NFT Details ({nftDetails.totalNFTs})</h4>
<h4 className="font-medium mb-3">{t('dashboard.nftDetails', { count: nftDetails.totalNFTs })}</h4>
<div className="space-y-3">
{nftDetails.citizenNFT && (
<div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-3">
<div className="flex items-center justify-between mb-2">
<span className="font-medium text-blue-900 dark:text-blue-100">
{nftDetails.citizenNFT.tikiEmoji} Citizen NFT
{nftDetails.citizenNFT.tikiEmoji} {t('dashboard.citizenNft')}
</span>
<Badge variant="outline" className="text-blue-700 border-blue-300">
Primary
{t('dashboard.primary')}
</Badge>
</div>
@@ -819,7 +815,7 @@ export default function Dashboard() {
<div className="grid grid-cols-2 gap-3 mb-3">
{/* NFT Number */}
<div className="p-2 bg-white dark:bg-blue-950 rounded border border-blue-300 dark:border-blue-700">
<span className="text-xs text-blue-600 dark:text-blue-400 font-medium">NFT Number:</span>
<span className="text-xs text-blue-600 dark:text-blue-400 font-medium">{t('dashboard.nftNumber')}</span>
<div className="font-mono text-lg font-bold text-blue-900 dark:text-blue-100">
#{nftDetails.citizenNFT.collectionId}-{nftDetails.citizenNFT.itemId}
</div>
@@ -827,7 +823,7 @@ export default function Dashboard() {
{/* Citizen Number = NFT Number + 6 digits */}
<div className="p-2 bg-white dark:bg-green-950 rounded border border-green-300 dark:border-green-700">
<span className="text-xs text-green-600 dark:text-green-400 font-medium">Citizen Number:</span>
<span className="text-xs text-green-600 dark:text-green-400 font-medium">{t('dashboard.citizenNumberLabel')}</span>
<div className="font-mono text-lg font-bold text-green-900 dark:text-green-100">
#{nftDetails.citizenNFT.collectionId}-{nftDetails.citizenNFT.itemId}-{generateCitizenNumber(
nftDetails.citizenNFT.owner,
@@ -840,25 +836,25 @@ export default function Dashboard() {
<div className="grid grid-cols-2 gap-2 text-sm">
<div>
<span className="text-blue-700 dark:text-blue-300 font-medium">Collection ID:</span>
<span className="text-blue-700 dark:text-blue-300 font-medium">{t('dashboard.collectionId')}</span>
<span className="ml-2 font-mono text-blue-900 dark:text-blue-100">
{nftDetails.citizenNFT.collectionId}
</span>
</div>
<div>
<span className="text-blue-700 dark:text-blue-300 font-medium">Item ID:</span>
<span className="text-blue-700 dark:text-blue-300 font-medium">{t('dashboard.itemId')}</span>
<span className="ml-2 font-mono text-blue-900 dark:text-blue-100">
{nftDetails.citizenNFT.itemId}
</span>
</div>
<div className="col-span-2">
<span className="text-blue-700 dark:text-blue-300 font-medium">Role:</span>
<span className="text-blue-700 dark:text-blue-300 font-medium">{t('dashboard.roleLabel')}</span>
<span className="ml-2 text-blue-900 dark:text-blue-100">
{nftDetails.citizenNFT.tikiDisplayName}
</span>
</div>
<div className="col-span-2">
<span className="text-blue-700 dark:text-blue-300 font-medium">Tiki Type:</span>
<span className="text-blue-700 dark:text-blue-300 font-medium">{t('dashboard.tikiType')}</span>
<span className="ml-2 font-semibold text-purple-600 dark:text-purple-400">
{nftDetails.citizenNFT.tikiRole}
</span>
@@ -869,7 +865,7 @@ export default function Dashboard() {
{nftDetails.roleNFTs.length > 0 && (
<div className="space-y-2">
<p className="text-sm text-muted-foreground font-medium">Additional Role NFTs:</p>
<p className="text-sm text-muted-foreground font-medium">{t('dashboard.additionalRoleNfts')}</p>
{nftDetails.roleNFTs.map((nft, /*index*/) => (
<div
key={`${nft.collectionId}-${nft.itemId}`}
@@ -885,15 +881,15 @@ export default function Dashboard() {
</div>
<div className="grid grid-cols-2 gap-2 text-sm text-muted-foreground">
<div>
<span className="font-medium">Collection:</span>
<span className="font-medium">{t('dashboard.collection')}</span>
<span className="ml-2 font-mono">{nft.collectionId}</span>
</div>
<div>
<span className="font-medium">Item:</span>
<span className="font-medium">{t('dashboard.item')}</span>
<span className="ml-2 font-mono">{nft.itemId}</span>
</div>
<div className="col-span-2">
<span className="font-medium">Tiki Type:</span>
<span className="font-medium">{t('dashboard.tikiType')}</span>
<span className="ml-2 font-semibold text-purple-600 dark:text-purple-400">{nft.tikiRole}</span>
</div>
</div>
@@ -906,7 +902,7 @@ export default function Dashboard() {
)}
<div className="border-t pt-4 bg-gray-50 dark:bg-gray-900 rounded-lg p-4">
<h4 className="font-medium mb-2">Blockchain Address</h4>
<h4 className="font-medium mb-2">{t('dashboard.blockchainAddress')}</h4>
<p className="text-sm text-muted-foreground font-mono break-all">
{selectedAccount.address}
</p>
@@ -917,18 +913,18 @@ export default function Dashboard() {
<div className="bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-lg p-4">
<h4 className="font-medium mb-2 text-yellow-800 dark:text-yellow-200 flex items-center gap-2">
<UserMinus className="h-4 w-4" />
Renounce Citizenship
{t('dashboard.renounceCitizenship')}
</h4>
<p className="text-sm text-yellow-700 dark:text-yellow-300 mb-3">
You can voluntarily renounce your citizenship at any time. This will:
{t('dashboard.renounceDesc')}
</p>
<ul className="text-sm text-yellow-700 dark:text-yellow-300 mb-3 list-disc list-inside space-y-1">
<li>Burn your Citizen (Welati) NFT</li>
<li>Reset your KYC status</li>
<li>Remove citizen privileges</li>
<li>{t('dashboard.renounceBurn')}</li>
<li>{t('dashboard.renounceReset')}</li>
<li>{t('dashboard.renounceRemove')}</li>
</ul>
<p className="text-xs text-yellow-600 dark:text-yellow-400 mb-3">
Note: You can always reapply for citizenship later if you change your mind.
{t('dashboard.renounceNote')}
</p>
<Button
variant="destructive"
@@ -936,7 +932,7 @@ export default function Dashboard() {
onClick={handleRenounceCitizenship}
disabled={renouncingCitizenship}
>
{renouncingCitizenship ? 'Renouncing...' : 'Renounce Citizenship'}
{renouncingCitizenship ? t('dashboard.renouncing') : t('dashboard.renounceBtn')}
</Button>
</div>
</div>
@@ -954,14 +950,14 @@ export default function Dashboard() {
<TabsContent value="security" className="space-y-4">
<Card>
<CardHeader>
<CardTitle>Security Settings</CardTitle>
<CardDescription>Manage your account security</CardDescription>
<CardTitle>{t('dashboard.securitySettings')}</CardTitle>
<CardDescription>{t('dashboard.manageAccountSecurity')}</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<h3 className="font-medium">Password</h3>
<p className="text-sm text-muted-foreground">Last changed: Never</p>
<Button onClick={() => navigate('/reset-password')}>Change Password</Button>
<h3 className="font-medium">{t('dashboard.password')}</h3>
<p className="text-sm text-muted-foreground">{t('dashboard.lastChanged')}</p>
<Button onClick={() => navigate('/reset-password')}>{t('dashboard.changePassword')}</Button>
</div>
{!user?.email_confirmed_at && !profile?.email_verified && (
@@ -969,8 +965,8 @@ export default function Dashboard() {
<div className="flex items-center">
<AlertCircle className="h-5 w-5 text-yellow-600 mr-2" />
<div>
<h4 className="font-medium text-gray-900">Verify your email</h4>
<p className="text-sm text-gray-900">Please verify your email to access all features</p>
<h4 className="font-medium text-gray-900">{t('dashboard.verifyYourEmail')}</h4>
<p className="text-sm text-gray-900">{t('dashboard.verifyEmailMsg')}</p>
</div>
</div>
</div>
@@ -982,11 +978,11 @@ export default function Dashboard() {
<TabsContent value="activity" className="space-y-4">
<Card>
<CardHeader>
<CardTitle>Recent Activity</CardTitle>
<CardDescription>Your recent actions and transactions</CardDescription>
<CardTitle>{t('dashboard.recentActivity')}</CardTitle>
<CardDescription>{t('dashboard.recentActivityDesc')}</CardDescription>
</CardHeader>
<CardContent>
<p className="text-muted-foreground">No recent activity</p>
<p className="text-muted-foreground">{t('dashboard.noRecentActivity')}</p>
</CardContent>
</Card>
</TabsContent>
+19 -17
View File
@@ -1,4 +1,5 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import Layout from '@/components/Layout';
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism';
@@ -11,6 +12,7 @@ const CodeSnippet = ({ language, code }: { language: string, code: string }) =>
);
const Developers: React.FC = () => {
const { t } = useTranslation();
const connectCode = `import { ApiPromise, WsProvider } from '@pezkuwi/api';
// Connect to the PezkuwiChain node
@@ -38,40 +40,40 @@ const unsub = await api.tx.balances
<Layout>
<div className="container mx-auto px-4 py-8 text-white">
<div className="text-center mb-12">
<h1 className="text-5xl font-bold mb-2">Developer Portal</h1>
<p className="text-xl text-gray-400">Everything you need to build on PezkuwiChain.</p>
<h1 className="text-5xl font-bold mb-2">{t('developers.title')}</h1>
<p className="text-xl text-gray-400">{t('developers.subtitle')}</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{/* Quick Start Guide */}
<div className="bg-gray-800 p-6 rounded-lg">
<Book className="text-blue-400 mb-4" size={32} />
<h2 className="text-2xl font-bold mb-2">Quick Start Guide</h2>
<p className="text-gray-400 mb-4">Your first steps to building a dApp on PezkuwiChain.</p>
<h2 className="text-2xl font-bold mb-2">{t('developers.quickStart')}</h2>
<p className="text-gray-400 mb-4">{t('developers.quickStartDesc')}</p>
<ol className="list-decimal list-inside space-y-2">
<li>Install the Pezkuwi.js extension.</li>
<li>Get some testnet HEZ from the Faucet.</li> <li>Clone a starter project from our GitHub.</li>
<li>Start building!</li>
<li>{t('developers.step1')}</li>
<li>{t('developers.step2')}</li> <li>{t('developers.step3')}</li>
<li>{t('developers.step4')}</li>
</ol>
</div>
{/* SDKs */}
<div className="bg-gray-800 p-6 rounded-lg">
<Download className="text-green-400 mb-4" size={32} />
<h2 className="text-2xl font-bold mb-2">SDK Downloads</h2>
<p className="text-gray-400 mb-4">Libraries to interact with the chain.</p>
<h2 className="text-2xl font-bold mb-2">{t('developers.sdkDownloads')}</h2>
<p className="text-gray-400 mb-4">{t('developers.sdkDesc')}</p>
<div className="space-y-3">
<a href="#" className="flex items-center text-blue-400 hover:text-white">
<Github className="mr-2" size={20} />
<span>Javascript/Typescript SDK</span>
<span>{t('developers.jsSdk')}</span>
</a>
<a href="#" className="flex items-center text-blue-400 hover:text-white">
<Github className="mr-2" size={20} />
<span>Rust SDK</span>
<span>{t('developers.rustSdk')}</span>
</a>
<a href="#" className="flex items-center text-blue-400 hover:text-white">
<Github className="mr-2" size={20} />
<span>Python SDK</span>
<span>{t('developers.pythonSdk')}</span>
</a>
</div>
</div>
@@ -79,8 +81,8 @@ const unsub = await api.tx.balances
{/* Community */}
<div className="bg-gray-800 p-6 rounded-lg">
<MessageCircle className="text-purple-400 mb-4" size={32} />
<h2 className="text-2xl font-bold mb-2">Community</h2>
<p className="text-gray-400 mb-4">Get help and connect with other developers.</p>
<h2 className="text-2xl font-bold mb-2">{t('developers.community')}</h2>
<p className="text-gray-400 mb-4">{t('developers.communityDesc')}</p>
<div className="space-y-3">
<a href="#" className="flex items-center text-blue-400 hover:text-white">
<Github className="mr-2" size={20} />
@@ -95,14 +97,14 @@ const unsub = await api.tx.balances
</div>
<div className="mt-12">
<h2 className="text-3xl font-bold text-center mb-8">Code Examples</h2>
<h2 className="text-3xl font-bold text-center mb-8">{t('developers.codeExamples')}</h2>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
<div>
<h3 className="text-xl font-semibold mb-2">Connect to the Network</h3>
<h3 className="text-xl font-semibold mb-2">{t('developers.connectNetwork')}</h3>
<CodeSnippet language="javascript" code={connectCode} />
</div>
<div>
<h3 className="text-xl font-semibold mb-2">Make a Transfer</h3>
<h3 className="text-xl font-semibold mb-2">{t('developers.makeTransfer')}</h3>
<CodeSnippet language="javascript" code={transferCode} />
</div>
</div>
+11 -9
View File
@@ -1,4 +1,5 @@
import React, { useState, useEffect, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useParams, useNavigate, Link } from 'react-router-dom';
import Layout from '@/components/Layout';
import { marked } from 'marked';
@@ -118,6 +119,7 @@ const SidebarNav: React.FC<{ structure: object, onLinkClick: () => void, onSDKCl
const Docs: React.FC = () => {
const { t } = useTranslation();
const { '*': splat } = useParams();
const navigate = useNavigate();
const [docStructure, setDocStructure] = useState<object | null>(null);
@@ -241,7 +243,7 @@ const Docs: React.FC = () => {
}}
/>
) : (
<p className="text-gray-400">Loading navigation...</p>
<p className="text-gray-400">{t('docs.loadingNav')}</p>
)}
</div>
</aside>
@@ -258,8 +260,8 @@ const Docs: React.FC = () => {
{/* Main Content */}
<main className="w-full lg:w-3/4 lg:pl-8 flex flex-col">
<div className="prose prose-invert prose-headings:text-cyan-400 prose-a:text-blue-400 hover:prose-a:text-blue-300 prose-code:text-yellow-400 prose-pre:bg-gray-800 prose-pre:p-4 prose-pre:rounded-md max-w-none flex-1 min-h-0">
{isLoading && <p className="text-gray-400">Loading...</p>}
{error && <p className="text-red-400">Error: {error}</p>}
{isLoading && <p className="text-gray-400">{t('docs.loading')}</p>}
{error && <p className="text-red-400">{t('docs.error')} {error}</p>}
{/* SDK Embedded View - window in window style */}
{(showSDKLanding || isSDKRoute) && (
@@ -276,25 +278,25 @@ const Docs: React.FC = () => {
<div className="text-center py-12">
<div className="mb-8">
<div className="text-6xl mb-4">📖</div>
<h1 className="text-3xl font-bold text-white mb-2">PezkuwiChain Documentation</h1>
<p className="text-lg text-gray-400">Learn how to build on PezkuwiChain</p>
<h1 className="text-3xl font-bold text-white mb-2">{t('docs.title')}</h1>
<p className="text-lg text-gray-400">{t('docs.subtitle')}</p>
</div>
<p className="text-xl text-gray-400 mb-4">
Select a document from the sidebar to get started.
{t('docs.selectDoc')}
</p>
<div className="flex flex-wrap gap-4 justify-center mt-8">
<Link to="/docs/GENESIS_ENGINEERING_PLAN" className="px-4 py-2 bg-green-600 hover:bg-green-500 text-white rounded-lg transition-colors">
📋 Introduction
{t('docs.introduction')}
</Link>
<Link
to="/docs/sdk"
onClick={() => setShowSDKLanding(true)}
className="px-4 py-2 bg-gray-700 hover:bg-gray-600 text-white rounded-lg transition-colors"
>
📚 SDK Docs
{t('docs.sdkDocs')}
</Link>
<Link to="/docs/whitepaper/whitepaper" className="px-4 py-2 bg-gray-700 hover:bg-gray-600 text-white rounded-lg transition-colors">
📄 Whitepaper
{t('docs.whitepaper')}
</Link>
</div>
</div>
+8 -6
View File
@@ -1,4 +1,5 @@
import React, { useState, useEffect, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { GraduationCap } from 'lucide-react';
import { useAuth } from '@/contexts/AuthContext';
import { usePezkuwi } from '@/contexts/PezkuwiContext';
@@ -13,6 +14,7 @@ import { useNavigate } from 'react-router-dom';
import { Button } from '@/components/ui/button';
export default function EducationPlatform() {
const { t } = useTranslation();
const { user } = useAuth();
const { selectedAccount } = usePezkuwi();
const [enrollments, setEnrollments] = useState<Enrollment[]>([]);
@@ -58,22 +60,22 @@ export default function EducationPlatform() {
<div>
<h1 className="text-4xl font-bold text-white mb-2 flex items-center gap-3">
<GraduationCap className="w-10 h-10 text-green-500" />
Perwerde - Education Platform
{t('education.title')}
</h1>
<p className="text-gray-400">
Decentralized learning for Digital Kurdistan. Build skills, earn credentials, empower our nation.
{t('education.subtitle')}
</p>
</div>
<Button onClick={() => navigate('/')} className="bg-gray-700 hover:bg-gray-600 text-white">
Back to Home
{t('education.backToHome')}
</Button>
</div>
<Tabs value={activeTab} onValueChange={setActiveTab} className="space-y-4">
<TabsList>
<TabsTrigger value="courses">Available Courses</TabsTrigger>
{selectedAccount && <TabsTrigger value="dashboard">My Dashboard</TabsTrigger>}
{isAdmin && <TabsTrigger value="create">Create Course</TabsTrigger>}
<TabsTrigger value="courses">{t('education.availableCourses')}</TabsTrigger>
{selectedAccount && <TabsTrigger value="dashboard">{t('education.myDashboard')}</TabsTrigger>}
{isAdmin && <TabsTrigger value="create">{t('education.createCourse')}</TabsTrigger>}
</TabsList>
<TabsContent value="courses">
+22 -18
View File
@@ -10,6 +10,7 @@
*/
import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
@@ -50,6 +51,7 @@ import {
// import { web3FromAddress } from '@pezkuwi/extension-dapp';
export default function Elections() {
const { t } = useTranslation();
const { api, isApiReady } = usePezkuwi();
const [loading, setLoading] = useState(true);
@@ -96,16 +98,16 @@ export default function Elections() {
}, [api, isApiReady]);
if (loading) {
return <LoadingState message="Loading elections and governance data..." />;
return <LoadingState message={t('elections.loading')} />;
}
return (
<div className="container mx-auto px-4 py-8 max-w-7xl">
{/* Header */}
<div className="mb-8">
<h1 className="text-4xl font-bold text-white mb-2">Welati - Elections & Governance</h1>
<h1 className="text-4xl font-bold text-white mb-2">{t('elections.title')}</h1>
<p className="text-gray-400">
Democratic governance for Digital Kurdistan. Vote, propose, and participate in building our nation.
{t('elections.subtitle')}
</p>
</div>
@@ -114,15 +116,15 @@ export default function Elections() {
<TabsList className="grid w-full grid-cols-3 lg:w-auto bg-gray-900">
<TabsTrigger value="elections">
<Vote className="w-4 h-4 mr-2" />
Elections
{t('elections.electionsTab')}
</TabsTrigger>
<TabsTrigger value="proposals">
<FileText className="w-4 h-4 mr-2" />
Proposals
{t('elections.proposalsTab')}
</TabsTrigger>
<TabsTrigger value="government">
<Crown className="w-4 h-4 mr-2" />
Government
{t('elections.governmentTab')}
</TabsTrigger>
</TabsList>
@@ -132,7 +134,7 @@ export default function Elections() {
<Alert>
<AlertCircle className="h-4 w-4" />
<AlertDescription>
No active elections at this time. Check back later for upcoming elections.
{t('elections.noActiveElections')}
</AlertDescription>
</Alert>
) : (
@@ -150,7 +152,7 @@ export default function Elections() {
<Alert>
<AlertCircle className="h-4 w-4" />
<AlertDescription>
No active proposals at this time. Parliament members can submit new proposals.
{t('elections.noActiveProposals')}
</AlertDescription>
</Alert>
) : (
@@ -177,6 +179,7 @@ export default function Elections() {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function ElectionCard({ election, api }: { election: ElectionInfo; api: any }) {
const { t } = useTranslation();
const [candidates, setCandidates] = useState<CandidateInfo[]>([]);
const [timeLeft, setTimeLeft] = useState<string | null>(null);
@@ -223,21 +226,21 @@ function ElectionCard({ election, api }: { election: ElectionInfo; api: any }) {
<div className="bg-gray-800/50 rounded-lg p-4">
<div className="flex items-center gap-2 text-gray-400 mb-1">
<Users className="w-4 h-4" />
<span className="text-sm">Candidates</span>
<span className="text-sm">{t('elections.candidates')}</span>
</div>
<div className="text-2xl font-bold text-white">{election.totalCandidates}</div>
</div>
<div className="bg-gray-800/50 rounded-lg p-4">
<div className="flex items-center gap-2 text-gray-400 mb-1">
<Vote className="w-4 h-4" />
<span className="text-sm">Votes Cast</span>
<span className="text-sm">{t('elections.votesCast')}</span>
</div>
<div className="text-2xl font-bold text-white">{election.totalVotes.toLocaleString()}</div>
</div>
<div className="bg-gray-800/50 rounded-lg p-4">
<div className="flex items-center gap-2 text-gray-400 mb-1">
<Clock className="w-4 h-4" />
<span className="text-sm">Time Left</span>
<span className="text-sm">{t('elections.timeLeft')}</span>
</div>
<div className="text-lg font-bold text-white">
{timeLeft ? `${timeLeft.days}d ${timeLeft.hours}h` : '-'}
@@ -248,7 +251,7 @@ function ElectionCard({ election, api }: { election: ElectionInfo; api: any }) {
{/* Top Candidates */}
{candidates.length > 0 && (
<div>
<h4 className="text-sm font-medium text-gray-400 mb-3">Leading Candidates</h4>
<h4 className="text-sm font-medium text-gray-400 mb-3">{t('elections.leadingCandidates')}</h4>
<div className="space-y-2">
{candidates.slice(0, 5).map((candidate, idx) => (
<div
@@ -279,17 +282,17 @@ function ElectionCard({ election, api }: { election: ElectionInfo; api: any }) {
<div className="flex gap-3">
{election.status === 'CandidacyPeriod' && (
<Button className="flex-1 bg-green-600 hover:bg-green-700">
Register as Candidate
{t('elections.registerCandidate')}
</Button>
)}
{election.status === 'VotingPeriod' && (
<Button className="flex-1 bg-green-600 hover:bg-green-700">
<Vote className="w-4 h-4 mr-2" />
Cast Your Vote
{t('elections.castVote')}
</Button>
)}
<Button variant="outline" className="flex-1">
View Details
{t('elections.viewDetails')}
</Button>
</div>
</CardContent>
@@ -395,6 +398,7 @@ function ProposalCard({ proposal, api }: { proposal: CollectiveProposal; api: an
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function GovernmentOfficials({ officials, ministers }: { officials: any; ministers: any }) {
const { t } = useTranslation();
return (
<div className="space-y-6">
{/* Executive */}
@@ -402,7 +406,7 @@ function GovernmentOfficials({ officials, ministers }: { officials: any; ministe
<CardHeader>
<CardTitle className="flex items-center gap-2 text-white">
<Crown className="w-5 h-5 text-yellow-500" />
Executive Branch
{t('elections.executiveBranch')}
</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
@@ -423,7 +427,7 @@ function GovernmentOfficials({ officials, ministers }: { officials: any; ministe
<CardHeader>
<CardTitle className="flex items-center gap-2 text-white">
<Building className="w-5 h-5" />
Cabinet Ministers
{t('elections.cabinetMinisters')}
</CardTitle>
</CardHeader>
<CardContent className="grid gap-3">
@@ -440,7 +444,7 @@ function GovernmentOfficials({ officials, ministers }: { officials: any; ministe
)
)}
{Object.values(ministers).every((v) => !v) && (
<div className="text-gray-400 text-sm text-center py-4">No ministers appointed yet</div>
<div className="text-gray-400 text-sm text-center py-4">{t('elections.noMinistersYet')}</div>
)}
</CardContent>
</Card>
+23 -21
View File
@@ -4,6 +4,7 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/com
import { Button } from '@/components/ui/button';
import { supabase } from '@/lib/supabase';
import { CheckCircle, XCircle, Loader2, ArrowLeft, Mail, RefreshCw } from 'lucide-react';
import { useTranslation } from 'react-i18next';
export default function EmailVerification() {
const [searchParams] = useSearchParams();
@@ -14,6 +15,7 @@ export default function EmailVerification() {
const [error, setError] = useState('');
const [resending, setResending] = useState(false);
const [resent, setResent] = useState(false);
const { t } = useTranslation();
// Get email from navigation state (after sign up)
const email = location.state?.email;
@@ -46,7 +48,7 @@ export default function EmailVerification() {
setVerified(true);
} catch (err: unknown) {
const errorMessage = err instanceof Error ? err.message : 'Failed to verify email';
const errorMessage = err instanceof Error ? err.message : t('emailVerify.failedToVerify');
setError(errorMessage);
} finally {
setVerifying(false);
@@ -91,17 +93,17 @@ export default function EmailVerification() {
<div className="mx-auto w-16 h-16 bg-green-500/20 rounded-full flex items-center justify-center mb-4">
<Mail className="w-8 h-8 text-green-500" />
</div>
<CardTitle className="text-2xl text-white">Check Your Email</CardTitle>
<CardTitle className="text-2xl text-white">{t('emailVerify.checkEmail')}</CardTitle>
<CardDescription className="text-gray-400">
We sent a verification link to
{t('emailVerify.sentVerificationTo')}
</CardDescription>
</CardHeader>
<CardContent className="text-center space-y-6">
<p className="text-lg font-medium text-green-400">{email}</p>
<div className="bg-gray-800/50 rounded-lg p-4 text-left space-y-2">
<p className="text-sm text-gray-300">Please check your email and click the verification link to activate your account.</p>
<p className="text-xs text-gray-500">If you don&apos;t see the email, check your spam folder.</p>
<p className="text-sm text-gray-300">{t('emailVerify.checkInbox')}</p>
<p className="text-xs text-gray-500">{t('emailVerify.checkSpam')}</p>
</div>
{error && (
@@ -109,7 +111,7 @@ export default function EmailVerification() {
)}
{resent && (
<p className="text-sm text-green-400">Verification email sent!</p>
<p className="text-sm text-green-400">{t('emailVerify.emailSent')}</p>
)}
<div className="space-y-3">
@@ -122,12 +124,12 @@ export default function EmailVerification() {
{resending ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Sending...
{t('emailVerify.sending')}
</>
) : (
<>
<RefreshCw className="mr-2 h-4 w-4" />
Resend Verification Email
{t('emailVerify.resend')}
</>
)}
</Button>
@@ -137,7 +139,7 @@ export default function EmailVerification() {
className="w-full text-gray-400 hover:text-white"
onClick={() => navigate('/login')}
>
Back to Login
{t('emailVerify.backToLogin')}
</Button>
</div>
</CardContent>
@@ -157,31 +159,31 @@ export default function EmailVerification() {
<ArrowLeft className="w-5 h-5" />
</button>
<CardHeader>
<CardTitle className="text-white">Email Verification</CardTitle>
<CardTitle className="text-white">{t('emailVerify.title')}</CardTitle>
<CardDescription className="text-gray-400">
{verifying ? 'Verifying your email...' : 'Email verification status'}
{verifying ? t('emailVerify.verifyingEmail') : t('emailVerify.verificationStatus')}
</CardDescription>
</CardHeader>
<CardContent className="text-center space-y-4">
{verifying && (
<div className="flex flex-col items-center space-y-4">
<Loader2 className="h-12 w-12 animate-spin text-green-500" />
<p className="text-gray-300">Please wait while we verify your email...</p>
<p className="text-gray-300">{t('emailVerify.pleaseWait')}</p>
</div>
)}
{!verifying && verified && (
<div className="flex flex-col items-center space-y-4">
<CheckCircle className="h-12 w-12 text-green-500" />
<h3 className="text-lg font-semibold text-white">Email Verified Successfully!</h3>
<h3 className="text-lg font-semibold text-white">{t('emailVerify.success')}</h3>
<p className="text-gray-400">
Your email has been verified. You can now access all features.
{t('emailVerify.successDesc')}
</p>
<Button
className="bg-green-600 hover:bg-green-500"
onClick={() => navigate('/login')}
>
Go to Login
{t('emailVerify.goToLogin')}
</Button>
</div>
)}
@@ -189,14 +191,14 @@ export default function EmailVerification() {
{!verifying && !verified && error && (
<div className="flex flex-col items-center space-y-4">
<XCircle className="h-12 w-12 text-red-500" />
<h3 className="text-lg font-semibold text-white">Verification Failed</h3>
<h3 className="text-lg font-semibold text-white">{t('emailVerify.failed')}</h3>
<p className="text-gray-400">{error}</p>
<div className="flex gap-2">
<Button variant="outline" onClick={() => navigate('/')}>
Go to Home
{t('emailVerify.goToHome')}
</Button>
<Button onClick={() => navigate('/login')}>
Login
{t('emailVerify.login')}
</Button>
</div>
</div>
@@ -205,12 +207,12 @@ export default function EmailVerification() {
{!verifying && !verified && !error && !token && !type && (
<div className="flex flex-col items-center space-y-4">
<Mail className="h-12 w-12 text-gray-500" />
<h3 className="text-lg font-semibold text-white">No Verification Token</h3>
<h3 className="text-lg font-semibold text-white">{t('emailVerify.noToken')}</h3>
<p className="text-gray-400">
Please click the verification link in your email.
{t('emailVerify.noTokenDesc')}
</p>
<Button onClick={() => navigate('/login')}>
Back to Login
{t('emailVerify.backToLogin')}
</Button>
</div>
)}
+50 -44
View File
@@ -1,5 +1,6 @@
import React, { useState, useEffect, useCallback } from 'react';
import { useNavigate, useLocation } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import Layout from '@/components/Layout';
import { usePezkuwi } from '@/contexts/PezkuwiContext';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
@@ -67,7 +68,8 @@ type ExplorerView = 'overview' | 'accounts' | 'assets' | 'account' | 'block' | '
const Explorer: React.FC = () => {
const navigate = useNavigate();
const location = useLocation();
const { api, isApiReady } = usePezkuwi();
const { t } = useTranslation();
const { api, isApiReady, assetHubApi, isAssetHubReady } = usePezkuwi();
// Parse URL to determine view
const getViewFromPath = (): { view: ExplorerView; param?: string } => {
@@ -131,22 +133,26 @@ const Explorer: React.FC = () => {
if (!api || !isApiReady) return;
try {
const [header, finalizedHash, validators, currentEra] = await Promise.all([
const [header, finalizedHash, validators] = await Promise.all([
api.rpc.chain.getHeader(),
api.rpc.chain.getFinalizedHead(),
api.query.session?.validators?.() || Promise.resolve([]),
api.query.staking?.currentEra?.() || Promise.resolve(null),
]);
const finalizedHeader = await api.rpc.chain.getHeader(finalizedHash);
// Safely extract era number
// Era lives on Asset Hub (staking moved from RC to AH)
let eraNumber = 0;
if (currentEra && currentEra.isSome) {
if (assetHubApi && isAssetHubReady) {
try {
eraNumber = currentEra.unwrap().toNumber();
const activeEra = await assetHubApi.query.staking.activeEra();
if (activeEra && activeEra.isSome) {
const unwrapped = activeEra.unwrap();
const json = unwrapped.toJSON() as { index?: number };
eraNumber = json?.index ?? 0;
}
} catch {
eraNumber = Number(currentEra.unwrap().toString()) || 0;
// AH staking query failed — leave era as 0
}
}
@@ -160,7 +166,7 @@ const Explorer: React.FC = () => {
} catch (error) {
console.error('Error fetching stats:', error);
}
}, [api, isApiReady]);
}, [api, isApiReady, assetHubApi, isAssetHubReady]);
// Fetch recent blocks
const fetchRecentBlocks = useCallback(async () => {
@@ -381,8 +387,8 @@ const Explorer: React.FC = () => {
<div className="min-h-screen bg-gray-950 flex items-center justify-center">
<div className="text-center">
<Loader2 className="w-12 h-12 text-green-500 animate-spin mx-auto mb-4" />
<h2 className="text-xl font-semibold text-white mb-2">Connecting to Blockchain</h2>
<p className="text-gray-400">Please wait while we establish connection...</p>
<h2 className="text-xl font-semibold text-white mb-2">{t('networkStats.connecting')}</h2>
<p className="text-gray-400">{t('networkStats.disconnectedDesc')}</p>
</div>
</div>
</Layout>
@@ -398,12 +404,12 @@ const Explorer: React.FC = () => {
<div>
<h1 className="text-3xl font-bold text-white flex items-center gap-3">
<Blocks className="w-8 h-8 text-green-500" />
Block Explorer
{t('explorer.title')}
</h1>
<div className="flex items-center gap-2 mt-2">
<span className="flex items-center gap-1 text-green-400 text-sm">
<span className="w-2 h-2 bg-green-400 rounded-full animate-pulse"></span>
Live
{t('explorer.live')}
</span>
<span className="text-gray-500 text-sm">
Last updated {formatDistanceToNow(lastUpdate, { addSuffix: true })}
@@ -417,7 +423,7 @@ const Explorer: React.FC = () => {
className="border-gray-700 text-gray-300 hover:text-white"
>
<RefreshCw className="w-4 h-4 mr-2" />
Refresh
{t('explorer.refresh')}
</Button>
</div>
@@ -430,7 +436,7 @@ const Explorer: React.FC = () => {
size="sm"
>
<Blocks className="w-4 h-4 mr-2" />
Overview
{t('explorer.overviewTab')}
</Button>
<Button
variant={currentView === 'accounts' ? 'default' : 'outline'}
@@ -439,7 +445,7 @@ const Explorer: React.FC = () => {
size="sm"
>
<Users className="w-4 h-4 mr-2" />
Accounts
{t('explorer.accountsTab')}
</Button>
<Button
variant={currentView === 'assets' ? 'default' : 'outline'}
@@ -448,7 +454,7 @@ const Explorer: React.FC = () => {
size="sm"
>
<Wallet className="w-4 h-4 mr-2" />
Assets
{t('explorer.assetsTab')}
</Button>
</div>
@@ -460,7 +466,7 @@ const Explorer: React.FC = () => {
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400 w-5 h-5" />
<Input
type="text"
placeholder="Search by Block Number / Hash / Address"
placeholder={t('explorer.searchPlaceholder')}
value={searchQuery}
onChange={(e) => {
setSearchQuery(e.target.value);
@@ -480,7 +486,7 @@ const Explorer: React.FC = () => {
) : (
<>
<Search className="w-4 h-4 mr-2" />
Search
{t('explorer.search')}
</>
)}
</Button>
@@ -500,16 +506,16 @@ const Explorer: React.FC = () => {
<CardHeader>
<CardTitle className="text-white flex items-center gap-2">
<Users className="w-5 h-5 text-blue-500" />
Accounts
{t('explorer.accountsTab')}
</CardTitle>
</CardHeader>
<CardContent>
<p className="text-gray-400 mb-4">
Search for an account address to view details, balances, and transaction history.
{t('explorer.noAccountsFound')}
</p>
<div className="bg-gray-800 rounded-lg p-6 text-center">
<Users className="w-12 h-12 text-gray-600 mx-auto mb-4" />
<p className="text-gray-500">Use the search bar above to find an account by address</p>
<p className="text-gray-500">{t('explorer.search')}</p>
</div>
</CardContent>
</Card>
@@ -520,7 +526,7 @@ const Explorer: React.FC = () => {
<CardHeader>
<CardTitle className="text-white flex items-center gap-2">
<Wallet className="w-5 h-5 text-yellow-500" />
Assets
{t('explorer.assetsTab')}
</CardTitle>
</CardHeader>
<CardContent>
@@ -528,7 +534,7 @@ const Explorer: React.FC = () => {
<div className="bg-gray-800 rounded-lg p-4">
<div className="flex items-center justify-between mb-2">
<span className="text-white font-semibold">HEZ</span>
<Badge className="bg-green-500/20 text-green-400">Native</Badge>
<Badge className="bg-green-500/20 text-green-400">{t('explorer.native')}</Badge>
</div>
<p className="text-gray-400 text-sm">Native token of PezkuwiChain</p>
</div>
@@ -563,12 +569,12 @@ const Explorer: React.FC = () => {
<CardHeader>
<CardTitle className="text-white flex items-center gap-2">
<Wallet className="w-5 h-5 text-purple-500" />
Account Details
{t('explorer.accountsTab')}
</CardTitle>
</CardHeader>
<CardContent>
<div className="bg-gray-800 rounded-lg p-4 mb-4">
<div className="text-xs text-gray-400 mb-1">Address</div>
<div className="text-xs text-gray-400 mb-1">{t('explorer.address')}</div>
<div className="flex items-center gap-2">
<code className="text-white font-mono text-sm break-all">{viewParam}</code>
<button
@@ -580,7 +586,7 @@ const Explorer: React.FC = () => {
</div>
</div>
<p className="text-gray-400 text-center py-8">
Account balance and transaction history loading...
{t('explorer.balance')}...
</p>
</CardContent>
</Card>
@@ -606,7 +612,7 @@ const Explorer: React.FC = () => {
<CardContent className="p-4">
<div className="flex items-center gap-2 text-gray-400 text-sm mb-1">
<Database className="w-4 h-4" />
Best Block
{t('explorer.bestBlock')}
</div>
<p className="text-2xl font-bold text-white">
#{stats.bestBlock.toLocaleString()}
@@ -618,7 +624,7 @@ const Explorer: React.FC = () => {
<CardContent className="p-4">
<div className="flex items-center gap-2 text-gray-400 text-sm mb-1">
<CheckCircle className="w-4 h-4 text-green-500" />
Finalized
{t('explorer.finalized')}
</div>
<p className="text-2xl font-bold text-white">
#{stats.finalizedBlock.toLocaleString()}
@@ -630,7 +636,7 @@ const Explorer: React.FC = () => {
<CardContent className="p-4">
<div className="flex items-center gap-2 text-gray-400 text-sm mb-1">
<Users className="w-4 h-4 text-blue-500" />
Validators
{t('explorer.validators')}
</div>
<p className="text-2xl font-bold text-white">
{stats.activeValidators}
@@ -642,7 +648,7 @@ const Explorer: React.FC = () => {
<CardContent className="p-4">
<div className="flex items-center gap-2 text-gray-400 text-sm mb-1">
<Activity className="w-4 h-4 text-purple-500" />
Era
{t('explorer.era')}
</div>
<p className="text-2xl font-bold text-white">
{stats.era}
@@ -654,7 +660,7 @@ const Explorer: React.FC = () => {
<CardContent className="p-4">
<div className="flex items-center gap-2 text-gray-400 text-sm mb-1">
<Timer className="w-4 h-4 text-yellow-500" />
Block Time
{t('explorer.blockTime')}
</div>
<p className="text-2xl font-bold text-white">
~{stats.avgBlockTime}s
@@ -666,7 +672,7 @@ const Explorer: React.FC = () => {
<CardContent className="p-4">
<div className="flex items-center gap-2 text-gray-400 text-sm mb-1">
<Zap className="w-4 h-4 text-orange-500" />
TPS
{t('explorer.tps')}
</div>
<p className="text-2xl font-bold text-white">
{stats.tps.toFixed(2)}
@@ -678,7 +684,7 @@ const Explorer: React.FC = () => {
<CardContent className="p-4">
<div className="flex items-center gap-2 text-gray-400 text-sm mb-1">
<ArrowRightLeft className="w-4 h-4 text-cyan-500" />
Recent Extrinsics
{t('explorer.recentExtrinsics')}
</div>
<p className="text-2xl font-bold text-white">
{recentExtrinsics.length} in last {recentBlocks.length} blocks
@@ -695,7 +701,7 @@ const Explorer: React.FC = () => {
<CardHeader className="pb-3">
<CardTitle className="text-lg text-white flex items-center gap-2">
<Blocks className="w-5 h-5 text-green-500" />
Recent Blocks
{t('explorer.recentBlocks')}
</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
@@ -708,7 +714,7 @@ const Explorer: React.FC = () => {
))
) : recentBlocks.length === 0 ? (
<div className="text-center py-8 text-gray-400">
No blocks found
{t('explorer.noBlocksFound')}
</div>
) : (
recentBlocks.slice(0, 8).map((block) => (
@@ -745,7 +751,7 @@ const Explorer: React.FC = () => {
</button>
</div>
<span className="text-sm">
<span className="text-gray-500">Extrinsics:</span>{' '}
<span className="text-gray-500">{t('explorer.recentExtrinsics')}:</span>{' '}
<span className="text-green-400 font-semibold">{block.extrinsicsCount}</span>
</span>
</div>
@@ -760,7 +766,7 @@ const Explorer: React.FC = () => {
<CardHeader className="pb-3">
<CardTitle className="text-lg text-white flex items-center gap-2">
<ArrowRightLeft className="w-5 h-5 text-purple-500" />
Recent Extrinsics
{t('explorer.recentExtrinsics')}
</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
@@ -773,7 +779,7 @@ const Explorer: React.FC = () => {
))
) : recentExtrinsics.length === 0 ? (
<div className="text-center py-8 text-gray-400">
No extrinsics found
{t('explorer.noExtrinsicsFound')}
</div>
) : (
recentExtrinsics.slice(0, 8).map((ext, idx) => (
@@ -801,7 +807,7 @@ const Explorer: React.FC = () => {
</Badge>
</div>
<span className="text-gray-400 text-sm">
Block #{ext.blockNumber.toLocaleString()}
{t('explorer.block')} #{ext.blockNumber.toLocaleString()}
</span>
</div>
<div className="flex items-center justify-between">
@@ -841,7 +847,7 @@ const Explorer: React.FC = () => {
<CardContent className="p-6">
<h3 className="text-lg font-semibold text-white mb-4 flex items-center gap-2">
<ExternalLink className="w-5 h-5 text-green-500" />
Quick Links
{t('explorer.quickLinks')}
</h3>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<a
@@ -851,28 +857,28 @@ const Explorer: React.FC = () => {
className="flex items-center gap-2 p-3 rounded-lg bg-gray-800 hover:bg-gray-750 text-gray-300 hover:text-white transition-colors"
>
<Activity className="w-4 h-4 text-red-400" />
Telemetry
{t('explorer.telemetry')}
</a>
<a
href="/governance"
className="flex items-center gap-2 p-3 rounded-lg bg-gray-800 hover:bg-gray-750 text-gray-300 hover:text-white transition-colors"
>
<Users className="w-4 h-4 text-blue-400" />
Governance
{t('explorer.governance')}
</a>
<a
href="/wallet"
className="flex items-center gap-2 p-3 rounded-lg bg-gray-800 hover:bg-gray-750 text-gray-300 hover:text-white transition-colors"
>
<Wallet className="w-4 h-4 text-yellow-400" />
Wallet
{t('explorer.wallet')}
</a>
<a
href="/docs"
className="flex items-center gap-2 p-3 rounded-lg bg-gray-800 hover:bg-gray-750 text-gray-300 hover:text-white transition-colors"
>
<Blocks className="w-4 h-4 text-purple-400" />
Documentation
{t('explorer.documentation')}
</a>
</div>
</CardContent>
+9 -7
View File
@@ -1,8 +1,10 @@
import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import Layout from '@/components/Layout';
import { Clock, Send } from 'lucide-react';
const Faucet: React.FC = () => {
const { t } = useTranslation();
const [address, setAddress] = useState('');
const [cooldown, setCooldown] = useState(0);
const [token, setToken] = useState('HEZ');
@@ -39,8 +41,8 @@ const Faucet: React.FC = () => {
<Layout>
<div className="container mx-auto px-4 py-8 text-white flex flex-col items-center">
<div className="w-full max-w-2xl text-center">
<h1 className="text-4xl font-bold mb-2 text-yellow-400">Testnet Faucet</h1>
<p className="text-gray-400 mb-8">Get testnet tokens to build and test your dApps on PezkuwiChain.</p>
<h1 className="text-4xl font-bold mb-2 text-yellow-400">{t('faucet.title')}</h1>
<p className="text-gray-400 mb-8">{t('faucet.subtitle')}</p>
<div className="bg-gray-800 p-8 rounded-lg shadow-lg">
<div className="flex mb-4">
@@ -55,7 +57,7 @@ const Faucet: React.FC = () => {
</select>
<input
type="text"
placeholder="Enter your wallet address"
placeholder={t('faucet.enterAddress')}
value={address}
onChange={(e) => setAddress(e.target.value)}
className="w-full p-3 bg-gray-700 text-white focus:outline-none rounded-r-lg"
@@ -70,24 +72,24 @@ const Faucet: React.FC = () => {
{cooldown > 0 ? (
<>
<Clock className="mr-2" size={20} />
<span>Wait for {formatTime(cooldown)}</span>
<span>{t('faucet.waitFor')} {formatTime(cooldown)}</span>
</>
) : (
<>
<Send className="mr-2" size={20} />
<span>Request Tokens</span>
<span>{t('faucet.requestTokens')}</span>
</>
)}
</button>
</div>
<div className="mt-12 w-full">
<h2 className="text-2xl font-bold mb-4 text-left">Recent Distributions</h2>
<h2 className="text-2xl font-bold mb-4 text-left">{t('faucet.recentDistributions')}</h2>
<div className="space-y-3">
{recentDistributions.map((dist, index) => (
<div key={index} className="bg-gray-800 p-3 rounded-lg flex justify-between items-center text-sm">
<div className="flex flex-col text-left">
<span><span className="font-bold text-yellow-400">{dist.token}</span> sent to <span className="font-mono text-gray-300">{dist.address.substring(0, 12)}...</span></span>
<span><span className="font-bold text-yellow-400">{dist.token}</span> {t('faucet.sentTo')} <span className="font-mono text-gray-300">{dist.address.substring(0, 12)}...</span></span>
<span className="font-mono text-xs text-gray-500">{dist.txHash}</span>
</div>
<span className="text-gray-400">{dist.time}</span>
+53 -51
View File
@@ -42,6 +42,7 @@ import {
LogIn,
} from 'lucide-react';
import { formatDistanceToNow } from 'date-fns';
import { useTranslation } from 'react-i18next';
interface Category {
id: string;
@@ -89,6 +90,7 @@ interface ForumStats {
const Forum: React.FC = () => {
const navigate = useNavigate();
const { user } = useAuth();
const { t } = useTranslation();
const [categories, setCategories] = useState<Category[]>([]);
const [discussions, setDiscussions] = useState<Discussion[]>([]);
@@ -284,10 +286,10 @@ const Forum: React.FC = () => {
<div>
<h1 className="text-3xl font-bold text-white flex items-center gap-3">
<MessageSquare className="w-8 h-8 text-green-500" />
Community Forum
{t('forum.communityForum')}
</h1>
<p className="text-gray-400 mt-1">
Discuss proposals, share ideas, and connect with the community
{t('forum.communityForumDesc')}
</p>
</div>
<Button
@@ -295,7 +297,7 @@ const Forum: React.FC = () => {
className="bg-gradient-to-r from-green-600 to-yellow-500 hover:from-green-700 hover:to-yellow-600"
>
<Plus className="w-4 h-4 mr-2" />
New Topic
{t('forum.newTopic')}
</Button>
</div>
@@ -306,8 +308,8 @@ const Forum: React.FC = () => {
<div className="flex items-center gap-3">
<Eye className="w-5 h-5 text-green-400" />
<div>
<p className="text-white font-medium">You are browsing as a guest</p>
<p className="text-gray-400 text-sm">Login to create topics, reply, and interact with the community</p>
<p className="text-white font-medium">{t('forum.guestBrowsing')}</p>
<p className="text-gray-400 text-sm">{t('forum.guestBrowsingDesc')}</p>
</div>
</div>
<Button
@@ -316,7 +318,7 @@ const Forum: React.FC = () => {
className="border-green-500 text-green-400 hover:bg-green-500/20"
>
<LogIn className="w-4 h-4 mr-2" />
Login
{t('forum.login')}
</Button>
</div>
</div>
@@ -348,24 +350,24 @@ const Forum: React.FC = () => {
<CardHeader className="pb-3">
<CardTitle className="text-lg text-white flex items-center gap-2">
<TrendingUp className="w-5 h-5 text-green-500" />
Forum Stats
{t('forum.forumStats')}
</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
<div className="flex justify-between text-sm">
<span className="text-gray-400">Topics</span>
<span className="text-gray-400">{t('forum.topics')}</span>
<span className="text-white font-semibold">{stats.totalDiscussions}</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-gray-400">Replies</span>
<span className="text-gray-400">{t('forum.replies')}</span>
<span className="text-white font-semibold">{stats.totalReplies}</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-gray-400">Members</span>
<span className="text-gray-400">{t('forum.members')}</span>
<span className="text-white font-semibold">{stats.totalUsers}</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-gray-400">Online Now</span>
<span className="text-gray-400">{t('forum.onlineNow')}</span>
<span className="text-green-400 font-semibold flex items-center gap-1">
<span className="w-2 h-2 bg-green-400 rounded-full animate-pulse"></span>
{stats.onlineNow}
@@ -379,7 +381,7 @@ const Forum: React.FC = () => {
<CardHeader className="pb-3">
<CardTitle className="text-lg text-white flex items-center gap-2">
<Filter className="w-5 h-5 text-yellow-500" />
Categories
{t('forum.categories')}
</CardTitle>
</CardHeader>
<CardContent className="space-y-1">
@@ -393,7 +395,7 @@ const Forum: React.FC = () => {
>
<span className="flex items-center gap-2">
<span>📋</span>
<span>All Topics</span>
<span>{t('forum.allTopics')}</span>
</span>
<ChevronRight className="w-4 h-4" />
</button>
@@ -423,18 +425,18 @@ const Forum: React.FC = () => {
<CardHeader className="pb-3">
<CardTitle className="text-lg text-white flex items-center gap-2">
<Users className="w-5 h-5 text-blue-500" />
Quick Links
{t('forum.quickLinks')}
</CardTitle>
</CardHeader>
<CardContent className="space-y-2">
<a href="/governance" className="block text-gray-400 hover:text-green-400 text-sm transition-colors">
Governance Dashboard
{t('forum.govDashboard')}
</a>
<a href="/docs" className="block text-gray-400 hover:text-green-400 text-sm transition-colors">
Documentation
{t('forum.documentation')}
</a>
<a href="https://discord.gg/pezkuwi" target="_blank" rel="noopener noreferrer" className="block text-gray-400 hover:text-green-400 text-sm transition-colors">
Join Discord
{t('forum.joinDiscord')}
</a>
</CardContent>
</Card>
@@ -448,7 +450,7 @@ const Forum: React.FC = () => {
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400 w-5 h-5" />
<Input
type="text"
placeholder="Search discussions..."
placeholder={t('forum.searchPlaceholder')}
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-10 bg-gray-900 border-gray-700 text-white placeholder:text-gray-500"
@@ -456,22 +458,22 @@ const Forum: React.FC = () => {
</div>
<Select value={sortBy} onValueChange={(v) => setSortBy(v as 'recent' | 'popular' | 'replies')}>
<SelectTrigger className="w-[180px] bg-gray-900 border-gray-700 text-white">
<SelectValue placeholder="Sort by" />
<SelectValue placeholder={t('forum.sortBy')} />
</SelectTrigger>
<SelectContent className="bg-gray-900 border-gray-700">
<SelectItem value="recent" className="text-white hover:bg-gray-800">
<span className="flex items-center gap-2">
<Clock className="w-4 h-4" /> Recent Activity
<Clock className="w-4 h-4" /> {t('forum.recentActivity')}
</span>
</SelectItem>
<SelectItem value="popular" className="text-white hover:bg-gray-800">
<span className="flex items-center gap-2">
<Eye className="w-4 h-4" /> Most Viewed
<Eye className="w-4 h-4" /> {t('forum.sortViewed')}
</span>
</SelectItem>
<SelectItem value="replies" className="text-white hover:bg-gray-800">
<span className="flex items-center gap-2">
<MessageSquare className="w-4 h-4" /> Most Replies
<MessageSquare className="w-4 h-4" /> {t('forum.sortReplies')}
</span>
</SelectItem>
</SelectContent>
@@ -495,14 +497,14 @@ const Forum: React.FC = () => {
<Card className="bg-gray-900 border-gray-800">
<CardContent className="py-16 text-center">
<MessageSquare className="w-16 h-16 text-gray-600 mx-auto mb-4" />
<h3 className="text-xl font-semibold text-white mb-2">No discussions yet</h3>
<p className="text-gray-400 mb-6">Be the first to start a conversation!</p>
<h3 className="text-xl font-semibold text-white mb-2">{t('forum.noDiscussionsYet')}</h3>
<p className="text-gray-400 mb-6">{t('forum.beFirstToStart')}</p>
<Button
onClick={() => requireAuth(() => setIsCreateModalOpen(true))}
className="bg-gradient-to-r from-green-600 to-yellow-500"
>
<Plus className="w-4 h-4 mr-2" />
Create First Topic
{t('forum.createFirstTopic')}
</Button>
</CardContent>
</Card>
@@ -529,13 +531,13 @@ const Forum: React.FC = () => {
{discussion.is_pinned && (
<Badge variant="outline" className="border-yellow-500 text-yellow-500 text-xs">
<Pin className="w-3 h-3 mr-1" />
Pinned
{t('forum.pinned')}
</Badge>
)}
{discussion.is_locked && (
<Badge variant="outline" className="border-gray-500 text-gray-500 text-xs">
<Lock className="w-3 h-3 mr-1" />
Locked
{t('forum.locked')}
</Badge>
)}
{discussion.category && (
@@ -559,7 +561,7 @@ const Forum: React.FC = () => {
<div className="flex items-center gap-4 mt-3 text-xs text-gray-500">
<span className="flex items-center gap-1">
by <span className="text-gray-300">{discussion.author_name}</span>
{t('forum.by')} <span className="text-gray-300">{discussion.author_name}</span>
</span>
<span className="flex items-center gap-1">
<Clock className="w-3 h-3" />
@@ -599,7 +601,7 @@ const Forum: React.FC = () => {
</span>
))}
{discussion.tags.length > 3 && (
<span className="text-xs text-gray-500">+{discussion.tags.length - 3} more</span>
<span className="text-xs text-gray-500">{t('forum.moreTags', { count: discussion.tags.length - 3 })}</span>
)}
</div>
)}
@@ -619,10 +621,10 @@ const Forum: React.FC = () => {
<DialogHeader>
<DialogTitle className="text-white text-xl flex items-center gap-2">
<LogIn className="w-6 h-6 text-green-500" />
Login Required
{t('forum.loginRequired')}
</DialogTitle>
<DialogDescription className="text-gray-400">
You need to be logged in to perform this action.
{t('forum.loginRequiredDesc')}
</DialogDescription>
</DialogHeader>
@@ -630,12 +632,12 @@ const Forum: React.FC = () => {
<div className="w-16 h-16 rounded-full bg-gradient-to-br from-green-500/20 to-yellow-500/20 flex items-center justify-center mx-auto mb-4">
<Users className="w-8 h-8 text-green-400" />
</div>
<p className="text-gray-300 mb-2">Join our community to:</p>
<p className="text-gray-300 mb-2">{t('forum.joinCommunity')}</p>
<ul className="text-gray-400 text-sm space-y-1">
<li> Create new discussion topics</li>
<li> Reply to existing discussions</li>
<li> Upvote helpful content</li>
<li> Participate in governance</li>
<li> {t('forum.joinCreateTopics')}</li>
<li> {t('forum.joinReply')}</li>
<li> {t('forum.joinUpvote')}</li>
<li> {t('forum.joinGov')}</li>
</ul>
</div>
@@ -645,7 +647,7 @@ const Forum: React.FC = () => {
onClick={() => setShowLoginPrompt(false)}
className="flex-1 border-gray-700 text-gray-300"
>
Continue Browsing
{t('forum.continueBrowsing')}
</Button>
<Button
onClick={() => {
@@ -655,7 +657,7 @@ const Forum: React.FC = () => {
className="flex-1 bg-gradient-to-r from-green-600 to-yellow-500 hover:from-green-700 hover:to-yellow-600"
>
<LogIn className="w-4 h-4 mr-2" />
Login
{t('forum.login')}
</Button>
</DialogFooter>
</DialogContent>
@@ -665,21 +667,21 @@ const Forum: React.FC = () => {
<Dialog open={isCreateModalOpen} onOpenChange={setIsCreateModalOpen}>
<DialogContent className="bg-gray-900 border-gray-800 max-w-2xl">
<DialogHeader>
<DialogTitle className="text-white text-xl">Create New Topic</DialogTitle>
<DialogTitle className="text-white text-xl">{t('forum.createNewTopic')}</DialogTitle>
<DialogDescription className="text-gray-400">
Start a new discussion with the community
{t('forum.startNewDiscussion')}
</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-4">
<div>
<label className="text-sm font-medium text-gray-300 mb-2 block">Category</label>
<label className="text-sm font-medium text-gray-300 mb-2 block">{t('forum.category')}</label>
<Select
value={newTopic.category_id}
onValueChange={(v) => setNewTopic({ ...newTopic, category_id: v })}
>
<SelectTrigger className="bg-gray-800 border-gray-700 text-white">
<SelectValue placeholder="Select a category" />
<SelectValue placeholder={t('forum.selectCategory')} />
</SelectTrigger>
<SelectContent className="bg-gray-800 border-gray-700">
{categories.map((category) => (
@@ -695,32 +697,32 @@ const Forum: React.FC = () => {
</div>
<div>
<label className="text-sm font-medium text-gray-300 mb-2 block">Title</label>
<label className="text-sm font-medium text-gray-300 mb-2 block">{t('forum.title')}</label>
<Input
value={newTopic.title}
onChange={(e) => setNewTopic({ ...newTopic, title: e.target.value })}
placeholder="Enter a descriptive title"
placeholder={t('forum.enterTitle')}
className="bg-gray-800 border-gray-700 text-white placeholder:text-gray-500"
/>
</div>
<div>
<label className="text-sm font-medium text-gray-300 mb-2 block">Content</label>
<label className="text-sm font-medium text-gray-300 mb-2 block">{t('forum.content')}</label>
<Textarea
value={newTopic.content}
onChange={(e) => setNewTopic({ ...newTopic, content: e.target.value })}
placeholder="Write your discussion content here..."
placeholder={t('forum.writeContent')}
rows={8}
className="bg-gray-800 border-gray-700 text-white placeholder:text-gray-500"
/>
</div>
<div>
<label className="text-sm font-medium text-gray-300 mb-2 block">Tags (optional)</label>
<label className="text-sm font-medium text-gray-300 mb-2 block">{t('forum.tagsOptional')}</label>
<Input
value={newTopic.tags}
onChange={(e) => setNewTopic({ ...newTopic, tags: e.target.value })}
placeholder="governance, proposal, treasury (comma separated)"
placeholder={t('forum.tagsPlaceholder')}
className="bg-gray-800 border-gray-700 text-white placeholder:text-gray-500"
/>
</div>
@@ -732,7 +734,7 @@ const Forum: React.FC = () => {
onClick={() => setIsCreateModalOpen(false)}
className="border-gray-700 text-gray-300"
>
Cancel
{t('common.cancel')}
</Button>
<Button
onClick={handleCreateTopic}
@@ -742,12 +744,12 @@ const Forum: React.FC = () => {
{isCreating ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Creating...
{t('forum.creating')}
</>
) : (
<>
<Plus className="w-4 h-4 mr-2" />
Create Topic
{t('forum.createTopic')}
</>
)}
</Button>
+38 -36
View File
@@ -37,6 +37,7 @@ import {
AlertTriangle,
} from 'lucide-react';
import { formatDistanceToNow, format } from 'date-fns';
import { useTranslation } from 'react-i18next';
import {
DropdownMenu,
DropdownMenuContent,
@@ -89,6 +90,7 @@ const ForumTopic: React.FC = () => {
const { id } = useParams<{ id: string }>();
const navigate = useNavigate();
const { user } = useAuth();
const { t } = useTranslation();
const [discussion, setDiscussion] = useState<Discussion | null>(null);
const [replies, setReplies] = useState<Reply[]>([]);
@@ -408,7 +410,7 @@ const ForumTopic: React.FC = () => {
className="flex items-center gap-1 text-gray-500 hover:text-green-400 text-sm transition-colors"
>
<Reply className="w-4 h-4" />
Reply
{t('discussion.reply')}
</button>
<DropdownMenu>
@@ -426,7 +428,7 @@ const ForumTopic: React.FC = () => {
className="text-gray-300 hover:bg-gray-800"
>
<Flag className="w-4 h-4 mr-2" />
Report
{t('forumTopic.report')}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
@@ -438,7 +440,7 @@ const ForumTopic: React.FC = () => {
<Textarea
value={nestedReplyContent}
onChange={(e) => setNestedReplyContent(e.target.value)}
placeholder={`Reply to ${reply.author_name}...`}
placeholder={t('forumTopic.replyTo', { name: reply.author_name })}
rows={2}
className="flex-1 bg-gray-800 border-gray-700 text-white placeholder:text-gray-500"
/>
@@ -482,11 +484,11 @@ const ForumTopic: React.FC = () => {
<div className="min-h-screen bg-gray-950 flex items-center justify-center">
<div className="text-center">
<MessageSquare className="w-16 h-16 text-gray-600 mx-auto mb-4" />
<h2 className="text-xl font-semibold text-white mb-2">Discussion Not Found</h2>
<p className="text-gray-400 mb-6">This discussion may have been removed or does not exist.</p>
<h2 className="text-xl font-semibold text-white mb-2">{t('forumTopic.notFound')}</h2>
<p className="text-gray-400 mb-6">{t('forumTopic.notFoundDesc')}</p>
<Button onClick={() => navigate('/forum')} variant="outline" className="border-gray-700">
<ArrowLeft className="w-4 h-4 mr-2" />
Back to Forum
{t('forum.backToForum')}
</Button>
</div>
</div>
@@ -505,7 +507,7 @@ const ForumTopic: React.FC = () => {
className="mb-6 text-gray-400 hover:text-white"
>
<ArrowLeft className="w-4 h-4 mr-2" />
Back to Forum
{t('forum.backToForum')}
</Button>
{/* Discussion Header */}
@@ -523,13 +525,13 @@ const ForumTopic: React.FC = () => {
{discussion.is_pinned && (
<Badge variant="outline" className="border-yellow-500 text-yellow-500">
<Pin className="w-3 h-3 mr-1" />
Pinned
{t('forum.pinned')}
</Badge>
)}
{discussion.is_locked && (
<Badge variant="outline" className="border-gray-500 text-gray-500">
<Lock className="w-3 h-3 mr-1" />
Locked
{t('forum.locked')}
</Badge>
)}
{discussion.category && (
@@ -551,18 +553,18 @@ const ForumTopic: React.FC = () => {
{/* Meta info */}
<div className="flex items-center gap-4 text-sm text-gray-500 mb-4">
<span>by <span className="text-gray-300">{discussion.author_name}</span></span>
<span>{t('forum.by')} <span className="text-gray-300">{discussion.author_name}</span></span>
<span className="flex items-center gap-1">
<Clock className="w-4 h-4" />
{format(new Date(discussion.created_at), 'MMM d, yyyy')}
</span>
<span className="flex items-center gap-1">
<Eye className="w-4 h-4" />
{discussion.views_count} views
{discussion.views_count} {t('forum.views')}
</span>
<span className="flex items-center gap-1">
<MessageSquare className="w-4 h-4" />
{discussion.replies_count} replies
{discussion.replies_count} {t('forum.replies')}
</span>
</div>
@@ -608,7 +610,7 @@ const ForumTopic: React.FC = () => {
}`}
>
<Bookmark className="w-4 h-4" />
{isBookmarked ? 'Saved' : 'Save'}
{isBookmarked ? t('forumTopic.saved') : t('forumTopic.save')}
</button>
<button
@@ -616,7 +618,7 @@ const ForumTopic: React.FC = () => {
className="flex items-center gap-1 px-3 py-1.5 rounded-lg text-gray-400 hover:bg-gray-800 transition-colors"
>
<Share2 className="w-4 h-4" />
Share
{t('forumTopic.share')}
</button>
<button
@@ -627,7 +629,7 @@ const ForumTopic: React.FC = () => {
className="flex items-center gap-1 px-3 py-1.5 rounded-lg text-gray-400 hover:bg-gray-800 transition-colors"
>
<Flag className="w-4 h-4" />
Report
{t('forumTopic.report')}
</button>
</div>
</div>
@@ -639,13 +641,13 @@ const ForumTopic: React.FC = () => {
{!discussion.is_locked ? (
<Card className="bg-gray-900 border-gray-800 mb-6">
<CardContent className="p-6">
<h3 className="text-lg font-semibold text-white mb-4">Leave a Reply</h3>
<h3 className="text-lg font-semibold text-white mb-4">{t('forumTopic.leaveReply')}</h3>
{user ? (
<div>
<Textarea
value={replyContent}
onChange={(e) => setReplyContent(e.target.value)}
placeholder="Write your reply..."
placeholder={t('forumTopic.writeReply')}
rows={4}
className="bg-gray-800 border-gray-700 text-white placeholder:text-gray-500 mb-4"
/>
@@ -657,25 +659,25 @@ const ForumTopic: React.FC = () => {
{isSubmitting ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Posting...
{t('forumTopic.posting')}
</>
) : (
<>
<Send className="w-4 h-4 mr-2" />
Post Reply
{t('forumTopic.postReply')}
</>
)}
</Button>
</div>
) : (
<div className="text-center py-4">
<p className="text-gray-400 mb-4">Login to join the discussion</p>
<p className="text-gray-400 mb-4">{t('forumTopic.loginToJoin')}</p>
<Button
onClick={() => navigate('/login')}
className="bg-gradient-to-r from-green-600 to-yellow-500"
>
<LogIn className="w-4 h-4 mr-2" />
Login to Reply
{t('forumTopic.loginToReply')}
</Button>
</div>
)}
@@ -685,7 +687,7 @@ const ForumTopic: React.FC = () => {
<Card className="bg-gray-900 border-gray-800 mb-6">
<CardContent className="p-6 text-center">
<Lock className="w-8 h-8 text-gray-500 mx-auto mb-2" />
<p className="text-gray-400">This discussion has been locked and no longer accepts new replies.</p>
<p className="text-gray-400">{t('forumTopic.lockedMsg')}</p>
</CardContent>
</Card>
)}
@@ -694,14 +696,14 @@ const ForumTopic: React.FC = () => {
<div>
<h3 className="text-lg font-semibold text-white mb-4 flex items-center gap-2">
<MessageSquare className="w-5 h-5 text-green-500" />
{replies.length} {replies.length === 1 ? 'Reply' : 'Replies'}
{t('forumTopic.replyCount_other', { count: replies.length })}
</h3>
{replies.length === 0 ? (
<Card className="bg-gray-900 border-gray-800">
<CardContent className="py-12 text-center">
<MessageSquare className="w-12 h-12 text-gray-600 mx-auto mb-3" />
<p className="text-gray-400">No replies yet. Be the first to respond!</p>
<p className="text-gray-400">{t('forumTopic.noReplies')}</p>
</CardContent>
</Card>
) : (
@@ -719,10 +721,10 @@ const ForumTopic: React.FC = () => {
<DialogHeader>
<DialogTitle className="text-white text-xl flex items-center gap-2">
<LogIn className="w-6 h-6 text-green-500" />
Login Required
{t('forum.loginRequired')}
</DialogTitle>
<DialogDescription className="text-gray-400">
You need to be logged in to perform this action.
{t('forum.loginRequiredDesc')}
</DialogDescription>
</DialogHeader>
@@ -730,11 +732,11 @@ const ForumTopic: React.FC = () => {
<div className="w-16 h-16 rounded-full bg-gradient-to-br from-green-500/20 to-yellow-500/20 flex items-center justify-center mx-auto mb-4">
<Users className="w-8 h-8 text-green-400" />
</div>
<p className="text-gray-300 mb-2">Join our community to:</p>
<p className="text-gray-300 mb-2">{t('forum.joinCommunity')}</p>
<ul className="text-gray-400 text-sm space-y-1">
<li> Reply to discussions</li>
<li> Upvote helpful content</li>
<li> Save topics for later</li>
<li> {t('forumTopic.joinReply')}</li>
<li> {t('forumTopic.joinUpvote')}</li>
<li> {t('forumTopic.joinSave')}</li>
</ul>
</div>
@@ -744,7 +746,7 @@ const ForumTopic: React.FC = () => {
onClick={() => setShowLoginPrompt(false)}
className="flex-1 border-gray-700 text-gray-300"
>
Continue Reading
{t('forumTopic.continueReading')}
</Button>
<Button
onClick={() => {
@@ -754,7 +756,7 @@ const ForumTopic: React.FC = () => {
className="flex-1 bg-gradient-to-r from-green-600 to-yellow-500"
>
<LogIn className="w-4 h-4 mr-2" />
Login
{t('forum.login')}
</Button>
</DialogFooter>
</DialogContent>
@@ -766,15 +768,15 @@ const ForumTopic: React.FC = () => {
<DialogHeader>
<DialogTitle className="text-white text-xl flex items-center gap-2">
<AlertTriangle className="w-6 h-6 text-yellow-500" />
Report Content
{t('forumTopic.reportContent')}
</DialogTitle>
<DialogDescription className="text-gray-400">
Why are you reporting this content?
{t('forumTopic.reportReason')}
</DialogDescription>
</DialogHeader>
<div className="py-4 space-y-2">
{['Spam or misleading', 'Harassment or hate speech', 'Inappropriate content', 'Off-topic', 'Other'].map((reason) => (
{[t('forumTopic.reportSpam'), t('forumTopic.reportHarassment'), t('forumTopic.reportInappropriate'), t('forumTopic.reportOffTopic'), t('forumTopic.reportOther')].map((reason) => (
<button
key={reason}
onClick={() => handleReport(reason)}
@@ -791,7 +793,7 @@ const ForumTopic: React.FC = () => {
onClick={() => setShowReportModal(false)}
className="border-gray-700 text-gray-300"
>
Cancel
{t('common.cancel')}
</Button>
</DialogFooter>
</DialogContent>
+63 -69
View File
@@ -1,5 +1,6 @@
import { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
@@ -24,6 +25,7 @@ export default function GovEntrance() {
const { nftDetails, kycStatus, loading: dashboardLoading } = useDashboard();
const navigate = useNavigate();
const { toast } = useToast();
const { t } = useTranslation();
const [loading, setLoading] = useState(true);
useEffect(() => {
@@ -46,8 +48,8 @@ export default function GovEntrance() {
if (!hasGovernmentRole) {
toast({
title: "Mafê Te Tuneye (No Access)",
description: "Divê hûn xwedîyê Rola Hikûmetê bin ku vê rûpelê bigihînin (You must have a Government Role to access this page)",
title: t('govEntrance.noAccess'),
description: t('govEntrance.noAccessMessage'),
variant: "destructive"
});
navigate('/citizens');
@@ -59,8 +61,8 @@ export default function GovEntrance() {
const handleFeatureClick = (feature: string) => {
toast({
title: "Çalakiyê di bin nîgehdariyek de ye (Under Development)",
description: `${feature} nûve tê avakirin (${feature} is currently under development)`,
title: t('govEntrance.underDevelopment'),
description: t('govEntrance.underDevelopmentMessage', { feature }),
});
};
@@ -71,7 +73,7 @@ export default function GovEntrance() {
<CardContent className="pt-6">
<div className="flex items-center space-x-4">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-red-600"></div>
<p className="text-gray-700 font-medium">Deriyê Hikûmetê barkirin... (Loading Government Portal...)</p>
<p className="text-gray-700 font-medium">{t('govEntrance.loading')}</p>
</div>
</CardContent>
</Card>
@@ -91,23 +93,20 @@ export default function GovEntrance() {
className="bg-red-600 hover:bg-red-700 border-yellow-400 border-2 text-white font-semibold shadow-lg"
>
<ArrowLeft className="mr-2 h-4 w-4" />
Vegere Portala Welatiyên (Back to Citizens Portal)
{t('govEntrance.backToCitizens')}
</Button>
</div>
{/* Hero Section */}
<div className="text-center mb-12">
<h1 className="text-5xl md:text-6xl font-bold text-red-700 mb-3 drop-shadow-lg">
🏛 Deriyê Hikûmetê (Government Entrance)
🏛 {t('govEntrance.title')}
</h1>
<p className="text-xl text-gray-800 font-semibold drop-shadow-md mb-2">
Beşdariya Demokratîk (Democratic Participation)
{t('govEntrance.subtitle')}
</p>
<p className="text-base text-gray-700">
Mafên xwe yên demokratîk bi kar bînin û di rêveberiya welêt de beşdar bibin
</p>
<p className="text-sm text-gray-600 italic">
(Exercise your democratic rights and participate in governance)
{t('govEntrance.description')}
</p>
</div>
@@ -116,7 +115,7 @@ export default function GovEntrance() {
{/* 1. Elections - Hilbijartinên (Elections) */}
<Card
className="bg-white/95 backdrop-blur border-2 border-blue-400 hover:border-blue-600 transition-all shadow-xl cursor-pointer group hover:scale-105"
onClick={() => handleFeatureClick('Hilbijartinên (Elections)')}
onClick={() => handleFeatureClick(t('govEntrance.elections.title'))}
>
<CardHeader className="pb-4">
<div className="flex items-center justify-between mb-2">
@@ -124,29 +123,29 @@ export default function GovEntrance() {
<Vote className="h-8 w-8 text-white" />
</div>
<Badge variant="outline" className="bg-blue-50 text-blue-700 border-blue-300">
Aktîf (Active)
{t('govEntrance.active')}
</Badge>
</div>
<CardTitle className="text-2xl text-blue-800">Hilbijartinên</CardTitle>
<CardDescription className="text-gray-600">(Elections & Voting)</CardDescription>
<CardTitle className="text-2xl text-blue-800">{t('govEntrance.elections.title')}</CardTitle>
<CardDescription className="text-gray-600">{t('govEntrance.elections.subtitle')}</CardDescription>
</CardHeader>
<CardContent>
<ul className="space-y-2 text-sm text-gray-700">
<li className="flex items-start">
<CheckCircle className="h-4 w-4 text-blue-500 mr-2 mt-0.5 flex-shrink-0" />
<span>Hilbijartina Serok (Presidential Election)</span>
<span>{t('govEntrance.elections.presidential')}</span>
</li>
<li className="flex items-start">
<CheckCircle className="h-4 w-4 text-blue-500 mr-2 mt-0.5 flex-shrink-0" />
<span>Hilbijartina Parlamentoyê (Parliamentary Elections)</span>
<span>{t('govEntrance.elections.parliamentary')}</span>
</li>
<li className="flex items-start">
<CheckCircle className="h-4 w-4 text-blue-500 mr-2 mt-0.5 flex-shrink-0" />
<span>Hilbijartina Serokê Meclisê (Speaker Election)</span>
<span>{t('govEntrance.elections.speaker')}</span>
</li>
<li className="flex items-start">
<CheckCircle className="h-4 w-4 text-blue-500 mr-2 mt-0.5 flex-shrink-0" />
<span>Dadgeha Destûrî (Constitutional Court)</span>
<span>{t('govEntrance.elections.constitutional')}</span>
</li>
</ul>
</CardContent>
@@ -155,7 +154,7 @@ export default function GovEntrance() {
{/* 2. Candidacy - Berjewendî (Candidacy) */}
<Card
className="bg-white/95 backdrop-blur border-2 border-green-400 hover:border-green-600 transition-all shadow-xl cursor-pointer group hover:scale-105"
onClick={() => handleFeatureClick('Berjewendî (Candidacy)')}
onClick={() => handleFeatureClick(t('govEntrance.candidacy.title'))}
>
<CardHeader className="pb-4">
<div className="flex items-center justify-between mb-2">
@@ -163,29 +162,29 @@ export default function GovEntrance() {
<Users className="h-8 w-8 text-white" />
</div>
<Badge variant="outline" className="bg-green-50 text-green-700 border-green-300">
Mafên Te (Your Rights)
{t('govEntrance.candidacy.badge')}
</Badge>
</div>
<CardTitle className="text-2xl text-green-800">Berjewendî</CardTitle>
<CardDescription className="text-gray-600">(Run for Office)</CardDescription>
<CardTitle className="text-2xl text-green-800">{t('govEntrance.candidacy.title')}</CardTitle>
<CardDescription className="text-gray-600"></CardDescription>
</CardHeader>
<CardContent>
<ul className="space-y-2 text-sm text-gray-700">
<li className="flex items-start">
<TrendingUp className="h-4 w-4 text-green-500 mr-2 mt-0.5 flex-shrink-0" />
<span>Bibe Berjewendiyê Serokbûnê (Presidential Candidate)</span>
<span>{t('govEntrance.candidacy.presidential')}</span>
</li>
<li className="flex items-start">
<TrendingUp className="h-4 w-4 text-green-500 mr-2 mt-0.5 flex-shrink-0" />
<span>Bibe Parlementêr (Parliamentary Candidate)</span>
<span>{t('govEntrance.candidacy.parliamentary')}</span>
</li>
<li className="flex items-start">
<AlertCircle className="h-4 w-4 text-yellow-600 mr-2 mt-0.5 flex-shrink-0" />
<span className="text-xs text-yellow-700">Pêdiviya Trust Score: 300-750</span>
<span className="text-xs text-yellow-700">{t('govEntrance.candidacy.trustScore')}</span>
</li>
<li className="flex items-start">
<AlertCircle className="h-4 w-4 text-yellow-600 mr-2 mt-0.5 flex-shrink-0" />
<span className="text-xs text-yellow-700">Piştgiriya pêwîst: 100-1000 endorsements</span>
<span className="text-xs text-yellow-700">{t('govEntrance.candidacy.endorsements')}</span>
</li>
</ul>
</CardContent>
@@ -194,7 +193,7 @@ export default function GovEntrance() {
{/* 3. Proposals - Pêşniyar (Legislative Proposals) */}
<Card
className="bg-white/95 backdrop-blur border-2 border-purple-400 hover:border-purple-600 transition-all shadow-xl cursor-pointer group hover:scale-105"
onClick={() => handleFeatureClick('Pêşniyar (Proposals)')}
onClick={() => handleFeatureClick(t('govEntrance.proposals.title'))}
>
<CardHeader className="pb-4">
<div className="flex items-center justify-between mb-2">
@@ -202,25 +201,25 @@ export default function GovEntrance() {
<FileText className="h-8 w-8 text-white" />
</div>
<Badge variant="outline" className="bg-purple-50 text-purple-700 border-purple-300">
Yasayan (Legislative)
{t('govEntrance.proposals.badge')}
</Badge>
</div>
<CardTitle className="text-2xl text-purple-800">Pêşniyar</CardTitle>
<CardDescription className="text-gray-600">(Submit Proposals)</CardDescription>
<CardTitle className="text-2xl text-purple-800">{t('govEntrance.proposals.title')}</CardTitle>
<CardDescription className="text-gray-600"></CardDescription>
</CardHeader>
<CardContent>
<ul className="space-y-2 text-sm text-gray-700">
<li className="flex items-start">
<FileText className="h-4 w-4 text-purple-500 mr-2 mt-0.5 flex-shrink-0" />
<span>Pêşniyarên Yasayê (Legislative Proposals)</span>
<span>{t('govEntrance.proposals.legislative')}</span>
</li>
<li className="flex items-start">
<FileText className="h-4 w-4 text-purple-500 mr-2 mt-0.5 flex-shrink-0" />
<span>Guheztinên Destûrî (Constitutional Amendments)</span>
<span>{t('govEntrance.proposals.constitutional')}</span>
</li>
<li className="flex items-start">
<FileText className="h-4 w-4 text-purple-500 mr-2 mt-0.5 flex-shrink-0" />
<span>Pêşniyarên Budçeyê (Budget Proposals)</span>
<span>{t('govEntrance.proposals.budget')}</span>
</li>
</ul>
</CardContent>
@@ -229,7 +228,7 @@ export default function GovEntrance() {
{/* 4. Voting on Proposals - Dengdayîn (Vote on Proposals) */}
<Card
className="bg-white/95 backdrop-blur border-2 border-orange-400 hover:border-orange-600 transition-all shadow-xl cursor-pointer group hover:scale-105"
onClick={() => handleFeatureClick('Dengdayîn (Voting)')}
onClick={() => handleFeatureClick(t('govEntrance.voting.title'))}
>
<CardHeader className="pb-4">
<div className="flex items-center justify-between mb-2">
@@ -237,29 +236,29 @@ export default function GovEntrance() {
<CheckCircle className="h-8 w-8 text-white" />
</div>
<Badge variant="outline" className="bg-orange-50 text-orange-700 border-orange-300">
Parlamenter (MPs Only)
{t('govEntrance.voting.badge')}
</Badge>
</div>
<CardTitle className="text-2xl text-orange-800">Dengdayîn</CardTitle>
<CardDescription className="text-gray-600">(Parliamentary Voting)</CardDescription>
<CardTitle className="text-2xl text-orange-800">{t('govEntrance.voting.title')}</CardTitle>
<CardDescription className="text-gray-600"></CardDescription>
</CardHeader>
<CardContent>
<ul className="space-y-2 text-sm text-gray-700">
<li className="flex items-start">
<CheckCircle className="h-4 w-4 text-green-500 mr-2 mt-0.5 flex-shrink-0" />
<span>Erê (Aye)</span>
<span>{t('govEntrance.voting.aye')}</span>
</li>
<li className="flex items-start">
<XCircle className="h-4 w-4 text-red-500 mr-2 mt-0.5 flex-shrink-0" />
<span>Na (Nay)</span>
<span>{t('govEntrance.voting.nay')}</span>
</li>
<li className="flex items-start">
<Clock className="h-4 w-4 text-gray-500 mr-2 mt-0.5 flex-shrink-0" />
<span>Bêalî (Abstain)</span>
<span>{t('govEntrance.voting.abstain')}</span>
</li>
<li className="flex items-start">
<AlertCircle className="h-4 w-4 text-blue-500 mr-2 mt-0.5 flex-shrink-0" />
<span className="text-xs text-blue-700">Majority types: Simple (50%+1), Super (2/3), Absolute (3/4)</span>
<span className="text-xs text-blue-700">{t('govEntrance.voting.majorityTypes')}</span>
</li>
</ul>
</CardContent>
@@ -268,7 +267,7 @@ export default function GovEntrance() {
{/* 5. Veto & Override - Veto û Têperbûn (Veto System) */}
<Card
className="bg-white/95 backdrop-blur border-2 border-red-400 hover:border-red-600 transition-all shadow-xl cursor-pointer group hover:scale-105"
onClick={() => handleFeatureClick('Veto û Têperbûn (Veto System)')}
onClick={() => handleFeatureClick(t('govEntrance.veto.title'))}
>
<CardHeader className="pb-4">
<div className="flex items-center justify-between mb-2">
@@ -276,25 +275,25 @@ export default function GovEntrance() {
<Scale className="h-8 w-8 text-white" />
</div>
<Badge variant="outline" className="bg-red-50 text-red-700 border-red-300">
Serok (President)
{t('govEntrance.veto.badge')}
</Badge>
</div>
<CardTitle className="text-2xl text-red-800">Veto û Têperbûn</CardTitle>
<CardDescription className="text-gray-600">(Veto & Override)</CardDescription>
<CardTitle className="text-2xl text-red-800">{t('govEntrance.veto.title')}</CardTitle>
<CardDescription className="text-gray-600"></CardDescription>
</CardHeader>
<CardContent>
<ul className="space-y-2 text-sm text-gray-700">
<li className="flex items-start">
<XCircle className="h-4 w-4 text-red-500 mr-2 mt-0.5 flex-shrink-0" />
<span>Vetoya Serok (Presidential Veto)</span>
<span>{t('govEntrance.veto.presidential')}</span>
</li>
<li className="flex items-start">
<CheckCircle className="h-4 w-4 text-green-500 mr-2 mt-0.5 flex-shrink-0" />
<span>Têperbûna Parlamentoyê (Parliamentary Override - 2/3)</span>
<span>{t('govEntrance.veto.override')}</span>
</li>
<li className="flex items-start">
<AlertCircle className="h-4 w-4 text-yellow-600 mr-2 mt-0.5 flex-shrink-0" />
<span className="text-xs text-yellow-700">Pêdiviya 134 deng ji 201 (Requires 134 of 201 votes)</span>
<span className="text-xs text-yellow-700">{t('govEntrance.veto.requires')}</span>
</li>
</ul>
</CardContent>
@@ -303,7 +302,7 @@ export default function GovEntrance() {
{/* 6. Government Appointments - Tayinên Hikûmetê (Official Appointments) */}
<Card
className="bg-white/95 backdrop-blur border-2 border-indigo-400 hover:border-indigo-600 transition-all shadow-xl cursor-pointer group hover:scale-105"
onClick={() => handleFeatureClick('Tayinên Hikûmetê (Appointments)')}
onClick={() => handleFeatureClick(t('govEntrance.appointments.title'))}
>
<CardHeader className="pb-4">
<div className="flex items-center justify-between mb-2">
@@ -311,25 +310,25 @@ export default function GovEntrance() {
<Megaphone className="h-8 w-8 text-white" />
</div>
<Badge variant="outline" className="bg-indigo-50 text-indigo-700 border-indigo-300">
Wezîr (Ministers)
{t('govEntrance.appointments.badge')}
</Badge>
</div>
<CardTitle className="text-2xl text-indigo-800">Tayinên Hikûmetê</CardTitle>
<CardDescription className="text-gray-600">(Government Officials)</CardDescription>
<CardTitle className="text-2xl text-indigo-800">{t('govEntrance.appointments.title')}</CardTitle>
<CardDescription className="text-gray-600"></CardDescription>
</CardHeader>
<CardContent>
<ul className="space-y-2 text-sm text-gray-700">
<li className="flex items-start">
<CheckCircle className="h-4 w-4 text-indigo-500 mr-2 mt-0.5 flex-shrink-0" />
<span>Wezîrên Kabîne (Cabinet Ministers)</span>
<span>{t('govEntrance.appointments.cabinet')}</span>
</li>
<li className="flex items-start">
<CheckCircle className="h-4 w-4 text-indigo-500 mr-2 mt-0.5 flex-shrink-0" />
<span>Dadger, Xezinedar, Noter (Judges, Treasury, Notary)</span>
<span>{t('govEntrance.appointments.officials')}</span>
</li>
<li className="flex items-start">
<AlertCircle className="h-4 w-4 text-yellow-600 mr-2 mt-0.5 flex-shrink-0" />
<span className="text-xs text-yellow-700">Piştgiriya Serok pêwîst e (Presidential approval required)</span>
<span className="text-xs text-yellow-700">{t('govEntrance.appointments.approval')}</span>
</li>
</ul>
</CardContent>
@@ -341,13 +340,13 @@ export default function GovEntrance() {
<CardHeader>
<CardTitle className="text-2xl text-gray-800 flex items-center">
<AlertCircle className="h-6 w-6 text-yellow-600 mr-2" />
Statûya Te ya Welatîbûnê (Your Citizenship Status)
{t('govEntrance.status.title')}
</CardTitle>
</CardHeader>
<CardContent>
<div className="grid md:grid-cols-3 gap-4">
<div className="bg-white p-4 rounded-lg border border-green-200">
<p className="text-sm text-gray-600 mb-1">KYC Status</p>
<p className="text-sm text-gray-600 mb-1">{t('govEntrance.status.kyc')}</p>
<div className="flex items-center">
<Badge className="bg-green-500 text-white">
<CheckCircle className="h-3 w-3 mr-1" />
@@ -356,32 +355,27 @@ export default function GovEntrance() {
</div>
</div>
<div className="bg-white p-4 rounded-lg border border-blue-200">
<p className="text-sm text-gray-600 mb-1">Mafên Dengdayînê (Voting Rights)</p>
<p className="text-sm text-gray-600 mb-1">{t('govEntrance.status.votingRights')}</p>
<div className="flex items-center">
<Badge className="bg-blue-500 text-white">
<Vote className="h-3 w-3 mr-1" />
Aktîf (Active)
{t('govEntrance.status.active')}
</Badge>
</div>
</div>
<div className="bg-white p-4 rounded-lg border border-purple-200">
<p className="text-sm text-gray-600 mb-1">Beşdariya Rêveberiyê (Participation)</p>
<p className="text-sm text-gray-600 mb-1">{t('govEntrance.status.participation')}</p>
<div className="flex items-center">
<Badge className="bg-purple-500 text-white">
<Users className="h-3 w-3 mr-1" />
Amade (Ready)
{t('govEntrance.status.ready')}
</Badge>
</div>
</div>
</div>
<div className="mt-4 p-4 bg-yellow-50 border border-yellow-200 rounded-lg">
<p className="text-sm text-yellow-800">
<strong>Bala xwe bidin (Important):</strong> Hemû mafên welatîbûnê yên te çalak in.
Tu dikarî di hemû hilbijartinên demokratîk de beşdar bibî û deng bidî.
<br />
<span className="italic text-xs">
(All your citizenship rights are active. You can participate and vote in all democratic elections.)
</span>
{t('govEntrance.status.notice')}
</p>
</div>
</CardContent>
+24 -22
View File
@@ -1,9 +1,11 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import Layout from '@/components/Layout';
import { Award, Lightbulb, CheckCircle } from 'lucide-react';
const Grants: React.FC = () => {
const { t } = useTranslation();
const fundedProjects = [
{ name: 'Pezkuwi DEX', description: 'A fast and secure decentralized exchange built on PezkuwiChain.', logo: '/pezkuwimarket.png' },
{ name: 'NFT Marketplace', description: 'A platform for creating and trading NFTs with low transaction fees.', logo: '/PezkuwiExchange.png' },
@@ -14,64 +16,64 @@ const Grants: React.FC = () => {
<Layout>
<div className="container mx-auto px-4 py-8 text-white">
<div className="text-center mb-12">
<h1 className="text-5xl font-bold mb-2 text-purple-400">PezkuwiChain Grants Program</h1>
<p className="text-xl text-gray-400">Funding the future of the PezkuwiChain ecosystem.</p>
<h1 className="text-5xl font-bold mb-2 text-purple-400">{t('grants.title')}</h1>
<p className="text-xl text-gray-400">{t('grants.subtitle')}</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 text-center mb-12">
<div className="bg-gray-800 p-6 rounded-lg">
<Lightbulb className="mx-auto text-purple-400 mb-4" size={32} />
<h3 className="text-xl font-bold">Bring Your Idea</h3>
<p className="text-gray-400">We support innovative projects that bring value to the ecosystem.</p>
<h3 className="text-xl font-bold">{t('grants.bringIdea')}</h3>
<p className="text-gray-400">{t('grants.bringIdeaDesc')}</p>
</div>
<div className="bg-gray-800 p-6 rounded-lg">
<Award className="mx-auto text-purple-400 mb-4" size={32} />
<h3 className="text-xl font-bold">Get Funded</h3>
<p className="text-gray-400">Receive funding and support to turn your idea into reality.</p>
<h3 className="text-xl font-bold">{t('grants.getFunded')}</h3>
<p className="text-gray-400">{t('grants.getFundedDesc')}</p>
</div>
<div className="bg-gray-800 p-6 rounded-lg">
<CheckCircle className="mx-auto text-purple-400 mb-4" size={32} />
<h3 className="text-xl font-bold">Grow the Ecosystem</h3>
<p className="text-gray-400">Contribute to the growth and decentralization of PezkuwiChain.</p>
<h3 className="text-xl font-bold">{t('grants.growEcosystem')}</h3>
<p className="text-gray-400">{t('grants.growEcosystemDesc')}</p>
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12 items-center">
<div className="bg-gray-800 p-8 rounded-lg">
<h2 className="text-3xl font-bold mb-6">Apply for a Grant</h2>
<h2 className="text-3xl font-bold mb-6">{t('grants.applyForGrant')}</h2>
<form>
<div className="mb-4">
<label className="block mb-2 font-semibold">Project Name</label>
<label className="block mb-2 font-semibold">{t('grants.projectName')}</label>
<input type="text" placeholder="Your awesome project" className="w-full p-3 rounded bg-gray-700 text-white focus:outline-none focus:ring-2 focus:ring-purple-500" />
</div>
<div className="mb-4">
<label className="block mb-2 font-semibold">Team Information</label>
<label className="block mb-2 font-semibold">{t('grants.teamInfo')}</label>
<textarea placeholder="Tell us about your team" rows={3} className="w-full p-3 rounded bg-gray-700 text-white focus:outline-none focus:ring-2 focus:ring-purple-500"></textarea>
</div>
<div className="mb-6">
<label className="block mb-2 font-semibold">Requested Amount (USD)</label>
<label className="block mb-2 font-semibold">{t('grants.requestedAmount')}</label>
<input type="number" placeholder="10000" className="w-full p-3 rounded bg-gray-700 text-white focus:outline-none focus:ring-2 focus:ring-purple-500" />
</div>
<button className="w-full bg-purple-500 hover:bg-purple-600 text-white font-bold py-3 px-4 rounded-lg transition-colors">
Submit Application
{t('grants.submitApplication')}
</button>
</form>
</div>
<div>
<h2 className="text-3xl font-bold mb-6">Our Focus Areas</h2>
<h2 className="text-3xl font-bold mb-6">{t('grants.focusAreas')}</h2>
<ul className="list-disc list-inside space-y-3 text-lg">
<li>Decentralized Finance (DeFi)</li>
<li>NFTs and Gaming</li>
<li>Infrastructure and Tooling</li>
<li>Governance and DAOs</li>
<li>Privacy and Identity</li>
<li>Mobile and Web3 Applications</li>
<li>{t('grants.defi')}</li>
<li>{t('grants.nfts')}</li>
<li>{t('grants.infrastructure')}</li>
<li>{t('grants.governanceDao')}</li>
<li>{t('grants.privacy')}</li>
<li>{t('grants.mobile')}</li>
</ul>
</div>
</div>
<div className="mt-16">
<h2 className="text-3xl font-bold text-center mb-8">Funded Projects</h2>
<h2 className="text-3xl font-bold text-center mb-8">{t('grants.fundedProjects')}</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{fundedProjects.map((project, index) => (
<div key={index} className="bg-gray-800 rounded-lg overflow-hidden">
+5 -3
View File
@@ -1,7 +1,9 @@
import { useLocation } from "react-router-dom";
import { useEffect } from "react";
import { useTranslation } from 'react-i18next';
const NotFound = () => {
const { t } = useTranslation();
const location = useLocation();
useEffect(() => {
@@ -14,10 +16,10 @@ const NotFound = () => {
return (
<div className="min-h-screen flex items-center justify-center bg-background">
<div className="text-center p-8 rounded-lg border border-border bg-card shadow-md animate-slide-in">
<h1 className="text-5xl font-bold mb-6 text-primary">404</h1>
<p className="text-xl text-card-foreground mb-6">Page not found</p>
<h1 className="text-5xl font-bold mb-6 text-primary">{t('notFound.code')}</h1>
<p className="text-xl text-card-foreground mb-6">{t('notFound.message')}</p>
<a href="/" className="text-primary hover:text-primary/80 underline transition-colors">
Return to Home
{t('notFound.backToHome')}
</a>
</div>
</div>
+51 -52
View File
@@ -71,39 +71,39 @@ interface Evidence {
created_at: string;
}
const STATUS_CONFIG: Record<string, { color: string; icon: React.ReactNode; label: string }> = {
const STATUS_CONFIG: Record<string, { color: string; icon: React.ReactNode; labelKey: string }> = {
open: {
color: 'bg-amber-500',
icon: <Clock className="h-4 w-4" />,
label: 'Open',
labelKey: 'p2pDispute.statusOpen',
},
under_review: {
color: 'bg-blue-500',
icon: <Scale className="h-4 w-4" />,
label: 'Under Review',
labelKey: 'p2pDispute.statusUnderReview',
},
resolved: {
color: 'bg-green-500',
icon: <CheckCircle className="h-4 w-4" />,
label: 'Resolved',
labelKey: 'p2pDispute.statusResolved',
},
closed: {
color: 'bg-gray-500',
icon: <XCircle className="h-4 w-4" />,
label: 'Closed',
labelKey: 'p2pDispute.statusClosed',
},
};
const RESOLUTION_LABELS: Record<string, string> = {
release_to_buyer: 'Released to Buyer',
refund_to_seller: 'Refunded to Seller',
split: 'Split Decision',
const RESOLUTION_LABEL_KEYS: Record<string, string> = {
release_to_buyer: 'p2pDispute.releasedToBuyer',
refund_to_seller: 'p2pDispute.refundedToSeller',
split: 'p2pDispute.splitDecision',
};
export default function P2PDispute() {
const { disputeId } = useParams<{ disputeId: string }>();
const navigate = useNavigate();
useTranslation();
const { t } = useTranslation();
const fileInputRef = useRef<HTMLInputElement>(null);
const [dispute, setDispute] = useState<DisputeDetails | null>(null);
@@ -150,7 +150,7 @@ export default function P2PDispute() {
}
} catch (error) {
console.error('Failed to fetch dispute:', error);
toast.error('Failed to load dispute details');
toast.error(t('p2pDispute.failedToLoad'));
} finally {
setIsLoading(false);
}
@@ -205,7 +205,7 @@ export default function P2PDispute() {
try {
for (const file of Array.from(files)) {
if (file.size > 10 * 1024 * 1024) {
toast.error(`File ${file.name} is too large (max 10MB)`);
toast.error(t('p2pDispute.fileTooLarge', { name: file.name }));
continue;
}
@@ -230,10 +230,10 @@ export default function P2PDispute() {
});
}
toast.success('Evidence uploaded successfully');
toast.success(t('p2pDispute.evidenceUploaded'));
} catch (error) {
console.error('Upload failed:', error);
toast.error('Failed to upload evidence');
toast.error(t('p2pDispute.failedToUpload'));
} finally {
setIsUploading(false);
if (fileInputRef.current) {
@@ -273,12 +273,12 @@ export default function P2PDispute() {
<Card>
<CardContent className="py-12 text-center">
<AlertTriangle className="h-12 w-12 mx-auto text-amber-500 mb-4" />
<h2 className="text-lg font-semibold mb-2">Dispute Not Found</h2>
<h2 className="text-lg font-semibold mb-2">{t('p2pDispute.notFound')}</h2>
<p className="text-muted-foreground mb-4">
The dispute you are looking for does not exist or you do not have access.
{t('p2pDispute.notFoundDesc')}
</p>
<Button onClick={() => navigate('/p2p/orders')}>
Go to My Trades
{t('p2pDispute.goToTrades')}
</Button>
</CardContent>
</Card>
@@ -297,7 +297,7 @@ export default function P2PDispute() {
className="gap-2"
>
<ArrowLeft className="h-4 w-4" />
Back
{t('p2p.back')}
</Button>
{/* Header Card */}
@@ -307,16 +307,16 @@ export default function P2PDispute() {
<div>
<CardTitle className="flex items-center gap-2">
<AlertTriangle className="h-5 w-5 text-amber-500" />
Dispute #{dispute.id.slice(0, 8)}
{t('p2pDispute.disputeId', { id: dispute.id.slice(0, 8) })}
</CardTitle>
<CardDescription className="flex items-center gap-2 mt-1">
<Calendar className="h-4 w-4" />
Opened {new Date(dispute.created_at).toLocaleDateString()}
{t('p2pDispute.opened', { date: new Date(dispute.created_at).toLocaleDateString() })}
</CardDescription>
</div>
<Badge className={`${statusConfig.color} text-white flex items-center gap-1`}>
{statusConfig.icon}
{statusConfig.label}
{t(statusConfig.labelKey)}
</Badge>
</div>
</CardHeader>
@@ -326,32 +326,31 @@ export default function P2PDispute() {
<div className="bg-muted rounded-lg p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-muted-foreground">Related Trade</p>
<p className="text-sm text-muted-foreground">{t('p2pDispute.relatedTrade')}</p>
<p className="font-medium">
{dispute.trade.crypto_amount} {dispute.trade.token} for{' '}
{dispute.trade.fiat_amount} {dispute.trade.fiat_currency}
{t('p2pDispute.tradeAmountFor', { cryptoAmount: dispute.trade.crypto_amount, token: dispute.trade.token, fiatAmount: dispute.trade.fiat_amount, currency: dispute.trade.fiat_currency })}
</p>
</div>
<Link to={`/p2p/trade/${dispute.trade_id}`}>
<Button variant="outline" size="sm">
View Trade
{t('p2pDispute.viewTrade')}
<ChevronRight className="h-4 w-4 ml-1" />
</Button>
</Link>
</div>
<div className="grid grid-cols-2 gap-4 mt-4 text-sm">
<div>
<span className="text-muted-foreground">Buyer:</span>{' '}
<span className="text-muted-foreground">{t('p2p.buyer')}:</span>{' '}
<span className={isBuyer ? 'font-medium text-primary' : ''}>
{formatAddress(dispute.trade.buyer_wallet, 6, 4)}
{isBuyer && ' (You)'}
{isBuyer && ` ${t('p2p.you')}`}
</span>
</div>
<div>
<span className="text-muted-foreground">Seller:</span>{' '}
<span className="text-muted-foreground">{t('p2p.seller')}:</span>{' '}
<span className={isSeller ? 'font-medium text-primary' : ''}>
{formatAddress(dispute.trade.seller_wallet, 6, 4)}
{isSeller && ' (You)'}
{isSeller && ` ${t('p2p.you')}`}
</span>
</div>
</div>
@@ -362,7 +361,7 @@ export default function P2PDispute() {
<div>
<h3 className="font-semibold mb-2 flex items-center gap-2">
<MessageSquare className="h-4 w-4" />
Dispute Reason
{t('p2pDispute.disputeReason')}
</h3>
<Badge variant="outline" className="mb-2">
{dispute.reason.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase())}
@@ -371,7 +370,7 @@ export default function P2PDispute() {
{dispute.description}
</p>
<p className="text-xs text-muted-foreground mt-2">
Opened by: {isOpener ? 'You' : (isBuyer ? 'Seller' : 'Buyer')}
{t('p2pDispute.openedBy', { party: isOpener ? t('p2p.you').replace(/[()]/g, '') : (isBuyer ? t('p2p.seller') : t('p2p.buyer')) })}
</p>
</div>
@@ -380,10 +379,10 @@ export default function P2PDispute() {
<div className="bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-lg p-4">
<h3 className="font-semibold mb-2 flex items-center gap-2 text-green-700 dark:text-green-300">
<CheckCircle className="h-4 w-4" />
Resolution
{t('p2pDispute.resolutionTitle')}
</h3>
<p className="font-medium text-green-800 dark:text-green-200">
{RESOLUTION_LABELS[dispute.resolution]}
{t(RESOLUTION_LABEL_KEYS[dispute.resolution])}
</p>
{dispute.resolution_notes && (
<p className="text-sm text-green-700 dark:text-green-300 mt-2">
@@ -392,7 +391,7 @@ export default function P2PDispute() {
)}
{dispute.resolved_at && (
<p className="text-xs text-green-600 dark:text-green-400 mt-2">
Resolved on {new Date(dispute.resolved_at).toLocaleString()}
{t('p2pDispute.resolvedOn', { date: new Date(dispute.resolved_at).toLocaleString() })}
</p>
)}
</div>
@@ -404,7 +403,7 @@ export default function P2PDispute() {
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<CardTitle className="text-lg">Evidence</CardTitle>
<CardTitle className="text-lg">{t('p2pDispute.evidenceTitle')}</CardTitle>
{isParticipant && dispute.status !== 'resolved' && (
<div>
<input
@@ -422,7 +421,7 @@ export default function P2PDispute() {
disabled={isUploading}
>
<Upload className="h-4 w-4 mr-2" />
{isUploading ? 'Uploading...' : 'Add Evidence'}
{isUploading ? t('p2pDispute.uploading') : t('p2pDispute.addEvidence')}
</Button>
</div>
)}
@@ -432,10 +431,10 @@ export default function P2PDispute() {
{evidence.length === 0 ? (
<div className="text-center py-8 text-muted-foreground">
<FileText className="h-12 w-12 mx-auto mb-2 opacity-50" />
<p>No evidence submitted yet</p>
<p>{t('p2pDispute.noEvidence')}</p>
{isParticipant && dispute.status !== 'resolved' && (
<p className="text-sm mt-1">
Upload screenshots, receipts, or documents to support your case
{t('p2pDispute.uploadEvidenceHelp')}
</p>
)}
</div>
@@ -468,8 +467,8 @@ export default function P2PDispute() {
<div className="p-2 text-xs">
<p className="truncate font-medium">{item.description}</p>
<p className="text-muted-foreground">
{isMyEvidence ? 'You' : (
item.uploaded_by === dispute.trade?.buyer_id ? 'Buyer' : 'Seller'
{isMyEvidence ? t('p2p.you').replace(/[()]/g, '') : (
item.uploaded_by === dispute.trade?.buyer_id ? t('p2p.buyer') : t('p2p.seller')
)}
</p>
</div>
@@ -491,7 +490,7 @@ export default function P2PDispute() {
{/* Status Timeline */}
<Card>
<CardHeader>
<CardTitle className="text-lg">Status Timeline</CardTitle>
<CardTitle className="text-lg">{t('p2pDispute.statusTimeline')}</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-4">
@@ -501,7 +500,7 @@ export default function P2PDispute() {
<Clock className="h-4 w-4 text-white" />
</div>
<div>
<p className="font-medium">Dispute Opened</p>
<p className="font-medium">{t('p2pDispute.disputeOpenedStep')}</p>
<p className="text-sm text-muted-foreground">
{new Date(dispute.created_at).toLocaleString()}
</p>
@@ -515,9 +514,9 @@ export default function P2PDispute() {
<Scale className="h-4 w-4 text-white" />
</div>
<div>
<p className="font-medium">Under Review</p>
<p className="font-medium">{t('p2pDispute.underReviewStep')}</p>
<p className="text-sm text-muted-foreground">
Admin is reviewing the case
{t('p2pDispute.adminReviewing')}
</p>
</div>
</div>
@@ -530,7 +529,7 @@ export default function P2PDispute() {
<CheckCircle className="h-4 w-4 text-white" />
</div>
<div>
<p className="font-medium">Resolved</p>
<p className="font-medium">{t('p2pDispute.resolvedStep')}</p>
<p className="text-sm text-muted-foreground">
{dispute.resolved_at && new Date(dispute.resolved_at).toLocaleString()}
</p>
@@ -546,8 +545,8 @@ export default function P2PDispute() {
<Scale className="h-4 w-4 text-white" />
</div>
<div>
<p className="font-medium">Under Review</p>
<p className="text-sm text-muted-foreground">Pending</p>
<p className="font-medium">{t('p2pDispute.underReviewStep')}</p>
<p className="text-sm text-muted-foreground">{t('p2pDispute.pending')}</p>
</div>
</div>
<div className="flex items-start gap-3 opacity-40">
@@ -555,8 +554,8 @@ export default function P2PDispute() {
<CheckCircle className="h-4 w-4 text-white" />
</div>
<div>
<p className="font-medium">Resolution</p>
<p className="text-sm text-muted-foreground">Pending</p>
<p className="font-medium">{t('p2pDispute.resolutionStep')}</p>
<p className="text-sm text-muted-foreground">{t('p2pDispute.pending')}</p>
</div>
</div>
</>
@@ -571,13 +570,13 @@ export default function P2PDispute() {
<div className="flex items-center gap-4">
<User className="h-10 w-10 text-muted-foreground" />
<div className="flex-1">
<h3 className="font-medium">Need Help?</h3>
<h3 className="font-medium">{t('p2pDispute.needHelp')}</h3>
<p className="text-sm text-muted-foreground">
Our support team typically responds within 24-48 hours.
{t('p2pDispute.supportResponse')}
</p>
</div>
<Button variant="outline" asChild>
<a href="mailto:support@pezkuwichain.io">Contact Support</a>
<a href="mailto:support@pezkuwichain.io">{t('p2pDispute.contactSupport')}</a>
</Button>
</div>
</CardContent>
+81 -79
View File
@@ -1,5 +1,6 @@
import { useState, useEffect, useCallback } from 'react';
import { useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { supabase } from '@/lib/supabase';
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
@@ -89,6 +90,7 @@ interface ChartDataPoint {
export default function P2PMerchantDashboard() {
const navigate = useNavigate();
const { t } = useTranslation();
const [loading, setLoading] = useState(true);
const [stats, setStats] = useState<MerchantStats | null>(null);
const [tierInfo, setTierInfo] = useState<MerchantTier | null>(null);
@@ -212,17 +214,17 @@ export default function P2PMerchantDashboard() {
if (error) throw error;
toast.success(`Ad ${newStatus === 'open' ? 'activated' : 'paused'}`);
toast.success(newStatus === 'open' ? t('p2pMerchant.adActivated') : t('p2pMerchant.adPaused'));
fetchData();
} catch (error) {
console.error('Error toggling ad status:', error);
toast.error('Failed to update ad status');
toast.error(t('p2pMerchant.failedToUpdateStatus'));
}
};
// Delete ad
const deleteAd = async (adId: string) => {
if (!confirm('Are you sure you want to delete this ad?')) return;
if (!confirm(t('p2pMerchant.confirmDelete'))) return;
try {
const { error } = await supabase
@@ -232,11 +234,11 @@ export default function P2PMerchantDashboard() {
if (error) throw error;
toast.success('Ad deleted successfully');
toast.success(t('p2pMerchant.adDeleted'));
fetchData();
} catch (error) {
console.error('Error deleting ad:', error);
toast.error('Failed to delete ad');
toast.error(t('p2pMerchant.failedToDelete'));
}
};
@@ -255,7 +257,7 @@ export default function P2PMerchantDashboard() {
const fiatAmt = parseFloat(editFiatAmount);
if (!fiatAmt || fiatAmt <= 0) {
toast.error('Invalid fiat amount');
toast.error(t('p2pMerchant.invalidFiatAmount'));
return;
}
@@ -274,13 +276,13 @@ export default function P2PMerchantDashboard() {
if (error) throw error;
toast.success('Ad updated successfully');
toast.success(t('p2pMerchant.adUpdated'));
setEditAdOpen(false);
setEditingAd(null);
fetchData();
} catch (error) {
console.error('Error updating ad:', error);
toast.error('Failed to update ad');
toast.error(t('p2pMerchant.failedToUpdate'));
} finally {
setSavingEdit(false);
}
@@ -298,11 +300,11 @@ export default function P2PMerchantDashboard() {
if (error) throw error;
toast.success('Auto-reply message saved');
toast.success(t('p2pMerchant.autoReplySaved'));
setAutoReplyOpen(false);
} catch (error) {
console.error('Error saving auto-reply:', error);
toast.error('Failed to save auto-reply');
toast.error(t('p2pMerchant.failedToSaveAutoReply'));
} finally {
setSavingAutoReply(false);
}
@@ -327,10 +329,10 @@ export default function P2PMerchantDashboard() {
<div>
<h1 className="text-2xl font-bold flex items-center gap-2">
<Crown className="h-6 w-6 text-kurdish-green" />
Merchant Dashboard
{t('p2pMerchant.dashboardTitle')}
</h1>
<p className="text-muted-foreground text-sm">
Manage your P2P trading business
{t('p2pMerchant.dashboardSubtitle')}
</p>
</div>
</div>
@@ -341,19 +343,19 @@ export default function P2PMerchantDashboard() {
<TabsList>
<TabsTrigger value="overview" className="gap-1">
<BarChart3 className="h-4 w-4" />
Overview
{t('p2pMerchant.tabOverview')}
</TabsTrigger>
<TabsTrigger value="ads" className="gap-1">
<ShoppingBag className="h-4 w-4" />
My Ads
{t('p2pMerchant.tabAds')}
</TabsTrigger>
<TabsTrigger value="upgrade" className="gap-1">
<TrendingUp className="h-4 w-4" />
Upgrade Tier
{t('p2pMerchant.tabUpgrade')}
</TabsTrigger>
<TabsTrigger value="settings" className="gap-1">
<Settings className="h-4 w-4" />
Settings
{t('p2pMerchant.tabSettings')}
</TabsTrigger>
</TabsList>
@@ -365,7 +367,7 @@ export default function P2PMerchantDashboard() {
<CardContent className="pt-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-muted-foreground">30-Day Volume</p>
<p className="text-sm text-muted-foreground">{t('p2pMerchant.thirtyDayVolumeLabel')}</p>
<p className="text-2xl font-bold">
${stats?.total_volume_30d?.toLocaleString() || '0'}
</p>
@@ -379,7 +381,7 @@ export default function P2PMerchantDashboard() {
<CardContent className="pt-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-muted-foreground">30-Day Trades</p>
<p className="text-sm text-muted-foreground">{t('p2pMerchant.thirtyDayTrades')}</p>
<p className="text-2xl font-bold">{stats?.total_trades_30d || 0}</p>
</div>
<ShoppingBag className="h-8 w-8 text-blue-500/50" />
@@ -391,7 +393,7 @@ export default function P2PMerchantDashboard() {
<CardContent className="pt-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-muted-foreground">Completion Rate</p>
<p className="text-sm text-muted-foreground">{t('p2pMerchant.completionRateLabel')}</p>
<p className="text-2xl font-bold">
{stats?.completion_rate_30d?.toFixed(1) || '0'}%
</p>
@@ -405,7 +407,7 @@ export default function P2PMerchantDashboard() {
<CardContent className="pt-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-muted-foreground">Avg Release Time</p>
<p className="text-sm text-muted-foreground">{t('p2pMerchant.avgReleaseTime')}</p>
<p className="text-2xl font-bold">
{stats?.avg_release_time_minutes || 0}m
</p>
@@ -421,8 +423,8 @@ export default function P2PMerchantDashboard() {
{/* Volume Chart */}
<Card>
<CardHeader>
<CardTitle className="text-lg">Volume Trend</CardTitle>
<CardDescription>Last 30 days trading volume</CardDescription>
<CardTitle className="text-lg">{t('p2pMerchant.volumeTrend')}</CardTitle>
<CardDescription>{t('p2pMerchant.last30dVolume')}</CardDescription>
</CardHeader>
<CardContent>
<ResponsiveContainer width="100%" height={250}>
@@ -451,8 +453,8 @@ export default function P2PMerchantDashboard() {
{/* Trades Chart */}
<Card>
<CardHeader>
<CardTitle className="text-lg">Trade Count</CardTitle>
<CardDescription>Daily trades over last 30 days</CardDescription>
<CardTitle className="text-lg">{t('p2pMerchant.tradeCount')}</CardTitle>
<CardDescription>{t('p2pMerchant.dailyTrades30d')}</CardDescription>
</CardHeader>
<CardContent>
<ResponsiveContainer width="100%" height={250}>
@@ -476,7 +478,7 @@ export default function P2PMerchantDashboard() {
{/* Quick Stats */}
<Card>
<CardHeader>
<CardTitle className="text-lg">Lifetime Statistics</CardTitle>
<CardTitle className="text-lg">{t('p2pMerchant.lifetimeStats')}</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
@@ -484,23 +486,23 @@ export default function P2PMerchantDashboard() {
<p className="text-2xl font-bold">
${stats?.total_volume_lifetime?.toLocaleString() || '0'}
</p>
<p className="text-sm text-muted-foreground">Total Volume</p>
<p className="text-sm text-muted-foreground">{t('p2pMerchant.totalVolume')}</p>
</div>
<div className="text-center p-4 bg-muted rounded-lg">
<p className="text-2xl font-bold">{stats?.total_trades_lifetime || 0}</p>
<p className="text-sm text-muted-foreground">Total Trades</p>
<p className="text-sm text-muted-foreground">{t('p2pMerchant.totalTrades')}</p>
</div>
<div className="text-center p-4 bg-muted rounded-lg">
<p className="text-2xl font-bold">
${stats?.buy_volume_30d?.toLocaleString() || '0'}
</p>
<p className="text-sm text-muted-foreground">Buy Volume (30d)</p>
<p className="text-sm text-muted-foreground">{t('p2pMerchant.buyVolume30d')}</p>
</div>
<div className="text-center p-4 bg-muted rounded-lg">
<p className="text-2xl font-bold">
${stats?.sell_volume_30d?.toLocaleString() || '0'}
</p>
<p className="text-sm text-muted-foreground">Sell Volume (30d)</p>
<p className="text-sm text-muted-foreground">{t('p2pMerchant.sellVolume30d')}</p>
</div>
</div>
</CardContent>
@@ -511,14 +513,14 @@ export default function P2PMerchantDashboard() {
<TabsContent value="ads" className="space-y-4">
<div className="flex items-center justify-between">
<h2 className="text-lg font-semibold">
Active Advertisements ({activeAds.length})
{t('p2pMerchant.activeAds', { count: activeAds.length })}
</h2>
<Button
className="bg-kurdish-green hover:bg-kurdish-green-dark"
onClick={() => setCreateAdOpen(true)}
>
<Plus className="h-4 w-4 mr-1" />
Create New Ad
{t('p2pMerchant.createNewAd')}
</Button>
</div>
@@ -527,11 +529,11 @@ export default function P2PMerchantDashboard() {
<CardContent className="py-12 text-center">
<ShoppingBag className="h-12 w-12 mx-auto text-muted-foreground/50 mb-4" />
<p className="text-muted-foreground mb-4">
You don&apos;t have any active ads yet.
{t('p2pMerchant.noAdsYet')}
</p>
<Button onClick={() => setCreateAdOpen(true)}>
<Plus className="h-4 w-4 mr-1" />
Create Your First Ad
{t('p2pMerchant.createFirstAd')}
</Button>
</CardContent>
</Card>
@@ -548,17 +550,17 @@ export default function P2PMerchantDashboard() {
{ad.status.toUpperCase()}
</Badge>
<span className="font-medium">
Sell {ad.token} for {ad.fiat_currency}
{t('p2pMerchant.sellTokenFor', { token: ad.token, currency: ad.fiat_currency })}
</span>
{ad.is_featured && (
<Badge className="bg-yellow-500/20 text-yellow-500 border-yellow-500/30">
<Star className="h-3 w-3 mr-1" />
Featured
{t('p2pMerchant.featured')}
</Badge>
)}
</div>
<p className="text-sm text-muted-foreground">
{ad.remaining_amount} / {ad.amount_crypto} {ad.token} remaining
{t('p2pMerchant.remaining', { remaining: ad.remaining_amount, total: ad.amount_crypto, token: ad.token })}
</p>
</div>
</div>
@@ -568,7 +570,7 @@ export default function P2PMerchantDashboard() {
{ad.price_per_unit?.toFixed(2)} {ad.fiat_currency}/{ad.token}
</p>
<p className="text-sm text-muted-foreground">
Total: {ad.fiat_amount?.toLocaleString()} {ad.fiat_currency}
{t('p2pMerchant.total', { amount: ad.fiat_amount?.toLocaleString(), currency: ad.fiat_currency })}
</p>
</div>
@@ -577,7 +579,7 @@ export default function P2PMerchantDashboard() {
variant="ghost"
size="icon"
onClick={() => toggleAdStatus(ad.id, ad.status)}
title={ad.status === 'open' ? 'Pause' : 'Activate'}
title={ad.status === 'open' ? t('p2pMerchant.pause') : t('p2pMerchant.activate')}
>
{ad.status === 'open' ? (
<Pause className="h-4 w-4" />
@@ -589,7 +591,7 @@ export default function P2PMerchantDashboard() {
variant="ghost"
size="icon"
onClick={() => openEditModal(ad)}
title="Edit"
title={t('p2pMerchant.edit')}
>
<Edit className="h-4 w-4" />
</Button>
@@ -597,7 +599,7 @@ export default function P2PMerchantDashboard() {
variant="ghost"
size="icon"
onClick={() => deleteAd(ad.id)}
title="Delete"
title={t('p2pMerchant.delete')}
className="text-destructive hover:text-destructive"
>
<Trash2 className="h-4 w-4" />
@@ -616,13 +618,13 @@ export default function P2PMerchantDashboard() {
<CardContent className="py-4">
<div className="flex items-center justify-between text-sm">
<span className="text-muted-foreground">
Active ads: {activeAds.filter(a => a.status === 'open').length} / {tierInfo.max_pending_orders}
{t('p2pMerchant.activeAdsCount', { current: activeAds.filter(a => a.status === 'open').length, max: tierInfo.max_pending_orders })}
</span>
<span className="text-muted-foreground">
Max order: ${tierInfo.max_order_amount.toLocaleString()}
{t('p2pMerchant.maxOrderLimit', { amount: tierInfo.max_order_amount.toLocaleString() })}
</span>
<span className="text-muted-foreground">
Featured ads: {activeAds.filter(a => a.is_featured).length} / {tierInfo.featured_ads_allowed}
{t('p2pMerchant.featuredAdsCount', { current: activeAds.filter(a => a.is_featured).length, max: tierInfo.featured_ads_allowed })}
</span>
</div>
</CardContent>
@@ -642,16 +644,16 @@ export default function P2PMerchantDashboard() {
<CardHeader>
<CardTitle className="text-lg flex items-center gap-2">
<MessageSquare className="h-5 w-5" />
Auto-Reply Message
{t('p2pMerchant.autoReplyTitle')}
</CardTitle>
<CardDescription>
Automatically send this message when someone starts a trade with you
{t('p2pMerchant.autoReplyDesc')}
</CardDescription>
</CardHeader>
<CardContent>
<Button variant="outline" onClick={() => setAutoReplyOpen(true)}>
<Edit className="h-4 w-4 mr-2" />
Configure Auto-Reply
{t('p2pMerchant.configureAutoReply')}
</Button>
</CardContent>
</Card>
@@ -659,32 +661,32 @@ export default function P2PMerchantDashboard() {
{/* Notification Settings */}
<Card>
<CardHeader>
<CardTitle className="text-lg">Notification Settings</CardTitle>
<CardTitle className="text-lg">{t('p2pMerchant.notificationSettings')}</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-center justify-between">
<div>
<p className="font-medium">New Order Notifications</p>
<p className="font-medium">{t('p2pMerchant.newOrderNotifications')}</p>
<p className="text-sm text-muted-foreground">
Get notified when someone accepts your offer
{t('p2pMerchant.newOrderNotificationsDesc')}
</p>
</div>
<Switch defaultChecked />
</div>
<div className="flex items-center justify-between">
<div>
<p className="font-medium">Payment Notifications</p>
<p className="font-medium">{t('p2pMerchant.paymentNotifications')}</p>
<p className="text-sm text-muted-foreground">
Get notified when buyer marks payment as sent
{t('p2pMerchant.paymentNotificationsDesc')}
</p>
</div>
<Switch defaultChecked />
</div>
<div className="flex items-center justify-between">
<div>
<p className="font-medium">Chat Notifications</p>
<p className="font-medium">{t('p2pMerchant.chatNotifications')}</p>
<p className="text-sm text-muted-foreground">
Get notified for new chat messages
{t('p2pMerchant.chatNotificationsDesc')}
</p>
</div>
<Switch defaultChecked />
@@ -695,17 +697,17 @@ export default function P2PMerchantDashboard() {
{/* Danger Zone */}
<Card className="border-destructive/30">
<CardHeader>
<CardTitle className="text-lg text-destructive">Danger Zone</CardTitle>
<CardTitle className="text-lg text-destructive">{t('p2pMerchant.dangerZone')}</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-center justify-between">
<div>
<p className="font-medium">Pause All Ads</p>
<p className="font-medium">{t('p2pMerchant.pauseAllAds')}</p>
<p className="text-sm text-muted-foreground">
Temporarily pause all your active advertisements
{t('p2pMerchant.pauseAllAdsDesc')}
</p>
</div>
<Button variant="outline">Pause All</Button>
<Button variant="outline">{t('p2pMerchant.pauseAll')}</Button>
</div>
</CardContent>
</Card>
@@ -716,25 +718,25 @@ export default function P2PMerchantDashboard() {
<Dialog open={autoReplyOpen} onOpenChange={setAutoReplyOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>Auto-Reply Message</DialogTitle>
<DialogTitle>{t('p2pMerchant.autoReplyDialogTitle')}</DialogTitle>
<DialogDescription>
This message will be automatically sent when someone starts a trade with you.
{t('p2pMerchant.autoReplyDialogDesc')}
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div>
<Label>Message</Label>
<Label>{t('p2pMerchant.messageLabel')}</Label>
<Textarea
value={autoReplyMessage}
onChange={(e) => setAutoReplyMessage(e.target.value)}
placeholder="Hello! Thank you for choosing to trade with me. Please follow the payment instructions and mark as paid once done."
placeholder={t('p2pMerchant.autoReplyPlaceholder')}
rows={4}
/>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setAutoReplyOpen(false)}>
Cancel
{t('p2p.cancel')}
</Button>
<Button
className="bg-kurdish-green hover:bg-kurdish-green-dark"
@@ -742,7 +744,7 @@ export default function P2PMerchantDashboard() {
disabled={savingAutoReply}
>
{savingAutoReply && <Loader2 className="h-4 w-4 mr-2 animate-spin" />}
Save Message
{t('p2pMerchant.saveMessage')}
</Button>
</DialogFooter>
</DialogContent>
@@ -752,9 +754,9 @@ export default function P2PMerchantDashboard() {
<Dialog open={createAdOpen} onOpenChange={setCreateAdOpen}>
<DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>Create New P2P Offer</DialogTitle>
<DialogTitle>{t('p2pMerchant.createNewOffer')}</DialogTitle>
<DialogDescription>
Lock your crypto in escrow and set your price
{t('p2pCreate.description')}
</DialogDescription>
</DialogHeader>
<CreateAd onAdCreated={() => {
@@ -768,9 +770,9 @@ export default function P2PMerchantDashboard() {
<Dialog open={editAdOpen} onOpenChange={setEditAdOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>Edit Ad</DialogTitle>
<DialogTitle>{t('p2pMerchant.editAdTitle')}</DialogTitle>
<DialogDescription>
Update your ad price and order limits
{t('p2pMerchant.editAdDesc')}
</DialogDescription>
</DialogHeader>
{editingAd && (
@@ -778,31 +780,31 @@ export default function P2PMerchantDashboard() {
{/* Ad Info (Read-only) */}
<div className="p-4 bg-muted rounded-lg">
<div className="flex items-center justify-between mb-2">
<span className="text-sm text-muted-foreground">Ad Type</span>
<span className="text-sm text-muted-foreground">{t('p2pMerchant.adType')}</span>
<Badge variant={editingAd.ad_type === 'sell' ? 'default' : 'secondary'}>
{editingAd.ad_type === 'sell' ? 'Selling' : 'Buying'} {editingAd.token}
{editingAd.ad_type === 'sell' ? t('p2pMerchant.selling') : t('p2pMerchant.buying')} {editingAd.token}
</Badge>
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground">Amount</span>
<span className="text-sm text-muted-foreground">{t('p2p.amount')}</span>
<span className="font-medium">{editingAd.amount_crypto} {editingAd.token}</span>
</div>
</div>
{/* Fiat Amount */}
<div>
<Label htmlFor="editFiatAmount">Total Fiat Amount ({editingAd.fiat_currency})</Label>
<Label htmlFor="editFiatAmount">{t('p2pMerchant.totalFiatAmount', { currency: editingAd.fiat_currency })}</Label>
<Input
id="editFiatAmount"
type="number"
step="0.01"
value={editFiatAmount}
onChange={(e) => setEditFiatAmount(e.target.value)}
placeholder="Enter fiat amount"
placeholder={t('p2pMerchant.enterFiatAmount')}
/>
{editFiatAmount && editingAd.amount_crypto > 0 && (
<p className="text-sm text-muted-foreground mt-1">
Price per {editingAd.token}: {(parseFloat(editFiatAmount) / editingAd.amount_crypto).toFixed(2)} {editingAd.fiat_currency}
{t('p2pMerchant.pricePerTokenCalc', { token: editingAd.token, price: (parseFloat(editFiatAmount) / editingAd.amount_crypto).toFixed(2), currency: editingAd.fiat_currency })}
</p>
)}
</div>
@@ -810,25 +812,25 @@ export default function P2PMerchantDashboard() {
{/* Order Limits */}
<div className="grid grid-cols-2 gap-4">
<div>
<Label htmlFor="editMinOrder">Min Order ({editingAd.token})</Label>
<Label htmlFor="editMinOrder">{t('p2pMerchant.minOrderLabel', { token: editingAd.token })}</Label>
<Input
id="editMinOrder"
type="number"
step="0.01"
value={editMinOrder}
onChange={(e) => setEditMinOrder(e.target.value)}
placeholder="Optional"
placeholder={t('p2pMerchant.optional')}
/>
</div>
<div>
<Label htmlFor="editMaxOrder">Max Order ({editingAd.token})</Label>
<Label htmlFor="editMaxOrder">{t('p2pMerchant.maxOrderLabel', { token: editingAd.token })}</Label>
<Input
id="editMaxOrder"
type="number"
step="0.01"
value={editMaxOrder}
onChange={(e) => setEditMaxOrder(e.target.value)}
placeholder="Optional"
placeholder={t('p2pMerchant.optional')}
/>
</div>
</div>
@@ -836,7 +838,7 @@ export default function P2PMerchantDashboard() {
)}
<DialogFooter>
<Button variant="outline" onClick={() => setEditAdOpen(false)}>
Cancel
{t('p2p.cancel')}
</Button>
<Button
className="bg-kurdish-green hover:bg-kurdish-green-dark"
@@ -844,7 +846,7 @@ export default function P2PMerchantDashboard() {
disabled={savingEdit}
>
{savingEdit && <Loader2 className="h-4 w-4 mr-2 animate-spin" />}
Save Changes
{t('p2pMerchant.saveChanges')}
</Button>
</DialogFooter>
</DialogContent>
+30 -28
View File
@@ -1,5 +1,6 @@
import React, { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { Card, CardContent } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
@@ -32,6 +33,7 @@ interface TradeWithOffer extends P2PFiatTrade {
export default function P2POrders() {
const navigate = useNavigate();
const { t } = useTranslation();
const { user } = useAuth();
const [trades, setTrades] = useState<TradeWithOffer[]>([]);
@@ -75,7 +77,7 @@ export default function P2POrders() {
setTrades(tradesWithOffers);
} catch (error) {
console.error('Fetch trades error:', error);
toast.error('Failed to load trades');
toast.error(t('p2pOrders.failedToLoad'));
} finally {
setLoading(false);
}
@@ -102,42 +104,42 @@ export default function P2POrders() {
return (
<Badge className="bg-yellow-500/20 text-yellow-400 border-yellow-500/30">
<Clock className="w-3 h-3 mr-1" />
Awaiting Payment
{t('p2pOrders.awaitingPayment')}
</Badge>
);
case 'payment_sent':
return (
<Badge className="bg-blue-500/20 text-blue-400 border-blue-500/30">
<Clock className="w-3 h-3 mr-1" />
Payment Sent
{t('p2pOrders.paymentSent')}
</Badge>
);
case 'completed':
return (
<Badge className="bg-green-500/20 text-green-400 border-green-500/30">
<CheckCircle2 className="w-3 h-3 mr-1" />
Completed
{t('p2pOrders.completed')}
</Badge>
);
case 'cancelled':
return (
<Badge className="bg-gray-500/20 text-gray-400 border-gray-500/30">
<XCircle className="w-3 h-3 mr-1" />
Cancelled
{t('p2pOrders.cancelled')}
</Badge>
);
case 'disputed':
return (
<Badge className="bg-red-500/20 text-red-400 border-red-500/30">
<AlertTriangle className="w-3 h-3 mr-1" />
Disputed
{t('p2pOrders.disputed')}
</Badge>
);
case 'refunded':
return (
<Badge className="bg-purple-500/20 text-purple-400 border-purple-500/30">
<RefreshCw className="w-3 h-3 mr-1" />
Refunded
{t('p2pOrders.refunded')}
</Badge>
);
default:
@@ -154,13 +156,13 @@ export default function P2POrders() {
const timeAgo = (date: string) => {
const seconds = Math.floor((Date.now() - new Date(date).getTime()) / 1000);
if (seconds < 60) return 'Just now';
if (seconds < 60) return t('p2p.justNow');
const minutes = Math.floor(seconds / 60);
if (minutes < 60) return `${minutes}m ago`;
if (minutes < 60) return t('p2p.minutesAgo', { count: minutes });
const hours = Math.floor(minutes / 60);
if (hours < 24) return `${hours}h ago`;
if (hours < 24) return t('p2p.hoursAgo', { count: hours });
const days = Math.floor(hours / 24);
return `${days}d ago`;
return t('p2p.daysAgo', { count: days });
};
return (
@@ -187,7 +189,7 @@ export default function P2POrders() {
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1">
<span className={`font-semibold ${isBuyer ? 'text-green-400' : 'text-red-400'}`}>
{isBuyer ? 'Buy' : 'Sell'}
{isBuyer ? t('p2p.buy') : t('p2p.sell')}
</span>
<span className="text-white font-medium">
{trade.crypto_amount} {trade.offer?.token || 'HEZ'}
@@ -225,7 +227,7 @@ export default function P2POrders() {
<div className="flex items-center gap-2 text-yellow-400 text-sm">
<Clock className="w-4 h-4" />
<span>
Payment deadline: {new Date(trade.payment_deadline).toLocaleTimeString()}
{t('p2pOrders.paymentDeadline', { time: new Date(trade.payment_deadline).toLocaleTimeString() })}
</span>
</div>
</div>
@@ -249,9 +251,9 @@ export default function P2POrders() {
<Card className="bg-gray-900 border-gray-800">
<CardContent className="py-12 text-center">
<AlertTriangle className="w-16 h-16 text-yellow-500 mx-auto mb-4" />
<h2 className="text-xl font-semibold text-white mb-2">Login Required</h2>
<p className="text-gray-400 mb-6">Please log in to view your P2P trades.</p>
<Button onClick={() => navigate('/login')}>Log In</Button>
<h2 className="text-xl font-semibold text-white mb-2">{t('p2p.loginRequired')}</h2>
<p className="text-gray-400 mb-6">{t('p2p.loginToView')}</p>
<Button onClick={() => navigate('/login')}>{t('p2p.logIn')}</Button>
</CardContent>
</Card>
</div>
@@ -269,7 +271,7 @@ export default function P2POrders() {
className="text-gray-400 hover:text-white"
>
<ArrowLeft className="w-4 h-4 mr-2" />
P2P Trading
{t('p2p.p2pTrading')}
</Button>
<div className="flex-1" />
<Button
@@ -285,8 +287,8 @@ export default function P2POrders() {
{/* Title */}
<div className="mb-6">
<h1 className="text-3xl font-bold text-white">My Trades</h1>
<p className="text-gray-400">View and manage your P2P trades</p>
<h1 className="text-3xl font-bold text-white">{t('p2pOrders.title')}</h1>
<p className="text-gray-400">{t('p2pOrders.subtitle')}</p>
</div>
{/* Stats Cards */}
@@ -294,19 +296,19 @@ export default function P2POrders() {
<Card className="bg-gray-900 border-gray-800">
<CardContent className="py-4 text-center">
<p className="text-2xl font-bold text-yellow-400">{activeTrades.length}</p>
<p className="text-sm text-gray-400">Active</p>
<p className="text-sm text-gray-400">{t('p2pOrders.active')}</p>
</CardContent>
</Card>
<Card className="bg-gray-900 border-gray-800">
<CardContent className="py-4 text-center">
<p className="text-2xl font-bold text-green-400">{completedTrades.length}</p>
<p className="text-sm text-gray-400">Completed</p>
<p className="text-sm text-gray-400">{t('p2pOrders.completed')}</p>
</CardContent>
</Card>
<Card className="bg-gray-900 border-gray-800">
<CardContent className="py-4 text-center">
<p className="text-2xl font-bold text-gray-400">{cancelledTrades.length}</p>
<p className="text-sm text-gray-400">Cancelled</p>
<p className="text-sm text-gray-400">{t('p2pOrders.cancelled')}</p>
</CardContent>
</Card>
</div>
@@ -315,15 +317,15 @@ export default function P2POrders() {
<Tabs value={activeTab} onValueChange={setActiveTab}>
<TabsList className="grid w-full grid-cols-3 mb-6">
<TabsTrigger value="active" className="relative">
Active
{t('p2pOrders.active')}
{activeTrades.length > 0 && (
<span className="ml-2 px-1.5 py-0.5 text-xs bg-yellow-500 text-black rounded-full">
{activeTrades.length}
</span>
)}
</TabsTrigger>
<TabsTrigger value="completed">Completed</TabsTrigger>
<TabsTrigger value="cancelled">Cancelled</TabsTrigger>
<TabsTrigger value="completed">{t('p2pOrders.completed')}</TabsTrigger>
<TabsTrigger value="cancelled">{t('p2pOrders.cancelled')}</TabsTrigger>
</TabsList>
{loading ? (
@@ -334,21 +336,21 @@ export default function P2POrders() {
<>
<TabsContent value="active" className="space-y-4">
{activeTrades.length === 0
? renderEmptyState('No active trades')
? renderEmptyState(t('p2pOrders.noActiveTrades'))
: activeTrades.map(renderTradeCard)
}
</TabsContent>
<TabsContent value="completed" className="space-y-4">
{completedTrades.length === 0
? renderEmptyState('No completed trades')
? renderEmptyState(t('p2pOrders.noCompletedTrades'))
: completedTrades.map(renderTradeCard)
}
</TabsContent>
<TabsContent value="cancelled" className="space-y-4">
{cancelledTrades.length === 0
? renderEmptyState('No cancelled trades')
? renderEmptyState(t('p2pOrders.noCancelledTrades'))
: cancelledTrades.map(renderTradeCard)
}
</TabsContent>
+66 -66
View File
@@ -1,5 +1,6 @@
import React, { useState, useEffect, useCallback } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
@@ -72,6 +73,7 @@ interface TimelineStep {
export default function P2PTrade() {
const { tradeId } = useParams<{ tradeId: string }>();
const navigate = useNavigate();
const { t } = useTranslation();
const { user } = useAuth();
const { api, selectedAccount } = usePezkuwi();
@@ -152,7 +154,7 @@ export default function P2PTrade() {
});
} catch (error) {
console.error('Fetch trade error:', error);
toast.error('Failed to load trade details');
toast.error(t('p2pTrade.failedToLoad'));
} finally {
setLoading(false);
}
@@ -204,7 +206,7 @@ export default function P2PTrade() {
if (remaining === 0 && trade.status === 'pending') {
// Auto-cancel logic could go here
toast.warning('Payment deadline expired');
toast.warning(t('p2pTrade.paymentDeadlineExpired'));
}
};
@@ -215,7 +217,7 @@ export default function P2PTrade() {
// Format time remaining
const formatTimeRemaining = (seconds: number): string => {
if (seconds <= 0) return 'Expired';
if (seconds <= 0) return t('p2pTrade.expired');
const mins = Math.floor(seconds / 60);
const secs = seconds % 60;
return `${mins}:${secs.toString().padStart(2, '0')}`;
@@ -227,30 +229,30 @@ export default function P2PTrade() {
const steps: TimelineStep[] = [
{
id: 'created',
label: 'Order Created',
description: 'Trade initiated',
label: t('p2pTrade.orderCreated'),
description: t('p2pTrade.tradeInitiatedStep'),
status: 'completed',
timestamp: trade?.created_at,
},
{
id: 'pending',
label: 'Awaiting Payment',
description: isBuyer ? 'Send payment to seller' : 'Waiting for buyer payment',
label: t('p2pTrade.awaitingPayment'),
description: isBuyer ? t('p2pTrade.sendPaymentToSeller') : t('p2pTrade.waitingForBuyerPayment'),
status: status === 'pending' ? 'current' :
['payment_sent', 'completed'].includes(status) ? 'completed' : 'pending',
},
{
id: 'payment_sent',
label: 'Payment Sent',
description: isSeller ? 'Verify and release crypto' : 'Waiting for confirmation',
label: t('p2pTrade.paymentSent'),
description: isSeller ? t('p2pTrade.verifyAndRelease') : t('p2pTrade.waitingForConfirmation'),
status: status === 'payment_sent' ? 'current' :
status === 'completed' ? 'completed' : 'pending',
timestamp: trade?.buyer_marked_paid_at,
},
{
id: 'completed',
label: 'Completed',
description: 'Crypto released to buyer',
label: t('p2pTrade.completedStep'),
description: t('p2pTrade.cryptoReleased'),
status: status === 'completed' ? 'completed' : 'pending',
timestamp: trade?.completed_at,
},
@@ -278,7 +280,7 @@ export default function P2PTrade() {
setShowProofModal(false);
setPaymentProof(null);
setPaymentReference('');
toast.success('Payment marked as sent');
toast.success(t('p2pTrade.paymentMarkedSent'));
fetchTrade();
} catch (error) {
console.error('Mark as paid error:', error);
@@ -290,14 +292,14 @@ export default function P2PTrade() {
// Handle release crypto
const handleReleaseCrypto = async () => {
if (!trade || !api || !selectedAccount) {
toast.error('Please connect your wallet');
toast.error(t('p2p.connectWallet'));
return;
}
setActionLoading(true);
try {
await confirmPaymentReceived(api, selectedAccount, trade.id);
toast.success('Crypto released to buyer!');
toast.success(t('p2pTrade.cryptoReleasedToast'));
fetchTrade();
} catch (error) {
console.error('Release crypto error:', error);
@@ -333,11 +335,11 @@ export default function P2PTrade() {
.eq('id', trade.offer_id);
setShowCancelModal(false);
toast.success('Trade cancelled');
toast.success(t('p2pTrade.tradeCancelledToast'));
fetchTrade();
} catch (error) {
console.error('Cancel trade error:', error);
toast.error('Failed to cancel trade');
toast.error(t('p2pTrade.failedToCancel'));
} finally {
setActionLoading(false);
}
@@ -346,7 +348,7 @@ export default function P2PTrade() {
// Copy to clipboard
const copyToClipboard = (text: string, label: string) => {
navigator.clipboard.writeText(text);
toast.success(`${label} copied!`);
toast.success(t('p2pTrade.addressCopied', { label }));
};
// Render loading state
@@ -367,9 +369,9 @@ export default function P2PTrade() {
<Card className="bg-gray-900 border-gray-800">
<CardContent className="py-12 text-center">
<XCircle className="w-16 h-16 text-red-500 mx-auto mb-4" />
<h2 className="text-xl font-semibold text-white mb-2">Trade Not Found</h2>
<p className="text-gray-400 mb-6">This trade does not exist or you do not have access.</p>
<Button onClick={() => navigate('/p2p')}>Back to P2P</Button>
<h2 className="text-xl font-semibold text-white mb-2">{t('p2pTrade.tradeNotFound')}</h2>
<p className="text-gray-400 mb-6">{t('p2pTrade.tradeNotFoundDesc')}</p>
<Button onClick={() => navigate('/p2p')}>{t('p2pTrade.backToP2P')}</Button>
</CardContent>
</Card>
</div>
@@ -390,8 +392,8 @@ export default function P2PTrade() {
};
const counterparty = isSeller ?
{ id: trade.buyer_id, wallet: trade.buyer_wallet, reputation: trade.buyer_reputation, label: 'Buyer' } :
{ id: trade.seller_id, wallet: trade.offer?.seller_wallet || '', reputation: trade.seller_reputation, label: 'Seller' };
{ id: trade.buyer_id, wallet: trade.buyer_wallet, reputation: trade.buyer_reputation, label: t('p2p.buyer') } :
{ id: trade.seller_id, wallet: trade.offer?.seller_wallet || '', reputation: trade.seller_reputation, label: t('p2p.seller') };
return (
<div className="container mx-auto px-4 py-8 max-w-4xl">
@@ -404,7 +406,7 @@ export default function P2PTrade() {
className="text-gray-400 hover:text-white"
>
<ArrowLeft className="w-4 h-4 mr-2" />
My Trades
{t('p2p.myTrades')}
</Button>
<div className="flex-1" />
<Button
@@ -423,7 +425,7 @@ export default function P2PTrade() {
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<CardTitle className="text-white">
{isBuyer ? 'Buy' : 'Sell'} {trade.offer?.token || 'HEZ'}
{isBuyer ? t('p2p.buy') : t('p2p.sell')} {trade.offer?.token || 'HEZ'}
</CardTitle>
<Badge className={getStatusColor(trade.status as TradeStatus)}>
{trade.status.replace('_', ' ').toUpperCase()}
@@ -442,25 +444,25 @@ export default function P2PTrade() {
<CardContent>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<div>
<p className="text-sm text-gray-400">Amount</p>
<p className="text-sm text-gray-400">{t('p2p.amount')}</p>
<p className="text-lg font-semibold text-white">
{trade.crypto_amount} {trade.offer?.token || 'HEZ'}
</p>
</div>
<div>
<p className="text-sm text-gray-400">Price</p>
<p className="text-sm text-gray-400">{t('p2p.price')}</p>
<p className="text-lg font-semibold text-green-400">
{trade.fiat_amount.toFixed(2)} {trade.offer?.fiat_currency || 'TRY'}
</p>
</div>
<div>
<p className="text-sm text-gray-400">Unit Price</p>
<p className="text-sm text-gray-400">{t('p2pTrade.unitPrice')}</p>
<p className="text-lg font-semibold text-white">
{trade.price_per_unit.toFixed(2)} {trade.offer?.fiat_currency || 'TRY'}
</p>
</div>
<div>
<p className="text-sm text-gray-400">Payment Method</p>
<p className="text-sm text-gray-400">{t('p2pTrade.paymentMethod')}</p>
<p className="text-lg font-semibold text-white">
{trade.payment_method_name}
</p>
@@ -472,7 +474,7 @@ export default function P2PTrade() {
{/* Timeline */}
<Card className="bg-gray-900 border-gray-800 mb-6">
<CardHeader>
<CardTitle className="text-white text-lg">Trade Progress</CardTitle>
<CardTitle className="text-white text-lg">{t('p2pTrade.tradeProgress')}</CardTitle>
</CardHeader>
<CardContent>
<div className="relative">
@@ -523,7 +525,7 @@ export default function P2PTrade() {
{/* Counterparty Info */}
<Card className="bg-gray-900 border-gray-800 mb-6">
<CardHeader>
<CardTitle className="text-white text-lg">{counterparty.label} Information</CardTitle>
<CardTitle className="text-white text-lg">{t('p2pTrade.counterpartyInfo', { role: counterparty.label })}</CardTitle>
</CardHeader>
<CardContent>
<div className="flex items-center gap-4">
@@ -546,16 +548,16 @@ export default function P2PTrade() {
<Copy className="w-3 h-3" />
</Button>
{counterparty.reputation?.verified_merchant && (
<Shield className="w-4 h-4 text-blue-400" title="Verified Merchant" />
<Shield className="w-4 h-4 text-blue-400" title={t('p2p.verifiedMerchant')} />
)}
{counterparty.reputation?.fast_trader && (
<Zap className="w-4 h-4 text-yellow-400" title="Fast Trader" />
<Zap className="w-4 h-4 text-yellow-400" title={t('p2p.fastTrader')} />
)}
</div>
{counterparty.reputation && (
<p className="text-sm text-gray-400">
{counterparty.reputation.completed_trades} trades
{' '}{((counterparty.reputation.completed_trades / (counterparty.reputation.total_trades || 1)) * 100).toFixed(0)}% completion
{t('p2p.trades', { count: counterparty.reputation.completed_trades })}
{' '}{t('p2p.completion', { percent: ((counterparty.reputation.completed_trades / (counterparty.reputation.total_trades || 1)) * 100).toFixed(0) })}
</p>
)}
</div>
@@ -567,14 +569,13 @@ export default function P2PTrade() {
{isBuyer && trade.status === 'pending' && trade.payment_details && Object.keys(trade.payment_details).length > 0 && (
<Card className="bg-gray-900 border-gray-800 mb-6">
<CardHeader>
<CardTitle className="text-white text-lg">Payment Details</CardTitle>
<CardTitle className="text-white text-lg">{t('p2pTrade.paymentDetailsTitle')}</CardTitle>
</CardHeader>
<CardContent>
<Alert className="bg-yellow-500/10 border-yellow-500/30 mb-4">
<AlertTriangle className="h-4 w-4 text-yellow-400" />
<AlertDescription className="text-yellow-200">
Send exactly <strong>{trade.fiat_amount.toFixed(2)} {trade.offer?.fiat_currency}</strong> to the account below.
Do not include any cryptocurrency references in your payment.
{t('p2pTrade.sendExactly', { amount: trade.fiat_amount.toFixed(2), currency: trade.offer?.fiat_currency })}
</AlertDescription>
</Alert>
<div className="space-y-3">
@@ -604,7 +605,7 @@ export default function P2PTrade() {
{trade.buyer_payment_proof_url && (
<Card className="bg-gray-900 border-gray-800 mb-6">
<CardHeader>
<CardTitle className="text-white text-lg">Payment Proof</CardTitle>
<CardTitle className="text-white text-lg">{t('p2pTrade.paymentProof')}</CardTitle>
</CardHeader>
<CardContent>
<a
@@ -614,7 +615,7 @@ export default function P2PTrade() {
className="flex items-center gap-2 text-blue-400 hover:text-blue-300"
>
<ExternalLink className="w-4 h-4" />
View Payment Proof
{t('p2pTrade.viewPaymentProof')}
</a>
</CardContent>
</Card>
@@ -634,7 +635,7 @@ export default function P2PTrade() {
disabled={actionLoading}
>
<CheckCircle2 className="w-4 h-4 mr-2" />
I Have Paid
{t('p2pTrade.iHavePaid')}
</Button>
<Button
variant="outline"
@@ -643,7 +644,7 @@ export default function P2PTrade() {
disabled={actionLoading}
>
<Ban className="w-4 h-4 mr-2" />
Cancel Trade
{t('p2pTrade.cancelTrade')}
</Button>
</>
)}
@@ -661,7 +662,7 @@ export default function P2PTrade() {
) : (
<CheckCircle2 className="w-4 h-4 mr-2" />
)}
Release Crypto
{t('p2pTrade.releaseCrypto')}
</Button>
<Button
variant="outline"
@@ -670,7 +671,7 @@ export default function P2PTrade() {
disabled={actionLoading}
>
<AlertTriangle className="w-4 h-4 mr-2" />
Open Dispute
{t('p2pTrade.openDispute')}
</Button>
</>
)}
@@ -682,7 +683,7 @@ export default function P2PTrade() {
onClick={() => setShowChat(!showChat)}
>
<MessageSquare className="w-4 h-4 mr-2" />
{showChat ? 'Hide Chat' : 'Chat'}
{showChat ? t('p2pTrade.hideChat') : t('p2pTrade.chat')}
</Button>
</div>
</CardContent>
@@ -706,11 +707,11 @@ export default function P2PTrade() {
<Card className="bg-green-500/10 border-green-500/30 mb-6">
<CardContent className="py-6 text-center">
<CheckCircle2 className="w-16 h-16 text-green-500 mx-auto mb-4" />
<h3 className="text-xl font-semibold text-green-400 mb-2">Trade Completed!</h3>
<h3 className="text-xl font-semibold text-green-400 mb-2">{t('p2pTrade.tradeCompleted')}</h3>
<p className="text-gray-400 mb-4">
{isBuyer
? `You received ${trade.crypto_amount} ${trade.offer?.token}`
: `You received ${trade.fiat_amount.toFixed(2)} ${trade.offer?.fiat_currency}`
? t('p2pTrade.youReceived', { amount: trade.crypto_amount, token: trade.offer?.token })
: t('p2pTrade.youReceivedFiat', { amount: trade.fiat_amount.toFixed(2), currency: trade.offer?.fiat_currency })
}
</p>
<Button
@@ -718,7 +719,7 @@ export default function P2PTrade() {
className="bg-yellow-500 hover:bg-yellow-600 text-black"
>
<Star className="w-4 h-4 mr-2" />
Rate This Trade
{t('p2pTrade.rateTrade')}
</Button>
</CardContent>
</Card>
@@ -729,9 +730,9 @@ export default function P2PTrade() {
<Card className="bg-gray-500/10 border-gray-500/30 mb-6">
<CardContent className="py-6 text-center">
<XCircle className="w-16 h-16 text-gray-500 mx-auto mb-4" />
<h3 className="text-xl font-semibold text-gray-400 mb-2">Trade Cancelled</h3>
<h3 className="text-xl font-semibold text-gray-400 mb-2">{t('p2pTrade.tradeCancelled')}</h3>
{trade.cancel_reason && (
<p className="text-gray-500">Reason: {trade.cancel_reason}</p>
<p className="text-gray-500">{t('p2pTrade.cancelReason', { reason: trade.cancel_reason })}</p>
)}
</CardContent>
</Card>
@@ -741,30 +742,30 @@ export default function P2PTrade() {
<Dialog open={showProofModal} onOpenChange={setShowProofModal}>
<DialogContent className="bg-gray-900 border-gray-800">
<DialogHeader>
<DialogTitle className="text-white">Confirm Payment</DialogTitle>
<DialogTitle className="text-white">{t('p2pTrade.confirmPayment')}</DialogTitle>
<DialogDescription className="text-gray-400">
Please confirm you have sent the payment to the seller.
{t('p2pTrade.confirmPaymentDesc')}
</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-4">
<div>
<Label htmlFor="reference">Payment Reference (Optional)</Label>
<Label htmlFor="reference">{t('p2pTrade.paymentReference')}</Label>
<Input
id="reference"
value={paymentReference}
onChange={(e) => setPaymentReference(e.target.value)}
placeholder="Transaction ID or reference number"
placeholder={t('p2pTrade.paymentReferencePlaceholder')}
className="bg-gray-800 border-gray-700"
/>
</div>
<div>
<Label>Payment Proof (Optional)</Label>
<Label>{t('p2pTrade.paymentProofOptional')}</Label>
<div className="mt-2">
<label className="flex flex-col items-center justify-center w-full h-32 border-2 border-gray-700 border-dashed rounded-lg cursor-pointer hover:bg-gray-800">
<div className="flex flex-col items-center justify-center pt-5 pb-6">
<Upload className="w-8 h-8 text-gray-400 mb-2" />
<p className="text-sm text-gray-400">
{paymentProof ? paymentProof.name : 'Click to upload screenshot'}
{paymentProof ? paymentProof.name : t('p2pTrade.clickToUpload')}
</p>
</div>
<input
@@ -779,8 +780,7 @@ export default function P2PTrade() {
<Alert className="bg-yellow-500/10 border-yellow-500/30">
<AlertTriangle className="h-4 w-4 text-yellow-400" />
<AlertDescription className="text-yellow-200">
Only click confirm after you have actually sent the payment.
Falsely marking payment as sent may result in account suspension.
{t('p2pTrade.falsePaymentWarning')}
</AlertDescription>
</Alert>
</div>
@@ -791,7 +791,7 @@ export default function P2PTrade() {
disabled={actionLoading}
className="border-gray-700"
>
Cancel
{t('p2p.cancel')}
</Button>
<Button
onClick={handleMarkAsPaid}
@@ -803,7 +803,7 @@ export default function P2PTrade() {
) : (
<CheckCircle2 className="w-4 h-4 mr-2" />
)}
Confirm Payment Sent
{t('p2pTrade.confirmPaymentSent')}
</Button>
</DialogFooter>
</DialogContent>
@@ -813,18 +813,18 @@ export default function P2PTrade() {
<Dialog open={showCancelModal} onOpenChange={setShowCancelModal}>
<DialogContent className="bg-gray-900 border-gray-800">
<DialogHeader>
<DialogTitle className="text-white">Cancel Trade</DialogTitle>
<DialogTitle className="text-white">{t('p2pTrade.cancelTradeTitle')}</DialogTitle>
<DialogDescription className="text-gray-400">
Are you sure you want to cancel this trade? The crypto will be returned to the seller.
{t('p2pTrade.cancelTradeDesc')}
</DialogDescription>
</DialogHeader>
<div className="py-4">
<Label htmlFor="cancelReason">Reason for cancellation</Label>
<Label htmlFor="cancelReason">{t('p2pTrade.cancelReasonLabel')}</Label>
<Input
id="cancelReason"
value={cancelReason}
onChange={(e) => setCancelReason(e.target.value)}
placeholder="Optional reason"
placeholder={t('p2pTrade.cancelReasonPlaceholder')}
className="bg-gray-800 border-gray-700 mt-2"
/>
</div>
@@ -835,7 +835,7 @@ export default function P2PTrade() {
disabled={actionLoading}
className="border-gray-700"
>
Keep Trade
{t('p2pTrade.keepTrade')}
</Button>
<Button
variant="destructive"
@@ -847,7 +847,7 @@ export default function P2PTrade() {
) : (
<XCircle className="w-4 h-4 mr-2" />
)}
Cancel Trade
{t('p2pTrade.cancelTrade')}
</Button>
</DialogFooter>
</DialogContent>
+28 -26
View File
@@ -7,11 +7,13 @@ import { Label } from '@/components/ui/label';
import { supabase } from '@/lib/supabase';
import { useToast } from '@/hooks/use-toast';
import { Loader2, ArrowLeft } from 'lucide-react';
import { useTranslation } from 'react-i18next';
export default function PasswordReset() {
const [searchParams] = useSearchParams();
const navigate = useNavigate();
const { toast } = useToast();
const { t } = useTranslation();
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
@@ -30,15 +32,15 @@ export default function PasswordReset() {
if (error) throw error;
toast({
title: "Reset Email Sent",
description: "If the email exists, you'll receive a password reset link",
title: t('passwordReset.resetEmailSent'),
description: t('passwordReset.resetEmailSentDesc'),
});
setEmail('');
} catch (error) {
toast({
title: "Error",
description: error instanceof Error ? error.message : "Failed to send reset email",
title: t('common.error'),
description: error instanceof Error ? error.message : t('passwordReset.failedToSend'),
variant: "destructive"
});
} finally {
@@ -51,8 +53,8 @@ export default function PasswordReset() {
if (password !== confirmPassword) {
toast({
title: "Error",
description: "Passwords do not match",
title: t('common.error'),
description: t('passwordReset.passwordMismatch'),
variant: "destructive"
});
return;
@@ -60,8 +62,8 @@ export default function PasswordReset() {
if (password.length < 8) {
toast({
title: "Error",
description: "Password must be at least 8 characters",
title: t('common.error'),
description: t('passwordReset.passwordTooShort'),
variant: "destructive"
});
return;
@@ -77,15 +79,15 @@ export default function PasswordReset() {
if (error) throw error;
toast({
title: "Password Reset Successful",
description: "Your password has been updated",
title: t('passwordReset.success'),
description: t('passwordReset.successDesc'),
});
navigate('/login');
} catch (error) {
toast({
title: "Error",
description: error instanceof Error ? error.message : "Failed to reset password",
title: t('common.error'),
description: error instanceof Error ? error.message : t('passwordReset.failedToReset'),
variant: "destructive"
});
} finally {
@@ -103,22 +105,22 @@ export default function PasswordReset() {
<ArrowLeft className="w-5 h-5" />
</button>
<CardHeader>
<CardTitle>{token ? 'Reset Password' : 'Forgot Password'}</CardTitle>
<CardTitle>{token ? t('passwordReset.resetPassword') : t('passwordReset.forgotPassword')}</CardTitle>
<CardDescription>
{token
? 'Enter your new password below'
: 'Enter your email to receive a password reset link'}
{token
? t('passwordReset.enterNewPassword')
: t('passwordReset.enterEmail')}
</CardDescription>
</CardHeader>
<CardContent>
{!token ? (
<form onSubmit={handleRequestReset} className="space-y-4">
<div className="space-y-2">
<Label htmlFor="email">Email</Label>
<Label htmlFor="email">{t('passwordReset.email')}</Label>
<Input
id="email"
type="email"
placeholder="Enter your email"
placeholder={t('passwordReset.emailPlaceholder')}
value={email}
onChange={(e) => setEmail(e.target.value)}
required
@@ -128,7 +130,7 @@ export default function PasswordReset() {
<Button type="submit" className="w-full" disabled={loading}>
{loading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
Send Reset Link
{t('passwordReset.sendResetLink')}
</Button>
<div className="text-center text-sm">
@@ -138,18 +140,18 @@ export default function PasswordReset() {
onClick={() => navigate('/login')}
className="text-primary"
>
Back to Login
{t('passwordReset.backToLogin')}
</Button>
</div>
</form>
) : (
<form onSubmit={handleResetPassword} className="space-y-4">
<div className="space-y-2">
<Label htmlFor="password">New Password</Label>
<Label htmlFor="password">{t('passwordReset.newPassword')}</Label>
<Input
id="password"
type="password"
placeholder="Enter new password"
placeholder={t('passwordReset.newPasswordPlaceholder')}
value={password}
onChange={(e) => setPassword(e.target.value)}
required
@@ -159,11 +161,11 @@ export default function PasswordReset() {
</div>
<div className="space-y-2">
<Label htmlFor="confirmPassword">Confirm Password</Label>
<Label htmlFor="confirmPassword">{t('passwordReset.confirmPassword')}</Label>
<Input
id="confirmPassword"
type="password"
placeholder="Confirm new password"
placeholder={t('passwordReset.confirmPlaceholder')}
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
required
@@ -174,7 +176,7 @@ export default function PasswordReset() {
<Button type="submit" className="w-full" disabled={loading}>
{loading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
Reset Password
{t('passwordReset.resetBtn')}
</Button>
<div className="text-center text-sm">
@@ -184,7 +186,7 @@ export default function PasswordReset() {
onClick={() => navigate('/login')}
className="text-primary"
>
Back to Login
{t('passwordReset.backToLogin')}
</Button>
</div>
</form>
+36 -34
View File
@@ -1,5 +1,6 @@
import { useState, useEffect, useCallback } from 'react';
import { useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { useAuth } from '@/contexts/AuthContext';
import { supabase } from '@/lib/supabase';
import { Button } from '@/components/ui/button';
@@ -16,6 +17,7 @@ import { User, Shield, Bell, Palette, ArrowLeft } from 'lucide-react';
import { TwoFactorSetup } from '@/components/auth/TwoFactorSetup';
export default function ProfileSettings() {
const navigate = useNavigate();
const { t } = useTranslation();
const { user } = useAuth();
const { toast } = useToast();
const [loading, setLoading] = useState(false);
@@ -217,36 +219,36 @@ export default function ProfileSettings() {
>
<ArrowLeft className="w-5 h-5" />
</button>
<h1 className="text-3xl font-bold mb-8">Profile Settings</h1>
<h1 className="text-3xl font-bold mb-8">{t('profileSettings.title')}</h1>
<Tabs defaultValue="profile" className="space-y-4">
<TabsList className="grid w-full grid-cols-4">
<TabsTrigger value="profile">
<User className="mr-2 h-4 w-4" />
Profile
{t('profileSettings.profileTab')}
</TabsTrigger>
<TabsTrigger value="notifications">
<Bell className="mr-2 h-4 w-4" />
Notifications
{t('profileSettings.notificationsTab')}
</TabsTrigger>
<TabsTrigger value="security">
<Shield className="mr-2 h-4 w-4" />
Security
{t('profileSettings.securityTab')}
</TabsTrigger>
<TabsTrigger value="preferences">
<Palette className="mr-2 h-4 w-4" />
Preferences
{t('profileSettings.preferencesTab')}
</TabsTrigger>
</TabsList>
<TabsContent value="profile">
<Card>
<CardHeader>
<CardTitle>Profile Information</CardTitle>
<CardTitle>{t('profileSettings.profileInfo')}</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div>
<Label>Username</Label>
<Label>{t('profileSettings.username')}</Label>
<Input
value={profile.username}
onChange={(e) => setProfile({ ...profile, username: e.target.value })}
@@ -254,7 +256,7 @@ export default function ProfileSettings() {
/>
</div>
<div>
<Label>Full Name</Label>
<Label>{t('profileSettings.fullName')}</Label>
<Input
value={profile.full_name}
onChange={(e) => setProfile({ ...profile, full_name: e.target.value })}
@@ -262,7 +264,7 @@ export default function ProfileSettings() {
/>
</div>
<div>
<Label>Bio</Label>
<Label>{t('profileSettings.bio')}</Label>
<Textarea
value={profile.bio}
onChange={(e) => setProfile({ ...profile, bio: e.target.value })}
@@ -271,7 +273,7 @@ export default function ProfileSettings() {
/>
</div>
<div>
<Label>Phone Number</Label>
<Label>{t('profileSettings.phoneNumber')}</Label>
<Input
value={profile.phone_number}
onChange={(e) => setProfile({ ...profile, phone_number: e.target.value })}
@@ -279,7 +281,7 @@ export default function ProfileSettings() {
/>
</div>
<div>
<Label>Location</Label>
<Label>{t('profileSettings.location')}</Label>
<Input
value={profile.location}
onChange={(e) => setProfile({ ...profile, location: e.target.value })}
@@ -287,7 +289,7 @@ export default function ProfileSettings() {
/>
</div>
<div>
<Label>Website</Label>
<Label>{t('profileSettings.website')}</Label>
<Input
value={profile.website}
onChange={(e) => setProfile({ ...profile, website: e.target.value })}
@@ -295,7 +297,7 @@ export default function ProfileSettings() {
/>
</div>
<Button onClick={updateProfile} disabled={loading}>
Save Changes
{t('profileSettings.saveChanges')}
</Button>
</CardContent>
</Card>
@@ -304,53 +306,53 @@ export default function ProfileSettings() {
<TabsContent value="notifications">
<Card>
<CardHeader>
<CardTitle>Notification Preferences</CardTitle>
<CardTitle>{t('profileSettings.notifPreferences')}</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-center justify-between">
<div>
<Label>Email Notifications</Label>
<Label>{t('profileSettings.emailNotif')}</Label>
<p className="text-sm text-muted-foreground">
Receive notifications via email
{t('profileSettings.emailNotifDesc')}
</p>
</div>
<Switch
checked={profile.notifications_email}
onCheckedChange={(checked) =>
onCheckedChange={(checked) =>
setProfile({ ...profile, notifications_email: checked })
}
/>
</div>
<div className="flex items-center justify-between">
<div>
<Label>Push Notifications</Label>
<Label>{t('profileSettings.pushNotif')}</Label>
<p className="text-sm text-muted-foreground">
Receive push notifications in browser
{t('profileSettings.pushNotifDesc')}
</p>
</div>
<Switch
checked={profile.notifications_push}
onCheckedChange={(checked) =>
onCheckedChange={(checked) =>
setProfile({ ...profile, notifications_push: checked })
}
/>
</div>
<div className="flex items-center justify-between">
<div>
<Label>SMS Notifications</Label>
<Label>{t('profileSettings.smsNotif')}</Label>
<p className="text-sm text-muted-foreground">
Receive notifications via SMS
{t('profileSettings.smsNotifDesc')}
</p>
</div>
<Switch
checked={profile.notifications_sms}
onCheckedChange={(checked) =>
onCheckedChange={(checked) =>
setProfile({ ...profile, notifications_sms: checked })
}
/>
</div>
<Button onClick={updateNotificationSettings} disabled={loading}>
Save Preferences
{t('profileSettings.savePreferences')}
</Button>
</CardContent>
</Card>
@@ -361,14 +363,14 @@ export default function ProfileSettings() {
<Card>
<CardHeader>
<CardTitle>Password Security</CardTitle>
<CardTitle>{t('profileSettings.passwordSecurity')}</CardTitle>
<CardDescription>
Manage your password settings
{t('profileSettings.passwordSecurityDesc')}
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<Button variant="outline" onClick={changePassword}>
Change Password
{t('profileSettings.changePassword')}
</Button>
</CardContent>
</Card>
@@ -378,11 +380,11 @@ export default function ProfileSettings() {
<TabsContent value="preferences">
<Card>
<CardHeader>
<CardTitle>App Preferences</CardTitle>
<CardTitle>{t('profileSettings.appPreferences')}</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div>
<Label>Language</Label>
<Label>{t('profileSettings.language')}</Label>
<Select
value={profile.language}
onValueChange={(value) => setProfile({ ...profile, language: value })}
@@ -401,7 +403,7 @@ export default function ProfileSettings() {
</Select>
</div>
<div>
<Label>Theme</Label>
<Label>{t('profileSettings.theme')}</Label>
<Select
value={profile.theme}
onValueChange={(value) => setProfile({ ...profile, theme: value })}
@@ -410,14 +412,14 @@ export default function ProfileSettings() {
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="light">Light</SelectItem>
<SelectItem value="dark">Dark</SelectItem>
<SelectItem value="system">System</SelectItem>
<SelectItem value="light">{t('profileSettings.themeLight')}</SelectItem>
<SelectItem value="dark">{t('profileSettings.themeDark')}</SelectItem>
<SelectItem value="system">{t('profileSettings.themeSystem')}</SelectItem>
</SelectContent>
</Select>
</div>
<Button onClick={updateProfile} disabled={loading}>
Save Preferences
{t('profileSettings.savePreferences')}
</Button>
</CardContent>
</Card>
+15 -13
View File
@@ -7,14 +7,16 @@ import { useEffect, useState } from 'react';
import { useNavigate, useSearchParams } from 'react-router-dom';
import { supabase } from '@/lib/supabase';
import { Loader2, AlertTriangle, CheckCircle2 } from 'lucide-react';
import { useTranslation } from 'react-i18next';
type Status = 'loading' | 'connecting' | 'success' | 'error';
export default function TelegramConnect() {
const [searchParams] = useSearchParams();
const navigate = useNavigate();
const { t } = useTranslation();
const [status, setStatus] = useState<Status>('loading');
const [message, setMessage] = useState('Girêdan tê kirin...');
const [message, setMessage] = useState(t('telegramConnect.connecting'));
const [error, setError] = useState<string | null>(null);
useEffect(() => {
@@ -29,7 +31,7 @@ export default function TelegramConnect() {
// Validate params
if (!telegramId || from !== 'miniapp') {
setStatus('error');
setError('Parametreyên nederbasdar. Ji kerema xwe ji mini app-ê dest pê bikin.');
setError(t('telegramConnect.invalidParams'));
return;
}
@@ -39,13 +41,13 @@ export default function TelegramConnect() {
const now = Date.now();
if (now - ts > 5 * 60 * 1000) {
setStatus('error');
setError('Lînk qediya. Ji kerema xwe dîsa biceribînin.');
setError(t('telegramConnect.linkExpired'));
return;
}
}
setStatus('connecting');
setMessage('Bikarhêner tê pejirandin...');
setMessage(t('telegramConnect.authenticating'));
// Find user by telegram_id
const { data: userData, error: userError } = await supabase
@@ -56,7 +58,7 @@ export default function TelegramConnect() {
if (userError || !userData) {
setStatus('error');
setError('Bikarhêner nehat dîtin. Ji kerema xwe berî dest bi P2P-ê bikin, di mini app-ê de cîzdanê xwe ava bikin.');
setError(t('telegramConnect.userNotFound'));
return;
}
@@ -78,7 +80,7 @@ export default function TelegramConnect() {
if (authData?.session) {
// Already logged in, redirect to P2P
setStatus('success');
setMessage('Serketî! Tê veguheztin...');
setMessage(t('telegramConnect.success'));
setTimeout(() => navigate('/p2p'), 1000);
return;
}
@@ -109,20 +111,20 @@ export default function TelegramConnect() {
}));
setStatus('success');
setMessage('Serketî! Tê veguheztin...');
setMessage(t('telegramConnect.success'));
setTimeout(() => navigate('/p2p'), 1000);
return;
}
// Success - redirect to P2P
setStatus('success');
setMessage('Serketî! Tê veguheztin...');
setMessage(t('telegramConnect.success'));
setTimeout(() => navigate('/p2p'), 1000);
} catch (err) {
console.error('Telegram connect error:', err);
setStatus('error');
setError('Xeletî di girêdanê de. Ji kerema xwe dîsa biceribînin.');
setError(t('telegramConnect.connectionError'));
}
};
@@ -145,7 +147,7 @@ export default function TelegramConnect() {
{/* Title */}
<h1 className="text-xl font-semibold text-white mb-2">
{status === 'error' ? 'Xeletî' : 'Telegram Connect'}
{status === 'error' ? t('telegramConnect.errorTitle') : t('telegramConnect.title')}
</h1>
{/* Status Message */}
@@ -160,10 +162,10 @@ export default function TelegramConnect() {
onClick={() => window.close()}
className="w-full py-3 bg-gray-800 hover:bg-gray-700 text-white rounded-xl font-medium transition-colors"
>
Pencereyê Bigire
{t('telegramConnect.closeWindow')}
</button>
<p className="text-xs text-gray-500">
Ji kerema xwe vegerin mini app-ê û dîsa biceribînin
{t('telegramConnect.returnToMiniApp')}
</p>
</div>
)}
@@ -173,7 +175,7 @@ export default function TelegramConnect() {
<div className="mt-6">
<div className="flex items-center justify-center gap-2 text-green-400">
<div className="w-2 h-2 bg-green-400 rounded-full animate-pulse" />
<span className="text-sm">P2P Platform vekirin...</span>
<span className="text-sm">{t('telegramConnect.openingP2P')}</span>
</div>
</div>
)}
+30 -28
View File
@@ -1,5 +1,6 @@
import React, { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { usePezkuwi } from '@/contexts/PezkuwiContext';
import { AccountBalance } from '@/components/AccountBalance';
import { TransferModal } from '@/components/TransferModal';
@@ -26,6 +27,7 @@ interface Transaction {
}
const WalletDashboard: React.FC = () => {
const { t } = useTranslation();
const navigate = useNavigate();
const { api, isApiReady, peopleApi, isPeopleReady, selectedAccount } = usePezkuwi();
const [isTransferModalOpen, setIsTransferModalOpen] = useState(false);
@@ -240,14 +242,14 @@ const WalletDashboard: React.FC = () => {
const injector = await web3FromAddress(selectedAccount.address);
const result = await recordTrustScore(peopleApi, selectedAccount.address, injector.signer);
if (result.success) {
toast.success('Trust score recorded for this epoch');
toast.success(t('wallet.trustScoreRecorded'));
const rewards = await getPezRewards(peopleApi, selectedAccount.address);
setPezRewards(rewards);
} else {
toast.error(result.error || 'Failed to record trust score');
toast.error(result.error || t('wallet.failedToRecordScore'));
}
} catch (error) {
toast.error(error instanceof Error ? error.message : 'Failed to record trust score');
toast.error(error instanceof Error ? error.message : t('wallet.failedToRecordScore'));
} finally {
setIsRecordingScore(false);
}
@@ -261,14 +263,14 @@ const WalletDashboard: React.FC = () => {
const result = await claimPezReward(peopleApi, selectedAccount.address, epochIndex, injector.signer);
if (result.success) {
const rewardInfo = pezRewards?.claimableRewards.find(r => r.epoch === epochIndex);
toast.success(`${rewardInfo?.amount || '0'} PEZ reward claimed!`);
toast.success(t('wallet.rewardClaimed', { amount: rewardInfo?.amount || '0' }));
const rewards = await getPezRewards(peopleApi, selectedAccount.address);
setPezRewards(rewards);
} else {
toast.error(result.error || 'Failed to claim reward');
toast.error(result.error || t('wallet.failedToClaimReward'));
}
} catch (error) {
toast.error(error instanceof Error ? error.message : 'Failed to claim reward');
toast.error(error instanceof Error ? error.message : t('wallet.failedToClaimReward'));
} finally {
setIsClaimingReward(false);
}
@@ -287,8 +289,8 @@ const WalletDashboard: React.FC = () => {
return (
<div className="min-h-screen bg-gray-950 flex items-center justify-center">
<div className="text-center">
<h2 className="text-2xl font-bold text-white mb-4">Wallet Not Connected</h2>
<p className="text-gray-400 mb-6">Please connect your wallet to view your dashboard</p>
<h2 className="text-2xl font-bold text-white mb-4">{t('wallet.notConnected')}</h2>
<p className="text-gray-400 mb-6">{t('wallet.connectToView')}</p>
</div>
</div>
);
@@ -304,8 +306,8 @@ const WalletDashboard: React.FC = () => {
<ArrowLeft className="w-5 h-5" />
</button>
<div className="mb-8">
<h1 className="text-3xl font-bold text-white mb-2">Wallet Dashboard</h1>
<p className="text-gray-400">Manage your HEZ and PEZ tokens</p>
<h1 className="text-3xl font-bold text-white mb-2">{t('wallet.dashboard')}</h1>
<p className="text-gray-400">{t('wallet.manageTokens')}</p>
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
@@ -314,7 +316,7 @@ const WalletDashboard: React.FC = () => {
{/* Recent Activity */}
<div className="bg-gray-900 border border-gray-800 rounded-lg p-6">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-white">Recent Activity</h3>
<h3 className="text-lg font-semibold text-white">{t('wallet.recentActivity')}</h3>
<Button
variant="ghost"
size="icon"
@@ -329,12 +331,12 @@ const WalletDashboard: React.FC = () => {
{isLoadingRecent ? (
<div className="text-center py-8">
<RefreshCw className="w-10 h-10 text-gray-600 mx-auto mb-3 animate-spin" />
<p className="text-gray-400 text-sm">Loading...</p>
<p className="text-gray-400 text-sm">{t('wallet.loading')}</p>
</div>
) : recentTransactions.length === 0 ? (
<div className="text-center py-8">
<History className="w-10 h-10 text-gray-600 mx-auto mb-3" />
<p className="text-gray-500 text-sm">No recent transactions</p>
<p className="text-gray-500 text-sm">{t('wallet.noRecentTx')}</p>
</div>
) : (
<div className="space-y-2">
@@ -356,7 +358,7 @@ const WalletDashboard: React.FC = () => {
)}
<div>
<div className="text-white font-semibold text-xs">
{isIncoming(tx) ? 'Received' : 'Sent'}
{isIncoming(tx) ? t('wallet.received') : t('wallet.sent')}
</div>
<div className="text-xs text-gray-500">
#{tx.blockNumber}
@@ -380,7 +382,7 @@ const WalletDashboard: React.FC = () => {
size="sm"
className="mt-3 w-full border-gray-700 hover:bg-gray-800 text-xs"
>
View All
{t('wallet.viewAll')}
</Button>
</div>
@@ -397,7 +399,7 @@ const WalletDashboard: React.FC = () => {
className="bg-gradient-to-r from-green-600 to-yellow-400 hover:from-green-700 hover:to-yellow-500 h-24 flex flex-col items-center justify-center"
>
<ArrowUpRight className="w-6 h-6 mb-2" />
<span>Send</span>
<span>{t('wallet.send')}</span>
</Button>
<Button
@@ -406,7 +408,7 @@ const WalletDashboard: React.FC = () => {
className="border-gray-700 hover:bg-gray-800 h-24 flex flex-col items-center justify-center"
>
<ArrowDownRight className="w-6 h-6 mb-2" />
<span>Receive</span>
<span>{t('wallet.receive')}</span>
</Button>
<Button
@@ -415,7 +417,7 @@ const WalletDashboard: React.FC = () => {
className="border-gray-700 hover:bg-gray-800 h-24 flex flex-col items-center justify-center"
>
<History className="w-6 h-6 mb-2" />
<span>History</span>
<span>{t('wallet.history')}</span>
</Button>
</div>
@@ -425,7 +427,7 @@ const WalletDashboard: React.FC = () => {
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-2">
<Coins className="w-5 h-5 text-orange-500" />
<h3 className="text-lg font-semibold text-white">PEZ Rewards</h3>
<h3 className="text-lg font-semibold text-white">{t('wallet.pezRewards')}</h3>
</div>
<span className={`text-xs px-2 py-0.5 rounded-full font-medium ${
pezRewards.epochStatus === 'Open'
@@ -434,7 +436,7 @@ const WalletDashboard: React.FC = () => {
? 'bg-orange-500/20 text-orange-400'
: 'bg-gray-500/20 text-gray-400'
}`}>
Epoch {pezRewards.currentEpoch} - {pezRewards.epochStatus === 'Open' ? 'Open' : pezRewards.epochStatus === 'ClaimPeriod' ? 'Claim Period' : 'Closed'}
{t('wallet.epoch', { epoch: pezRewards.currentEpoch, status: pezRewards.epochStatus === 'Open' ? t('wallet.epochOpen') : pezRewards.epochStatus === 'ClaimPeriod' ? t('wallet.epochClaimPeriod') : t('wallet.epochClosed') })}
</span>
</div>
@@ -442,8 +444,8 @@ const WalletDashboard: React.FC = () => {
{pezRewards.epochStatus === 'Open' && (
pezRewards.hasRecordedThisEpoch ? (
<div className="flex items-center gap-2 mb-3">
<span className="text-green-400 font-semibold">Score: {pezRewards.userScoreCurrentEpoch}</span>
<span className="text-xs text-gray-500">Recorded for this epoch</span>
<span className="text-green-400 font-semibold">{t('wallet.score', { score: pezRewards.userScoreCurrentEpoch })}</span>
<span className="text-xs text-gray-500">{t('wallet.recordedForEpoch')}</span>
</div>
) : (
<Button
@@ -455,9 +457,9 @@ const WalletDashboard: React.FC = () => {
{isRecordingScore ? (
<>
<Loader2 className="w-3 h-3 mr-1 animate-spin" />
Recording...
{t('wallet.recording')}
</>
) : 'Record Trust Score'}
) : t('wallet.recordTrustScore')}
</Button>
)
)}
@@ -468,24 +470,24 @@ const WalletDashboard: React.FC = () => {
<div className="text-2xl font-bold text-orange-500">
{parseFloat(pezRewards.totalClaimable).toFixed(2)} PEZ
</div>
<p className="text-xs text-gray-500 mb-2">{pezRewards.claimableRewards.length} epoch(s) to claim</p>
<p className="text-xs text-gray-500 mb-2">{t('wallet.epochsToClaim', { count: pezRewards.claimableRewards.length })}</p>
{pezRewards.claimableRewards.map((reward) => (
<div key={reward.epoch} className="flex items-center justify-between bg-gray-800/50 rounded-lg px-3 py-2">
<span className="text-xs text-gray-400">Epoch {reward.epoch}: {reward.amount} PEZ</span>
<span className="text-xs text-gray-400">{t('wallet.epochReward', { epoch: reward.epoch, amount: reward.amount })}</span>
<Button
size="sm"
onClick={() => handleClaimReward(reward.epoch)}
disabled={isClaimingReward}
className="h-6 text-xs px-3 bg-orange-600 hover:bg-orange-700"
>
{isClaimingReward ? '...' : 'Claim'}
{isClaimingReward ? '...' : t('wallet.claim')}
</Button>
</div>
))}
</div>
) : (
!pezRewards.hasRecordedThisEpoch && pezRewards.epochStatus !== 'Open' && (
<div className="text-gray-500 text-sm">No claimable rewards</div>
<div className="text-gray-500 text-sm">{t('wallet.noClaimableRewards')}</div>
)
)}
</div>
+7 -5
View File
@@ -1,8 +1,10 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import Layout from '@/components/Layout';
import { Search, BookOpen, Star } from 'lucide-react';
const Wiki: React.FC = () => {
const { t } = useTranslation();
const categories = [
{ name: 'General', description: 'Learn the basics of PezkuwiChain.' },
@@ -24,14 +26,14 @@ const Wiki: React.FC = () => {
<Layout>
<div className="container mx-auto px-4 py-8 text-white">
<div className="text-center mb-12">
<h1 className="text-5xl font-bold mb-2">Community Wiki</h1>
<p className="text-xl text-gray-400">Your community-driven knowledge base for all things PezkuwiChain.</p>
<h1 className="text-5xl font-bold mb-2">{t('wiki.title')}</h1>
<p className="text-xl text-gray-400">{t('wiki.subtitle')}</p>
<div className="mt-6 max-w-2xl mx-auto">
<div className="relative">
<Search className="absolute left-4 top-1/2 -translate-y-1/2 text-gray-400" size={24} />
<input
type="text"
placeholder="Search the wiki..."
placeholder={t('wiki.searchPlaceholder')}
className="w-full pl-14 pr-4 py-3 rounded-lg bg-gray-800 text-white text-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
@@ -42,7 +44,7 @@ const Wiki: React.FC = () => {
<div className="lg:col-span-2">
<h2 className="text-3xl font-bold mb-6 flex items-center">
<BookOpen className="mr-3 text-blue-400" />
Categories
{t('wiki.categories')}
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{categories.map((category, index) => (
@@ -56,7 +58,7 @@ const Wiki: React.FC = () => {
<div>
<h2 className="text-3xl font-bold mb-6 flex items-center">
<Star className="mr-3 text-yellow-400" />
Popular Articles
{t('wiki.popularArticles')}
</h2>
<div className="bg-gray-800 p-6 rounded-lg">
<ul className="space-y-4">