mirror of
https://github.com/pezkuwichain/pwap.git
synced 2026-04-23 00:07:55 +00:00
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:
@@ -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
@@ -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'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
@@ -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 tê 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'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'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
@@ -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>
|
||||
|
||||
@@ -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
@@ -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>
|
||||
|
||||
@@ -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
@@ -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>
|
||||
|
||||
@@ -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'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
@@ -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>
|
||||
|
||||
@@ -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
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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ê tê 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îneyê (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
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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'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
@@ -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
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 tê vekirin...</span>
|
||||
<span className="text-sm">{t('telegramConnect.openingP2P')}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user