wish i was dead

This commit is contained in:
DeathKaioken 2026-05-03 22:10:33 +02:00
parent ffe357a05a
commit afeff4f474
98 changed files with 6400 additions and 2183 deletions

View File

@ -1,5 +1,8 @@
'use client'
import { useTranslation } from '../i18n/useTranslation';
import {
AcademicCapIcon,
CheckCircleIcon,
@ -206,6 +209,7 @@ const footerNavigation = {
}
export default function AboutUsPage() {
const { t } = useTranslation();
return (
<PageLayout>
<div className="bg-gray-900 pb-24 sm:pb-32">
@ -227,7 +231,7 @@ export default function AboutUsPage() {
{/* Header section */}
<div className="px-6 pt-14 lg:px-8">
<div className="mx-auto max-w-2xl pt-24 text-center sm:pt-40">
<h1 className="text-5xl font-semibold tracking-tight text-white sm:text-7xl">We are a community</h1>
<h1 className="text-5xl font-semibold tracking-tight text-white sm:text-7xl">{t('autofix.kbd979e13')}</h1>
<p className="mt-8 text-lg font-medium text-pretty text-gray-400 sm:text-xl/8">
Anim aute id magna aliqua ad ad non deserunt sunt. Qui irure qui lorem cupidatat commodo. Elit sunt amet
fugiat veniam occaecat fugiat.
@ -288,7 +292,7 @@ export default function AboutUsPage() {
{/* Feature section */}
<div className="mx-auto mt-32 max-w-7xl px-6 sm:mt-40 lg:px-8">
<div className="mx-auto max-w-2xl lg:mx-0">
<h2 className="text-4xl font-semibold tracking-tight text-pretty text-white sm:text-5xl">Our values</h2>
<h2 className="text-4xl font-semibold tracking-tight text-pretty text-white sm:text-5xl">{t('autofix.kf0646f35')}</h2>
<p className="mt-6 text-lg/8 text-gray-300">
Lorem ipsum, dolor sit amet consectetur adipisicing elit. Maiores impedit perferendis suscipit eaque, iste
dolor cupiditate blanditiis.
@ -310,7 +314,7 @@ export default function AboutUsPage() {
{/* Team section */}
<div className="mx-auto mt-32 max-w-7xl px-6 sm:mt-40 lg:px-8">
<div className="mx-auto max-w-2xl lg:mx-0">
<h2 className="text-4xl font-semibold tracking-tight text-pretty text-white sm:text-5xl">Our team</h2>
<h2 className="text-4xl font-semibold tracking-tight text-pretty text-white sm:text-5xl">{t('autofix.k3777e830')}</h2>
<p className="mt-6 text-lg/8 text-gray-400">
Were a dynamic group of individuals who are passionate about what we do and dedicated to delivering the
best results for our clients.
@ -345,9 +349,7 @@ export default function AboutUsPage() {
className="h-96 w-full flex-none rounded-2xl object-cover shadow-xl lg:aspect-square lg:h-auto lg:max-w-sm"
/>
<div className="w-full flex-auto">
<h2 className="text-4xl font-semibold tracking-tight text-pretty text-white sm:text-5xl">
Join our team
</h2>
<h2 className="text-4xl font-semibold tracking-tight text-pretty text-white sm:text-5xl">{t('autofix.k5ef19112')}</h2>
<p className="mt-6 text-lg/8 text-pretty text-gray-400">
Lorem ipsum dolor sit amet consect adipisicing elit. Possimus magnam voluptatum cupiditate veritatis
in accusamus quisquam.
@ -364,9 +366,7 @@ export default function AboutUsPage() {
))}
</ul>
<div className="mt-10 flex">
<a href="#" className="text-sm/6 font-semibold text-indigo-400 hover:text-indigo-300">
See our job postings
<span aria-hidden="true">&rarr;</span>
<a href="#" className="text-sm/6 font-semibold text-indigo-400 hover:text-indigo-300">{t('autofix.k81b056f2')}<span aria-hidden="true">&rarr;</span>
</a>
</div>
</div>

View File

@ -1,4 +1,7 @@
'use client'
import { useTranslation } from '../../../i18n/useTranslation';
import React, { useState, useCallback } from 'react'
import Cropper from 'react-easy-crop'
import { Point, Area } from 'react-easy-crop'
@ -11,6 +14,7 @@ interface AffiliateCropModalProps {
}
export default function AffiliateCropModal({ isOpen, imageSrc, onClose, onCropComplete }: AffiliateCropModalProps) {
const { t } = useTranslation();
const [crop, setCrop] = useState<Point>({ x: 0, y: 0 })
const [zoom, setZoom] = useState(1)
const [croppedAreaPixels, setCroppedAreaPixels] = useState<Area | null>(null)
@ -70,7 +74,7 @@ export default function AffiliateCropModal({ isOpen, imageSrc, onClose, onCropCo
<div className="relative w-full max-w-4xl mx-4 bg-white rounded-2xl shadow-2xl overflow-hidden">
{/* Header */}
<div className="px-6 py-4 border-b border-gray-200 flex items-center justify-between bg-gradient-to-r from-blue-50 to-white">
<h2 className="text-xl font-semibold text-blue-900">Crop Affiliate Logo</h2>
<h2 className="text-xl font-semibold text-blue-900">{t('autofix.kcf4ba87d')}</h2>
<button
onClick={onClose}
className="text-gray-500 hover:text-gray-700 transition"
@ -120,9 +124,7 @@ export default function AffiliateCropModal({ isOpen, imageSrc, onClose, onCropCo
<button
onClick={handleSave}
className="px-5 py-2.5 text-sm font-semibold text-white bg-blue-900 rounded-lg hover:bg-blue-800 shadow transition"
>
Apply Crop
</button>
>{t('autofix.kef1656df')}</button>
</div>
</div>
</div>

View File

@ -1,5 +1,8 @@
'use client'
import { useTranslation } from '../../i18n/useTranslation';
import React, { useState } from 'react'
import Header from '../../components/nav/Header'
import Footer from '../../components/Footer'
@ -40,6 +43,7 @@ const AFFILIATE_CATEGORIES = [
] as const
export default function AffiliateManagementPage() {
const { t } = useTranslation();
const router = useRouter()
const user = useAuthStore(s => s.user)
const isAdmin = !!user && (
@ -133,9 +137,7 @@ export default function AffiliateManagementPage() {
<button
onClick={refresh}
className="mt-2 text-sm text-red-600 hover:text-red-800 font-medium"
>
Try again
</button>
>{t('autofix.k3b7dd87a')}</button>
</div>
)}
@ -143,20 +145,14 @@ export default function AffiliateManagementPage() {
<header className="bg-white/90 backdrop-blur border-b border-blue-100 py-6 md:py-10 px-4 md:px-8 rounded-2xl shadow-lg mb-6 md:mb-8">
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-4 md:gap-6 mb-4 md:mb-6">
<div className="min-w-0">
<h1 className="text-2xl md:text-4xl font-extrabold text-blue-900 tracking-tight">
Affiliate Management
</h1>
<p className="text-sm md:text-lg text-blue-700 mt-1 md:mt-2">
Manage your affiliate partners and tracking links
</p>
<h1 className="text-2xl md:text-4xl font-extrabold text-blue-900 tracking-tight">{t('autofix.k0fe28e0b')}</h1>
<p className="text-sm md:text-lg text-blue-700 mt-1 md:mt-2">{t('autofix.k49568342')}</p>
</div>
<button
onClick={() => setShowCreateModal(true)}
className="w-full md:w-auto inline-flex items-center justify-center gap-2 rounded-lg bg-blue-900 hover:bg-blue-800 text-blue-50 px-5 py-3 text-sm font-semibold shadow transition"
>
<PlusIcon className="h-5 w-5" />
Add Affiliate
</button>
<PlusIcon className="h-5 w-5" />{t('autofix.ke1abc7d9')}</button>
</div>
{/* Search and Filter */}
@ -165,7 +161,7 @@ export default function AffiliateManagementPage() {
<MagnifyingGlassIcon className="absolute left-3 top-1/2 -translate-y-1/2 h-5 w-5 text-gray-400" />
<input
type="text"
placeholder="Search affiliates..."
placeholder={t('autofix.k832a032b')}
value={searchQuery}
onChange={e => setSearchQuery(e.target.value)}
className="w-full pl-10 pr-4 py-2.5 rounded-lg border border-gray-300 bg-white text-gray-900 placeholder-gray-400 focus:ring-2 focus:ring-blue-900 focus:border-transparent"
@ -193,7 +189,7 @@ export default function AffiliateManagementPage() {
<LinkIcon className="h-6 w-6 text-blue-900" />
</div>
<div>
<p className="text-sm text-gray-600">Total Affiliates</p>
<p className="text-sm text-gray-600">{t('autofix.k410ff9a9')}</p>
<p className="text-2xl font-bold text-gray-900">{affiliates.length}</p>
</div>
</div>
@ -231,7 +227,7 @@ export default function AffiliateManagementPage() {
{loading && (
<div className="col-span-full text-center py-12">
<div className="inline-block h-8 w-8 animate-spin rounded-full border-4 border-solid border-blue-600 border-r-transparent"></div>
<p className="mt-4 text-sm text-gray-600">Loading affiliates...</p>
<p className="mt-4 text-sm text-gray-600">{t('autofix.ka991f523')}</p>
</div>
)}
@ -276,7 +272,7 @@ export default function AffiliateManagementPage() {
{affiliate.commissionRate && (
<div className="mb-4 flex items-center gap-2">
<span className="text-xs text-gray-500">Commission:</span>
<span className="text-xs text-gray-500">{t('autofix.k03cd9b72')}</span>
<span className="text-sm font-semibold text-blue-900">{affiliate.commissionRate}</span>
</div>
)}
@ -350,7 +346,7 @@ export default function AffiliateManagementPage() {
{!loading && filteredAffiliates.length === 0 && (
<div className="col-span-full text-center py-12">
<PhotoIcon className="mx-auto h-12 w-12 text-gray-400" />
<h3 className="mt-2 text-sm font-medium text-gray-900">No affiliates found</h3>
<h3 className="mt-2 text-sm font-medium text-gray-900">{t('autofix.k19f2c5dc')}</h3>
<p className="mt-1 text-sm text-gray-500">
{searchQuery || categoryFilter !== 'all'
? 'Try adjusting your search or filter'
@ -551,7 +547,7 @@ function CreateAffiliateModal({ onClose, onCreate }: { onClose: () => void; onCr
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm">
<div className="relative w-full max-w-2xl mx-0 sm:mx-4 bg-white rounded-none sm:rounded-2xl shadow-2xl max-h-[100vh] sm:max-h-[90vh] overflow-y-auto">
<div className="sticky top-0 bg-white border-b border-gray-200 px-4 sm:px-6 py-3 sm:py-4 flex items-center justify-between rounded-none sm:rounded-t-2xl">
<h2 className="text-xl sm:text-2xl font-bold text-blue-900">Add New Affiliate</h2>
<h2 className="text-xl sm:text-2xl font-bold text-blue-900">{t('autofix.k8d84b4c5')}</h2>
<button onClick={onClose} className="text-gray-500 hover:text-gray-700 transition">
<XMarkIcon className="h-6 w-6" />
</button>
@ -559,30 +555,30 @@ function CreateAffiliateModal({ onClose, onCreate }: { onClose: () => void; onCr
<form onSubmit={handleSubmit} className="p-4 sm:p-6 space-y-5 sm:space-y-6">
<div>
<label className="block text-sm font-medium text-blue-900 mb-2">Partner Name *</label>
<label className="block text-sm font-medium text-blue-900 mb-2">{t('autofix.k31cadca6')}</label>
<input
required
value={name}
onChange={e => setName(e.target.value)}
className="w-full rounded-lg border border-gray-300 px-4 py-3 text-gray-900 focus:ring-2 focus:ring-blue-900 focus:border-transparent"
placeholder="e.g., Coffee Equipment Co."
placeholder={t('autofix.k890ff52f')}
/>
</div>
<div>
<label className="block text-sm font-medium text-blue-900 mb-2">Description *</label>
<label className="block text-sm font-medium text-blue-900 mb-2">{t('autofix.kfb92efe9')}</label>
<textarea
required
value={description}
onChange={e => setDescription(e.target.value)}
rows={3}
className="w-full rounded-lg border border-gray-300 px-4 py-3 text-gray-900 focus:ring-2 focus:ring-blue-900 focus:border-transparent"
placeholder="Brief description of the affiliate partner..."
placeholder={t('autofix.k2a37c394')}
/>
</div>
<div>
<label className="block text-sm font-medium text-blue-900 mb-2">Affiliate URL *</label>
<label className="block text-sm font-medium text-blue-900 mb-2">{t('autofix.k6828cdd9')}</label>
<input
required
type="url"
@ -594,7 +590,7 @@ function CreateAffiliateModal({ onClose, onCreate }: { onClose: () => void; onCr
</div>
<div>
<label className="block text-sm font-medium text-blue-900 mb-2">Logo Image</label>
<label className="block text-sm font-medium text-blue-900 mb-2">{t('autofix.kde1c3c69')}</label>
<div
className="relative flex justify-center items-center rounded-lg border-2 border-dashed border-gray-300 bg-gray-50 cursor-pointer overflow-hidden transition hover:border-blue-400 hover:bg-blue-100"
style={{ minHeight: '200px' }}
@ -608,16 +604,16 @@ function CreateAffiliateModal({ onClose, onCreate }: { onClose: () => void; onCr
<div className="text-center w-full px-6 py-10">
<PhotoIcon aria-hidden="true" className="mx-auto h-12 w-12 text-gray-400" />
<div className="mt-4 text-sm font-medium text-gray-700">
<span>Click to upload logo</span>
<span>{t('autofix.k05626798')}</span>
</div>
<p className="text-xs text-gray-500 mt-2">PNG, JPG, WebP, SVG up to 5MB</p>
<p className="text-xs text-gray-500 mt-2">{t('autofix.k578dcc0b')}</p>
</div>
)}
{previewUrl && (
<div className="relative w-full h-full min-h-[200px] flex items-center justify-center bg-white p-4">
<img
src={previewUrl}
alt="Logo preview"
alt={t('autofix.k1af107a4')}
className="max-h-[180px] max-w-full object-contain"
/>
<button
@ -644,7 +640,7 @@ function CreateAffiliateModal({ onClose, onCreate }: { onClose: () => void; onCr
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-blue-900 mb-2">Category *</label>
<label className="block text-sm font-medium text-blue-900 mb-2">{t('autofix.k3def5ebf')}</label>
<select
value={category}
onChange={e => setCategory(e.target.value)}
@ -657,12 +653,12 @@ function CreateAffiliateModal({ onClose, onCreate }: { onClose: () => void; onCr
</div>
<div>
<label className="block text-sm font-medium text-blue-900 mb-2">Commission Rate</label>
<label className="block text-sm font-medium text-blue-900 mb-2">{t('autofix.k1e62338a')}</label>
<input
value={commissionRate}
onChange={e => setCommissionRate(e.target.value)}
className="w-full rounded-lg border border-gray-300 px-4 py-3 text-gray-900 focus:ring-2 focus:ring-blue-900 focus:border-transparent"
placeholder="e.g., 10%"
placeholder={t('autofix.k7c19388f')}
/>
</div>
</div>
@ -691,9 +687,7 @@ function CreateAffiliateModal({ onClose, onCreate }: { onClose: () => void; onCr
<button
type="submit"
className="px-5 py-2.5 text-sm font-semibold text-white bg-blue-900 rounded-lg hover:bg-blue-800 shadow transition"
>
Add Affiliate
</button>
>{t('autofix.ke1abc7d9')}</button>
</div>
</form>
</div>
@ -815,7 +809,7 @@ function EditAffiliateModal({ affiliate, onClose, onUpdate }: {
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm">
<div className="relative w-full max-w-2xl mx-0 sm:mx-4 bg-white rounded-none sm:rounded-2xl shadow-2xl max-h-[100vh] sm:max-h-[90vh] overflow-y-auto">
<div className="sticky top-0 bg-white border-b border-gray-200 px-4 sm:px-6 py-3 sm:py-4 flex items-center justify-between rounded-none sm:rounded-t-2xl">
<h2 className="text-xl sm:text-2xl font-bold text-blue-900">Edit Affiliate</h2>
<h2 className="text-xl sm:text-2xl font-bold text-blue-900">{t('autofix.k09def344')}</h2>
<button onClick={onClose} className="text-gray-500 hover:text-gray-700 transition">
<XMarkIcon className="h-6 w-6" />
</button>
@ -823,7 +817,7 @@ function EditAffiliateModal({ affiliate, onClose, onUpdate }: {
<form onSubmit={handleSubmit} className="p-4 sm:p-6 space-y-5 sm:space-y-6">
<div>
<label className="block text-sm font-medium text-blue-900 mb-2">Partner Name *</label>
<label className="block text-sm font-medium text-blue-900 mb-2">{t('autofix.k31cadca6')}</label>
<input
required
value={name}
@ -833,7 +827,7 @@ function EditAffiliateModal({ affiliate, onClose, onUpdate }: {
</div>
<div>
<label className="block text-sm font-medium text-blue-900 mb-2">Description *</label>
<label className="block text-sm font-medium text-blue-900 mb-2">{t('autofix.kfb92efe9')}</label>
<textarea
required
value={description}
@ -844,7 +838,7 @@ function EditAffiliateModal({ affiliate, onClose, onUpdate }: {
</div>
<div>
<label className="block text-sm font-medium text-blue-900 mb-2">Affiliate URL *</label>
<label className="block text-sm font-medium text-blue-900 mb-2">{t('autofix.k6828cdd9')}</label>
<input
required
type="url"
@ -855,7 +849,7 @@ function EditAffiliateModal({ affiliate, onClose, onUpdate }: {
</div>
<div>
<label className="block text-sm font-medium text-blue-900 mb-2">Logo Image</label>
<label className="block text-sm font-medium text-blue-900 mb-2">{t('autofix.kde1c3c69')}</label>
<div
className="relative flex justify-center items-center rounded-lg border-2 border-dashed border-gray-300 bg-gray-50 cursor-pointer overflow-hidden transition hover:border-blue-400 hover:bg-blue-100"
style={{ minHeight: '200px' }}
@ -869,9 +863,9 @@ function EditAffiliateModal({ affiliate, onClose, onUpdate }: {
<div className="text-center w-full px-6 py-10">
<PhotoIcon aria-hidden="true" className="mx-auto h-12 w-12 text-gray-400" />
<div className="mt-4 text-sm font-medium text-gray-700">
<span>Click to upload logo</span>
<span>{t('autofix.k05626798')}</span>
</div>
<p className="text-xs text-gray-500 mt-2">PNG, JPG, WebP, SVG up to 5MB</p>
<p className="text-xs text-gray-500 mt-2">{t('autofix.k578dcc0b')}</p>
</div>
)}
{displayLogoUrl && (
@ -908,7 +902,7 @@ function EditAffiliateModal({ affiliate, onClose, onUpdate }: {
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-blue-900 mb-2">Category *</label>
<label className="block text-sm font-medium text-blue-900 mb-2">{t('autofix.k3def5ebf')}</label>
<select
value={category}
onChange={e => setCategory(e.target.value)}
@ -921,7 +915,7 @@ function EditAffiliateModal({ affiliate, onClose, onUpdate }: {
</div>
<div>
<label className="block text-sm font-medium text-blue-900 mb-2">Commission Rate</label>
<label className="block text-sm font-medium text-blue-900 mb-2">{t('autofix.k1e62338a')}</label>
<input
value={commissionRate}
onChange={e => setCommissionRate(e.target.value)}
@ -954,9 +948,7 @@ function EditAffiliateModal({ affiliate, onClose, onUpdate }: {
<button
type="submit"
className="px-5 py-2.5 text-sm font-semibold text-white bg-blue-900 rounded-lg hover:bg-blue-800 shadow transition"
>
Save Changes
</button>
>{t('autofix.k5a489751')}</button>
</div>
</form>
</div>
@ -979,10 +971,8 @@ function DeleteConfirmModal({ affiliateName, onClose, onConfirm, isDeleting }: {
<div className="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-red-100">
<TrashIcon className="h-6 w-6 text-red-600" />
</div>
<h3 className="mt-4 text-lg font-semibold text-center text-gray-900">Delete Affiliate</h3>
<p className="mt-2 text-sm text-center text-gray-600">
Are you sure you want to delete <span className="font-semibold">{affiliateName}</span>? This action cannot be undone.
</p>
<h3 className="mt-4 text-lg font-semibold text-center text-gray-900">{t('autofix.kccbc54c1')}</h3>
<p className="mt-2 text-sm text-center text-gray-600">{t('autofix.k055bba0c')}<span className="font-semibold">{affiliateName}</span>{t('autofix.kd5cca6e9')}</p>
</div>
<div className="flex flex-col sm:flex-row items-stretch sm:items-center gap-3">

View File

@ -1,5 +1,8 @@
'use client'
import { useTranslation } from '../../../i18n/useTranslation';
import { useState, useEffect } from 'react'
import useContractManagement from '../hooks/useContractManagement'
@ -31,6 +34,7 @@ function summarizeForLog(payload: Record<string, any>) {
}
export default function CompanySettingsPanel() {
const { t } = useTranslation();
const { getCompanySettings, updateCompanySettings } = useContractManagement()
const [form, setForm] = useState({
@ -155,9 +159,7 @@ export default function CompanySettingsPanel() {
if (loading) {
return (
<div className="flex items-center gap-2 text-sm text-gray-500 py-4">
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-blue-900" />
Loading settings
</div>
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-blue-900" />{t('autofix.k81a1b900')}</div>
)
}
@ -165,9 +167,7 @@ export default function CompanySettingsPanel() {
<form onSubmit={handleSubmit} className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label htmlFor="company_name" className="block text-sm font-medium text-gray-700 mb-1">
Company Name
</label>
<label htmlFor="company_name" className="block text-sm font-medium text-gray-700 mb-1">{t('autofix.k33918465')}</label>
<input
type="text"
id="company_name"
@ -175,7 +175,7 @@ export default function CompanySettingsPanel() {
value={form.company_name}
onChange={handleChange}
className="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 focus:border-transparent"
placeholder="ProfitPlanet GmbH"
placeholder={t('autofix.k91e69df1')}
/>
</div>
<div>
@ -189,7 +189,7 @@ export default function CompanySettingsPanel() {
value={form.company_street}
onChange={handleChange}
className="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 focus:border-transparent"
placeholder="Musterstraße 1"
placeholder={t('autofix.k81c7c2f2')}
/>
</div>
<div>
@ -203,7 +203,7 @@ export default function CompanySettingsPanel() {
value={form.company_postal_city}
onChange={handleChange}
className="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 focus:border-transparent"
placeholder="12345 Berlin"
placeholder={t('autofix.k93165aea')}
/>
</div>
<div>
@ -265,7 +265,7 @@ export default function CompanySettingsPanel() {
{saving ? 'Saving…' : 'Save'}
</button>
{saved && (
<span className="text-sm text-green-600 font-medium">Saved successfully</span>
<span className="text-sm text-green-600 font-medium">{t('autofix.ka29ac729')}</span>
)}
</div>
</form>

View File

@ -4,6 +4,8 @@ import React, { useEffect, useRef, useState } from 'react';
import useContractManagement from '../hooks/useContractManagement';
import ConfirmActionModal from '../../../components/modals/ConfirmActionModal';
import { useTranslation } from '../../../i18n/useTranslation';
type Props = {
editingTemplateId?: string | null;
onCancelEdit?: () => void;
@ -11,6 +13,7 @@ type Props = {
};
export default function ContractEditor({ onSaved, editingTemplateId, onCancelEdit }: Props) {
const { t } = useTranslation();
const [name, setName] = useState('');
const [htmlCode, setHtmlCode] = useState('');
const [isPreview, setIsPreview] = useState(false);
@ -251,7 +254,7 @@ export default function ContractEditor({ onSaved, editingTemplateId, onCancelEdi
{editingMeta && (
<div className="rounded-lg border border-indigo-200 bg-indigo-50 px-4 py-3 flex items-center justify-between gap-3">
<div className="text-sm text-indigo-900">
<span className="font-semibold">Editing:</span> {name || 'Untitled'} (v{editingMeta.version}) state: {editingMeta.state}
<span className="font-semibold">{t('autofix.k41afd863')}</span> {name || 'Untitled'} (v{editingMeta.version}) state: {editingMeta.state}
</div>
{onCancelEdit && (
<button
@ -261,9 +264,7 @@ export default function ContractEditor({ onSaved, editingTemplateId, onCancelEdi
onCancelEdit();
}}
className="inline-flex items-center rounded-lg bg-white hover:bg-gray-50 text-gray-900 px-3 py-1.5 text-sm font-medium shadow border border-gray-200 transition"
>
Cancel editing
</button>
>{t('autofix.k06d4487f')}</button>
)}
</div>
)}
@ -271,7 +272,7 @@ export default function ContractEditor({ onSaved, editingTemplateId, onCancelEdi
<div className="flex flex-col sm:flex-row gap-4">
<input
type="text"
placeholder="Template name"
placeholder={t('autofix.k2fac9ff2')}
value={name}
onChange={(e) => setName(e.target.value)}
required
@ -360,9 +361,9 @@ export default function ContractEditor({ onSaved, editingTemplateId, onCancelEdi
<div className="space-y-3">
{type === 'invoice' && (
<div className="rounded-lg border border-amber-200 bg-amber-50 px-4 py-3 text-xs text-amber-900">
<p className="font-semibold">Invoice template variables</p>
<p className="mt-1">Use these placeholders in your HTML: invoiceNumber, customerName, issuedAt, totalNet, totalTax, totalGross, itemsHtml.</p>
<p className="mt-1">Important: include <span className="font-semibold">itemsHtml</span> to render invoice line items.</p>
<p className="font-semibold">{t('autofix.k221fa311')}</p>
<p className="mt-1">{t('autofix.kb791958e')}</p>
<p className="mt-1">Important: include <span className="font-semibold">itemsHtml</span>{t('autofix.k7a3a6ea3')}</p>
</div>
)}
<textarea
@ -379,7 +380,7 @@ export default function ContractEditor({ onSaved, editingTemplateId, onCancelEdi
<div className="rounded-lg border border-gray-300 bg-white shadow">
<iframe
ref={iframeRef}
title="Contract Preview"
title={t('autofix.kd9e4bcbd')}
className="w-full rounded-lg"
style={{ height: 1200, background: 'transparent' }}
/>
@ -398,19 +399,17 @@ export default function ContractEditor({ onSaved, editingTemplateId, onCancelEdi
onClick={() => save(true)}
disabled={saving || !canSave}
className="inline-flex items-center rounded-lg bg-indigo-600 hover:bg-indigo-500 text-white px-4 py-2 text-sm font-medium shadow disabled:opacity-60 transition"
>
Create & Activate
</button>
>{t('autofix.k0af6c6be')}</button>
{/* NEW: helper text */}
{!canSave && <span className="text-xs text-red-600">Fill all fields to proceed.</span>}
{saving && <span className="text-xs text-gray-500">Saving</span>}
{!canSave && <span className="text-xs text-red-600">{t('autofix.k99bffb65')}</span>}
{saving && <span className="text-xs text-gray-500">{t('autofix.kac6cedc7')}</span>}
{statusMsg && <span className="text-xs text-gray-600">{statusMsg}</span>}
</div>
<ConfirmActionModal
open={publishConfirmOpen}
pending={saving}
title="Activate template now?"
title={t('autofix.k0c51fa85')}
description={publishConfirmMessage || 'This will activate this template.'}
confirmText="Activate"
onClose={() => !saving && setPublishConfirmOpen(false)}

View File

@ -4,6 +4,8 @@ import React, { useEffect, useMemo, useState } from 'react';
import useContractManagement, { DocumentTemplate } from '../hooks/useContractManagement';
import ConfirmActionModal from '../../../components/modals/ConfirmActionModal';
import { useTranslation } from '../../../i18n/useTranslation';
type Props = {
refreshKey?: number;
onEdit?: (id: string) => void;
@ -345,6 +347,7 @@ function StatusBadge({ status }: { status: string }) {
}
export default function ContractTemplateList({ refreshKey = 0, onEdit }: Props) {
const { t } = useTranslation();
const [items, setItems] = useState<ContractTemplate[]>([]);
const [loading, setLoading] = useState(false);
const [q, setQ] = useState('');
@ -720,7 +723,7 @@ export default function ContractTemplateList({ refreshKey = 0, onEdit }: Props)
<div className="mt-5 flex flex-col gap-3 lg:flex-row lg:items-center">
<input
placeholder="Search by name, language, version or status"
placeholder={t('autofix.k35ac864e')}
value={q}
onChange={(e) => setQ(e.target.value)}
className="w-full rounded-2xl border border-slate-200 bg-white px-4 py-3 text-sm text-slate-900 outline-none transition focus:border-slate-300 focus:ring-4 focus:ring-sky-100"
@ -846,7 +849,7 @@ export default function ContractTemplateList({ refreshKey = 0, onEdit }: Props)
<div className="rounded-3xl border border-dashed border-slate-300 bg-slate-50 px-6 py-12 text-center text-sm text-slate-500">
{items.length === 0
? 'No templates available yet. Create the first template to populate this workspace.'
: 'No templates match the current search.'}
: t('autofix.k047a175d')}
</div>
)}
</div>
@ -854,7 +857,7 @@ export default function ContractTemplateList({ refreshKey = 0, onEdit }: Props)
<ConfirmActionModal
open={Boolean(pendingToggle?.requiresConfirm)}
title="Activate template now?"
title={t('autofix.k0c51fa85')}
description={pendingToggle?.message || 'This action will update template activation status.'}
confirmText="Activate"
onClose={() => setPendingToggle(null)}

View File

@ -4,11 +4,14 @@ import React, { useEffect, useMemo, useRef, useState } from 'react';
import useContractManagement, { CompanyStamp } from '../hooks/useContractManagement';
import DeleteConfirmationModal from '../../../components/delete/deleteConfirmationModal';
import { useTranslation } from '../../../i18n/useTranslation';
type Props = {
onUploaded?: () => void;
};
export default function ContractUploadCompanyStamp({ onUploaded }: Props) {
const { t } = useTranslation();
const [file, setFile] = useState<File | null>(null);
const [label, setLabel] = useState<string>('');
const [uploading, setUploading] = useState(false);
@ -215,15 +218,13 @@ export default function ContractUploadCompanyStamp({ onUploaded }: Props) {
<div className="space-y-6">
{/* Header with Add New Stamp modal trigger */}
<div className="flex items-center justify-between">
<p className="text-sm text-gray-700">Manage your company stamps. One active at a time.</p>
<p className="text-sm text-gray-700">{t('autofix.k096f4013')}</p>
<button
type="button"
onClick={openModal}
className="inline-flex items-center gap-2 rounded-lg bg-indigo-600 hover:bg-indigo-500 text-white px-4 py-2 text-sm font-semibold shadow transition"
>
<svg width="18" height="18" fill="currentColor" className="opacity-90"><path d="M7 1a1 1 0 0 1 2 0v5h5a1 1 0 1 1 0 2H9v5a1 1 0 1 1-2 0V8H2a1 1 0 1 1 0-2h5V1z"/></svg>
Add New Stamp
</button>
<svg width="18" height="18" fill="currentColor" className="opacity-90"><path d="M7 1a1 1 0 0 1 2 0v5h5a1 1 0 1 1 0 2H9v5a1 1 0 1 1-2 0V8H2a1 1 0 1 1 0-2h5V1z"/></svg>{t('autofix.k6070f6e3')}</button>
</div>
{/* Emphasized Active stamp */}
@ -234,13 +235,11 @@ export default function ContractUploadCompanyStamp({ onUploaded }: Props) {
{activeStamp.base64 ? (
<img
src={toImgSrc(activeStamp)}
alt="Active stamp"
alt={t('autofix.k134e3932')}
className="h-24 w-24 object-contain rounded-xl ring-2 ring-indigo-200 shadow"
/>
) : (
<div className="h-24 w-24 flex items-center justify-center rounded-xl ring-2 ring-gray-200 bg-gray-50 text-xs text-gray-500">
no image
</div>
<div className="h-24 w-24 flex items-center justify-center rounded-xl ring-2 ring-gray-200 bg-gray-50 text-xs text-gray-500">{t('autofix.k56717603')}</div>
)}
<span className="absolute -top-2 -right-2 rounded-full bg-green-600 text-white text-xs px-3 py-1 shadow font-bold">
Active
@ -248,7 +247,7 @@ export default function ContractUploadCompanyStamp({ onUploaded }: Props) {
</div>
<div className="min-w-0">
<p className="text-base font-semibold text-gray-900 truncate">{activeStamp.label || activeStamp.id}</p>
<p className="text-xs text-gray-500">Auto-applied to documents where applicable.</p>
<p className="text-xs text-gray-500">{t('autofix.kf1a9384b')}</p>
</div>
<div className="ml-auto flex items-center gap-2">
<button
@ -265,7 +264,7 @@ export default function ContractUploadCompanyStamp({ onUploaded }: Props) {
{/* Stamps list */}
{!!stamps.length && (
<div className="mt-2">
<p className="text-sm font-medium text-gray-900 mb-2">Your Company Stamps</p>
<p className="text-sm font-medium text-gray-900 mb-2">{t('autofix.k7775eddb')}</p>
<ul className="grid grid-cols-1 sm:grid-cols-2 gap-3">
{stamps.map((s) => {
const src = toImgSrc(s);
@ -285,9 +284,7 @@ export default function ContractUploadCompanyStamp({ onUploaded }: Props) {
className="h-14 w-14 object-contain rounded-lg ring-1 ring-gray-200 bg-white"
/>
) : (
<div className="h-14 w-14 flex items-center justify-center rounded-lg ring-1 ring-gray-200 bg-white text-xs text-gray-500">
no image
</div>
<div className="h-14 w-14 flex items-center justify-center rounded-lg ring-1 ring-gray-200 bg-white text-xs text-gray-500">{t('autofix.k56717603')}</div>
)}
<div className="flex flex-col">
<span className="text-sm text-gray-900">{s.label || s.id}</span>
@ -335,10 +332,8 @@ export default function ContractUploadCompanyStamp({ onUploaded }: Props) {
<div className="absolute inset-0 flex items-center justify-center p-4">
<div className="w-full max-w-lg rounded-2xl bg-white shadow-2xl ring-1 ring-black/5">
<div className="p-6 border-b border-gray-100">
<h3 className="text-lg font-bold text-indigo-700">Add New Stamp</h3>
<p className="mt-1 text-xs text-gray-500">
Accepted types: PNG, JPEG, WebP, SVG. Max size: 5MB.
</p>
<h3 className="text-lg font-bold text-indigo-700">{t('autofix.k6070f6e3')}</h3>
<p className="mt-1 text-xs text-gray-500">{t('autofix.k825359ab')}</p>
</div>
<div className="p-6 space-y-4">
<div>
@ -347,7 +342,7 @@ export default function ContractUploadCompanyStamp({ onUploaded }: Props) {
type="text"
value={modalLabel}
onChange={(e) => setModalLabel(e.target.value)}
placeholder="e.g., Company Seal 2025"
placeholder={t('autofix.kcb65c692')}
className="w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm text-gray-900 outline-none focus:ring-2 focus:ring-indigo-500/50"
/>
</div>
@ -365,13 +360,11 @@ export default function ContractUploadCompanyStamp({ onUploaded }: Props) {
className="h-20 w-20 object-contain rounded-lg ring-1 ring-gray-200 bg-white"
/>
) : (
<div className="h-20 w-20 flex items-center justify-center rounded-lg ring-1 ring-gray-200 bg-white text-xs text-gray-500">
No image
</div>
<div className="h-20 w-20 flex items-center justify-center rounded-lg ring-1 ring-gray-200 bg-white text-xs text-gray-500">{t('autofix.k18872b63')}</div>
)}
<div className="min-w-0">
<p className="text-sm text-gray-900">Drag and drop your stamp here</p>
<p className="text-xs text-gray-500">or click to browse</p>
<p className="text-sm text-gray-900">{t('autofix.ke58b7627')}</p>
<p className="text-xs text-gray-500">{t('autofix.kba6bd6f3')}</p>
<div className="mt-2">
<label className="inline-block">
<input
@ -381,9 +374,7 @@ export default function ContractUploadCompanyStamp({ onUploaded }: Props) {
className="hidden"
/>
<span className="cursor-pointer text-xs px-3 py-2 rounded-lg bg-white hover:bg-gray-50 text-gray-800 border border-gray-200 inline-flex items-center gap-1">
<svg width="14" height="14" fill="currentColor"><path d="M12 10v2H2v-2H0v4h14v-4h-2zM7 0l4 4H9v5H5V4H3l4-4z"/></svg>
Choose file
</span>
<svg width="14" height="14" fill="currentColor"><path d="M12 10v2H2v-2H0v4h14v-4h-2zM7 0l4 4H9v5H5V4H3l4-4z"/></svg>{t('autofix.kfeac3f7e')}</span>
</label>
</div>
</div>
@ -418,7 +409,7 @@ export default function ContractUploadCompanyStamp({ onUploaded }: Props) {
{/* Delete Confirmation Modal */}
<DeleteConfirmationModal
open={deleteModal.open}
title="Delete Company Stamp"
title={t('autofix.ka8f53660')}
description={
deleteModal.active
? `This stamp (${deleteModal.label || deleteModal.id}) is currently active. Are you sure you want to delete it? This action cannot be undone.`

View File

@ -9,6 +9,8 @@ import ContractTemplateList from './components/contractTemplateList';
import useAuthStore from '../../store/authStore';
import { useRouter } from 'next/navigation';
import { useTranslation } from '../../i18n/useTranslation';
const NAV = [
{ key: 'stamp', label: 'Company Stamp', icon: <svg className="w-5 h-5" fill="none" stroke="currentColor"><circle cx="12" cy="12" r="10"/><path d="M12 8v4l3 3"/></svg> },
{ key: 'templates', label: 'Templates', icon: <svg className="w-5 h-5" fill="none" stroke="currentColor"><path d="M4 6h16M4 12h16M4 18h16"/></svg> },
@ -16,6 +18,7 @@ const NAV = [
];
export default function ContractManagementPage() {
const { t } = useTranslation();
const [refreshKey, setRefreshKey] = useState(0);
const user = useAuthStore((s) => s.user);
const mounted = useSyncExternalStore(
@ -89,9 +92,9 @@ export default function ContractManagementPage() {
Admin workspace
</div>
<div>
<h1 className="text-3xl font-black tracking-tight text-slate-950 md:text-5xl">Template Management</h1>
<h1 className="text-3xl font-black tracking-tight text-slate-950 md:text-5xl">{t('autofix.k67cb36a4')}</h1>
<p className="mt-3 max-w-2xl text-sm leading-6 text-slate-600 md:text-base">
Keep contract, invoice and custom templates tidy in one place, with clearer navigation between active versions, languages and revisions.
{t('autofix.k39791457')}
</p>
</div>
<div className="flex flex-wrap gap-2 text-xs text-slate-600">
@ -106,16 +109,16 @@ export default function ContractManagementPage() {
<section className="rounded-[28px] border border-white/80 bg-white/85 p-4 shadow-[0_24px_70px_-40px_rgba(15,23,42,0.38)] backdrop-blur md:p-6 xl:p-7">
<h2 className="mb-4 flex items-center gap-2 text-xl font-semibold text-slate-900">
<svg className="w-6 h-6" fill="none" stroke="currentColor"><circle cx="12" cy="12" r="10"/><path d="M12 8v4l3 3"/></svg>
Company Stamp
{t('autofix.ka5f38d19')}
</h2>
<ContractUploadCompanyStamp onUploaded={bumpRefresh} />
<div className="mt-8 border-t border-slate-200 pt-6">
<h3 className="mb-3 flex items-center gap-2 text-lg font-semibold text-slate-900">
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" /></svg>
Company Information
{t('autofix.kaa8bbc8e')}
</h3>
<p className="mb-4 text-sm text-slate-500">Address details used on invoices.</p>
<p className="mb-4 text-sm text-slate-500">{t('autofix.k15bea9bb')}</p>
<CompanySettingsPanel />
</div>
</section>
@ -136,7 +139,7 @@ export default function ContractManagementPage() {
<section className="rounded-[28px] border border-white/80 bg-white/85 p-4 shadow-[0_24px_70px_-40px_rgba(15,23,42,0.38)] backdrop-blur md:p-6 xl:p-7">
<h2 className="mb-4 flex items-center gap-2 text-xl font-semibold text-slate-900">
<svg className="w-6 h-6" fill="none" stroke="currentColor"><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 113 3L7 19l-4 1 1-4 12.5-12.5z"/></svg>
Create Template
{t('autofix.k22c8f7f1')}
</h2>
<ContractEditor
key={`${editorKey}-${editingTemplateId ?? 'new'}`}

View File

@ -1,5 +1,8 @@
'use client'
import { useTranslation } from '../../i18n/useTranslation';
import { useState } from 'react'
import PageLayout from '../../components/PageLayout'
import {
@ -11,6 +14,7 @@ import { PlusIcon, TrashIcon, CheckIcon, ChevronDownIcon, ChevronUpIcon } from '
import { useAdminDashboardPlatforms, type PlatformRow } from './hooks/useAdminDashboardPlatforms'
export default function AdminDashboardManagementPage() {
const { t } = useTranslation();
const {
platforms,
loading,
@ -46,10 +50,8 @@ export default function AdminDashboardManagementPage() {
<div className="rounded-3xl bg-white/70 backdrop-blur-md border border-white/60 shadow-lg p-6 sm:p-8">
<header className="flex flex-col sm:flex-row sm:items-start sm:justify-between gap-4 mb-6">
<div>
<h1 className="text-3xl sm:text-4xl font-extrabold text-blue-900 tracking-tight">Dashboard Management</h1>
<p className="text-sm sm:text-base text-blue-700 mt-2">
Manage the Platforms cards shown on the user dashboard.
</p>
<h1 className="text-3xl sm:text-4xl font-extrabold text-blue-900 tracking-tight">{t('autofix.kc4315932')}</h1>
<p className="text-sm sm:text-base text-blue-700 mt-2">{t('autofix.k098ec0b9')}</p>
</div>
<div className="flex flex-col sm:flex-row gap-2 sm:items-center">
@ -59,9 +61,7 @@ export default function AdminDashboardManagementPage() {
disabled={loading || saving}
className="inline-flex items-center justify-center gap-2 rounded-lg bg-blue-900 text-white px-4 py-2 text-sm font-semibold hover:bg-blue-800"
>
<PlusIcon className="h-5 w-5" />
Add Platform
</button>
<PlusIcon className="h-5 w-5" />{t('autofix.k39e2c5db')}</button>
<button
type="button"
onClick={save}
@ -98,9 +98,7 @@ export default function AdminDashboardManagementPage() {
<div className="grid grid-cols-1 gap-4">
{loading && (
<div className="rounded-2xl border border-gray-200 bg-white p-6 text-sm text-gray-600">
Loading
</div>
<div className="rounded-2xl border border-gray-200 bg-white p-6 text-sm text-gray-600">{t('autofix.k832387c5')}</div>
)}
{!loading && platforms.map(platform => (
@ -168,7 +166,7 @@ export default function AdminDashboardManagementPage() {
value={platform.href}
onChange={e => updatePlatform(platform.id, { href: e.target.value })}
disabled={saving}
placeholder="Example: /shop or https://example.com"
placeholder={t('autofix.k17f65c37')}
className={
'mt-1 w-full rounded-lg border px-3 py-2 text-sm ' +
(isValidHref(platform.href) ? 'border-gray-200' : 'border-red-300')
@ -229,7 +227,7 @@ export default function AdminDashboardManagementPage() {
{platform.disabled && (
<label className="block md:col-span-2">
<div className="text-xs font-semibold text-gray-700">Disabled message</div>
<div className="text-xs font-semibold text-gray-700">{t('autofix.kab99811e')}</div>
<input
value={platform.disabledText || ''}
onChange={e => updatePlatform(platform.id, { disabledText: e.target.value })}
@ -246,9 +244,7 @@ export default function AdminDashboardManagementPage() {
))}
{!loading && platforms.length === 0 && (
<div className="rounded-2xl border border-gray-200 bg-white p-8 text-center text-sm text-gray-600">
No platforms configured.
</div>
<div className="rounded-2xl border border-gray-200 bg-white p-8 text-center text-sm text-gray-600">{t('autofix.kbce9fbea')}</div>
)}
</div>
</div>

View File

@ -1,5 +1,8 @@
'use client'
import { useTranslation } from '../../i18n/useTranslation';
import React from 'react'
import Header from '../../components/nav/Header'
import Footer from '../../components/Footer'
@ -11,6 +14,7 @@ import { importSqlDump, SqlExecutionData, SqlExecutionMeta } from './hooks/execu
import { createFolderStructure, listFolderStructureIssues, listLooseFiles, listGhostDirectories, moveLooseFilesToContract, FixResult, FolderStructureIssueUser, GhostDirectory, LooseFileUser } from './hooks/exoscaleMaintenance'
export default function DevManagementPage() {
const { t } = useTranslation();
const router = useRouter()
const user = useAuthStore(s => s.user)
@ -264,18 +268,16 @@ export default function DevManagementPage() {
<CommandLineIcon className="h-6 w-6 text-blue-700" />
</div>
<div className="min-w-0">
<h1 className="text-2xl sm:text-3xl font-extrabold text-blue-900">Dev Management</h1>
<p className="text-sm sm:text-base text-blue-700">
Import SQL dump files to run database migrations.
</p>
<h1 className="text-2xl sm:text-3xl font-extrabold text-blue-900">{t('autofix.k664072a1')}</h1>
<p className="text-sm sm:text-base text-blue-700">{t('autofix.k6e4a6069')}</p>
</div>
</div>
<div className="rounded-xl border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-800 flex items-start gap-2">
<ExclamationTriangleIcon className="h-5 w-5 mt-0.5 flex-shrink-0" />
<div>
<div className="font-semibold">Use with caution</div>
<div>SQL dumps run immediately and can modify production data.</div>
<div className="font-semibold">{t('autofix.k6c6e5c0f')}</div>
<div>{t('autofix.k8a35cc53')}</div>
</div>
</div>
</header>
@ -288,32 +290,28 @@ export default function DevManagementPage() {
activeTab === 'sql' ? 'bg-blue-900 text-white' : 'text-blue-800 hover:bg-blue-50'
}`}
>
<CommandLineIcon className="h-4 w-4" /> SQL Import
</button>
<CommandLineIcon className="h-4 w-4" />{t('autofix.k4db68c96')}</button>
<button
onClick={() => setActiveTab('structure')}
className={`flex-1 sm:flex-none inline-flex items-center justify-center gap-2 rounded-lg px-3 py-2 text-sm font-semibold transition ${
activeTab === 'structure' ? 'bg-blue-900 text-white' : 'text-blue-800 hover:bg-blue-50'
}`}
>
<WrenchScrewdriverIcon className="h-4 w-4" /> Folder Structure
</button>
<WrenchScrewdriverIcon className="h-4 w-4" />{t('autofix.kcb491706')}</button>
<button
onClick={() => setActiveTab('loose')}
className={`flex-1 sm:flex-none inline-flex items-center justify-center gap-2 rounded-lg px-3 py-2 text-sm font-semibold transition ${
activeTab === 'loose' ? 'bg-blue-900 text-white' : 'text-blue-800 hover:bg-blue-50'
}`}
>
<FolderOpenIcon className="h-4 w-4" /> Loose Files
</button>
<FolderOpenIcon className="h-4 w-4" />{t('autofix.k04b5cbca')}</button>
<button
onClick={() => setActiveTab('ghost')}
className={`flex-1 sm:flex-none inline-flex items-center justify-center gap-2 rounded-lg px-3 py-2 text-sm font-semibold transition ${
activeTab === 'ghost' ? 'bg-blue-900 text-white' : 'text-blue-800 hover:bg-blue-50'
}`}
>
<FolderOpenIcon className="h-4 w-4" /> Ghost Directories
</button>
<FolderOpenIcon className="h-4 w-4" />{t('autofix.k6838438d')}</button>
</div>
</div>
@ -322,7 +320,7 @@ export default function DevManagementPage() {
<section className="mt-6 md:mt-8 grid grid-cols-1 lg:grid-cols-3 gap-4 md:gap-6">
<div className="lg:col-span-2 rounded-2xl bg-white border border-gray-100 shadow-lg p-4 md:p-6">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3 mb-4">
<h2 className="text-lg font-semibold text-blue-900">SQL Dump Import</h2>
<h2 className="text-lg font-semibold text-blue-900">{t('autofix.k981b1f1a')}</h2>
{/* actions: stack on mobile, full width */}
<div className="flex flex-col sm:flex-row sm:flex-wrap items-stretch sm:items-center gap-2 sm:gap-3">
@ -330,8 +328,7 @@ export default function DevManagementPage() {
onClick={() => fileInputRef.current?.click()}
className="w-full sm:w-auto inline-flex items-center justify-center gap-2 rounded-lg border border-gray-200 bg-white px-3 py-2 text-sm text-gray-700 hover:bg-gray-50"
>
<ArrowUpTrayIcon className="h-4 w-4" /> Import SQL
</button>
<ArrowUpTrayIcon className="h-4 w-4" />{t('autofix.k8a59b156')}</button>
<button
onClick={clearResults}
className="w-full sm:w-auto inline-flex items-center justify-center gap-2 rounded-lg border border-gray-200 bg-white px-3 py-2 text-sm text-gray-700 hover:bg-gray-50"
@ -349,8 +346,8 @@ export default function DevManagementPage() {
</div>
<div className="rounded-xl border border-dashed border-gray-200 bg-slate-50 px-4 sm:px-6 py-8 sm:py-10 text-center">
<div className="text-sm text-gray-600">Select a .sql dump file using Import SQL.</div>
<div className="mt-2 text-xs text-gray-500">Only SQL dump files are supported.</div>
<div className="text-sm text-gray-600">{t('autofix.kb6eacc9d')}</div>
<div className="mt-2 text-xs text-gray-500">{t('autofix.k3ac8ca10')}</div>
{selectedFile && (
<div className="mt-4 text-sm text-blue-900 font-semibold break-words">
Selected: {selectedFile.name}
@ -374,10 +371,10 @@ export default function DevManagementPage() {
</div>
<div className="rounded-2xl bg-white border border-gray-100 shadow-lg p-4 md:p-6">
<h3 className="text-lg font-semibold text-blue-900 mb-3">Result Summary</h3>
<h3 className="text-lg font-semibold text-blue-900 mb-3">{t('autofix.kde2b4fa0')}</h3>
<div className="space-y-2 text-sm text-gray-700">
<div className="flex justify-between">
<span>Result Sets</span>
<span>{t('autofix.k7938d4fd')}</span>
<span className="font-semibold text-blue-900">
{Array.isArray(result?.result) ? (result?.result as any[]).length : result?.result ? 1 : 0}
</span>
@ -391,19 +388,17 @@ export default function DevManagementPage() {
<span className="font-semibold text-blue-900">{meta?.durationMs ? `${meta.durationMs} ms` : '-'}</span>
</div>
</div>
<div className="mt-6 text-xs text-gray-500">
Multi-statement SQL and dump files are supported. Use with caution.
</div>
<div className="mt-6 text-xs text-gray-500">{t('autofix.k0f0395ca')}</div>
</div>
</section>
<section className="mt-6 md:mt-8 rounded-2xl bg-white border border-gray-100 shadow-lg p-4 md:p-6">
<div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-semibold text-blue-900">Import Results</h2>
<h2 className="text-lg font-semibold text-blue-900">{t('autofix.kb4675362')}</h2>
</div>
{!result && (
<div className="text-sm text-gray-500">No results yet. Import a SQL dump to see output.</div>
<div className="text-sm text-gray-500">{t('autofix.k23c9f0ff')}</div>
)}
{result?.result && (
@ -419,10 +414,8 @@ export default function DevManagementPage() {
<section className="mt-6 md:mt-8 rounded-2xl bg-white border border-gray-100 shadow-lg p-4 md:p-6">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3 mb-4">
<div className="space-y-1">
<h2 className="text-lg font-semibold text-blue-900">Exoscale Folder Structure</h2>
<p className="text-sm text-gray-600">
Ensures both contract and gdpr folders exist for each user.
</p>
<h2 className="text-lg font-semibold text-blue-900">{t('autofix.kd51f320c')}</h2>
<p className="text-sm text-gray-600">{t('autofix.kb1341138')}</p>
{structureStatus && (
<div className="text-xs text-slate-500">{structureStatus}</div>
)}
@ -462,9 +455,9 @@ export default function DevManagementPage() {
)}
{exoscaleLoading ? (
<div className="text-sm text-gray-500">Loading folder issues...</div>
<div className="text-sm text-gray-500">{t('autofix.k8358f1d1')}</div>
) : structureUsers.length === 0 ? (
<div className="text-sm text-gray-500">No missing folders found. Run Refresh to scan again.</div>
<div className="text-sm text-gray-500">{t('autofix.k9e609523')}</div>
) : (
<div className="divide-y divide-gray-100 rounded-xl border border-gray-100">
{structureUsers.map(user => {
@ -478,7 +471,7 @@ export default function DevManagementPage() {
<div className="text-xs text-gray-500 truncate">
#{user.userId} {user.email} {user.userType} {user.contractCategory}
</div>
<div className="mt-1 text-xs text-gray-600">Missing: <span className="font-semibold text-blue-900">{missing || 'none'}</span></div>
<div className="mt-1 text-xs text-gray-600">{t('autofix.kd058bb7b')}<span className="font-semibold text-blue-900">{missing || 'none'}</span></div>
{fix && (
<div className="mt-2 text-xs text-emerald-700">
Created {fix.created || 0}{fix.errors && fix.errors.length > 0 ? `, errors ${fix.errors.length}` : ''}.
@ -502,7 +495,7 @@ export default function DevManagementPage() {
{(structureActionMeta || structureActionResults.length > 0) && (
<div className="mt-6 rounded-xl border border-slate-200 bg-slate-50 p-4">
<div className="text-sm font-semibold text-blue-900 mb-2">Last Folder Structure Action</div>
<div className="text-sm font-semibold text-blue-900 mb-2">{t('autofix.k941fd092')}</div>
<div className="text-xs text-gray-600 flex flex-wrap gap-3">
<span>Processed: {structureActionMeta?.processedUsers ?? '-'}</span>
<span>Created: {structureActionMeta?.createdTotal ?? '-'}</span>
@ -533,10 +526,8 @@ export default function DevManagementPage() {
<section className="mt-6 md:mt-8 rounded-2xl bg-white border border-gray-100 shadow-lg p-4 md:p-6">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3 mb-4">
<div className="space-y-1">
<h2 className="text-lg font-semibold text-blue-900">Loose Files</h2>
<p className="text-sm text-gray-600">
Shows files directly under the user folder that are not in contract or gdpr.
</p>
<h2 className="text-lg font-semibold text-blue-900">{t('autofix.k04b5cbca')}</h2>
<p className="text-sm text-gray-600">{t('autofix.kbff01823')}</p>
{looseStatus && (
<div className="text-xs text-slate-500">{looseStatus}</div>
)}
@ -576,9 +567,9 @@ export default function DevManagementPage() {
)}
{exoscaleLoading ? (
<div className="text-sm text-gray-500">Loading loose files...</div>
<div className="text-sm text-gray-500">{t('autofix.k8193b7a2')}</div>
) : looseUsers.length === 0 ? (
<div className="text-sm text-gray-500">No loose files found. Run Refresh to scan again.</div>
<div className="text-sm text-gray-500">{t('autofix.k1db0c7cd')}</div>
) : (
<div className="divide-y divide-gray-100 rounded-xl border border-gray-100">
{looseUsers.map(user => {
@ -591,8 +582,7 @@ export default function DevManagementPage() {
<div className="text-xs text-gray-500 truncate">
#{user.userId} {user.email} {user.userType} {user.contractCategory}
</div>
<div className="mt-1 text-xs text-gray-600">
Loose files: <span className="font-semibold text-blue-900">{user.looseObjects}</span>
<div className="mt-1 text-xs text-gray-600">{t('autofix.kf340aa10')}<span className="font-semibold text-blue-900">{user.looseObjects}</span>
</div>
{user.sampleKeys && user.sampleKeys.length > 0 && (
<div className="mt-2 text-[11px] text-gray-400 break-all">
@ -622,7 +612,7 @@ export default function DevManagementPage() {
{(looseActionMeta || looseActionResults.length > 0) && (
<div className="mt-6 rounded-xl border border-slate-200 bg-slate-50 p-4">
<div className="text-sm font-semibold text-blue-900 mb-2">Last Loose Files Action</div>
<div className="text-sm font-semibold text-blue-900 mb-2">{t('autofix.kcf61fc9e')}</div>
<div className="text-xs text-gray-600 flex flex-wrap gap-3">
<span>Processed: {looseActionMeta?.processedUsers ?? '-'}</span>
<span>Moved: {looseActionMeta?.movedTotal ?? '-'}</span>
@ -654,10 +644,8 @@ export default function DevManagementPage() {
<section className="mt-6 md:mt-8 rounded-2xl bg-white border border-gray-100 shadow-lg p-4 md:p-6">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3 mb-4">
<div className="space-y-1">
<h2 className="text-lg font-semibold text-blue-900">Ghost Directories</h2>
<p className="text-sm text-gray-600">
Exoscale directories that do not have a matching user in the database.
</p>
<h2 className="text-lg font-semibold text-blue-900">{t('autofix.k6838438d')}</h2>
<p className="text-sm text-gray-600">{t('autofix.k77444d5b')}</p>
{ghostStatus && (
<div className="text-xs text-slate-500">{ghostStatus}</div>
)}
@ -682,9 +670,9 @@ export default function DevManagementPage() {
)}
{exoscaleLoading ? (
<div className="text-sm text-gray-500">Loading ghost directories...</div>
<div className="text-sm text-gray-500">{t('autofix.k883ea8c5')}</div>
) : ghostDirs.length === 0 ? (
<div className="text-sm text-gray-500">No ghost directories found. Run Refresh to scan again.</div>
<div className="text-sm text-gray-500">{t('autofix.k12a7170a')}</div>
) : (
<div className="divide-y divide-gray-100 rounded-xl border border-gray-100">
{ghostDirs.map(dir => (

View File

@ -1,4 +1,7 @@
'use client'
import { useTranslation } from '../../i18n/useTranslation';
import React, { useMemo, useState } from 'react'
import PageLayout from '../../components/PageLayout'
import { useRouter } from 'next/navigation'
@ -8,6 +11,7 @@ import useAuthStore from '../../store/authStore'
import InvoiceDetailModal from './components/InvoiceDetailModal'
export default function FinanceManagementPage() {
const { t } = useTranslation();
const router = useRouter()
const accessToken = useAuthStore(s => s.accessToken)
const { rates, loading: vatLoading, error: vatError } = useVatRates()
@ -241,8 +245,8 @@ export default function FinanceManagementPage() {
<div className="bg-gradient-to-tr from-blue-50 via-white to-blue-100 min-h-screen">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8 space-y-8">
<header className="rounded-2xl bg-white border border-blue-100 shadow-lg px-8 py-8 flex flex-col gap-2">
<h1 className="text-3xl font-extrabold text-blue-900">Finance Management</h1>
<p className="text-sm text-blue-700">Overview of taxes, revenue, and invoices.</p>
<h1 className="text-3xl font-extrabold text-blue-900">{t('autofix.k777299de')}</h1>
<p className="text-sm text-blue-700">{t('autofix.k01ad6d49')}</p>
</header>
{/* Stats */}
@ -266,9 +270,9 @@ export default function FinanceManagementPage() {
onChange={e => setTimeframe(e.target.value as any)}
className="mt-2 w-full rounded-lg border border-gray-200 px-3 py-2 text-sm focus:ring-2 focus:ring-blue-900 focus:border-transparent"
>
<option value="7d">Last 7 days</option>
<option value="30d">Last 30 days</option>
<option value="90d">Last 90 days</option>
<option value="7d">{t('autofix.k502a0057')}</option>
<option value="30d">{t('autofix.k5f74c123')}</option>
<option value="90d">{t('autofix.k915115a9')}</option>
<option value="ytd">YTD</option>
</select>
</div>
@ -278,15 +282,13 @@ export default function FinanceManagementPage() {
<section className="rounded-2xl border border-gray-100 bg-white shadow-lg p-6 space-y-3">
<div className="flex items-center justify-between">
<div>
<h2 className="text-lg font-semibold text-[#1C2B4A]">Manage VAT rates</h2>
<h2 className="text-lg font-semibold text-[#1C2B4A]">{t('autofix.kf2180ff6')}</h2>
<p className="text-xs text-gray-600">Live data from backend; edit on a separate page.</p>
</div>
<button
onClick={() => router.push('/admin/finance-management/vat-edit')}
className="rounded-lg bg-[#1C2B4A] px-4 py-2 text-sm font-semibold text-white shadow hover:bg-[#1C2B4A]/90"
>
Edit VAT
</button>
>{t('autofix.k4191cdba')}</button>
</div>
<div className="text-sm text-gray-700">
{vatLoading && 'Loading VAT rates...'}
@ -302,10 +304,10 @@ export default function FinanceManagementPage() {
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<h2 className="text-lg font-semibold text-[#1C2B4A]">Invoices</h2>
<div className="flex flex-wrap gap-2 text-sm">
<button onClick={() => { setUploadError(null); setUploadModalOpen(true) }} className="rounded-lg bg-[#1C2B4A] px-3 py-2 text-white font-medium hover:bg-[#1C2B4A]/90">Upload Invoice</button>
<button onClick={() => { setReportMsg(null); setEmailDialogOpen(true) }} className="rounded-lg border border-blue-200 bg-blue-50 px-3 py-2 text-blue-900 font-medium hover:bg-blue-100">Send Email Report</button>
<button onClick={() => exportBills('csv')} className="rounded-lg border border-gray-200 px-3 py-2 hover:bg-gray-50">Export CSV</button>
<button onClick={() => exportBills('pdf')} className="rounded-lg border border-gray-200 px-3 py-2 hover:bg-gray-50">Export PDF</button>
<button onClick={() => { setUploadError(null); setUploadModalOpen(true) }} className="rounded-lg bg-[#1C2B4A] px-3 py-2 text-white font-medium hover:bg-[#1C2B4A]/90">{t('autofix.kec5a5357')}</button>
<button onClick={() => { setReportMsg(null); setEmailDialogOpen(true) }} className="rounded-lg border border-blue-200 bg-blue-50 px-3 py-2 text-blue-900 font-medium hover:bg-blue-100">{t('autofix.kfdcad59b')}</button>
<button onClick={() => exportBills('csv')} className="rounded-lg border border-gray-200 px-3 py-2 hover:bg-gray-50">{t('autofix.k4c5e8e87')}</button>
<button onClick={() => exportBills('pdf')} className="rounded-lg border border-gray-200 px-3 py-2 hover:bg-gray-50">{t('autofix.k4c5ecd73')}</button>
<button onClick={reload} className="rounded-lg border border-gray-200 px-3 py-2 hover:bg-gray-50">Reload</button>
</div>
</div>
@ -356,17 +358,15 @@ export default function FinanceManagementPage() {
)}
{(diagLoading || diagError || diagData) && (
<div className="rounded-md border border-blue-100 bg-blue-50/60 px-3 py-3 text-sm mb-3">
{diagLoading && <div className="text-blue-800">Checking pool inflow...</div>}
{diagLoading && <div className="text-blue-800">{t('autofix.k37d7b9c4')}</div>}
{!diagLoading && diagError && <div className="text-red-700">{diagError}</div>}
{!diagLoading && !diagError && diagData && (
<div className="space-y-2">
<div className="text-blue-900 font-semibold">Pool inflow diagnostic for invoice #{diagData.invoice_id ?? '—'}</div>
<div className="text-gray-700">
Status: <span className="font-medium">{diagData.ok ? 'OK' : 'Blocked'}</span> Reason: <span className="font-mono">{diagData.reason}</span>
<div className="text-gray-700">{t('autofix.k81c0b74b')}<span className="font-medium">{diagData.ok ? 'OK' : 'Blocked'}</span>{t('autofix.k77049179')}<span className="font-mono">{diagData.reason}</span>
</div>
{diagData.ok && (
<div className="text-gray-700">
Abonement: <span className="font-medium">{diagData.abonement_id}</span> Will book: <span className="font-medium">{diagData.will_book_count}</span> Already booked: <span className="font-medium">{diagData.already_booked_count}</span>
<div className="text-gray-700">{t('autofix.k4968eb2a')}<span className="font-medium">{diagData.abonement_id}</span>{t('autofix.kfaa8fc4a')}<span className="font-medium">{diagData.will_book_count}</span>{t('autofix.kd2e5e813')}<span className="font-medium">{diagData.already_booked_count}</span>
</div>
)}
{Array.isArray(diagData.candidates) && diagData.candidates.length > 0 && (
@ -405,7 +405,7 @@ export default function FinanceManagementPage() {
<th className="px-3 py-2 font-semibold">Invoice</th>
<th className="px-3 py-2 font-semibold">Customer</th>
<th className="px-3 py-2 font-semibold">Issued</th>
<th className="px-3 py-2 font-semibold">Due Date</th>
<th className="px-3 py-2 font-semibold">{t('autofix.k867f8265')}</th>
<th className="px-3 py-2 font-semibold">Amount</th>
<th className="px-3 py-2 font-semibold">Status</th>
<th className="px-3 py-2 font-semibold">Actions</th>
@ -419,9 +419,7 @@ export default function FinanceManagementPage() {
</>
) : filteredBills.length === 0 ? (
<tr>
<td colSpan={7} className="px-3 py-4 text-center text-gray-500">
Keine Rechnungen gefunden.
</td>
<td colSpan={7} className="px-3 py-4 text-center text-gray-500">{t('autofix.kbdb02e32')}</td>
</tr>
) : (
filteredBills.map(inv => (
@ -505,22 +503,22 @@ export default function FinanceManagementPage() {
{uploadModalOpen && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 backdrop-blur-sm">
<div className="w-full max-w-2xl rounded-2xl bg-white p-6 shadow-2xl overflow-y-auto max-h-[90vh]">
<h3 className="text-lg font-semibold text-[#1C2B4A] mb-4">Upload Invoice</h3>
<h3 className="text-lg font-semibold text-[#1C2B4A] mb-4">{t('autofix.kec5a5357')}</h3>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3 text-sm">
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">Customer Name</label>
<input className="w-full rounded-lg border border-gray-200 px-3 py-2" value={uploadForm.buyer_name} onChange={e => setUploadForm(f => ({ ...f, buyer_name: e.target.value }))} placeholder="Max Mustermann" />
<label className="block text-xs font-medium text-gray-700 mb-1">{t('autofix.kf2b5c1a6')}</label>
<input className="w-full rounded-lg border border-gray-200 px-3 py-2" value={uploadForm.buyer_name} onChange={e => setUploadForm(f => ({ ...f, buyer_name: e.target.value }))} placeholder={t('autofix.k1882bd75')} />
</div>
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">Customer Email</label>
<input type="email" className="w-full rounded-lg border border-gray-200 px-3 py-2" value={uploadForm.buyer_email} onChange={e => setUploadForm(f => ({ ...f, buyer_email: e.target.value }))} placeholder="kunde@example.com" />
<label className="block text-xs font-medium text-gray-700 mb-1">{t('autofix.k48852b8d')}</label>
<input type="email" className="w-full rounded-lg border border-gray-200 px-3 py-2" value={uploadForm.buyer_email} onChange={e => setUploadForm(f => ({ ...f, buyer_email: e.target.value }))} placeholder={t('autofix.kf8c220d3')} />
</div>
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">Street</label>
<input className="w-full rounded-lg border border-gray-200 px-3 py-2" value={uploadForm.buyer_street} onChange={e => setUploadForm(f => ({ ...f, buyer_street: e.target.value }))} placeholder="Musterstraße 1" />
<input className="w-full rounded-lg border border-gray-200 px-3 py-2" value={uploadForm.buyer_street} onChange={e => setUploadForm(f => ({ ...f, buyer_street: e.target.value }))} placeholder={t('autofix.k81c7c2f2')} />
</div>
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">Postal Code</label>
<label className="block text-xs font-medium text-gray-700 mb-1">{t('autofix.kc9d9d15d')}</label>
<input className="w-full rounded-lg border border-gray-200 px-3 py-2" value={uploadForm.buyer_postal_code} onChange={e => setUploadForm(f => ({ ...f, buyer_postal_code: e.target.value }))} placeholder="8010" />
</div>
<div>
@ -532,7 +530,7 @@ export default function FinanceManagementPage() {
<input className="w-full rounded-lg border border-gray-200 px-3 py-2" value={uploadForm.buyer_country} onChange={e => setUploadForm(f => ({ ...f, buyer_country: e.target.value }))} placeholder="Austria" />
</div>
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">Total Gross / Brutto <span className="text-red-500">*</span></label>
<label className="block text-xs font-medium text-gray-700 mb-1">{t('autofix.k002455d8')}<span className="text-red-500">*</span></label>
<input type="number" step="0.01" min="0" className="w-full rounded-lg border border-gray-200 px-3 py-2" value={uploadForm.total_gross} onChange={e => setUploadForm(f => ({ ...f, total_gross: e.target.value }))} placeholder="0.00" />
</div>
<div>
@ -575,15 +573,15 @@ export default function FinanceManagementPage() {
</select>
</div>
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">Issue Date</label>
<label className="block text-xs font-medium text-gray-700 mb-1">{t('autofix.kd4af6368')}</label>
<input type="date" className="w-full rounded-lg border border-gray-200 px-3 py-2" value={uploadForm.issued_at} onChange={e => setUploadForm(f => ({ ...f, issued_at: e.target.value }))} />
</div>
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">Due Date</label>
<label className="block text-xs font-medium text-gray-700 mb-1">{t('autofix.k867f8265')}</label>
<input type="date" className="w-full rounded-lg border border-gray-200 px-3 py-2" value={uploadForm.due_at} onChange={e => setUploadForm(f => ({ ...f, due_at: e.target.value }))} />
</div>
<div className="sm:col-span-2">
<label className="block text-xs font-medium text-gray-700 mb-1">PDF File</label>
<label className="block text-xs font-medium text-gray-700 mb-1">{t('autofix.kd6024811')}</label>
<input
type="file" accept="application/pdf"
className="w-full text-sm text-gray-700 file:mr-3 file:rounded-lg file:border-0 file:bg-blue-50 file:px-3 file:py-2 file:text-blue-900 file:font-medium hover:file:bg-blue-100"
@ -609,19 +607,19 @@ export default function FinanceManagementPage() {
{emailDialogOpen && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 backdrop-blur-sm">
<div className="w-full max-w-md rounded-2xl bg-white p-6 shadow-2xl">
<h3 className="text-lg font-semibold text-[#1C2B4A] mb-1">Send Email Report</h3>
<h3 className="text-lg font-semibold text-[#1C2B4A] mb-1">{t('autofix.kfdcad59b')}</h3>
<div className="text-xs text-amber-700 bg-amber-50 border border-amber-200 rounded-lg px-3 py-2 mb-4">
Only <strong>paid</strong> invoices will be included in the report, regardless of the status filter.
{(billFilter.from || billFilter.to) && (
<span> The current date range filter ({billFilter.from || '…'} {billFilter.to || '…'}) will be applied.</span>
)}
</div>
<label className="block text-sm font-medium text-gray-700 mb-1">Recipient Email</label>
<label className="block text-sm font-medium text-gray-700 mb-1">{t('autofix.kd56a13f2')}</label>
<input
type="email"
value={reportEmail}
onChange={e => setReportEmail(e.target.value)}
placeholder="email@example.com"
placeholder={t('autofix.k51ee3aae')}
className="w-full rounded-lg border border-gray-200 px-3 py-2 text-sm text-gray-900 placeholder:text-gray-400 focus:ring-2 focus:ring-blue-900 focus:border-transparent"
autoFocus
onKeyDown={e => { if (e.key === 'Enter' && !sendingReport) sendEmailReport() }}

View File

@ -1,4 +1,7 @@
'use client'
import { useTranslation } from '../../../i18n/useTranslation';
import React, { useState } from 'react'
import PageLayout from '../../../components/PageLayout'
import { useRouter } from 'next/navigation'
@ -7,6 +10,7 @@ import { importVatCsv } from './hooks/TaxImporter'
import { exportVatCsv, exportVatPdf } from './hooks/TaxExporter'
export default function VatEditPage() {
const { t } = useTranslation();
const router = useRouter()
const { rates, loading, error, reload } = useVatRates()
const [filter, setFilter] = useState('')
@ -42,7 +46,7 @@ export default function VatEditPage() {
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8 space-y-6">
<div className="rounded-2xl bg-white border border-blue-100 shadow-lg px-8 py-8 flex items-center justify-between">
<div>
<h1 className="text-3xl font-extrabold text-blue-900">Edit VAT rates</h1>
<h1 className="text-3xl font-extrabold text-blue-900">{t('autofix.k7572cceb')}</h1>
<p className="text-sm text-blue-700">Import, export, and review (dummy data).</p>
</div>
<button
@ -68,15 +72,11 @@ export default function VatEditPage() {
<button
onClick={() => exportVatCsv(rates)}
className="rounded-lg border border-gray-200 px-3 py-2 hover:bg-gray-50"
>
Export CSV
</button>
>{t('autofix.k4c5e8e87')}</button>
<button
onClick={() => exportVatPdf(rates)}
className="rounded-lg border border-gray-200 px-3 py-2 hover:bg-gray-50"
>
Export PDF
</button>
>{t('autofix.k4c5ecd73')}</button>
{importResult && <span className="text-xs text-blue-900 break-all">{importResult}</span>}
</div>
@ -85,7 +85,7 @@ export default function VatEditPage() {
<input
value={filter}
onChange={e => { setFilter(e.target.value); setPage(1); }}
placeholder="Filter by country or code"
placeholder={t('autofix.kc7bb0c06')}
className="w-full rounded-lg border border-gray-200 px-3 py-2 mb-3 focus:ring-2 focus:ring-blue-900 focus:border-transparent"
/>
<div className="overflow-x-auto">
@ -96,13 +96,13 @@ export default function VatEditPage() {
<th className="px-3 py-2 font-semibold">Code</th>
<th className="px-3 py-2 font-semibold">Standard</th>
<th className="px-3 py-2 font-semibold">Reduced</th>
<th className="px-3 py-2 font-semibold">Super reduced</th>
<th className="px-3 py-2 font-semibold">{t('autofix.k678d2b40')}</th>
<th className="px-3 py-2 font-semibold">Parking</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-100">
{loading && (
<tr><td colSpan={6} className="px-3 py-4 text-center text-gray-500">Loading VAT rates</td></tr>
<tr><td colSpan={6} className="px-3 py-4 text-center text-gray-500">{t('autofix.ka5d50257')}</td></tr>
)}
{!loading && pageData.map(v => (
<tr key={v.country_code} className="border-b last:border-0">
@ -115,14 +115,14 @@ export default function VatEditPage() {
</tr>
))}
{!loading && !error && pageData.length === 0 && (
<tr><td colSpan={6} className="px-3 py-4 text-center text-gray-500">No entries found.</td></tr>
<tr><td colSpan={6} className="px-3 py-4 text-center text-gray-500">{t('autofix.kb337d94e')}</td></tr>
)}
</tbody>
</table>
</div>
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3 mt-4 text-sm text-gray-700">
<div className="flex items-center gap-2">
<span>Rows per page:</span>
<span>{t('autofix.k2f4ebc32')}</span>
<select
value={pageSize}
onChange={e => { setPageSize(Number(e.target.value)); setPage(1); }}

View File

@ -0,0 +1,76 @@
'use client';
import { useTranslation } from '../../../i18n/useTranslation';
type Props = {
isOpen: boolean;
newCode: string;
setNewCode: (value: string) => void;
newName: string;
setNewName: (value: string) => void;
addError: string;
setAddError: (value: string) => void;
onClose: () => void;
onAdd: () => void;
};
export default function AddLanguageModal({
isOpen,
newCode,
setNewCode,
newName,
setNewName,
addError,
setAddError,
onClose,
onAdd,
}: Props) {
const { t } = useTranslation();
if (!isOpen) return null;
return (
<div className="fixed inset-0 z-[100] flex items-center justify-center bg-black/40 backdrop-blur-sm">
<div className="mx-4 w-full max-w-sm rounded-2xl bg-white p-6 shadow-2xl">
<h2 className="text-lg font-bold text-[#1C2B4A] mb-4">{t('autofix.kf4e45236')}</h2>
<div className="space-y-3">
<div>
<label className="block text-sm font-medium mb-1">{t('autofix.k92639a9a')}</label>
<input
value={newCode}
onChange={(e) => setNewCode(e.target.value)}
placeholder={t('autofix.k03538639')}
className="w-full rounded border border-gray-300 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-[#1C2B4A]"
/>
</div>
<div>
<label className="block text-sm font-medium mb-1">{t('autofix.k926966d0')}</label>
<input
value={newName}
onChange={(e) => setNewName(e.target.value)}
placeholder={t('autofix.ka019b3c0')}
className="w-full rounded border border-gray-300 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-[#1C2B4A]"
/>
</div>
{addError && <p className="text-xs text-red-600">{addError}</p>}
</div>
<div className="mt-5 flex justify-end gap-3">
<button
onClick={() => {
onClose();
setAddError('');
setNewCode('');
setNewName('');
}}
className="rounded-md border border-gray-300 px-4 py-2 text-sm hover:bg-gray-50"
>
Cancel
</button>
<button onClick={onAdd} className="rounded-md bg-[#1C2B4A] text-white px-4 py-2 text-sm font-semibold hover:bg-[#1C2B4A]/90">
Add
</button>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,213 @@
'use client';
import { useTranslation } from '../../../i18n/useTranslation';
import type { NamespaceCategory } from '../hooks/useNamespaceCategories';
type Props = {
isOpen: boolean;
onClose: () => void;
newCategoryLabel: string;
setNewCategoryLabel: (value: string) => void;
onCreateCategory: () => void;
uncategorizedNamespaces: string[];
categoriesWithKnownNamespaces: NamespaceCategory[];
namespaces: string[];
assignNamespaceByCategory: Record<string, string>;
setAssignNamespaceByCategory: (next: Record<string, string> | ((prev: Record<string, string>) => Record<string, string>)) => void;
expandedCategoryId: string | null;
setExpandedCategoryId: (value: string | null | ((prev: string | null) => string | null)) => void;
dragNamespace: string | null;
setDragNamespace: (value: string | null) => void;
addNamespaceToCategory: (categoryId: string, namespace: string) => void;
removeNamespaceFromCategory: (categoryId: string, namespace: string) => void;
deleteCategory: (categoryId: string) => void;
};
export default function CategoryManagerModal({
isOpen,
onClose,
newCategoryLabel,
setNewCategoryLabel,
onCreateCategory,
uncategorizedNamespaces,
categoriesWithKnownNamespaces,
namespaces,
assignNamespaceByCategory,
setAssignNamespaceByCategory,
expandedCategoryId,
setExpandedCategoryId,
dragNamespace,
setDragNamespace,
addNamespaceToCategory,
removeNamespaceFromCategory,
deleteCategory,
}: Props) {
const { t } = useTranslation();
if (!isOpen) return null;
return (
<div className="fixed inset-0 z-[150] flex items-center justify-center bg-black/45 backdrop-blur-sm">
<div className="mx-4 w-full max-w-5xl rounded-2xl border border-slate-200 bg-white shadow-2xl overflow-hidden">
<div className="px-6 py-4 border-b border-slate-200 bg-slate-50 flex items-start justify-between gap-4">
<div>
<h2 className="text-lg font-bold text-[#1C2B4A]">{t('autofix.kef9de7f0')}</h2>
<p className="text-xs text-slate-600 mt-1">{t('autofix.kc4671abe')}</p>
</div>
<button onClick={onClose} className="text-slate-400 hover:text-slate-700 text-xl leading-none">
</button>
</div>
<div className="px-6 py-5 max-h-[70vh] overflow-y-auto space-y-4">
<div className="flex items-center gap-2 flex-wrap">
<input
value={newCategoryLabel}
onChange={(e) => setNewCategoryLabel(e.target.value)}
placeholder={t('autofix.ke52ed6e9')}
className="w-56 rounded-md border border-slate-300 bg-white px-2.5 py-1.5 text-sm focus:outline-none focus:ring-2 focus:ring-[#1C2B4A]"
/>
<button
type="button"
onClick={onCreateCategory}
className="rounded-md bg-[#1C2B4A] text-white px-3 py-1.5 text-sm font-medium hover:bg-[#152344]"
>{t('autofix.k1db86f96')}</button>
</div>
<div className="space-y-3">
<div className="rounded-xl border border-slate-200 bg-white p-3">
<p className="text-xs font-semibold text-slate-700 mb-2">{t('autofix.k505ebdae')}</p>
<div className="flex flex-wrap gap-2 min-h-10">
{uncategorizedNamespaces.map((ns) => (
<span
key={ns}
draggable
onDragStart={() => setDragNamespace(ns)}
className="cursor-grab rounded-full border border-slate-300 bg-slate-50 px-2 py-1 text-xs text-slate-700"
title={t('autofix.k66edf1eb')}
>
{ns}
</span>
))}
{uncategorizedNamespaces.length === 0 && (
<span className="text-xs text-slate-400">{t('autofix.k741a01f7')}</span>
)}
</div>
</div>
<div className="space-y-2">
{categoriesWithKnownNamespaces.map((cat) => {
const availableToAssign = namespaces.filter((ns) => !cat.namespaces.includes(ns));
const selectValue = assignNamespaceByCategory[cat.id] ?? '';
const isExpanded = expandedCategoryId === cat.id;
return (
<div
key={cat.id}
onDragEnter={() => {
if (!dragNamespace) return;
if (expandedCategoryId !== cat.id) {
setExpandedCategoryId(cat.id);
}
}}
onDragOver={(e) => e.preventDefault()}
onDrop={() => {
if (dragNamespace) {
addNamespaceToCategory(cat.id, dragNamespace);
setDragNamespace(null);
}
}}
className="rounded-xl border border-slate-200 bg-white"
>
<div
className="px-3 py-2.5 border-b border-slate-100 flex items-center justify-between gap-2 cursor-pointer"
onClick={() => setExpandedCategoryId((prev) => (prev === cat.id ? null : cat.id))}
>
<div className="flex items-center gap-2 text-left">
<span className="text-sm font-semibold text-[#1C2B4A]">{cat.label}</span>
<span className="rounded-full bg-slate-100 px-1.5 py-0.5 text-[10px] text-slate-700">
{cat.namespaces.length}
</span>
<span className="text-xs text-slate-500">{isExpanded ? 'Hide' : 'Manage'}</span>
</div>
{cat.isCustom && (
<button
type="button"
onClick={(e) => {
e.stopPropagation();
deleteCategory(cat.id);
}}
className="text-xs rounded border border-red-200 bg-red-50 px-2 py-0.5 text-red-600 hover:bg-red-100"
>
Delete
</button>
)}
</div>
{isExpanded && (
<div className="p-3 space-y-2">
<div className="flex flex-col lg:flex-row items-stretch gap-2">
<select
value={selectValue}
onChange={(e) => setAssignNamespaceByCategory((prev) => ({ ...prev, [cat.id]: e.target.value }))}
className="w-full rounded-md border border-slate-300 bg-white px-2 py-2 text-xs"
>
<option value="">{t('autofix.k0cdc3ee9')}</option>
{availableToAssign.map((ns) => (
<option key={ns} value={ns}>{ns}</option>
))}
</select>
<button
type="button"
onClick={() => {
if (!selectValue) return;
addNamespaceToCategory(cat.id, selectValue);
setAssignNamespaceByCategory((prev) => ({ ...prev, [cat.id]: '' }));
}}
className="rounded-md border border-slate-300 bg-white px-3 py-2 text-xs text-slate-700 hover:bg-slate-50"
>
Add
</button>
</div>
<div className="flex flex-wrap gap-2 min-h-10 rounded-md border border-dashed border-slate-200 p-2">
{cat.namespaces.map((ns) => (
<span
key={ns}
draggable
onDragStart={() => setDragNamespace(ns)}
className="group cursor-grab rounded-full border border-indigo-200 bg-indigo-50 px-2 py-1 text-xs text-indigo-700"
>
{ns}
<button
type="button"
onClick={() => removeNamespaceFromCategory(cat.id, ns)}
className="ml-1 text-indigo-500 group-hover:text-red-500"
title={t('autofix.ka6791a02')}
>
×
</button>
</span>
))}
{cat.namespaces.length === 0 && (
<span className="text-xs text-slate-400">{t('autofix.kf3c3223a')}</span>
)}
</div>
</div>
)}
</div>
);
})}
</div>
</div>
</div>
<div className="px-6 py-4 border-t border-slate-200 bg-white flex justify-end">
<button onClick={onClose} className="rounded-md bg-[#1C2B4A] text-white px-4 py-2 text-sm font-semibold hover:bg-[#1C2B4A]/90">
Done
</button>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,37 @@
'use client';
import type { LanguageEntry } from '../hooks/useLanguageManagementTranslations';
import { useTranslation } from '../../../i18n/useTranslation';
type Props = {
deleteTarget: string | null;
allLanguages: LanguageEntry[];
onClose: () => void;
onDelete: (code: string) => void;
};
export default function DeleteLanguageModal({ deleteTarget, allLanguages, onClose, onDelete }: Props) {
const { t } = useTranslation();
if (!deleteTarget) return null;
return (
<div className="fixed inset-0 z-[100] flex items-center justify-center bg-black/40 backdrop-blur-sm">
<div className="mx-4 w-full max-w-sm rounded-2xl bg-white p-6 shadow-2xl">
<h2 className="text-lg font-bold text-red-600 mb-3">{t('autofix.kda5f982e')}</h2>
<p className="text-sm text-gray-600 mb-5">
Delete <strong>{allLanguages.find((l) => l.code === deleteTarget)?.name ?? deleteTarget}</strong>?
All translations for this language will be removed.
</p>
<div className="flex justify-end gap-3">
<button onClick={onClose} className="rounded-md border border-gray-300 px-4 py-2 text-sm hover:bg-gray-50">
Cancel
</button>
<button onClick={() => onDelete(deleteTarget)} className="rounded-md bg-red-600 text-white px-4 py-2 text-sm font-semibold hover:bg-red-700">
Delete
</button>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,180 @@
'use client';
import type { RefObject } from 'react';
import { useTranslation } from '../../../i18n/useTranslation';
type LanguageEntry = {
code: string;
name: string;
};
type Props = {
headerRef: RefObject<HTMLDivElement | null>;
totalKeys: number;
onScan: () => void;
isScanning: boolean;
isAutoFixing: boolean;
onBackToAdmin: () => void;
isDirty: boolean;
onSave: () => void;
saved: boolean;
saveError: string;
allLanguages: LanguageEntry[];
activeLang: string;
setActiveLang: (code: string) => void;
isBuiltin: (code: string) => boolean;
onDeleteLanguageRequest: (code: string) => void;
onOpenAddLanguage: () => void;
allTabStats: { total: number; translated: number; missing: number };
translationProgressPercent: number;
wizardMissingKeysCount: number;
onOpenTranslationWizard: () => void;
};
export default function LanguageManagementTopSection({
headerRef,
totalKeys,
onScan,
isScanning,
isAutoFixing,
onBackToAdmin,
isDirty,
onSave,
saved,
saveError,
allLanguages,
activeLang,
setActiveLang,
isBuiltin,
onDeleteLanguageRequest,
onOpenAddLanguage,
allTabStats,
translationProgressPercent,
wizardMissingKeysCount,
onOpenTranslationWizard,
}: Props) {
const { t } = useTranslation();
return (
<>
<div ref={headerRef} className="flex items-center justify-between flex-wrap gap-4">
<div>
<h1 className="text-3xl font-bold text-[#1C2B4A]">{t('autofix.k346a2c64')}</h1>
<p className="text-sm text-gray-500 mt-1">
Manage UI translations. All {totalKeys} keys scanned from the English source file.
</p>
</div>
<div className="flex items-center gap-3 flex-wrap">
<button
onClick={onScan}
disabled={isScanning || isAutoFixing}
className="rounded-md border border-[#1C2B4A] text-[#1C2B4A] px-3 py-2 text-sm font-medium hover:bg-[#1C2B4A] hover:text-white transition-colors flex items-center gap-2 disabled:opacity-50"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4" />
</svg>
{isScanning ? 'Scanning...' : 'Scan & review fixes'}
</button>
<button
onClick={onBackToAdmin}
className="rounded-md border border-gray-300 px-3 py-2 text-sm hover:bg-gray-50"
>{t('autofix.kea7cde7a')}</button>
{isDirty && (
<button
onClick={onSave}
className="rounded-md bg-[#1C2B4A] text-white px-4 py-2 text-sm font-semibold hover:bg-[#1C2B4A]/90"
>{t('autofix.k4be6f631')}</button>
)}
{saved && !isDirty && (
<span className="rounded-md bg-green-50 border border-green-200 text-green-700 px-3 py-2 text-sm font-medium">
Saved
</span>
)}
</div>
</div>
{saveError && (
<div className="rounded-xl border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">
{saveError}
</div>
)}
<div className="flex items-center gap-2 flex-wrap">
{allLanguages.map((lang) => (
<button
key={lang.code}
onClick={() => setActiveLang(lang.code)}
className={`relative rounded-lg px-4 py-2 text-sm font-medium transition flex items-center gap-2 ${
activeLang === lang.code
? 'bg-[#1C2B4A] text-white shadow'
: 'border border-gray-300 bg-white text-gray-700 hover:bg-gray-50'
}`}
>
{lang.name}
<span className="text-xs opacity-60">({lang.code})</span>
{!isBuiltin(lang.code) && (
<button
onClick={(e) => {
e.stopPropagation();
onDeleteLanguageRequest(lang.code);
}}
title={t('autofix.k5fcc9b0e')}
className={`ml-1 inline-flex items-center justify-center rounded-full w-4 h-4 text-xs leading-none ${
activeLang === lang.code
? 'bg-white/20 hover:bg-white/40 text-white'
: 'bg-gray-200 hover:bg-red-100 text-gray-500 hover:text-red-600'
}`}
>
×
</button>
)}
</button>
))}
<button
onClick={onOpenAddLanguage}
className="rounded-lg border-2 border-dashed border-gray-300 px-4 py-2 text-sm text-gray-500 hover:border-[#1C2B4A] hover:text-[#1C2B4A] transition"
>
+ Add language
</button>
</div>
{activeLang !== 'en' && (
<div className="rounded-xl border border-gray-200 bg-white p-4 flex items-center gap-4">
<div className="flex-1">
<div className="flex justify-between text-xs text-gray-500 mb-1">
<span>{t('autofix.kb8f33873')}</span>
<span>{allTabStats.translated} / {allTabStats.total} keys translated</span>
</div>
<div className="h-2 rounded-full bg-gray-100 overflow-hidden">
<div
className="h-full rounded-full bg-[#1C2B4A] transition-all"
style={{ width: `${translationProgressPercent}%` }}
/>
</div>
</div>
<span className="text-lg font-bold text-[#1C2B4A]">
{translationProgressPercent}%
</span>
</div>
)}
{activeLang !== 'en' && allTabStats.missing > 0 && wizardMissingKeysCount > 0 && (
<div className="rounded-xl border border-indigo-200 bg-indigo-50 p-4 flex items-start justify-between gap-4">
<div>
<p className="text-sm font-semibold text-indigo-900">{t('autofix.k5e5e8744')}</p>
<p className="text-xs text-indigo-800 mt-1">
{allLanguages.find((l) => l.code === activeLang)?.name ?? activeLang} still has {wizardMissingKeysCount} missing keys. Start the wizard to fill them step by step.
</p>
</div>
<div className="flex items-center gap-2 shrink-0">
<button
type="button"
onClick={onOpenTranslationWizard}
className="rounded-md bg-[#1C2B4A] text-white px-3 py-1.5 text-xs font-semibold hover:bg-[#1C2B4A]/90"
>{t('autofix.k725dd1d6')}</button>
</div>
</div>
)}
</>
);
}

View File

@ -0,0 +1,100 @@
'use client';
import { useTranslation } from '../../../i18n/useTranslation';
type Props = {
fixableFiles: string[]
selectedFiles: string[]
isAutoFixing: boolean
forceConvertToClient: boolean
onToggleFile: (file: string) => void
onSelectAll: () => void
onClear: () => void
onToggleForceConvertToClient: () => void
onRunFixSelected: () => void
}
export default function ScanFixPanel({
fixableFiles,
selectedFiles,
isAutoFixing,
forceConvertToClient,
onToggleFile,
onSelectAll,
onClear,
onToggleForceConvertToClient,
onRunFixSelected,
}: Props) {
const { t } = useTranslation();
return (
<div className="mb-5 rounded-xl border border-indigo-200 bg-indigo-50/40 p-4">
<div className="flex items-start justify-between gap-3 flex-wrap">
<div>
<h3 className="text-sm font-semibold text-indigo-800">{t('autofix.k43218db0')}</h3>
<p className="text-xs text-indigo-700/90 mt-1">{t('autofix.k34a0a2e4')}</p>
<label className="mt-2 inline-flex items-center gap-2 text-xs text-indigo-800">
<input
type="checkbox"
checked={forceConvertToClient}
onChange={onToggleForceConvertToClient}
className="h-3.5 w-3.5 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
/>{t('autofix.k68c88f41')}</label>
<p className="text-[11px] text-indigo-700/80 mt-1">{t('autofix.k6569783c')}</p>
</div>
<div className="flex items-center gap-2">
<button
type="button"
onClick={onSelectAll}
className="rounded-md border border-indigo-200 bg-white px-2.5 py-1.5 text-xs font-medium text-indigo-700 hover:bg-indigo-50"
>{t('autofix.k4c6eb72c')}</button>
<button
type="button"
onClick={onClear}
className="rounded-md border border-gray-200 bg-white px-2.5 py-1.5 text-xs font-medium text-gray-700 hover:bg-gray-50"
>
Clear
</button>
</div>
</div>
{fixableFiles.length === 0 ? (
<div className="mt-3 rounded-lg border border-emerald-200 bg-emerald-50 px-3 py-2 text-xs text-emerald-700">{t('autofix.ke3480838')}</div>
) : (
<div className="mt-3 space-y-2 max-h-44 overflow-y-auto pr-1">
{fixableFiles.map((file) => {
const checked = selectedFiles.includes(file)
return (
<label
key={file}
className="flex items-center gap-2 rounded-md border border-indigo-100 bg-white px-3 py-2 text-xs text-indigo-900"
>
<input
type="checkbox"
checked={checked}
onChange={() => onToggleFile(file)}
className="h-3.5 w-3.5 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
/>
<span className="font-mono break-all">{file}</span>
</label>
)
})}
</div>
)}
<div className="mt-3 flex items-center justify-between gap-3 flex-wrap">
<p className="text-xs text-indigo-700">
Selected: {selectedFiles.length} / {fixableFiles.length}
</p>
<button
type="button"
disabled={isAutoFixing || selectedFiles.length === 0}
onClick={onRunFixSelected}
className="rounded-md bg-indigo-700 text-white px-3 py-1.5 text-xs font-semibold hover:bg-indigo-600 disabled:opacity-50"
>
{isAutoFixing ? 'Applying fix...' : `Fix selected files (${selectedFiles.length})`}
</button>
</div>
</div>
)
}

View File

@ -0,0 +1,342 @@
'use client';
import ScanFixPanel from './ScanFixPanel';
import { useTranslation } from '../../../i18n/useTranslation';
import type { WorkspaceScanResult } from '../hooks/useI18nScanWorkflow';
type LanguageEntry = {
code: string;
name: string;
};
type NamespaceScanResult = {
ns: string;
total: number;
translated: number;
missing: number;
};
type Props = {
isOpen: boolean;
onClose: () => void;
lastScanTime: Date | null;
workspaceScan: WorkspaceScanResult | null;
totalKeys: number;
namespacesCount: number;
allLanguages: LanguageEntry[];
activeLang: string;
scanResults: NamespaceScanResult[];
scanError: string | null;
isScanning: boolean;
isAutoFixing: boolean;
fixableFiles: string[];
selectedFiles: string[];
forceConvertToClient: boolean;
onToggleFile: (file: string) => void;
onSelectAll: () => void;
onClear: () => void;
onToggleForceConvertToClient: () => void;
onRunFixSelected: () => void;
};
export default function ScanResultsModal({
isOpen,
onClose,
lastScanTime,
workspaceScan,
totalKeys,
namespacesCount,
allLanguages,
activeLang,
scanResults,
scanError,
isScanning,
isAutoFixing,
fixableFiles,
selectedFiles,
forceConvertToClient,
onToggleFile,
onSelectAll,
onClear,
onToggleForceConvertToClient,
onRunFixSelected,
}: Props) {
const { t } = useTranslation();
if (!isOpen) return null;
const totalTranslated = scanResults.reduce((sum, row) => sum + row.translated, 0);
const coveragePercent = totalKeys === 0
? 100
: Math.round((totalTranslated / totalKeys) * 100);
return (
<div className="fixed inset-0 z-[9999] flex items-start justify-center pt-14 pb-6 bg-black/50 backdrop-blur-sm">
<div className="mx-3 w-full max-w-[1400px] rounded-3xl border border-slate-200 bg-white shadow-2xl flex flex-col max-h-[calc(100vh-5rem)] overflow-hidden">
<div className="px-8 pt-6 pb-5 border-b border-slate-200 bg-gradient-to-b from-slate-50 to-white">
<div className="flex items-start justify-between gap-4">
<div>
<h2 className="text-2xl font-bold text-[#1C2B4A]">{t('autofix.kb2217bdf')}</h2>
<p className="text-sm text-slate-600 mt-1">
{workspaceScan
? `${workspaceScan.scannedFiles} files across ${workspaceScan.scannedDirectories} directories scanned`
: `${totalKeys} keys across ${namespacesCount} namespaces`}
{lastScanTime && (
<span className="ml-2 text-xs text-slate-500">
· Scanned {lastScanTime.toLocaleTimeString()}
</span>
)}
</p>
</div>
<button
onClick={onClose}
className="text-slate-400 hover:text-slate-700 ml-4 text-xl leading-none"
>
</button>
</div>
<div className="mt-4">
<div className="flex justify-between text-xs text-slate-500 mb-1">
<span className="font-medium">Overall coverage ({allLanguages.find((l) => l.code === activeLang)?.name ?? activeLang})</span>
<span>
{activeLang === 'en'
? `${totalKeys} / ${totalKeys}`
: `${totalTranslated} / ${totalKeys}`
}
</span>
</div>
<div className="h-2.5 rounded-full bg-slate-100 overflow-hidden">
<div
className="h-full rounded-full bg-[#1C2B4A] transition-all"
style={{ width: activeLang === 'en' ? '100%' : `${coveragePercent}%` }}
/>
</div>
</div>
{workspaceScan && (
<div className="mt-5 grid grid-cols-2 lg:grid-cols-6 gap-2.5">
<div className="rounded-xl border border-slate-200 bg-white px-3 py-2.5">
<p className="text-[11px] uppercase tracking-wide text-slate-500">{t('autofix.k91052e3f')}</p>
<p className="text-sm font-semibold text-[#1C2B4A]">{workspaceScan.translationCallCount}</p>
</div>
<div className="rounded-xl border border-slate-200 bg-white px-3 py-2.5">
<p className="text-[11px] uppercase tracking-wide text-slate-500">{t('autofix.k90a6e795')}</p>
<p className="text-sm font-semibold text-[#1C2B4A]">{workspaceScan.uniqueKeyCount}</p>
</div>
<div className="rounded-xl border border-red-200 bg-red-50 px-3 py-2.5">
<p className="text-[11px] uppercase tracking-wide text-red-600">{t('autofix.k8cf40180')}</p>
<p className="text-sm font-semibold text-red-600">{workspaceScan.missingKeys.length}</p>
</div>
<div className="rounded-xl border border-amber-200 bg-amber-50 px-3 py-2.5">
<p className="text-[11px] uppercase tracking-wide text-amber-700">{t('autofix.k1bf4ffa4')}</p>
<p className="text-sm font-semibold text-amber-700">{workspaceScan.untranslatedLiterals.length}</p>
</div>
<div className="rounded-xl border border-green-200 bg-green-50 px-3 py-2.5">
<p className="text-[11px] uppercase tracking-wide text-green-700">{t('autofix.k9b173204')}</p>
<p className="text-sm font-semibold text-green-700">{workspaceScan.changedFileCount ?? 0}</p>
</div>
<div className="rounded-xl border border-indigo-200 bg-indigo-50 px-3 py-2.5">
<p className="text-[11px] uppercase tracking-wide text-indigo-700">{t('autofix.k60874ea3')}</p>
<p className="text-sm font-semibold text-indigo-700">{workspaceScan.createdKeyCount ?? 0}</p>
</div>
</div>
)}
</div>
<div className="overflow-y-auto flex-1 px-8 py-5">
{scanError && (
<div className="mb-4 rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-700">
{scanError}
</div>
)}
{isScanning && (
<div className="mb-4 rounded-lg border border-blue-200 bg-blue-50 px-3 py-2 text-sm text-blue-700">{t('autofix.k0d9c63c5')}</div>
)}
{isAutoFixing && (
<div className="mb-4 rounded-lg border border-indigo-200 bg-indigo-50 px-3 py-2 text-sm text-indigo-700">{t('autofix.ka802064d')}</div>
)}
<div className="grid grid-cols-1 xl:grid-cols-12 gap-5 items-start">
<div className="xl:col-span-5 space-y-4">
<ScanFixPanel
fixableFiles={fixableFiles}
selectedFiles={selectedFiles}
isAutoFixing={isAutoFixing}
forceConvertToClient={forceConvertToClient}
onToggleFile={onToggleFile}
onSelectAll={onSelectAll}
onClear={onClear}
onToggleForceConvertToClient={onToggleForceConvertToClient}
onRunFixSelected={onRunFixSelected}
/>
{workspaceScan && (workspaceScan.changedFileCount ?? 0) > 0 && (
<div className="rounded-lg border border-green-200 bg-green-50 px-3 py-2 text-sm text-green-700">
Auto-fix updated {(workspaceScan.changedFileCount ?? 0)} files and created {(workspaceScan.createdKeyCount ?? 0)} translation keys.
</div>
)}
{workspaceScan && Array.isArray(workspaceScan.changedFiles) && workspaceScan.changedFiles.length > 0 && (
<div className="rounded-xl border border-green-200 bg-green-50/50 p-4">
<h3 className="text-sm font-semibold text-green-700 mb-2">{t('autofix.k5ad4d864')}</h3>
<div className="space-y-2 max-h-48 overflow-y-auto pr-1">
{workspaceScan.changedFiles.map((entry) => (
<div key={entry.file} className="rounded-md border border-green-100 bg-white px-3 py-2">
<p className="font-mono text-xs text-green-800">{entry.file}</p>
<p className="text-[11px] text-gray-600 mt-1">
{entry.replacements} replacements
{entry.addedImport ? ' · import added' : ''}
{entry.addedHook ? ' · hook added' : ''}
</p>
</div>
))}
</div>
</div>
)}
{workspaceScan && Array.isArray(workspaceScan.skippedFiles) && workspaceScan.skippedFiles.length > 0 && (
<div className="rounded-xl border border-gray-200 bg-gray-50 p-4">
<h3 className="text-sm font-semibold text-gray-700 mb-2">{t('autofix.k56a52520')}</h3>
<div className="space-y-2 max-h-48 overflow-y-auto pr-1">
{workspaceScan.skippedFiles.map((entry) => (
<div key={entry.file} className="rounded-md border border-gray-100 bg-white px-3 py-2">
<p className="font-mono text-xs text-gray-700">{entry.file}</p>
<p className="text-[11px] text-gray-500 mt-1">{entry.reason}</p>
</div>
))}
</div>
</div>
)}
{workspaceScan && Array.isArray(workspaceScan.autoFixDebug) && workspaceScan.autoFixDebug.length > 0 && (
<div className="rounded-xl border border-slate-200 bg-slate-50 p-4">
<h3 className="text-sm font-semibold text-slate-700 mb-2">{t('autofix.kc8034db6')}</h3>
<p className="text-xs text-slate-600 mb-3">{t('autofix.k9bd0812b')}</p>
<div className="space-y-2 max-h-64 overflow-y-auto pr-1">
{workspaceScan.autoFixDebug.slice(0, 200).map((entry) => (
<div key={`${entry.file}-${entry.status}-${entry.beforeLiteralCount}-${entry.afterLiteralCount}`} className="rounded-md border border-slate-200 bg-white px-3 py-2">
<div className="flex items-center justify-between gap-2">
<p className="font-mono text-xs text-slate-800 break-all">{entry.file}</p>
<span className={`text-[10px] uppercase tracking-wide px-2 py-0.5 rounded-full ${
entry.status === 'changed'
? 'bg-green-100 text-green-700'
: entry.status === 'skipped'
? 'bg-amber-100 text-amber-700'
: 'bg-slate-100 text-slate-700'
}`}>
{entry.status}
</span>
</div>
<p className="text-[11px] text-slate-600 mt-1">
before: {entry.beforeLiteralCount} · text: {entry.textReplacements} · attr: {entry.attrReplacements} · after: {entry.afterLiteralCount}
</p>
{entry.reason && <p className="text-[11px] text-slate-500 mt-1">{entry.reason}</p>}
</div>
))}
</div>
</div>
)}
</div>
<div className="xl:col-span-7 space-y-4">
<div className="rounded-xl border border-slate-200 bg-slate-50/40 p-4">
<h3 className="text-sm font-semibold text-[#1C2B4A] mb-3">{t('autofix.kbe30c353')}</h3>
<table className="w-full text-sm">
<thead>
<tr className="border-b border-gray-100">
<th className="pb-2 text-left font-medium text-gray-500">Namespace</th>
<th className="pb-2 text-right font-medium text-gray-500">Keys</th>
<th className="pb-2 text-right font-medium text-gray-500">Translated</th>
<th className="pb-2 text-right font-medium text-gray-500">Missing</th>
<th className="pb-2 text-left pl-4 font-medium text-gray-500">Coverage</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-50">
{scanResults.map(({ ns, total, translated, missing }) => {
const pct = total === 0 ? 100 : Math.round((translated / total) * 100);
return (
<tr key={ns} className="hover:bg-gray-50">
<td className="py-2 font-mono text-xs text-[#1C2B4A]">{ns}</td>
<td className="py-2 text-right text-gray-500">{total}</td>
<td className="py-2 text-right text-green-600 font-medium">{translated}</td>
<td className={`py-2 text-right font-medium ${missing > 0 ? 'text-red-500' : 'text-gray-400'}`}>
{missing}
</td>
<td className="py-2 pl-4">
<div className="flex items-center gap-2">
<div className="w-24 h-1.5 rounded-full bg-gray-100 overflow-hidden">
<div
className={`h-full rounded-full ${
pct === 100 ? 'bg-green-500' : pct >= 50 ? 'bg-yellow-400' : 'bg-red-400'
}`}
style={{ width: `${pct}%` }}
/>
</div>
<span className="text-xs text-gray-500">{pct}%</span>
</div>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
{workspaceScan && workspaceScan.missingKeys.length > 0 && (
<div className="rounded-xl border border-red-200 bg-red-50/50 p-4">
<h3 className="text-sm font-semibold text-red-700 mb-2">{t('autofix.kae63e46a')}</h3>
<div className="space-y-2 max-h-48 overflow-y-auto pr-1">
{workspaceScan.missingKeys.map((entry) => (
<div key={entry.key} className="rounded-md border border-red-100 bg-white px-3 py-2">
<p className="font-mono text-xs text-red-700">{entry.key}</p>
<p className="text-[11px] text-gray-500 mt-1">
{entry.files.slice(0, 3).join(', ')}
{entry.files.length > 3 ? ` (+${entry.files.length - 3} more)` : ''}
</p>
</div>
))}
</div>
</div>
)}
{workspaceScan && workspaceScan.untranslatedLiterals.length > 0 && (
<div className="rounded-xl border border-amber-200 bg-amber-50/50 p-4">
<h3 className="text-sm font-semibold text-amber-700 mb-2">{t('autofix.k14eb468b')}</h3>
<p className="text-xs text-amber-700/80 mb-3">
These literals appear directly in JSX. Replace them with t('...') to make the page translatable.
</p>
<div className="space-y-2 max-h-56 overflow-y-auto pr-1">
{workspaceScan.untranslatedLiterals.slice(0, 80).map((entry) => (
<div key={entry.text} className="rounded-md border border-amber-100 bg-white px-3 py-2">
<p className="text-xs font-medium text-amber-800">{entry.text}</p>
<p className="text-[11px] text-gray-500 mt-1">
{entry.files.slice(0, 3).join(', ')}
{entry.files.length > 3 ? ` (+${entry.files.length - 3} more)` : ''}
</p>
</div>
))}
</div>
</div>
)}
</div>
</div>
</div>
<div className="px-8 py-4 border-t border-slate-200 flex justify-between items-center bg-white">
<p className="text-xs text-slate-500">
Scan now checks workspace files (pages, components, hooks, utils) and compares used keys against en.ts.
</p>
<button
onClick={onClose}
className="rounded-md bg-[#1C2B4A] text-white px-4 py-2 text-sm font-semibold hover:bg-[#1C2B4A]/90"
>
Close
</button>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,494 @@
'use client';
import type { MutableRefObject, RefObject } from 'react';
import { getEnglishValue, useTranslation } from '../../../i18n/useTranslation';
import type { NamespaceCategory } from '../hooks/useNamespaceCategories';
type LanguageEntry = {
code: string;
name: string;
};
type Props = {
activeCategory: string;
setActiveCategory: (category: string) => void;
categoriesWithKnownNamespaces: NamespaceCategory[];
allTabStats: { total: number; translated: number; missing: number };
categoryTabStats: Record<string, { total: number; translated: number; missing: number }>;
globalTabStats: { total: number; translated: number; missing: number };
activeLang: string;
allLanguages: LanguageEntry[];
search: string;
setSearch: (value: string) => void;
autoScrollOnPanelOpen: boolean;
setAutoScrollOnPanelOpen: (value: boolean) => void;
newGlobalKeySelection: string;
setNewGlobalKeySelection: (value: string) => void;
availableGlobalKeyOptions: string[];
addGlobalKey: (key: string) => void;
globalFilteredKeys: string[];
removeGlobalKey: (key: string) => void;
getDisplayValue: (key: string) => string;
translations: Record<string, Record<string, string>>;
handleChange: (key: string, value: string) => void;
globalKeySet: Set<string>;
filteredNs: string[];
filteredGroups: Record<string, string[]>;
activeNamespacePanel: string | null;
setActiveNamespacePanel: (next: string | null | ((prev: string | null) => string | null)) => void;
namespaceTranslationStats: Record<string, { total: number; translated: number; missing: number }>;
openedNamespacePanelRef: RefObject<HTMLDivElement | null>;
openFromPanelClickRef: MutableRefObject<boolean>;
onBackToPanels: () => void;
onOpenCategoryManager: () => void;
};
export default function TranslationCoverageEditor({
activeCategory,
setActiveCategory,
categoriesWithKnownNamespaces,
allTabStats,
categoryTabStats,
globalTabStats,
activeLang,
allLanguages,
search,
setSearch,
autoScrollOnPanelOpen,
setAutoScrollOnPanelOpen,
newGlobalKeySelection,
setNewGlobalKeySelection,
availableGlobalKeyOptions,
addGlobalKey,
globalFilteredKeys,
removeGlobalKey,
getDisplayValue,
translations,
handleChange,
globalKeySet,
filteredNs,
filteredGroups,
activeNamespacePanel,
setActiveNamespacePanel,
namespaceTranslationStats,
openedNamespacePanelRef,
openFromPanelClickRef,
onBackToPanels,
onOpenCategoryManager,
}: Props) {
const { t } = useTranslation();
return (
<>
<div className="rounded-2xl border border-slate-200 bg-slate-50/50 p-4">
<div className="flex items-center justify-between gap-3 flex-wrap mb-3">
<div>
<h2 className="text-base font-semibold text-[#1C2B4A]">{t('autofix.k5f978731')}</h2>
<p className="text-xs text-slate-600 mt-1">{t('autofix.kb7a30760')}</p>
</div>
<button
type="button"
onClick={onOpenCategoryManager}
className="rounded-md border border-slate-300 bg-white px-3 py-1.5 text-sm font-medium text-slate-700 hover:bg-slate-100"
>{t('autofix.kd6e42900')}</button>
</div>
<div className="flex items-center gap-2 flex-wrap">
<button
onClick={() => setActiveCategory('all')}
className={`rounded-md px-3 py-1.5 text-sm font-medium transition ${
activeCategory === 'all'
? (activeLang !== 'en' && allTabStats.missing > 0
? 'bg-red-600 text-white shadow'
: 'bg-[#1C2B4A] text-white shadow')
: (activeLang !== 'en' && allTabStats.missing > 0
? 'border border-red-200 bg-red-50 text-red-700 hover:bg-red-100'
: 'text-gray-500 hover:text-[#1C2B4A] hover:bg-gray-100')
}`}
>
<span className="inline-flex items-center gap-1.5">
<span>All</span>
<span className={`rounded-full px-1.5 py-0.5 text-[10px] leading-none ${
activeCategory === 'all' ? 'bg-white/20 text-white' : 'bg-slate-200 text-slate-700'
}`}>
{allTabStats.translated}/{allTabStats.total}
</span>
{activeLang !== 'en' && allTabStats.missing > 0 && (
<span className={`rounded-full px-1.5 py-0.5 text-[10px] leading-none ${
activeCategory === 'all' ? 'bg-red-500/70 text-white' : 'bg-red-100 text-red-700'
}`}>
{allTabStats.missing} missing
</span>
)}
</span>
</button>
<button
onClick={() => setActiveCategory('global')}
className={`rounded-md px-3 py-1.5 text-sm font-medium transition ${
activeCategory === 'global'
? (activeLang !== 'en' && globalTabStats.missing > 0
? 'bg-red-600 text-white shadow'
: 'bg-[#1C2B4A] text-white shadow')
: (activeLang !== 'en' && globalTabStats.missing > 0
? 'border border-red-200 bg-red-50 text-red-700 hover:bg-red-100'
: 'text-gray-500 hover:text-[#1C2B4A] hover:bg-gray-100')
}`}
>
<span className="inline-flex items-center gap-1.5">
<span>Global</span>
<span className={`rounded-full px-1.5 py-0.5 text-[10px] leading-none ${
activeCategory === 'global' ? 'bg-white/20 text-white' : 'bg-slate-200 text-slate-700'
}`}>
{globalTabStats.translated}/{globalTabStats.total}
</span>
{activeLang !== 'en' && globalTabStats.missing > 0 && (
<span className={`rounded-full px-1.5 py-0.5 text-[10px] leading-none ${
activeCategory === 'global' ? 'bg-red-500/70 text-white' : 'bg-red-100 text-red-700'
}`}>
{globalTabStats.missing} missing
</span>
)}
</span>
</button>
{categoriesWithKnownNamespaces.map((cat) => {
const catStats = categoryTabStats[cat.id] ?? { total: 0, translated: 0, missing: 0 };
return (
<button
key={cat.id}
onClick={() => setActiveCategory(cat.id)}
className={`rounded-md px-3 py-1.5 text-sm font-medium transition ${
activeCategory === cat.id
? (activeLang !== 'en' && catStats.missing > 0
? 'bg-red-600 text-white shadow'
: 'bg-[#1C2B4A] text-white shadow')
: (activeLang !== 'en' && catStats.missing > 0
? 'border border-red-200 bg-red-50 text-red-700 hover:bg-red-100'
: 'text-gray-500 hover:text-[#1C2B4A] hover:bg-gray-100')
}`}
>
<span className="inline-flex items-center gap-1.5">
<span>{cat.label}</span>
<span className={`rounded-full px-1.5 py-0.5 text-[10px] leading-none ${
activeCategory === cat.id ? 'bg-white/20 text-white' : 'bg-slate-200 text-slate-700'
}`}>
{catStats.translated}/{catStats.total}
</span>
{activeLang !== 'en' && catStats.missing > 0 && (
<span className={`rounded-full px-1.5 py-0.5 text-[10px] leading-none ${
activeCategory === cat.id ? 'bg-red-500/70 text-white' : 'bg-red-100 text-red-700'
}`}>
{catStats.missing} missing
</span>
)}
</span>
</button>
);
})}
</div>
</div>
<div className="flex items-center gap-3 flex-wrap">
<input
type="search"
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder={t('autofix.kbf49d59b')}
className="w-full max-w-sm rounded-lg border border-gray-300 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-[#1C2B4A]"
/>
<label className="inline-flex items-center gap-2 text-sm text-slate-700">
<input
type="checkbox"
checked={autoScrollOnPanelOpen}
onChange={(e) => setAutoScrollOnPanelOpen(e.target.checked)}
className="h-4 w-4 rounded border-slate-300 text-[#1C2B4A] focus:ring-[#1C2B4A]"
/>{t('autofix.kfd1e0089')}
</label>
</div>
<div className="space-y-3">
{activeCategory === 'global' && (
<div className="rounded-xl border border-gray-200 bg-white overflow-hidden shadow-sm">
<div className="px-5 py-3 bg-gray-50 border-b border-gray-100 flex items-center justify-between gap-3 flex-wrap">
<div>
<span className="font-semibold text-[#1C2B4A]">{t('autofix.k6cfeedd3')}</span>
<p className="text-xs text-gray-500 mt-0.5">{t('autofix.kad7d8c49')}</p>
</div>
<div className="flex items-center gap-2">
<select
value={newGlobalKeySelection}
onChange={(e) => setNewGlobalKeySelection(e.target.value)}
className="w-72 max-w-full rounded-md border border-slate-300 bg-white px-2 py-1.5 text-xs"
>
<option value="">{t('autofix.k47bce570')}</option>
{availableGlobalKeyOptions.map((key) => (
<option key={key} value={key}>{key}</option>
))}
</select>
<button
type="button"
onClick={() => {
if (!newGlobalKeySelection) return;
addGlobalKey(newGlobalKeySelection);
setNewGlobalKeySelection('');
}}
className="rounded-md border border-slate-300 bg-white px-2 py-1.5 text-xs text-slate-700 hover:bg-slate-50"
>
Add
</button>
</div>
</div>
{globalFilteredKeys.length > 0 ? (
<table className="w-full text-sm">
<thead>
<tr className="border-b border-gray-100 bg-gray-50/50">
<th className="px-5 py-2 text-left font-medium text-gray-500 w-1/3">Key</th>
{activeLang !== 'en' && (
<th className="px-5 py-2 text-left font-medium text-gray-500 w-1/3">English (reference)</th>
)}
<th className="px-5 py-2 text-left font-medium text-gray-500">
{allLanguages.find((l) => l.code === activeLang)?.name ?? activeLang}
</th>
</tr>
</thead>
<tbody>
{globalFilteredKeys.map((key) => {
const enVal = getEnglishValue(key);
const currentVal = getDisplayValue(key);
const hasOverride = (translations[activeLang]?.[key] ?? '') !== '';
return (
<tr key={key} className="border-b border-gray-50 last:border-0 hover:bg-blue-50/30">
<td className="px-5 py-2 font-mono text-xs text-gray-500 align-top pt-3">
<div className="flex items-start justify-between gap-2">
<span>{key}</span>
<button
type="button"
title={t('autofix.kc02b17c3')}
onClick={() => removeGlobalKey(key)}
className="text-[10px] rounded border border-red-200 px-1.5 py-0.5 text-red-500 hover:bg-red-50"
>
Remove
</button>
</div>
</td>
{activeLang !== 'en' && (
<td className="px-5 py-2 text-gray-500 align-top pt-3 text-xs">{enVal}</td>
)}
<td className="px-5 py-2">
<div className="relative">
<textarea
rows={1}
value={activeLang === 'en' ? currentVal : (translations[activeLang]?.[key] ?? '')}
onChange={(e) => handleChange(key, e.target.value)}
placeholder={activeLang === 'en' ? '' : enVal}
className={`w-full rounded border px-2 py-1.5 text-sm resize-none focus:outline-none focus:ring-1 focus:ring-[#1C2B4A] ${
hasOverride && activeLang !== 'en'
? 'border-green-300 bg-green-50'
: 'border-gray-200 bg-white'
}`}
style={{ minHeight: '2.25rem', fieldSizing: 'content' } as React.CSSProperties}
onInput={(e) => {
const target = e.currentTarget;
target.style.height = 'auto';
target.style.height = `${target.scrollHeight}px`;
}}
/>
{hasOverride && activeLang !== 'en' && (
<button
type="button"
title="Clear override (revert to built-in)"
onClick={() => handleChange(key, '')}
className="absolute top-1 right-1 text-xs text-gray-400 hover:text-red-500"
>
×
</button>
)}
</div>
</td>
</tr>
);
})}
</tbody>
</table>
) : (
<div className="p-8 text-center text-sm text-gray-500">{t('autofix.k0700b1f2')}</div>
)}
</div>
)}
{activeCategory !== 'global' && filteredNs.length > 0 && (
<>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-2">
{filteredNs.map((ns, idx) => {
const keys = filteredGroups[ns] ?? [];
const nsStats = namespaceTranslationStats[ns] ?? {
total: keys.length,
translated: keys.length,
missing: 0,
};
const hasMissing = activeLang !== 'en' && nsStats.missing > 0;
const isActive = activeNamespacePanel === ns;
const activeIdx = activeNamespacePanel ? filteredNs.indexOf(activeNamespacePanel) : -1;
const shiftClass = !activeNamespacePanel || isActive
? 'translate-x-0'
: idx < activeIdx
? '-translate-x-1'
: 'translate-x-1';
return (
<button
key={ns}
type="button"
onClick={() => {
openFromPanelClickRef.current = true;
setActiveNamespacePanel((prev) => (prev === ns ? null : ns));
}}
className={`w-full rounded-xl border px-3 py-2 text-left transition-all duration-300 ${shiftClass} ${
isActive
? (hasMissing ? 'border-red-400 bg-red-50 shadow-sm' : 'border-[#1C2B4A] bg-[#1C2B4A]/5 shadow-sm')
: (hasMissing ? 'border-red-200 bg-red-50/70 hover:bg-red-50' : 'border-gray-200 bg-white hover:border-slate-300 hover:bg-slate-50')
}`}
>
<div className="flex items-center justify-between gap-2">
<span className="text-sm font-semibold text-[#1C2B4A] capitalize">{ns}</span>
<div className="flex items-center gap-1">
<span className={`rounded-full px-1.5 py-0.5 text-[10px] font-semibold ${
isActive
? (hasMissing ? 'bg-red-500 text-white' : 'bg-[#1C2B4A] text-white')
: (hasMissing ? 'bg-red-100 text-red-700' : 'bg-slate-100 text-slate-700')
}`}>
{nsStats.translated}/{nsStats.total}
</span>
{hasMissing && (
<span className="rounded-full bg-red-100 px-1.5 py-0.5 text-[10px] font-semibold text-red-700">
{nsStats.missing} missing
</span>
)}
</div>
</div>
<p className="mt-1 text-[11px] text-slate-500">{isActive ? 'Open' : 'Click to open'}</p>
</button>
);
})}
</div>
{activeNamespacePanel && filteredGroups[activeNamespacePanel] && (
<div ref={openedNamespacePanelRef} className="rounded-xl border border-gray-200 bg-white overflow-hidden shadow-sm transition-all duration-300">
<div className="w-full flex items-center justify-between px-5 py-3 bg-gray-50 border-b border-gray-100">
<span className="font-semibold text-[#1C2B4A] capitalize">{activeNamespacePanel}</span>
<div className="flex items-center gap-1">
<span className="text-xs rounded-full bg-slate-100 px-1.5 py-0.5 text-slate-700">
{(namespaceTranslationStats[activeNamespacePanel]?.translated ?? 0)}/{(namespaceTranslationStats[activeNamespacePanel]?.total ?? 0)}
</span>
{activeLang !== 'en' && (namespaceTranslationStats[activeNamespacePanel]?.missing ?? 0) > 0 && (
<span className="text-xs rounded-full bg-red-100 px-1.5 py-0.5 text-red-700">
{namespaceTranslationStats[activeNamespacePanel]?.missing} missing
</span>
)}
</div>
</div>
<table className="w-full text-sm">
<thead>
<tr className="border-b border-gray-100 bg-gray-50/50">
<th className="px-5 py-2 text-left font-medium text-gray-500 w-1/3">Key</th>
{activeLang !== 'en' && (
<th className="px-5 py-2 text-left font-medium text-gray-500 w-1/3">English (reference)</th>
)}
<th className="px-5 py-2 text-left font-medium text-gray-500">
{allLanguages.find((l) => l.code === activeLang)?.name ?? activeLang}
</th>
</tr>
</thead>
<tbody>
{filteredGroups[activeNamespacePanel].map((key) => {
const enVal = getEnglishValue(key);
const currentVal = getDisplayValue(key);
const visibleValue = activeLang === 'en'
? currentVal
: (translations[activeLang]?.[key] ?? '');
const hasOverride = (translations[activeLang]?.[key] ?? '') !== '';
const isMissingInOpenedPanel =
activeLang !== 'en' && (
visibleValue.trim() === '' ||
visibleValue.trim() === enVal.trim()
);
return (
<tr key={key} className={`border-b border-gray-50 last:border-0 ${
isMissingInOpenedPanel ? 'bg-red-50/50 hover:bg-red-50' : 'hover:bg-blue-50/30'
}`}>
<td className={`px-5 py-2 font-mono text-xs align-top pt-3 ${
isMissingInOpenedPanel ? 'text-red-700' : 'text-gray-500'
}`}>
<div className="flex items-start justify-between gap-2">
<span>{key}</span>
{globalKeySet.has(key) && (
<span className="rounded-full border border-indigo-200 bg-indigo-50 px-1.5 py-0.5 text-[10px] font-semibold text-indigo-700">
Global
</span>
)}
</div>
</td>
{activeLang !== 'en' && (
<td className="px-5 py-2 text-gray-500 align-top pt-3 text-xs">{enVal}</td>
)}
<td className="px-5 py-2">
<div className="relative">
<textarea
rows={1}
value={visibleValue}
onChange={(e) => handleChange(key, e.target.value)}
placeholder={activeLang === 'en' ? '' : enVal}
className={`w-full rounded border px-2 py-1.5 text-sm resize-none focus:outline-none focus:ring-1 focus:ring-[#1C2B4A] ${
isMissingInOpenedPanel
? 'border-red-300 bg-red-50'
: hasOverride && activeLang !== 'en'
? 'border-green-300 bg-green-50'
: 'border-gray-200 bg-white'
}`}
style={{ minHeight: '2.25rem', fieldSizing: 'content' } as React.CSSProperties}
onInput={(e) => {
const target = e.currentTarget;
target.style.height = 'auto';
target.style.height = `${target.scrollHeight}px`;
}}
/>
{hasOverride && activeLang !== 'en' && (
<button
type="button"
title="Clear override (revert to built-in)"
onClick={() => handleChange(key, '')}
className="absolute top-1 right-1 text-xs text-gray-400 hover:text-red-500"
>
×
</button>
)}
</div>
</td>
</tr>
);
})}
</tbody>
</table>
<div className="border-t border-gray-100 px-5 py-3 bg-white flex justify-end">
<button
type="button"
onClick={onBackToPanels}
className="rounded-md border border-slate-300 bg-white px-3 py-1.5 text-xs font-medium text-slate-700 hover:bg-slate-50"
>
{t('autofix.k6aba2cb0')}
</button>
</div>
</div>
)}
</>
)}
{activeCategory !== 'global' && filteredNs.length === 0 && (
<div className="rounded-xl border border-gray-200 bg-white p-8 text-center text-sm text-gray-500">{t('autofix.k6a892262')}</div>
)}
</div>
</>
);
}

View File

@ -0,0 +1,123 @@
'use client';
import { useTranslation } from '../../../i18n/useTranslation';
import type { LanguageEntry } from '../hooks/useLanguageManagementTranslations';
type Props = {
isOpen: boolean;
currentWizardKey: string | null;
wizardIndex: number;
wizardMissingCount: number;
activeLang: string;
allLanguages: LanguageEntry[];
wizardInput: string;
setWizardInput: (value: string) => void;
wizardMarkGlobal: boolean;
setWizardMarkGlobal: (value: boolean) => void;
englishValue: string;
onClose: () => void;
onPrevious: () => void;
onSkip: () => void;
onNext: () => void;
};
export default function TranslationWizardModal({
isOpen,
currentWizardKey,
wizardIndex,
wizardMissingCount,
activeLang,
allLanguages,
wizardInput,
setWizardInput,
wizardMarkGlobal,
setWizardMarkGlobal,
englishValue,
onClose,
onPrevious,
onSkip,
onNext,
}: Props) {
const { t } = useTranslation();
if (!isOpen || !currentWizardKey) return null;
return (
<div className="fixed inset-0 z-[120] flex items-center justify-center bg-black/45 backdrop-blur-sm">
<div className="mx-4 w-full max-w-2xl rounded-2xl border border-slate-200 bg-white shadow-2xl overflow-hidden">
<div className="px-6 py-4 border-b border-slate-200 bg-slate-50 flex items-start justify-between gap-4">
<div>
<h2 className="text-lg font-bold text-[#1C2B4A]">{t('autofix.kcd190bdd')}</h2>
<p className="text-xs text-slate-600 mt-1">
Step {wizardIndex + 1} of {wizardMissingCount} for {allLanguages.find((l) => l.code === activeLang)?.name ?? activeLang}
</p>
</div>
<button onClick={onClose} className="text-slate-400 hover:text-slate-700 text-xl leading-none">
</button>
</div>
<div className="px-6 py-5 space-y-4">
<div className="rounded-lg border border-slate-200 bg-slate-50 px-3 py-2">
<p className="text-[11px] uppercase tracking-wide text-slate-500">Key</p>
<p className="font-mono text-xs text-slate-700 mt-1 break-all">{currentWizardKey}</p>
</div>
<div className="rounded-lg border border-slate-200 bg-white px-3 py-2">
<p className="text-[11px] uppercase tracking-wide text-slate-500">{t('autofix.kc518ff5c')}</p>
<p className="text-sm text-slate-700 mt-1">{englishValue}</p>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Translation</label>
<textarea
value={wizardInput}
onChange={(e) => setWizardInput(e.target.value)}
placeholder={t('languageManagement.wizardInputPlaceholder')}
rows={4}
className="w-full rounded-lg border border-slate-300 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-[#1C2B4A]"
/>
</div>
<label className="inline-flex items-center gap-2 text-sm text-slate-700">
<input
type="checkbox"
checked={wizardMarkGlobal}
onChange={(e) => setWizardMarkGlobal(e.target.checked)}
className="h-4 w-4 rounded border-slate-300 text-[#1C2B4A] focus:ring-[#1C2B4A]"
/>
Mark as global term (for shared words like IBAN)
</label>
</div>
<div className="px-6 py-4 border-t border-slate-200 bg-white flex items-center justify-between gap-3">
<button
type="button"
onClick={onPrevious}
disabled={wizardIndex === 0}
className="rounded-md border border-slate-300 px-3 py-2 text-sm text-slate-700 hover:bg-slate-50 disabled:opacity-50"
>
Back
</button>
<div className="flex items-center gap-2">
<button
type="button"
onClick={onSkip}
className="rounded-md border border-slate-300 px-3 py-2 text-sm text-slate-700 hover:bg-slate-50"
>
Skip
</button>
<button
type="button"
onClick={onNext}
disabled={wizardInput.trim() === ''}
className="rounded-md bg-[#1C2B4A] text-white px-4 py-2 text-sm font-semibold hover:bg-[#1C2B4A]/90 disabled:opacity-50"
>
{wizardIndex >= wizardMissingCount - 1 ? 'Save and finish' : 'Save and next'}
</button>
</div>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,181 @@
import { useEffect, useMemo, useState } from 'react'
export type WorkspaceScanResult = {
scannedFiles: number
scannedDirectories: number
translationCallCount: number
uniqueKeyCount: number
missingKeys: Array<{ key: string; files: string[] }>
untranslatedLiterals: Array<{ text: string; files: string[] }>
autoFixEligibleFiles?: string[]
autoFixForceConvertibleFiles?: string[]
changedFileCount?: number
createdKeyCount?: number
changedFiles?: Array<{ file: string; replacements: number; addedImport: boolean; addedHook: boolean }>
skippedFiles?: Array<{ file: string; reason: string }>
autoFixDebug?: Array<{
file: string
status: 'changed' | 'skipped' | 'no-op'
beforeLiteralCount: number
textReplacements: number
attrReplacements: number
afterLiteralCount: number
reason?: string
}>
}
function normalizeScanResult(result: any): WorkspaceScanResult {
return {
scannedFiles: Number(result?.scannedFiles ?? 0),
scannedDirectories: Number(result?.scannedDirectories ?? 0),
translationCallCount: Number(result?.translationCallCount ?? 0),
uniqueKeyCount: Number(result?.uniqueKeyCount ?? 0),
missingKeys: Array.isArray(result?.missingKeys) ? result.missingKeys : [],
untranslatedLiterals: Array.isArray(result?.untranslatedLiterals) ? result.untranslatedLiterals : [],
autoFixEligibleFiles: Array.isArray(result?.autoFixEligibleFiles) ? result.autoFixEligibleFiles : undefined,
autoFixForceConvertibleFiles: Array.isArray(result?.autoFixForceConvertibleFiles) ? result.autoFixForceConvertibleFiles : undefined,
changedFileCount: Number(result?.changedFileCount ?? 0),
createdKeyCount: Number(result?.createdKeyCount ?? 0),
changedFiles: Array.isArray(result?.changedFiles) ? result.changedFiles : [],
skippedFiles: Array.isArray(result?.skippedFiles) ? result.skippedFiles : [],
autoFixDebug: Array.isArray(result?.autoFixDebug) ? result.autoFixDebug : [],
}
}
function getFixableFiles(scan: WorkspaceScanResult | null, forceConvertToClient: boolean): string[] {
if (!scan) return []
if (forceConvertToClient && Array.isArray(scan.autoFixForceConvertibleFiles)) {
return [...scan.autoFixForceConvertibleFiles].sort((a, b) => a.localeCompare(b))
}
// Prefer backend-provided eligibility, including the intentional empty list.
if (Array.isArray(scan.autoFixEligibleFiles)) {
return [...scan.autoFixEligibleFiles].sort((a, b) => a.localeCompare(b))
}
const fileSet = new Set<string>()
for (const entry of scan.untranslatedLiterals) {
for (const file of entry.files) {
fileSet.add(file)
}
}
return Array.from(fileSet).sort((a, b) => a.localeCompare(b))
}
export function useI18nScanWorkflow() {
const [showScanModal, setShowScanModal] = useState(false)
const [lastScanTime, setLastScanTime] = useState<Date | null>(null)
const [isScanning, setIsScanning] = useState(false)
const [isAutoFixing, setIsAutoFixing] = useState(false)
const [scanError, setScanError] = useState<string | null>(null)
const [workspaceScan, setWorkspaceScan] = useState<WorkspaceScanResult | null>(null)
const [selectedFiles, setSelectedFiles] = useState<string[]>([])
const [forceConvertToClient, setForceConvertToClient] = useState(false)
const fixableFiles = useMemo(
() => getFixableFiles(workspaceScan, forceConvertToClient),
[workspaceScan, forceConvertToClient]
)
useEffect(() => {
setSelectedFiles(fixableFiles)
}, [fixableFiles])
const applyScanResult = (result: any) => {
const normalized = normalizeScanResult(result)
const files = getFixableFiles(normalized, forceConvertToClient)
setWorkspaceScan(normalized)
setSelectedFiles(files)
setLastScanTime(new Date())
}
const scan = async () => {
setShowScanModal(true)
setIsScanning(true)
setScanError(null)
try {
const response = await fetch('/api/i18n/scan', { method: 'GET' })
const result = await response.json()
if (!response.ok || !result?.ok) {
throw new Error(result?.message || 'Scan failed.')
}
applyScanResult(result)
} catch (error) {
setScanError(error instanceof Error ? error.message : 'Scan failed.')
} finally {
setIsScanning(false)
}
}
const runFixSelected = async () => {
const selectedEligible = selectedFiles.filter((file) => fixableFiles.includes(file))
if (selectedEligible.length === 0) {
setScanError('Select at least one file before running auto-fix.')
return
}
setShowScanModal(true)
setIsAutoFixing(true)
setScanError(null)
try {
const response = await fetch('/api/i18n/scan', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ targetFiles: selectedEligible, forceConvertToClient }),
})
const result = await response.json()
if (!response.ok || !result?.ok) {
throw new Error(result?.message || 'Auto-fix failed.')
}
applyScanResult(result)
} catch (error) {
setScanError(error instanceof Error ? error.message : 'Auto-fix failed.')
} finally {
setIsAutoFixing(false)
}
}
const toggleFileSelection = (file: string) => {
setSelectedFiles((prev) => {
if (prev.includes(file)) return prev.filter((f) => f !== file)
return [...prev, file]
})
}
const selectAllFiles = () => {
setSelectedFiles(fixableFiles)
}
const clearSelectedFiles = () => {
setSelectedFiles([])
}
return {
showScanModal,
setShowScanModal,
lastScanTime,
isScanning,
isAutoFixing,
scanError,
workspaceScan,
selectedFiles,
fixableFiles,
forceConvertToClient,
setForceConvertToClient,
scan,
runFixSelected,
toggleFileSelection,
selectAllFiles,
clearSelectedFiles,
}
}

View File

@ -0,0 +1,217 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
export type LanguageEntry = {
code: string;
name: string;
};
export type FileBackedI18nData = {
languages: LanguageEntry[];
translations: Record<string, Record<string, string>>;
};
type UseLanguageManagementTranslationsOptions = {
coreLanguages: Set<string>;
};
export function useLanguageManagementTranslations({ coreLanguages }: UseLanguageManagementTranslationsOptions) {
const [data, setData] = useState<FileBackedI18nData>({ languages: [], translations: {} });
const [isDirty, setIsDirty] = useState(false);
const [saved, setSaved] = useState(false);
const [saveError, setSaveError] = useState('');
const [activeLang, setActiveLang] = useState('en');
const [showAddModal, setShowAddModal] = useState(false);
const [newCode, setNewCode] = useState('');
const [newName, setNewName] = useState('');
const [addError, setAddError] = useState('');
const [deleteTarget, setDeleteTarget] = useState<string | null>(null);
const fetchTranslationFiles = useCallback(async () => {
const response = await fetch('/api/i18n/translations', { cache: 'no-store' });
const result = await response.json();
if (!response.ok || !result?.ok) {
throw new Error(result?.message || 'Failed to load translation files.');
}
const languages = Array.isArray(result.languages)
? result.languages.filter((lang: unknown): lang is LanguageEntry => {
if (!lang || typeof lang !== 'object') return false;
const entry = lang as LanguageEntry;
return typeof entry.code === 'string' && typeof entry.name === 'string';
})
: [];
const translations = result.translations && typeof result.translations === 'object'
? result.translations as Record<string, Record<string, string>>
: {};
setData({ languages, translations });
}, []);
useEffect(() => {
void fetchTranslationFiles();
}, [fetchTranslationFiles]);
const allLanguages: LanguageEntry[] = useMemo(() => data.languages, [data.languages]);
useEffect(() => {
if (allLanguages.length === 0) return;
if (allLanguages.some((lang) => lang.code === activeLang)) return;
const fallback = allLanguages.find((lang) => lang.code === 'en')?.code ?? allLanguages[0].code;
setActiveLang(fallback);
}, [allLanguages, activeLang]);
const getDisplayValue = useCallback(
(key: string): string => data.translations[activeLang]?.[key] ?? '',
[activeLang, data.translations]
);
const handleChange = useCallback((key: string, value: string) => {
setData((prev) => {
const langTranslations = { ...(prev.translations[activeLang] ?? {}) };
langTranslations[key] = value;
return {
...prev,
translations: { ...prev.translations, [activeLang]: langTranslations },
};
});
setIsDirty(true);
setSaved(false);
}, [activeLang]);
const handleSave = useCallback(async () => {
try {
setSaveError('');
const response = await fetch('/api/i18n/translations', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ translations: data.translations }),
});
const result = await response.json();
if (!response.ok || !result?.ok) {
throw new Error(result?.message || 'Failed to save translation files.');
}
const languages = Array.isArray(result.languages) ? result.languages as LanguageEntry[] : [];
const translations = result.translations && typeof result.translations === 'object'
? result.translations as Record<string, Record<string, string>>
: {};
setData({ languages, translations });
setIsDirty(false);
setSaved(true);
setTimeout(() => setSaved(false), 2500);
} catch (error) {
setSaveError(error instanceof Error ? error.message : 'Failed to save translation files.');
setSaved(false);
}
}, [data.translations]);
const handleAddLanguage = useCallback(async () => {
const code = newCode.trim().toLowerCase();
const name = newName.trim();
if (!code) { setAddError('Language code is required.'); return; }
if (!name) { setAddError('Language name is required.'); return; }
if (!/^[a-z]{2,5}(-[a-zA-Z]{2,4})?$/.test(code)) {
setAddError('Use a valid BCP-47 code, e.g. fr, es, zh-TW.');
return;
}
if (allLanguages.some((l) => l.code === code)) {
setAddError(`Language "${code}" already exists.`);
return;
}
try {
const response = await fetch('/api/i18n/translations', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ code, name }),
});
const result = await response.json();
if (!response.ok || !result?.ok) {
throw new Error(result?.message || 'Failed to create language file.');
}
const languages = Array.isArray(result.languages) ? result.languages as LanguageEntry[] : [];
const translations = result.translations && typeof result.translations === 'object'
? result.translations as Record<string, Record<string, string>>
: {};
setData({ languages, translations });
setShowAddModal(false);
setNewCode('');
setNewName('');
setAddError('');
setActiveLang(code);
setIsDirty(false);
setSaved(false);
} catch (error) {
setAddError(error instanceof Error ? error.message : 'Failed to create language file.');
}
}, [allLanguages, newCode, newName]);
const handleDeleteLanguage = useCallback(async (code: string) => {
if (coreLanguages.has(code)) return;
try {
const response = await fetch('/api/i18n/translations', {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ code }),
});
const result = await response.json();
if (!response.ok || !result?.ok) {
throw new Error(result?.message || 'Failed to delete language file.');
}
const languages = Array.isArray(result.languages) ? result.languages as LanguageEntry[] : [];
const translations = result.translations && typeof result.translations === 'object'
? result.translations as Record<string, Record<string, string>>
: {};
setData({ languages, translations });
setIsDirty(false);
setSaved(false);
setDeleteTarget(null);
if (activeLang === code) setActiveLang('en');
} catch (error) {
setAddError(error instanceof Error ? error.message : 'Failed to delete language file.');
}
}, [activeLang, coreLanguages]);
const isBuiltin = useCallback((code: string) => coreLanguages.has(code), [coreLanguages]);
return {
data,
allLanguages,
activeLang,
setActiveLang,
getDisplayValue,
isDirty,
saved,
saveError,
handleChange,
handleSave,
showAddModal,
setShowAddModal,
newCode,
setNewCode,
newName,
setNewName,
addError,
setAddError,
handleAddLanguage,
deleteTarget,
setDeleteTarget,
handleDeleteLanguage,
isBuiltin,
};
}

View File

@ -0,0 +1,206 @@
import { useEffect, useMemo, useState } from 'react';
const CATEGORY_LAYOUT_STORAGE_KEY = 'pp_i18n_category_layout_v1';
const GLOBAL_KEYS_STORAGE_KEY = 'pp_i18n_global_keys_v1';
type NamespaceCategorySeed = { label: string; namespaces: string[] };
export type NamespaceCategory = {
id: string;
label: string;
namespaces: string[];
isCustom: boolean;
};
function sanitizeCategoryId(label: string): string {
return `custom-${label
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/(^-|-$)/g, '')}`;
}
function createDefaultCategories(seeds: NamespaceCategorySeed[]): NamespaceCategory[] {
return seeds.map((cat) => ({
id: `default-${sanitizeCategoryId(cat.label)}`,
label: cat.label,
namespaces: [...cat.namespaces],
isCustom: false,
}));
}
type UseNamespaceCategoriesOptions = {
namespaces: string[];
allKeys: string[];
defaultCategories: NamespaceCategorySeed[];
};
export function useNamespaceCategories({ namespaces, allKeys, defaultCategories }: UseNamespaceCategoriesOptions) {
const [activeCategory, setActiveCategory] = useState<string>('all');
const [showCategoryManagerModal, setShowCategoryManagerModal] = useState(false);
const [categories, setCategories] = useState<NamespaceCategory[]>(() => createDefaultCategories(defaultCategories));
const [categoriesHydrated, setCategoriesHydrated] = useState(false);
const [globalKeys, setGlobalKeys] = useState<string[]>([]);
const [globalKeysHydrated, setGlobalKeysHydrated] = useState(false);
const [newCategoryLabel, setNewCategoryLabel] = useState('');
const [assignNamespaceByCategory, setAssignNamespaceByCategory] = useState<Record<string, string>>({});
const [expandedCategoryId, setExpandedCategoryId] = useState<string | null>(null);
const [dragNamespace, setDragNamespace] = useState<string | null>(null);
useEffect(() => {
if (typeof window !== 'undefined') {
try {
const raw = localStorage.getItem(CATEGORY_LAYOUT_STORAGE_KEY);
if (raw) {
const parsed = JSON.parse(raw) as Array<{ id: string; label: string; namespaces: string[]; isCustom: boolean }>;
if (Array.isArray(parsed) && parsed.length > 0) {
const normalized = parsed
.filter((c) => c && typeof c.id === 'string' && typeof c.label === 'string')
.map((c) => ({
id: c.id,
label: c.label,
namespaces: Array.isArray(c.namespaces) ? c.namespaces : [],
isCustom: Boolean(c.isCustom),
}));
if (normalized.length > 0) setCategories(normalized);
}
}
const rawGlobalKeys = localStorage.getItem(GLOBAL_KEYS_STORAGE_KEY);
if (rawGlobalKeys) {
const parsedGlobalKeys = JSON.parse(rawGlobalKeys) as unknown;
if (Array.isArray(parsedGlobalKeys)) {
const normalizedGlobalKeys = parsedGlobalKeys.filter((k): k is string => typeof k === 'string');
setGlobalKeys(normalizedGlobalKeys);
}
}
} catch {
// Ignore corrupted local storage.
} finally {
setCategoriesHydrated(true);
setGlobalKeysHydrated(true);
}
} else {
setCategoriesHydrated(true);
setGlobalKeysHydrated(true);
}
}, []);
useEffect(() => {
if (typeof window === 'undefined') return;
if (!categoriesHydrated) return;
localStorage.setItem(CATEGORY_LAYOUT_STORAGE_KEY, JSON.stringify(categories));
}, [categories, categoriesHydrated]);
useEffect(() => {
if (typeof window === 'undefined') return;
if (!globalKeysHydrated) return;
localStorage.setItem(GLOBAL_KEYS_STORAGE_KEY, JSON.stringify(globalKeys));
}, [globalKeys, globalKeysHydrated]);
const categoriesWithKnownNamespaces = useMemo(() => {
const namespaceSet = new Set(namespaces);
return categories.map((cat) => ({
...cat,
namespaces: cat.namespaces.filter((ns) => namespaceSet.has(ns)),
}));
}, [categories, namespaces]);
const categorizedNamespaceSet = useMemo(() => {
const set = new Set<string>();
for (const cat of categoriesWithKnownNamespaces) {
for (const ns of cat.namespaces) set.add(ns);
}
return set;
}, [categoriesWithKnownNamespaces]);
const uncategorizedNamespaces = useMemo(
() => namespaces.filter((ns) => !categorizedNamespaceSet.has(ns)),
[namespaces, categorizedNamespaceSet]
);
const addNamespaceToCategory = (categoryId: string, namespace: string) => {
if (!namespace) return;
setCategories((prev) =>
prev.map((cat) => {
if (cat.id === categoryId) {
if (cat.namespaces.includes(namespace)) return cat;
return { ...cat, namespaces: [...cat.namespaces, namespace].sort((a, b) => a.localeCompare(b)) };
}
return { ...cat, namespaces: cat.namespaces.filter((ns) => ns !== namespace) };
})
);
};
const removeNamespaceFromCategory = (categoryId: string, namespace: string) => {
setCategories((prev) =>
prev.map((cat) => (cat.id === categoryId ? { ...cat, namespaces: cat.namespaces.filter((ns) => ns !== namespace) } : cat))
);
};
const handleCreateCategory = () => {
const label = newCategoryLabel.trim();
if (!label) return;
const duplicate = categories.some((cat) => cat.label.toLowerCase() === label.toLowerCase());
if (duplicate) return;
const baseId = sanitizeCategoryId(label);
let id = baseId;
let i = 2;
while (categories.some((cat) => cat.id === id)) {
id = `${baseId}-${i}`;
i += 1;
}
setCategories((prev) => [...prev, { id, label, namespaces: [], isCustom: true }]);
setNewCategoryLabel('');
};
const deleteCategory = (categoryId: string) => {
setCategories((prev) => prev.filter((cat) => cat.id !== categoryId));
if (activeCategory === categoryId) setActiveCategory('all');
};
const globalKnownKeys = useMemo(() => {
const allKeySet = new Set(allKeys);
return globalKeys.filter((key, index, arr) => allKeySet.has(key) && arr.indexOf(key) === index);
}, [allKeys, globalKeys]);
const globalKeySet = useMemo(() => new Set(globalKnownKeys), [globalKnownKeys]);
const addGlobalKey = (key: string) => {
if (!key) return;
setGlobalKeys((prev) => (prev.includes(key) ? prev : [...prev, key]));
};
const removeGlobalKey = (key: string) => {
setGlobalKeys((prev) => prev.filter((k) => k !== key));
};
return {
activeCategory,
setActiveCategory,
showCategoryManagerModal,
setShowCategoryManagerModal,
categoriesWithKnownNamespaces,
uncategorizedNamespaces,
addNamespaceToCategory,
removeNamespaceFromCategory,
newCategoryLabel,
setNewCategoryLabel,
assignNamespaceByCategory,
setAssignNamespaceByCategory,
expandedCategoryId,
setExpandedCategoryId,
dragNamespace,
setDragNamespace,
handleCreateCategory,
deleteCategory,
globalKnownKeys,
globalKeySet,
addGlobalKey,
removeGlobalKey,
};
}

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,7 @@
'use client'
import { useTranslation } from '../../../../i18n/useTranslation';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { createPortal } from 'react-dom'
import { MagnifyingGlassIcon, XMarkIcon, BuildingOffice2Icon, UserIcon } from '@heroicons/react/24/outline'
@ -29,6 +32,7 @@ export default function SearchModal({
onAdd,
policyMaxDepth // NEW
}: Props) {
const { t } = useTranslation();
const [query, setQuery] = useState('')
const [typeFilter, setTypeFilter] = useState<'all' | UserType>('all')
const [loading, setLoading] = useState(false)
@ -277,9 +281,7 @@ export default function SearchModal({
<XMarkIcon className="h-5 w-5" />
</button>
</div>
<p className="mt-1 text-xs text-blue-200">
Search by name or email. Minimum 3 characters. Existing matrix members are hidden.
</p>
<p className="mt-1 text-xs text-blue-200">{t('autofix.kd642e230')}</p>
</div>
{/* Form */}
@ -298,7 +300,7 @@ export default function SearchModal({
<input
value={query}
onChange={e => setQuery(e.target.value)}
placeholder="Search name or email…"
placeholder={t('autofix.kb35549bb')}
className="w-full rounded-md bg-[#173456] border border-blue-800 text-sm text-blue-100 placeholder-blue-300 pl-8 pr-3 py-2 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition"
/>
</div>
@ -310,7 +312,7 @@ export default function SearchModal({
onChange={e => setTypeFilter(e.target.value as any)}
className="w-full rounded-md bg-[#173456] border border-blue-800 text-sm text-blue-100 px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition"
>
<option value="all">All Types</option>
<option value="all">{t('autofix.k10e2568f')}</option>
<option value="personal">Personal</option>
<option value="company">Company</option>
</select>
@ -333,8 +335,7 @@ export default function SearchModal({
</button>
</div>
{/* Total */}
<div className="text-sm text-blue-200 self-center">
Total: <span className="font-semibold text-white">{total}</span>
<div className="text-sm text-blue-200 self-center">{t('autofix.kc0e3b03d')}<span className="font-semibold text-white">{total}</span>
</div>
</form>
@ -346,14 +347,10 @@ export default function SearchModal({
<div className="text-sm text-red-400 mb-4">{error}</div>
)}
{!error && query.trim().length < 3 && (
<div className="py-12 text-sm text-blue-300 text-center">
Enter at least 3 characters and click Search.
</div>
<div className="py-12 text-sm text-blue-300 text-center">{t('autofix.kb87eb38b')}</div>
)}
{!error && query.trim().length >= 3 && !hasSearched && !loading && (
<div className="py-12 text-sm text-blue-300 text-center">
Ready to search. Click the Search button to fetch candidates.
</div>
<div className="py-12 text-sm text-blue-300 text-center">{t('autofix.k7c740cd5')}</div>
)}
{/* Skeleton only for first-time load (when no items yet) */}
{!error && query.trim().length >= 3 && loading && items.length === 0 && (
@ -367,9 +364,7 @@ export default function SearchModal({
</ul>
)}
{!error && hasSearched && !loading && query.trim().length >= 3 && items.length === 0 && (
<div className="py-12 text-sm text-blue-300 text-center">
No users match your filters.
</div>
<div className="py-12 text-sm text-blue-300 text-center">{t('autofix.k1e5d5139')}</div>
)}
{!error && hasSearched && items.length > 0 && (
<ul className="divide-y divide-blue-900/40 border border-blue-900/40 rounded-lg bg-[#132c4e]">
@ -427,9 +422,7 @@ export default function SearchModal({
<button
onClick={() => { setSelected(null); setParentId(undefined); }}
className="text-xs text-blue-300 hover:text-white transition"
>
Clear selection
</button>
>{t('autofix.kadd80fbc')}</button>
</div>
<label className="flex items-center gap-2 text-xs text-blue-200">
@ -438,9 +431,7 @@ export default function SearchModal({
checked={advanced}
onChange={e => setAdvanced(e.target.checked)}
className="h-3 w-3 rounded border-blue-700 bg-blue-900 text-indigo-500 focus:ring-indigo-400"
/>
Advanced: choose parent manually
</label>
/>{t('autofix.k11974e0f')}</label>
{advanced && (
<div className="space-y-2">
@ -481,9 +472,7 @@ export default function SearchModal({
checked={forceFallback}
onChange={e => setForceFallback(e.target.checked)}
className="h-3 w-3 rounded border-blue-700 bg-blue-900 text-indigo-500 focus:ring-indigo-400"
/>
Fallback to root if referral parent not in matrix
</label>
/>{t('autofix.kf823daf7')}</label>
<p className="text-[11px] text-blue-300">
If the referrer is outside the matrix, the user is placed under root; enabling fallback may mark the user as rogue.
</p>

View File

@ -1,5 +1,8 @@
'use client'
import { useTranslation } from '../../../i18n/useTranslation';
import React, { useEffect, useMemo, useState, Suspense } from 'react' // CHANGED: add Suspense
import { useSearchParams, useRouter } from 'next/navigation'
import PageLayout from '../../../components/PageLayout'
@ -13,6 +16,7 @@ const DEFAULT_FETCH_DEPTH = 50 // provisional large depth to approximate unlimit
const LEVEL_CAP = (level: number) => Math.pow(5, level) // L1=5, L2=25, ...
function MatrixDetailPageInner() {
const { t } = useTranslation()
const sp = useSearchParams()
const router = useRouter()
@ -378,7 +382,7 @@ function MatrixDetailPageInner() {
<div className="fixed inset-0 z-[9999] flex items-center justify-center bg-white/50 backdrop-blur-sm transition-opacity">
<div className="flex items-center gap-3 rounded-lg bg-white shadow-md border border-gray-200 px-4 py-3">
<span className="h-5 w-5 rounded-full border-2 border-blue-900 border-b-transparent animate-spin" />
<span className="text-sm text-gray-700">Refreshing</span>
<span className="text-sm text-gray-700">{t('autofix.k14a4b43e')}</span>
</div>
</div>
)}
@ -393,12 +397,9 @@ function MatrixDetailPageInner() {
onClick={() => router.push('/admin/matrix-management')}
className="inline-flex items-center gap-2 text-sm text-blue-900 hover:text-blue-700"
>
<ArrowLeftIcon className="h-4 w-4" />
Back to matrices
</button>
<ArrowLeftIcon className="h-4 w-4" />{t('autofix.k65b67dc3')}</button>
<h1 className="text-3xl font-extrabold text-blue-900">{matrixName}</h1>
<p className="text-base text-blue-700">
Top node: <span className="font-semibold text-blue-900">{topNodeEmail}</span>
<p className="text-base text-blue-700">{t('autofix.k31d46514')}<span className="font-semibold text-blue-900">{topNodeEmail}</span>
</p>
<div className="mt-2 flex flex-wrap items-center gap-2">
<span className="inline-flex items-center rounded-full bg-gray-100 border border-gray-200 px-3 py-1 text-xs text-gray-800">
@ -431,9 +432,7 @@ function MatrixDetailPageInner() {
onClick={() => { setOpen(true) }}
className="inline-flex items-center gap-2 rounded-lg bg-blue-900 hover:bg-blue-800 text-blue-50 px-5 py-3 text-base font-semibold shadow transition"
>
<PlusIcon className="h-5 w-5" />
Add users to matrix
</button>
<PlusIcon className="h-5 w-5" />{t('autofix.kc7c429a6')}</button>
</div>
</div>
</header>
@ -452,7 +451,7 @@ function MatrixDetailPageInner() {
<input
value={globalSearch}
onChange={e => setGlobalSearch(e.target.value)}
placeholder="Global search..."
placeholder={t('autofix.kd304af2e')}
className="pl-8 pr-2 py-2 rounded-lg border border-gray-200 text-xs focus:ring-1 focus:ring-blue-900 focus:border-transparent w-full"
/>
</div>
@ -470,27 +469,27 @@ function MatrixDetailPageInner() {
{/* Small stats (CHANGED wording) */}
<div className="mb-8 grid grid-cols-1 sm:grid-cols-4 gap-6">
<div className="rounded-xl border border-gray-100 bg-white p-5 shadow">
<div className="text-xs text-gray-500 mb-1">Total users fetched</div>
<div className="text-xs text-gray-500 mb-1">{t('autofix.k65e33378')}</div>
<div className="text-xl font-semibold text-blue-900">{users.length}</div>
</div>
<div className="rounded-xl border border-gray-100 bg-white p-5 shadow">
<div className="text-xs text-gray-500 mb-1">Rogue users</div>
<div className="text-xs text-gray-500 mb-1">{t('autofix.kb343460d')}</div>
<div className="text-xl font-semibold text-blue-900">{rogueCount}</div>
</div>
<div className="rounded-xl border border-gray-100 bg-white p-5 shadow">
<div className="text-xs text-gray-500 mb-1">Structure</div>
<div className="text-xl font-semibold text-blue-900">5ary Tree</div>
<div className="text-xl font-semibold text-blue-900">{t('autofix.kf3557acd')}</div>
</div>
<div className="rounded-xl border border-gray-100 bg-white p-5 shadow">
<div className="text-xs text-gray-500 mb-1">Policy Max Depth</div>
<div className="text-xs text-gray-500 mb-1">{t('autofix.k776b751c')}</div>
<div className="text-xl font-semibold text-blue-900">{isUnlimited ? 'Unlimited' : policyMaxDepth}</div>
</div>
<div className="rounded-xl border border-gray-100 bg-white p-5 shadow">
<div className="text-xs text-gray-500 mb-1">Fill %</div>
<div className="text-xs text-gray-500 mb-1">{t('autofix.k9683262f')}</div>
<div className="text-xl font-semibold text-blue-900">{fillMetrics.label}</div>
</div>
<div className="rounded-xl border border-gray-100 bg-white p-5 shadow">
<div className="text-xs text-gray-500 mb-1">Highest full level</div>
<div className="text-xs text-gray-500 mb-1">{t('autofix.k7f9568ec')}</div>
<div className="text-xl font-semibold text-blue-900">{fillMetrics.highestFull}</div>
</div>
</div>
@ -499,11 +498,11 @@ function MatrixDetailPageInner() {
<div className="rounded-2xl bg-white border border-gray-100 shadow-lg overflow-hidden mb-8">
<div className="px-8 py-6 border-b border-gray-100">
<h2 className="text-xl font-semibold text-blue-900">Matrix Tree (Unlimited Depth)</h2>
<p className="text-xs text-blue-700">Each node can hold up to 5 direct children. Depth unbounded.</p>
<p className="text-xs text-blue-700">{t('autofix.kab4f5159')}</p>
</div>
<div className="px-8 py-6">
{!rootNode && (
<div className="text-xs text-gray-500 italic">Root not yet loaded.</div>
<div className="text-xs text-gray-500 italic">{t('autofix.k4e61bc77')}</div>
)}
{rootNode && (
<ul className="flex flex-col gap-1">
@ -516,9 +515,7 @@ function MatrixDetailPageInner() {
{/* Vacancies placeholder */}
<div className="rounded-2xl bg-white border border-dashed border-blue-200 shadow-sm p-6 mb-8">
<h3 className="text-lg font-semibold text-blue-900 mb-2">Vacancies</h3>
<p className="text-sm text-blue-700">
Pending backend wiring to MatrixController.listVacancies. This section will surface empty slots and allow reassignment.
</p>
<p className="text-sm text-blue-700">{t('autofix.k9b3266b5')}</p>
</div>
{/* Add Users Modal */}

View File

@ -1,5 +1,8 @@
'use client'
import { useTranslation } from '../../i18n/useTranslation';
import React, { useMemo, useState, useEffect } from 'react'
import {
ChartBarIcon,
@ -28,6 +31,7 @@ type Matrix = {
}
export default function MatrixManagementPage() {
const { t } = useTranslation();
const router = useRouter()
const user = useAuthStore(s => s.user)
const token = useAuthStore(s => s.accessToken)
@ -289,37 +293,35 @@ export default function MatrixManagementPage() {
<header className="sticky top-0 z-10 bg-white/90 backdrop-blur border-b border-blue-100 py-10 px-8 rounded-2xl shadow-lg flex flex-col gap-4 mb-8">
<div className="flex items-center justify-between">
<div>
<h1 className="text-4xl font-extrabold text-blue-900 tracking-tight">Matrix Management</h1>
<p className="text-lg text-blue-700 mt-2">Manage matrices, see stats, and create new ones.</p>
<h1 className="text-4xl font-extrabold text-blue-900 tracking-tight">{t('autofix.kd09be3cd')}</h1>
<p className="text-lg text-blue-700 mt-2">{t('autofix.kdc22ad8a')}</p>
</div>
<button
onClick={() => setCreateOpen(true)}
className="inline-flex items-center gap-2 rounded-lg bg-blue-900 hover:bg-blue-800 text-blue-50 px-5 py-3 text-base font-semibold shadow transition"
>
<PlusIcon className="h-5 w-5" />
Create Matrix
</button>
<PlusIcon className="h-5 w-5" />{t('autofix.kb7849a5a')}</button>
</div>
</header>
{/* Filters */}
<div className="flex flex-wrap items-center gap-3 mb-6">
<div className="flex items-center gap-2 text-xs text-blue-900">
<span className="font-semibold">Policy filter:</span>
<span className="font-semibold">{t('autofix.ka72e833f')}</span>
<button onClick={() => setPolicyFilter('all')} className={`px-3 py-1 rounded-full border text-xs ${policyFilter==='all'?'bg-blue-900 text-white':'border-blue-200 text-blue-800'}`}>All</button>
<button onClick={() => setPolicyFilter('unlimited')} className={`px-3 py-1 rounded-full border text-xs ${policyFilter==='unlimited'?'bg-blue-900 text-white':'border-blue-200 text-blue-800'}`}>Unlimited</button>
<button onClick={() => setPolicyFilter('five')} className={`px-3 py-1 rounded-full border text-xs ${policyFilter==='five'?'bg-blue-900 text-white':'border-blue-200 text-blue-800'}`}>Depth 5</button>
<button onClick={() => setPolicyFilter('five')} className={`px-3 py-1 rounded-full border text-xs ${policyFilter==='five'?'bg-blue-900 text-white':'border-blue-200 text-blue-800'}`}>{t('autofix.kefd5231d')}</button>
</div>
<div className="flex items-center gap-2 text-xs text-blue-900">
<span className="font-semibold">Sort:</span>
<span className="font-semibold">{t('autofix.k0dca1445')}</span>
<select value={sortByPolicy} onChange={e => setSortByPolicy(e.target.value as any)} className="border rounded px-2 py-1">
<option value="none">None</option>
<option value="asc">Policy </option>
<option value="desc">Policy </option>
<option value="asc">{t('autofix.kf7a91674')}</option>
<option value="desc">{t('autofix.kf7a91676')}</option>
</select>
<select value={sortByUsers} onChange={e => setSortByUsers(e.target.value as any)} className="border rounded px-2 py-1">
<option value="desc">Users </option>
<option value="asc">Users </option>
<option value="desc">{t('autofix.k8c3085f4')}</option>
<option value="asc">{t('autofix.k8c3085f6')}</option>
</select>
</div>
</div>
@ -352,7 +354,7 @@ export default function MatrixManagementPage() {
</div>
))
) : matricesView.length === 0 ? (
<div className="text-sm text-gray-600">No matrices found.</div>
<div className="text-sm text-gray-600">{t('autofix.k0dcb69ea')}</div>
) : (
matricesView.map(m => (
<article key={m.id} className="rounded-2xl bg-white border border-gray-100 shadow-lg overflow-hidden flex flex-col">
@ -370,7 +372,7 @@ export default function MatrixManagementPage() {
</span>
</div>
<div className="mt-4 grid grid-cols-1 gap-3 text-sm text-gray-700">
<div className="flex items-center gap-2" title="Users count respects each matrixs max depth policy.">
<div className="flex items-center gap-2" title={t('autofix.k111c49d8')}>
<UsersIcon className="h-5 w-5 text-gray-500" />
<span className="font-medium">{m.usersCount}</span>
<span className="text-gray-500">users</span>
@ -399,9 +401,7 @@ export default function MatrixManagementPage() {
? (m.status === 'active' ? 'Deactivating…' : 'Activating…')
: (m.status === 'active' ? 'Deactivate' : 'Activate')}
</button>
<span className="text-[11px] text-gray-500">
State change will affect add/remove operations.
</span>
<span className="text-[11px] text-gray-500">{t('autofix.k27f56959')}</span>
<button
className="text-sm font-medium text-blue-900 hover:text-blue-700"
onClick={() => {
@ -417,9 +417,7 @@ export default function MatrixManagementPage() {
})
router.push(`/admin/matrix-management/detail?${params.toString()}`)
}}
>
View details
</button>
>{t('autofix.ka3c41ff8')}</button>
</div>
</div>
</article>
@ -435,7 +433,7 @@ export default function MatrixManagementPage() {
<div className="absolute inset-0 flex items-center justify-center p-4">
<div className="w-full max-w-md rounded-2xl bg-white shadow-2xl ring-1 ring-black/10">
<div className="px-6 py-5 border-b border-gray-100 flex items-center justify-between">
<h4 className="text-lg font-semibold text-blue-900">Create Matrix</h4>
<h4 className="text-lg font-semibold text-blue-900">{t('autofix.kb7849a5a')}</h4>
<button
onClick={() => { setCreateOpen(false); resetForm() }}
className="text-sm text-gray-500 hover:text-gray-700"
@ -446,20 +444,16 @@ export default function MatrixManagementPage() {
<form onSubmit={handleCreate} className="p-6 space-y-5">
{/* Success banner */}
{createSuccess && (
<div className="rounded-md border border-green-200 bg-green-50 px-3 py-2 text-sm text-green-700">
Matrix created successfully.
<div className="mt-1 text-green-800">
<span className="font-semibold">Name:</span> {createSuccess.name}{' '}
<span className="font-semibold ml-3">Top node:</span> {createSuccess.email}
<div className="rounded-md border border-green-200 bg-green-50 px-3 py-2 text-sm text-green-700">{t('autofix.k5738c039')}<div className="mt-1 text-green-800">
<span className="font-semibold">{t('autofix.k0cdde8f8')}</span> {createSuccess.name}{' '}
<span className="font-semibold ml-3">{t('autofix.k31d46514')}</span> {createSuccess.email}
</div>
</div>
)}
{/* 409 force prompt */}
{forcePrompt && (
<div className="rounded-md border border-amber-200 bg-amber-50 px-3 py-2 text-sm text-amber-800">
A matrix configuration already exists for this selection.
<div className="mt-2 flex items-center gap-2">
<div className="rounded-md border border-amber-200 bg-amber-50 px-3 py-2 text-sm text-amber-800">{t('autofix.k815ca9ba')}<div className="mt-2 flex items-center gap-2">
<button
type="button"
onClick={confirmForce}
@ -482,29 +476,29 @@ export default function MatrixManagementPage() {
{/* Form fields */}
<div>
<label className="block text-sm font-medium text-blue-900 mb-1">Matrix Name</label>
<label className="block text-sm font-medium text-blue-900 mb-1">{t('autofix.kd04a7c59')}</label>
<input
type="text"
value={createName}
onChange={e => setCreateName(e.target.value)}
disabled={createLoading}
className="w-full rounded-lg border border-gray-300 px-4 py-3 text-sm focus:ring-2 focus:ring-blue-900 focus:border-transparent disabled:bg-gray-100"
placeholder="e.g., Platinum Matrix"
placeholder={t('autofix.k3f833ce6')}
/>
</div>
<div>
<label className="block text-sm font-medium text-blue-900 mb-1">Top-node Email</label>
<label className="block text-sm font-medium text-blue-900 mb-1">{t('autofix.k3ee27b4f')}</label>
<input
type="email"
value={createEmail}
onChange={e => setCreateEmail(e.target.value)}
disabled={createLoading}
className="w-full rounded-lg border border-gray-300 px-4 py-3 text-sm focus:ring-2 focus:ring-blue-900 focus:border-transparent disabled:bg-gray-100"
placeholder="owner@example.com"
placeholder={t('autofix.k383672e3')}
/>
</div>
<div>
<label className="block text-sm font-medium text-blue-900 mb-1">Matrix Depth</label>
<label className="block text-sm font-medium text-blue-900 mb-1">{t('autofix.kda96f5b3')}</label>
<input
type="number"
min={1}
@ -513,7 +507,7 @@ export default function MatrixManagementPage() {
onChange={e => setCreateDepth(Number(e.target.value))}
disabled={createLoading}
className="w-full rounded-lg border border-gray-300 px-4 py-3 text-sm focus:ring-2 focus:ring-blue-900 focus:border-transparent disabled:bg-gray-100"
placeholder="e.g., 5"
placeholder={t('autofix.k8f46c81e')}
/>
</div>

View File

@ -1,5 +1,8 @@
'use client'
import { useTranslation } from '../../i18n/useTranslation';
import React from 'react'
import Header from '../../components/nav/Header'
import Footer from '../../components/Footer'
@ -12,6 +15,7 @@ import { updateNews } from './hooks/updateNews'
import { deleteNews } from './hooks/deleteNews'
export default function NewsManagementPage() {
const { t } = useTranslation();
const { items, loading, error, refresh } = useAdminNews()
const [showCreate, setShowCreate] = React.useState(false)
const [selected, setSelected] = React.useState<any | null>(null)
@ -25,10 +29,9 @@ export default function NewsManagementPage() {
<main className="bg-white min-h-screen pb-20">
<div className="mx-auto max-w-7xl px-6 pt-8 pb-12">
<div className="flex items-center justify-between">
<h1 className="text-2xl font-bold text-blue-900">News Manager</h1>
<h1 className="text-2xl font-bold text-blue-900">{t('autofix.k471ba099')}</h1>
<button onClick={() => setShowCreate(true)} className="flex items-center gap-2 px-4 py-2 bg-blue-900 text-white rounded-lg hover:bg-blue-800">
<PlusIcon className="h-5 w-5" /> Add News
</button>
<PlusIcon className="h-5 w-5" />{t('autofix.k75078d0b')}</button>
</div>
{error && <div className="mt-4 text-red-600">{error}</div>}
@ -101,7 +104,7 @@ export default function NewsManagementPage() {
<div className="absolute inset-0 flex items-center justify-center p-4">
<div className="w-full max-w-md rounded-2xl bg-white shadow-xl ring-1 ring-gray-200">
<div className="px-6 pt-6">
<h3 className="text-lg font-semibold text-blue-900">Delete news?</h3>
<h3 className="text-lg font-semibold text-blue-900">{t('autofix.k088d8f6c')}</h3>
<p className="mt-2 text-sm text-gray-700">You are about to delete "{deleteTarget.title}". This action cannot be undone.</p>
</div>
<div className="px-6 pb-6 pt-4 flex justify-end gap-3">
@ -199,7 +202,7 @@ function CreateNewsModal({
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
<div className="relative w-full max-w-2xl bg-white rounded-2xl shadow-2xl mx-4 max-h-[90vh] overflow-y-auto">
<div className="sticky top-0 bg-white border-b px-6 py-4 flex items-center justify-between">
<h2 className="text-2xl font-bold text-blue-900">Add News</h2>
<h2 className="text-2xl font-bold text-blue-900">{t('autofix.k75078d0b')}</h2>
<button onClick={onClose} className="text-gray-500 hover:text-gray-700"><XMarkIcon className="h-6 w-6"/></button>
</div>
<form onSubmit={submit} className="p-6 space-y-4">
@ -217,7 +220,7 @@ function CreateNewsModal({
onChange={e=>{ setSlugTouched(true); setSlug(e.target.value) }}
required
/>
<p className="mt-1 text-xs text-gray-500">Used in the URL. Auto-generated from title unless edited.</p>
<p className="mt-1 text-xs text-gray-500">{t('autofix.kf3b81ba3')}</p>
</div>
<input className="w-full border rounded px-4 py-2 text-gray-900" placeholder="Category" value={category} onChange={e=>setCategory(e.target.value)} />
<textarea className="w-full border rounded px-4 py-2 text-gray-900" placeholder="Summary" value={summary} onChange={e=>setSummary(e.target.value)} rows={3} />
@ -228,8 +231,8 @@ function CreateNewsModal({
{!previewUrl ? (
<div className="text-center w-full px-6 py-10">
<PhotoIcon className="mx-auto h-12 w-12 text-gray-400" />
<div className="mt-4 text-sm font-medium text-gray-700">Click to upload</div>
<p className="text-xs text-gray-500 mt-2">PNG, JPG, WebP up to 5MB</p>
<div className="mt-4 text-sm font-medium text-gray-700">{t('autofix.kf4868273')}</div>
<p className="text-xs text-gray-500 mt-2">{t('autofix.kcd9890e5')}</p>
</div>
) : (
<div className="relative w-full h-full min-h-[200px] flex items-center justify-center bg-white p-4">
@ -314,7 +317,7 @@ function EditNewsModal({ item, onClose, onUpdate }: { item: any; onClose: () =>
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
<div className="relative w-full max-w-2xl bg-white rounded-2xl shadow-2xl mx-4 max-h-[90vh] overflow-y-auto">
<div className="sticky top-0 bg-white border-b px-6 py-4 flex items-center justify-between">
<h2 className="text-2xl font-bold text-blue-900">Edit News</h2>
<h2 className="text-2xl font-bold text-blue-900">{t('autofix.k73cf4fb6')}</h2>
<button onClick={onClose} className="text-gray-500 hover:text-gray-700"><XMarkIcon className="h-6 w-6"/></button>
</div>
<form onSubmit={submit} className="p-6 space-y-4">
@ -329,8 +332,8 @@ function EditNewsModal({ item, onClose, onUpdate }: { item: any; onClose: () =>
{!displayUrl ? (
<div className="text-center w-full px-6 py-10">
<PhotoIcon className="mx-auto h-12 w-12 text-gray-400" />
<div className="mt-4 text-sm font-medium text-gray-700">Click to upload</div>
<p className="text-xs text-gray-500 mt-2">PNG, JPG, WebP up to 5MB</p>
<div className="mt-4 text-sm font-medium text-gray-700">{t('autofix.kf4868273')}</div>
<p className="text-xs text-gray-500 mt-2">{t('autofix.kcd9890e5')}</p>
</div>
) : (
<div className="relative w-full h-full min-h-[200px] flex items-center justify-center bg-white p-4">
@ -348,7 +351,7 @@ function EditNewsModal({ item, onClose, onUpdate }: { item: any; onClose: () =>
</div>
<div className="flex items-center justify-end gap-2 pt-4 border-t">
<button type="button" onClick={onClose} className="px-5 py-2.5 text-sm bg-red-50 text-red-700 rounded-lg hover:bg-red-100">Cancel</button>
<button type="submit" className="px-5 py-2.5 text-sm text-white bg-blue-900 rounded-lg">Save Changes</button>
<button type="submit" className="px-5 py-2.5 text-sm text-white bg-blue-900 rounded-lg">{t('autofix.k5a489751')}</button>
</div>
</form>
</div>

View File

@ -1,4 +1,7 @@
'use client'
import { useTranslation } from '../../../i18n/useTranslation';
import React from 'react'
interface Props {
@ -22,6 +25,7 @@ export default function CreateNewPoolModal({
success,
clearMessages
}: Props) {
const { t } = useTranslation();
const [poolName, setPoolName] = React.useState('')
const [description, setDescription] = React.useState('')
const [price, setPrice] = React.useState('0.00')
@ -52,7 +56,7 @@ export default function CreateNewPoolModal({
{/* Modal */}
<div className="relative w-full max-w-lg mx-4 rounded-2xl bg-white shadow-xl border border-blue-100 p-6">
<div className="flex items-center justify-between mb-4">
<h2 className="text-xl font-semibold text-blue-900">Create New Pool</h2>
<h2 className="text-xl font-semibold text-blue-900">{t('autofix.k209ba561')}</h2>
<button
onClick={() => { clearMessages(); onClose(); }}
className="text-gray-500 hover:text-gray-700 transition text-sm"
@ -88,10 +92,10 @@ export default function CreateNewPoolModal({
className="space-y-4"
>
<div>
<label className="block text-sm font-medium text-blue-900 mb-1">Pool Name</label>
<label className="block text-sm font-medium text-blue-900 mb-1">{t('autofix.kd4a0fd1e')}</label>
<input
className="w-full rounded-lg border border-gray-300 px-4 py-3 text-sm bg-gray-50 text-gray-900 focus:ring-2 focus:ring-blue-900 focus:border-transparent"
placeholder="e.g., VIP Members"
placeholder={t('autofix.k0925e287')}
value={poolName}
onChange={e => setPoolName(e.target.value)}
disabled={isDisabled}
@ -103,7 +107,7 @@ export default function CreateNewPoolModal({
<textarea
className="w-full rounded-lg border border-gray-300 px-4 py-3 text-sm bg-gray-50 text-gray-900 focus:ring-2 focus:ring-blue-900 focus:border-transparent"
rows={3}
placeholder="Short description of the pool"
placeholder={t('autofix.kb573897d')}
value={description}
onChange={e => setDescription(e.target.value)}
disabled={isDisabled}
@ -122,10 +126,10 @@ export default function CreateNewPoolModal({
disabled={isDisabled}
required
/>
<p className="mt-1 text-xs text-gray-500">This value is stored as net price.</p>
<p className="mt-1 text-xs text-gray-500">{t('autofix.k75cb45a7')}</p>
</div>
<div>
<label className="block text-sm font-medium text-blue-900 mb-1">Pool Type</label>
<label className="block text-sm font-medium text-blue-900 mb-1">{t('autofix.kd49dc1e1')}</label>
<select
className="w-full rounded-lg border border-gray-300 px-4 py-3 text-sm bg-gray-50 text-gray-900 focus:ring-2 focus:ring-blue-900 focus:border-transparent"
value={poolType}
@ -137,7 +141,7 @@ export default function CreateNewPoolModal({
</select>
</div>
<div>
<label className="block text-sm font-medium text-blue-900 mb-1">Linked Subscription</label>
<label className="block text-sm font-medium text-blue-900 mb-1">{t('autofix.k59422f07')}</label>
<select
className="w-full rounded-lg border border-gray-300 px-4 py-3 text-sm bg-gray-50 text-gray-900 focus:ring-2 focus:ring-blue-900 focus:border-transparent"
value={subscriptionCoffeeId}

View File

@ -1,5 +1,8 @@
'use client'
import { useTranslation } from '../../../i18n/useTranslation';
import React, { Suspense } from 'react' // CHANGED: add Suspense
import Header from '../../../components/nav/Header'
import Footer from '../../../components/Footer'
@ -19,6 +22,7 @@ type PoolUser = {
}
function PoolManagePageInner() {
const { t } = useTranslation()
const router = useRouter()
const searchParams = useSearchParams()
const user = useAuthStore(s => s.user)
@ -281,9 +285,7 @@ function PoolManagePageInner() {
}`}>
{isCore && (
<div className="inline-flex items-center gap-1.5 self-start rounded-full bg-amber-500 px-3 py-1 text-xs font-bold text-white uppercase tracking-wider shadow-sm mb-2">
<svg className="h-3.5 w-3.5" fill="currentColor" viewBox="0 0 20 20"><path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.286 3.957a1 1 0 00.95.69h4.162c.969 0 1.371 1.24.588 1.81l-3.37 2.448a1 1 0 00-.364 1.118l1.287 3.957c.3.921-.755 1.688-1.54 1.118l-3.37-2.448a1 1 0 00-1.176 0l-3.37 2.448c-.784.57-1.838-.197-1.539-1.118l1.287-3.957a1 1 0 00-.364-1.118L2.063 9.384c-.783-.57-.38-1.81.588-1.81h4.162a1 1 0 00.95-.69l1.286-3.957z" /></svg>
Core Pool 1¢ per capsule per member
</div>
<svg className="h-3.5 w-3.5" fill="currentColor" viewBox="0 0 20 20"><path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.286 3.957a1 1 0 00.95.69h4.162c.969 0 1.371 1.24.588 1.81l-3.37 2.448a1 1 0 00-.364 1.118l1.287 3.957c.3.921-.755 1.688-1.54 1.118l-3.37-2.448a1 1 0 00-1.176 0l-3.37 2.448c-.784.57-1.838-.197-1.539-1.118l1.287-3.957a1 1 0 00-.364-1.118L2.063 9.384c-.783-.57-.38-1.81.588-1.81h4.162a1 1 0 00.95-.69l1.286-3.957z" /></svg>{t('autofix.k39437388')}</div>
)}
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
@ -315,10 +317,8 @@ function PoolManagePageInner() {
<button
onClick={() => router.push('/admin/pool-management')}
className="inline-flex items-center gap-2 rounded-lg bg-white text-blue-900 border border-blue-200 px-4 py-2 text-sm font-medium hover:bg-blue-50 transition"
title="Back to Pool Management"
>
Back
</button>
title={t('autofix.k6285753a')}
>{t('autofix.k0ac84efe')}</button>
</div>
</header>
@ -330,7 +330,7 @@ function PoolManagePageInner() {
<BanknotesIcon className="h-5 w-5 text-white" />
</div>
<div>
<p className="text-sm text-gray-600">Total in Pool</p>
<p className="text-sm text-gray-600">{t('autofix.ke8b9f33c')}</p>
<p className="text-2xl font-semibold text-gray-900"> {totalAmount.toLocaleString()}</p>
</div>
</div>
@ -341,7 +341,7 @@ function PoolManagePageInner() {
<CalendarDaysIcon className="h-5 w-5 text-white" />
</div>
<div>
<p className="text-sm text-gray-600">This Year</p>
<p className="text-sm text-gray-600">{t('autofix.kaa8231ec')}</p>
<p className="text-2xl font-semibold text-gray-900"> {amountThisYear.toLocaleString()}</p>
</div>
</div>
@ -352,7 +352,7 @@ function PoolManagePageInner() {
<CalendarDaysIcon className="h-5 w-5 text-white" />
</div>
<div>
<p className="text-sm text-gray-600">Current Month</p>
<p className="text-sm text-gray-600">{t('autofix.k86aa4f9c')}</p>
<p className="text-2xl font-semibold text-gray-900"> {amountThisMonth.toLocaleString()}</p>
</div>
</div>
@ -372,9 +372,7 @@ function PoolManagePageInner() {
onClick={() => { setSearchOpen(true); setQuery(''); setCandidates([]); setHasSearched(false); setError(''); }}
className="inline-flex items-center gap-2 rounded-lg bg-blue-900 hover:bg-blue-800 text-blue-50 px-5 py-3 text-sm font-semibold shadow transition"
>
<PlusIcon className="h-5 w-5" />
Add User
</button>
<PlusIcon className="h-5 w-5" />{t('autofix.k750c1eb5')}</button>
</div>
{removeError && (
<div className="mb-4 rounded-md border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-700">
@ -383,13 +381,13 @@ function PoolManagePageInner() {
)}
{membersLoading && (
<div className="text-center text-gray-500 italic py-8">Loading members...</div>
<div className="text-center text-gray-500 italic py-8">{t('autofix.k5d4d494e')}</div>
)}
{membersError && !membersLoading && (
<div className="text-center text-red-600 py-8">{membersError}</div>
)}
{users.length === 0 && !membersLoading && !membersError && (
<div className="text-center text-gray-500 italic py-8">No users in this pool yet.</div>
<div className="text-center text-gray-500 italic py-8">{t('autofix.kcbc17bbd')}</div>
)}
{users.length > 0 && !membersLoading && (
@ -399,7 +397,7 @@ function PoolManagePageInner() {
<tr>
<th className="px-4 py-3 text-left font-semibold text-gray-700">Name</th>
<th className="px-4 py-3 text-left font-semibold text-gray-700">Email</th>
<th className="px-4 py-3 text-left font-semibold text-gray-700">Member Since</th>
<th className="px-4 py-3 text-left font-semibold text-gray-700">{t('autofix.k7bed84a7')}</th>
<th className="px-4 py-3 text-right font-semibold text-gray-700">{isCore ? 'Total Earned' : 'Share'}</th>
<th className="px-4 py-3 text-right font-semibold text-gray-700" />
</tr>
@ -456,7 +454,7 @@ function PoolManagePageInner() {
<div className="w-full max-w-2xl max-h-[90vh] rounded-2xl overflow-hidden bg-white shadow-2xl ring-1 ring-black/10 flex flex-col">
{/* Header */}
<div className="px-6 py-5 border-b border-gray-100 flex items-center justify-between">
<h4 className="text-lg font-semibold text-blue-900">Add user to pool</h4>
<h4 className="text-lg font-semibold text-blue-900">{t('autofix.ka6be28d2')}</h4>
<button
onClick={() => setSearchOpen(false)}
className="p-1.5 rounded-md text-gray-500 hover:bg-gray-100 hover:text-gray-700 transition"
@ -477,7 +475,7 @@ function PoolManagePageInner() {
<input
value={query}
onChange={e => setQuery(e.target.value)}
placeholder="Search name or email…"
placeholder={t('autofix.kb35549bb')}
className="w-full rounded-md bg-gray-50 border border-gray-300 text-sm text-gray-900 placeholder-gray-400 pl-8 pr-3 py-2 focus:ring-2 focus:ring-blue-900 focus:border-transparent transition"
/>
</div>
@ -499,17 +497,13 @@ function PoolManagePageInner() {
</button>
</div>
</form>
<div className="px-6 pt-1 pb-3 text-right text-xs text-gray-500">
Min. 3 characters
</div>
<div className="px-6 pt-1 pb-3 text-right text-xs text-gray-500">{t('autofix.ke4c4a858')}</div>
{/* Results */}
<div className="px-6 py-4 overflow-y-auto min-h-0 flex-1">
{error && <div className="text-sm text-red-600 mb-3">{error}</div>}
{!error && query.trim().length < 3 && (
<div className="py-8 text-sm text-gray-500 text-center">
Enter at least 3 characters and click Search.
</div>
<div className="py-8 text-sm text-gray-500 text-center">{t('autofix.kb87eb38b')}</div>
)}
{!error && hasSearched && loading && candidates.length === 0 && (
<ul className="space-y-0 divide-y divide-gray-200 border border-gray-200 rounded-md bg-gray-50">
@ -522,9 +516,7 @@ function PoolManagePageInner() {
</ul>
)}
{!error && hasSearched && !loading && candidates.length === 0 && (
<div className="py-8 text-sm text-gray-500 text-center">
No users match your search.
</div>
<div className="py-8 text-sm text-gray-500 text-center">{t('autofix.k54f49724')}</div>
)}
{!error && candidates.length > 0 && (
<ul className="divide-y divide-gray-200 border border-gray-200 rounded-lg bg-white">
@ -594,7 +586,7 @@ function PoolManagePageInner() {
open={Boolean(removeConfirm)}
pending={Boolean(removingMemberId)}
intent="danger"
title="Remove member from pool?"
title={t('autofix.k959fb1a6')}
description={`This will remove ${removeConfirm?.label || 'this user'} from the pool.`}
confirmText="Remove"
onClose={() => { if (!removingMemberId) setRemoveConfirm(null) }}

View File

@ -1,5 +1,8 @@
'use client'
import { useTranslation } from '../../i18n/useTranslation';
import React from 'react'
import Header from '../../components/nav/Header'
import Footer from '../../components/Footer'
@ -23,6 +26,7 @@ type Pool = {
}
export default function PoolManagementPage() {
const { t } = useTranslation();
const router = useRouter()
const [archiveError, setArchiveError] = React.useState<string>('')
@ -107,31 +111,27 @@ export default function PoolManagementPage() {
<header className="bg-white/90 backdrop-blur border-b border-blue-100 py-10 px-8 rounded-2xl shadow-lg flex flex-col gap-4 mb-8 relative z-0">
<div className="flex items-center justify-between">
<div>
<h1 className="text-4xl font-extrabold text-blue-900 tracking-tight">Pool Management</h1>
<p className="text-lg text-blue-700 mt-2">Manage system pools and members.</p>
<h1 className="text-4xl font-extrabold text-blue-900 tracking-tight">{t('autofix.k21440f8a')}</h1>
<p className="text-lg text-blue-700 mt-2">{t('autofix.k67391c88')}</p>
</div>
</div>
<div className="flex items-center gap-2">
<span className="text-sm text-gray-600">Show:</span>
<span className="text-sm text-gray-600">{t('autofix.k0dd01c1c')}</span>
<button
onClick={() => setShowInactive(false)}
className={`px-4 py-2 text-sm font-medium rounded-lg transition ${!showInactive ? 'bg-blue-900 text-white' : 'bg-gray-100 text-gray-700 hover:bg-gray-200'}`}
>
Active Pools
</button>
>{t('autofix.k15843a06')}</button>
<button
onClick={() => setShowInactive(true)}
className={`px-4 py-2 text-sm font-medium rounded-lg transition ${showInactive ? 'bg-blue-900 text-white' : 'bg-gray-100 text-gray-700 hover:bg-gray-200'}`}
>
Inactive Pools
</button>
>{t('autofix.kb5e0b861')}</button>
</div>
</header>
{/* Pools List card */}
<div className="rounded-2xl bg-white border border-gray-100 shadow-lg p-6 relative z-0">
<div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-semibold text-blue-900">Existing Pools</h2>
<h2 className="text-lg font-semibold text-blue-900">{t('autofix.k5857ef79')}</h2>
<span className="text-sm text-gray-600">{pools.length} total</span>
</div>
@ -172,9 +172,7 @@ export default function PoolManagementPage() {
}`}>
{isCore && (
<div className="absolute -top-2.5 left-4 inline-flex items-center gap-1 rounded-full bg-amber-500 px-2.5 py-0.5 text-[10px] font-bold text-white uppercase tracking-wider shadow-sm">
<svg className="h-3 w-3" fill="currentColor" viewBox="0 0 20 20"><path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.286 3.957a1 1 0 00.95.69h4.162c.969 0 1.371 1.24.588 1.81l-3.37 2.448a1 1 0 00-.364 1.118l1.287 3.957c.3.921-.755 1.688-1.54 1.118l-3.37-2.448a1 1 0 00-1.176 0l-3.37 2.448c-.784.57-1.838-.197-1.539-1.118l1.287-3.957a1 1 0 00-.364-1.118L2.063 9.384c-.783-.57-.38-1.81.588-1.81h4.162a1 1 0 00.95-.69l1.286-3.957z" /></svg>
Core Pool
</div>
<svg className="h-3 w-3" fill="currentColor" viewBox="0 0 20 20"><path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.286 3.957a1 1 0 00.95.69h4.162c.969 0 1.371 1.24.588 1.81l-3.37 2.448a1 1 0 00-.364 1.118l1.287 3.957c.3.921-.755 1.688-1.54 1.118l-3.37-2.448a1 1 0 00-1.176 0l-3.37 2.448c-.784.57-1.838-.197-1.539-1.118l1.287-3.957a1 1 0 00-.364-1.118L2.063 9.384c-.783-.57-.38-1.81.588-1.81h4.162a1 1 0 00.95-.69l1.286-3.957z" /></svg>{t('autofix.k87e4b9a2')}</div>
)}
<div className="flex items-start justify-between">
<div className="flex items-center gap-3">
@ -225,15 +223,13 @@ export default function PoolManagementPage() {
<button
className="px-4 py-2 text-xs font-medium rounded-lg bg-green-100 text-green-800 hover:bg-green-200 transition"
onClick={() => handleSetActive(pool.id)}
title="Activate this pool"
>
Set Active
</button>
title={t('autofix.kd40c4f86')}
>{t('autofix.ke697b8cb')}</button>
) : (
<button
className="px-4 py-2 text-xs font-medium rounded-lg bg-amber-100 text-amber-800 hover:bg-amber-200 transition"
onClick={() => handleArchive(pool.id)}
title="Archive this pool"
title={t('autofix.ke19afb3d')}
>
Archive
</button>

View File

@ -1,4 +1,7 @@
'use client'
import { useTranslation } from '../../../i18n/useTranslation';
import React, { useState, useCallback } from 'react'
import Cropper from 'react-easy-crop'
import { Point, Area } from 'react-easy-crop'
@ -11,6 +14,7 @@ interface ImageCropModalProps {
}
export default function ImageCropModal({ isOpen, imageSrc, onClose, onCropComplete }: ImageCropModalProps) {
const { t } = useTranslation();
const [crop, setCrop] = useState<Point>({ x: 0, y: 0 })
const [zoom, setZoom] = useState(1)
const [croppedAreaPixels, setCroppedAreaPixels] = useState<Area | null>(null)
@ -70,7 +74,7 @@ export default function ImageCropModal({ isOpen, imageSrc, onClose, onCropComple
<div className="relative w-full max-w-4xl mx-4 bg-white rounded-2xl shadow-2xl overflow-hidden">
{/* Header */}
<div className="px-6 py-4 border-b border-gray-200 flex items-center justify-between bg-gradient-to-r from-blue-50 to-white">
<h2 className="text-xl font-semibold text-blue-900">Crop & Adjust Image</h2>
<h2 className="text-xl font-semibold text-blue-900">{t('autofix.k8f528877')}</h2>
<button
onClick={onClose}
className="text-gray-500 hover:text-gray-700 transition"
@ -120,9 +124,7 @@ export default function ImageCropModal({ isOpen, imageSrc, onClose, onCropComple
<button
onClick={handleSave}
className="px-5 py-2.5 text-sm font-semibold text-white bg-blue-900 rounded-lg hover:bg-blue-800 shadow transition"
>
Apply Crop
</button>
>{t('autofix.kef1656df')}</button>
</div>
</div>
</div>

View File

@ -7,7 +7,10 @@ import Link from 'next/link';
import { useRouter } from 'next/navigation';
import ImageCropModal from '../components/ImageCropModal';
import { useTranslation } from '../../../i18n/useTranslation';
export default function CreateSubscriptionPage() {
const { t } = useTranslation();
const { createProduct } = useCoffeeManagement();
const router = useRouter();
@ -92,15 +95,13 @@ export default function CreateSubscriptionPage() {
<header className="sticky top-0 z-10 bg-white/90 backdrop-blur border-b border-blue-100 py-10 px-8 rounded-2xl shadow-lg flex flex-col gap-4 mb-8">
<div className="flex items-center justify-between">
<div>
<h1 className="text-4xl font-extrabold text-blue-900 tracking-tight">Create Coffee</h1>
<p className="text-lg text-blue-700 mt-2">Add a new coffee.</p>
<h1 className="text-4xl font-extrabold text-blue-900 tracking-tight">{t('autofix.kaa30f0cd')}</h1>
<p className="text-lg text-blue-700 mt-2">{t('autofix.kf72d41db')}</p>
</div>
<Link href="/admin/subscriptions"
className="inline-flex items-center gap-2 rounded-lg bg-blue-900 hover:bg-blue-800 text-blue-50 px-5 py-3 text-base font-semibold shadow transition"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor"><path d="M6 18L18 6M6 6l12 12"/></svg>
Back to list
</Link>
<svg className="w-5 h-5" fill="none" stroke="currentColor"><path d="M6 18L18 6M6 6l12 12"/></svg>{t('autofix.kd8a5ad17')}</Link>
</div>
</header>
@ -124,10 +125,10 @@ export default function CreateSubscriptionPage() {
<div className="text-center w-full px-6 py-10">
<PhotoIcon aria-hidden="true" className="mx-auto h-16 w-16 text-blue-400" />
<div className="mt-4 text-base font-medium text-blue-700">
<span>Click or drag and drop an image here</span>
<span>{t('autofix.k6ee0a1b6')}</span>
</div>
<p className="text-sm text-blue-600 mt-2">PNG, JPG, WebP up to 10MB</p>
<p className="text-xs text-gray-500 mt-2">You'll be able to crop and adjust the image after uploading</p>
<p className="text-sm text-blue-600 mt-2">{t('autofix.k80ac9651')}</p>
<p className="text-xs text-gray-500 mt-2">{t('autofix.k41ab9eb6')}</p>
</div>
)}
{previewUrl && (
@ -145,9 +146,7 @@ export default function CreateSubscriptionPage() {
setShowCropModal(true);
}}
className="bg-white/90 backdrop-blur px-3 py-1.5 rounded-lg text-sm font-medium text-blue-900 shadow hover:bg-white transition"
>
Edit Crop
</button>
>{t('autofix.k73d1d7d7')}</button>
<button
type="button"
onClick={e => {
@ -196,11 +195,11 @@ export default function CreateSubscriptionPage() {
required
className="mt-1 block w-full rounded-lg border-gray-300 shadow focus:border-blue-900 focus:ring-blue-900 px-4 py-3 text-black placeholder:text-gray-400"
rows={3}
placeholder="Describe the product"
placeholder={t('autofix.k3477c83a')}
value={description}
onChange={e => setDescription(e.target.value)}
/>
<p className="mt-1 text-xs text-gray-600">Shown to users in the shop and checkout.</p>
<p className="mt-1 text-xs text-gray-600">{t('autofix.k0affa826')}</p>
</div>
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2">
@ -242,7 +241,7 @@ export default function CreateSubscriptionPage() {
{/* Subscription Billing (Locked) + Availability */}
<div className="sm:col-span-2 grid grid-cols-1 sm:grid-cols-2 gap-6">
<div>
<label className="block text-sm font-medium text-blue-900">Subscription Billing</label>
<label className="block text-sm font-medium text-blue-900">{t('autofix.ka3ee9ded')}</label>
<p className="mt-1 text-xs text-gray-600">Fixed monthly subscription billing (interval count = 1). These settings are locked.</p>
<div className="mt-2 flex gap-4">
<input disabled value={billingInterval} className="w-40 rounded-lg border-gray-300 bg-gray-100 px-4 py-3 text-sm text-gray-600" />
@ -264,9 +263,7 @@ export default function CreateSubscriptionPage() {
<Link href="/admin/subscriptions" className="text-sm font-medium text-blue-900 hover:text-blue-700">
Cancel
</Link>
<button type="submit" className="inline-flex justify-center rounded-lg bg-blue-900 px-5 py-3 text-sm font-semibold text-blue-50 shadow hover:bg-blue-800 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-900 transition">
Create Coffee
</button>
<button type="submit" className="inline-flex justify-center rounded-lg bg-blue-900 px-5 py-3 text-sm font-semibold text-blue-50 shadow hover:bg-blue-800 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-900 transition">{t('autofix.kaa30f0cd')}</button>
</div>
{error && <p className="text-sm text-red-600">{error}</p>}

View File

@ -6,7 +6,10 @@ import PageLayout from '../../../../components/PageLayout';
import useCoffeeManagement, { CoffeeItem } from '../../hooks/useCoffeeManagement';
import { PhotoIcon } from '@heroicons/react/24/solid';
import { useTranslation } from '../../../../i18n/useTranslation';
export default function EditSubscriptionPage() {
const { t } = useTranslation();
const router = useRouter();
// next/navigation app router dynamic param
const params = useParams();
@ -125,20 +128,18 @@ export default function EditSubscriptionPage() {
<header className="sticky top-0 z-10 bg-white/90 backdrop-blur border-b border-blue-100 py-10 px-8 rounded-2xl shadow-lg flex flex-col gap-4 mb-8">
<div className="flex items-center justify-between">
<div>
<h1 className="text-4xl font-extrabold text-blue-900 tracking-tight">Edit Coffee</h1>
<p className="text-lg text-blue-700 mt-2">Update details of the coffee.</p>
<h1 className="text-4xl font-extrabold text-blue-900 tracking-tight">{t('autofix.kb06fa395')}</h1>
<p className="text-lg text-blue-700 mt-2">{t('autofix.kb9e483c4')}</p>
</div>
<Link href="/admin/subscriptions"
className="inline-flex items-center gap-2 rounded-lg bg-blue-900 hover:bg-blue-800 text-blue-50 px-5 py-3 text-base font-semibold shadow transition"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor"><path d="M6 18L18 6M6 6l12 12"/></svg>
Back to list
</Link>
<svg className="w-5 h-5" fill="none" stroke="currentColor"><path d="M6 18L18 6M6 6l12 12"/></svg>{t('autofix.kd8a5ad17')}</Link>
</div>
</header>
{loading && (
<div className="rounded-md bg-blue-50 p-4 text-blue-700 text-sm mb-6">Loading subscription</div>
<div className="rounded-md bg-blue-50 p-4 text-blue-700 text-sm mb-6">{t('autofix.k2d0798a6')}</div>
)}
{error && !loading && (
<div className="rounded-md bg-red-50 p-4 text-red-700 text-sm mb-6">{error}</div>
@ -220,9 +221,9 @@ export default function EditSubscriptionPage() {
<div className="text-center w-full px-6 py-10">
<PhotoIcon aria-hidden="true" className="mx-auto h-16 w-16 text-blue-400" />
<div className="mt-4 text-base font-medium text-blue-700">
<span>Click or drag and drop a new image here</span>
<span>{t('autofix.k2e43a9c4')}</span>
</div>
<p className="text-sm text-blue-600 mt-2">PNG, JPG, WebP up to 10MB</p>
<p className="text-sm text-blue-600 mt-2">{t('autofix.k80ac9651')}</p>
</div>
)}
{(previewUrl || (!removeExistingPicture && item.pictureUrl)) && (
@ -255,9 +256,9 @@ export default function EditSubscriptionPage() {
<div className="text-center w-full px-6 py-10">
<PhotoIcon aria-hidden="true" className="mx-auto h-16 w-16 text-gray-400" />
<div className="mt-4 text-base font-medium text-gray-600">
<span>Image removed - Click to upload a new one</span>
<span>{t('autofix.kd2a00802')}</span>
</div>
<p className="text-sm text-gray-500 mt-2">PNG, JPG, WebP up to 10MB</p>
<p className="text-sm text-gray-500 mt-2">{t('autofix.k80ac9651')}</p>
</div>
)}
<input
@ -274,9 +275,7 @@ export default function EditSubscriptionPage() {
<Link href="/admin/subscriptions" className="text-sm font-medium text-blue-900 hover:text-blue-700">
Cancel
</Link>
<button type="submit" className="inline-flex justify-center rounded-lg bg-blue-900 px-5 py-3 text-sm font-semibold text-blue-50 shadow hover:bg-blue-800 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-900 transition">
Save Changes
</button>
<button type="submit" className="inline-flex justify-center rounded-lg bg-blue-900 px-5 py-3 text-sm font-semibold text-blue-50 shadow hover:bg-blue-800 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-900 transition">{t('autofix.k5a489751')}</button>
</div>
{error && <p className="text-sm text-red-600">{error}</p>}
</form>

View File

@ -4,12 +4,15 @@ import { PhotoIcon } from '@heroicons/react/24/solid';
import Link from 'next/link';
import PageLayout from '../../components/PageLayout';
import useCoffeeManagement, { CoffeeItem } from './hooks/useCoffeeManagement';
import { useTranslation } from '../../i18n/useTranslation';
import useCoffeeShippingFees, {
CoffeeShippingFee,
CoffeeShippingFeePieceCount,
} from './hooks/useCoffeeShippingFees';
export default function AdminSubscriptionsPage() {
const { t } = useTranslation();
const { listProducts, setProductState, deleteProduct } = useCoffeeManagement();
const { listShippingFees, updateShippingFee } = useCoffeeShippingFees();
@ -142,15 +145,13 @@ export default function AdminSubscriptionsPage() {
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div>
<h1 className="text-4xl font-extrabold text-blue-900 tracking-tight">Coffees</h1>
<p className="text-lg text-blue-700 mt-2">Manage all coffees.</p>
<p className="text-lg text-blue-700 mt-2">{t('autofix.k875f4054')}</p>
</div>
<Link
href="/admin/subscriptions/createSubscription"
className="inline-flex items-center gap-2 rounded-lg bg-blue-900 hover:bg-blue-800 text-blue-50 px-5 py-3 text-base font-semibold shadow transition self-start sm:self-auto"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor"><path d="M7 1a1 1 0 0 1 2 0v5h5a1 1 0 1 1 0 2H9v5a1 1 0 1 1-2 0V8H2a1 1 0 1 1 0-2h5V1z"/></svg>
Create Coffee
</Link>
<svg className="w-5 h-5" fill="none" stroke="currentColor"><path d="M7 1a1 1 0 0 1 2 0v5h5a1 1 0 1 1 0 2H9v5a1 1 0 1 1-2 0V8H2a1 1 0 1 1 0-2h5V1z"/></svg>{t('autofix.kaa30f0cd')}</Link>
</div>
</header>
@ -163,7 +164,7 @@ export default function AdminSubscriptionsPage() {
<div className="flex flex-col sm:flex-row sm:items-start sm:justify-between gap-4">
<div>
<h2 className="text-xl font-semibold text-blue-900">Shipping Fees (ABO)</h2>
<p className="mt-1 text-sm text-gray-600">Edit the shipping prices for 60 and 120 pieces.</p>
<p className="mt-1 text-sm text-gray-600">{t('autofix.k027bd82e')}</p>
</div>
<button
className="inline-flex items-center rounded-lg bg-gray-50 px-4 py-2 text-xs font-medium text-gray-700 ring-1 ring-inset ring-gray-200 hover:bg-gray-100 shadow transition self-start"
@ -249,7 +250,7 @@ export default function AdminSubscriptionsPage() {
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{loading && (
<div className="col-span-full text-sm text-gray-700">Loading</div>
<div className="col-span-full text-sm text-gray-700">{t('autofix.k832387c5')}</div>
)}
{!loading && items.map(item => (
<div key={item.id} className="rounded-2xl border border-gray-100 bg-white shadow-lg p-6 flex flex-col gap-3 hover:shadow-xl transition">
@ -304,7 +305,7 @@ export default function AdminSubscriptionsPage() {
</div>
))}
{!loading && !items.length && (
<div className="col-span-full py-8 text-center text-sm text-gray-500">No subscriptions found.</div>
<div className="col-span-full py-8 text-center text-sm text-gray-500">{t('autofix.k8c75468c')}</div>
)}
</div>
{/* Confirm Delete Modal */}
@ -314,7 +315,7 @@ export default function AdminSubscriptionsPage() {
<div className="absolute inset-0 flex items-center justify-center p-4">
<div className="w-full max-w-md rounded-2xl bg-white shadow-xl ring-1 ring-gray-200">
<div className="px-6 pt-6">
<h3 className="text-lg font-semibold text-blue-900">Delete coffee?</h3>
<h3 className="text-lg font-semibold text-blue-900">{t('autofix.kddd4832f')}</h3>
<p className="mt-2 text-sm text-gray-700">You are about to delete the coffee {deleteTarget.title}. This action cannot be undone.</p>
</div>
<div className="px-6 pb-6 pt-4 flex justify-end gap-3">

View File

@ -1,5 +1,8 @@
'use client'
import { useTranslation } from '../../i18n/useTranslation';
import { useMemo, useState, useEffect, useCallback } from 'react'
import PageLayout from '../../components/PageLayout'
import UserDetailModal from '../../components/UserDetailModal'
@ -35,6 +38,7 @@ const TYPES: UserType[] = ['personal','company']
const ROLES: UserRole[] = ['user','admin','guest']
export default function AdminUserManagementPage() {
const { t } = useTranslation();
const { isAdmin } = useAdminUsers()
const token = useAuthStore(state => state.accessToken)
const [isClient, setIsClient] = useState(false)
@ -151,8 +155,8 @@ export default function AdminUserManagementPage() {
<div className="mx-auto w-full max-w-xl rounded-2xl bg-white shadow ring-1 ring-red-500/20 p-8">
<div className="text-center">
<ExclamationTriangleIcon className="mx-auto h-12 w-12 text-red-500 mb-4" />
<h1 className="text-2xl font-bold text-red-600 mb-2">Access Denied</h1>
<p className="text-gray-600">You need admin privileges to access this page.</p>
<h1 className="text-2xl font-bold text-red-600 mb-2">{t('autofix.k26fbc186')}</h1>
<p className="text-gray-600">{t('autofix.k661c032b')}</p>
</div>
</div>
</div>
@ -248,10 +252,8 @@ export default function AdminUserManagementPage() {
{/* Header */}
<header className="sticky top-0 z-10 bg-white/90 backdrop-blur border-b border-blue-100 py-10 px-8 rounded-2xl shadow-lg flex flex-col gap-4 mb-8">
<div>
<h1 className="text-4xl font-extrabold text-blue-900 tracking-tight">User Management</h1>
<p className="text-lg text-blue-700 mt-2">
Manage all users, view statistics, and handle verification.
</p>
<h1 className="text-4xl font-extrabold text-blue-900 tracking-tight">{t('autofix.k1af97a07')}</h1>
<p className="text-lg text-blue-700 mt-2">{t('autofix.k79e1c459')}</p>
</div>
</header>
@ -259,7 +261,7 @@ export default function AdminUserManagementPage() {
<div className="mb-8 flex flex-col sm:flex-row sm:items-center gap-6">
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-7 gap-6 flex-1">
<div className="rounded-xl bg-white border border-gray-100 p-5 text-center shadow">
<div className="text-xs text-gray-500">Total Users</div>
<div className="text-xs text-gray-500">{t('autofix.kb324fb25')}</div>
<div className="text-xl font-semibold text-blue-900">{stats.total}</div>
</div>
<div className="rounded-xl bg-white border border-gray-100 p-5 text-center shadow">
@ -292,9 +294,7 @@ export default function AdminUserManagementPage() {
type="button"
className="inline-flex items-center gap-2 rounded-lg bg-amber-100 hover:bg-amber-200 border border-amber-200 text-amber-800 text-base font-semibold px-5 py-3 shadow transition"
onClick={() => window.location.href = '/admin/user-verify'}
>
Go to User Verification
</button>
>{t('autofix.k2f78fabe')}</button>
</div>
</div>
@ -303,14 +303,12 @@ export default function AdminUserManagementPage() {
<div className="rounded-xl border border-red-300 bg-red-50 text-red-700 px-6 py-5 flex gap-3 items-start mb-8 shadow">
<ExclamationTriangleIcon className="h-5 w-5 flex-shrink-0 text-red-500 mt-0.5" />
<div>
<p className="font-semibold">Error loading users</p>
<p className="font-semibold">{t('autofix.kbbefb159')}</p>
<p className="text-sm text-red-600">{error}</p>
<button
onClick={fetchAllUsers}
className="mt-2 text-sm underline hover:no-underline"
>
Try again
</button>
>{t('autofix.k3b7dd87a')}</button>
</div>
</div>
)}
@ -320,9 +318,7 @@ export default function AdminUserManagementPage() {
onSubmit={applyFilter}
className="bg-white rounded-2xl shadow-lg ring-1 ring-gray-100 px-8 py-8 flex flex-col gap-6 mb-8"
>
<h2 className="text-lg font-semibold text-blue-900">
Search & Filter Users
</h2>
<h2 className="text-lg font-semibold text-blue-900">{t('autofix.kd1f35ccf')}</h2>
<div className="grid grid-cols-1 md:grid-cols-5 gap-6">
{/* Search */}
<div className="md:col-span-2">
@ -332,7 +328,7 @@ export default function AdminUserManagementPage() {
<input
value={search}
onChange={e => setSearch(e.target.value)}
placeholder="Email, name, company..."
placeholder={t('autofix.k8b71f0c7')}
className="w-full rounded-lg border border-gray-300 pl-10 pr-3 py-3 text-sm focus:ring-2 focus:ring-blue-900 focus:border-transparent shadow"
/>
</div>
@ -344,7 +340,7 @@ export default function AdminUserManagementPage() {
onChange={e => setFType(e.target.value as any)}
className="w-full rounded-lg border border-gray-300 px-4 py-3 text-sm text-blue-900 focus:ring-2 focus:ring-blue-900 focus:border-transparent shadow"
>
<option value="all">All Types</option>
<option value="all">{t('autofix.k10e2568f')}</option>
<option value="personal">Personal</option>
<option value="company">Company</option>
</select>
@ -356,7 +352,7 @@ export default function AdminUserManagementPage() {
onChange={e => setFStatus(e.target.value as any)}
className="w-full rounded-lg border border-gray-300 px-4 py-3 text-sm text-blue-900 focus:ring-2 focus:ring-blue-900 focus:border-transparent shadow"
>
<option value="all">All Status</option>
<option value="all">{t('autofix.k2e8f3110')}</option>
{STATUSES.map(s => <option key={s} value={s}>{s[0].toUpperCase()+s.slice(1)}</option>)}
</select>
</div>
@ -367,7 +363,7 @@ export default function AdminUserManagementPage() {
onChange={e => setFRole(e.target.value as any)}
className="w-full rounded-lg border border-gray-300 px-4 py-3 text-sm text-blue-900 focus:ring-2 focus:ring-blue-900 focus:border-transparent shadow"
>
<option value="all">All Roles</option>
<option value="all">{t('autofix.k110bae43')}</option>
{ROLES.map(r => <option key={r} value={r}>{r[0].toUpperCase()+r.slice(1)}</option>)}
</select>
</div>
@ -377,10 +373,8 @@ export default function AdminUserManagementPage() {
type="button"
onClick={exportCsv}
className="inline-flex items-center gap-2 rounded-lg border border-gray-300 bg-white hover:bg-gray-50 text-blue-900 text-sm font-semibold px-5 py-3 shadow transition"
title="Export all filtered users to CSV"
>
Export all users as CSV
</button>
title={t('autofix.k1387f81e')}
>{t('autofix.k1521a376')}</button>
<button
type="submit"
className="inline-flex items-center gap-2 rounded-lg bg-blue-900 hover:bg-blue-800 text-blue-50 text-sm font-semibold px-5 py-3 shadow transition"
@ -393,9 +387,7 @@ export default function AdminUserManagementPage() {
{/* Users Table */}
<div className="bg-white rounded-2xl shadow-lg ring-1 ring-gray-100 overflow-hidden mb-8">
<div className="px-8 py-6 border-b border-gray-100 flex items-center justify-between">
<div className="text-lg font-semibold text-blue-900">
All Users
</div>
<div className="text-lg font-semibold text-blue-900">{t('autofix.k10ccb626')}</div>
<div className="text-xs text-gray-500">
Showing {current.length} of {filtered.length} users
</div>
@ -409,7 +401,7 @@ export default function AdminUserManagementPage() {
<th className="px-4 py-3 text-left">Status</th>
<th className="px-4 py-3 text-left">Role</th>
<th className="px-4 py-3 text-left">Created</th>
<th className="px-4 py-3 text-left">Last Login</th>
<th className="px-4 py-3 text-left">{t('autofix.kb24782ec')}</th>
<th className="px-4 py-3 text-left">Actions</th>
</tr>
</thead>
@ -419,7 +411,7 @@ export default function AdminUserManagementPage() {
<td colSpan={7} className="px-4 py-10 text-center">
<div className="flex items-center justify-center gap-2">
<div className="h-4 w-4 rounded-full border-2 border-blue-900 border-b-transparent animate-spin" />
<span className="text-sm text-blue-900">Loading users...</span>
<span className="text-sm text-blue-900">{t('autofix.k7fa2c4af')}</span>
</div>
</td>
</tr>
@ -478,9 +470,7 @@ export default function AdminUserManagementPage() {
})}
{current.length === 0 && (
<tr>
<td colSpan={7} className="px-4 py-10 text-center text-sm text-blue-700">
No users match current filters.
</td>
<td colSpan={7} className="px-4 py-10 text-center text-sm text-blue-700">{t('autofix.k748bf541')}</td>
</tr>
)}
</tbody>
@ -496,16 +486,12 @@ export default function AdminUserManagementPage() {
disabled={page===1}
onClick={() => setPage(p => Math.max(1,p-1))}
className="px-4 py-2 text-xs font-medium rounded-lg border border-gray-300 bg-white hover:bg-blue-50 disabled:opacity-40 disabled:cursor-not-allowed"
>
Previous
</button>
>{t('autofix.kdb27a82d')}</button>
<button
disabled={page===totalPages}
onClick={() => setPage(p => Math.min(totalPages,p+1))}
className="px-4 py-2 text-xs font-medium rounded-lg border border-gray-300 bg-white hover:bg-blue-50 disabled:opacity-40 disabled:cursor-not-allowed"
>
Next
</button>
>{t('autofix.ka8ea17b8')}</button>
</div>
</div>
</div>

View File

@ -1,5 +1,8 @@
'use client'
import { useTranslation } from '../../i18n/useTranslation';
import { useMemo, useState, useEffect } from 'react'
import PageLayout from '../../components/PageLayout'
import UserDetailModal from '../../components/UserDetailModal'
@ -17,6 +20,7 @@ type VerificationReadyFilter = 'all' | 'ready' | 'not_ready'
type StatusFilter = 'all' | 'pending' | 'active'
export default function AdminUserVerifyPage() {
const { t } = useTranslation();
const {
pendingUsers,
loading,
@ -138,8 +142,8 @@ export default function AdminUserVerifyPage() {
<div className="mx-auto w-full max-w-xl rounded-2xl bg-white shadow ring-1 ring-red-500/20 p-8">
<div className="text-center">
<ExclamationTriangleIcon className="mx-auto h-12 w-12 text-red-500 mb-4" />
<h1 className="text-2xl font-bold text-red-600 mb-2">Access Denied</h1>
<p className="text-gray-600">You need admin privileges to access this page.</p>
<h1 className="text-2xl font-bold text-red-600 mb-2">{t('autofix.k26fbc186')}</h1>
<p className="text-gray-600">{t('autofix.k661c032b')}</p>
</div>
</div>
</div>
@ -154,10 +158,8 @@ export default function AdminUserVerifyPage() {
{/* Header */}
<header className="sticky top-0 z-10 bg-white/90 backdrop-blur border-b border-blue-100 py-10 px-8 rounded-2xl shadow-lg flex flex-col gap-4 mb-8">
<div>
<h1 className="text-4xl font-extrabold text-blue-900 tracking-tight">User Verification Center</h1>
<p className="text-lg text-blue-700 mt-2">
Review and verify all users who need admin approval. Users must complete all steps before verification.
</p>
<h1 className="text-4xl font-extrabold text-blue-900 tracking-tight">{t('autofix.kccde6d86')}</h1>
<p className="text-lg text-blue-700 mt-2">{t('autofix.k5614c806')}</p>
</div>
</header>
@ -166,14 +168,12 @@ export default function AdminUserVerifyPage() {
<div className="rounded-xl border border-red-300 bg-red-50 text-red-700 px-6 py-5 flex gap-3 items-start mb-8 shadow">
<ExclamationTriangleIcon className="h-5 w-5 flex-shrink-0 text-red-500 mt-0.5" />
<div>
<p className="font-semibold">Error loading data</p>
<p className="font-semibold">{t('autofix.k62d12fab')}</p>
<p className="text-sm text-red-600">{error}</p>
<button
onClick={fetchPendingUsers}
className="mt-2 text-sm underline hover:no-underline"
>
Try again
</button>
>{t('autofix.k3b7dd87a')}</button>
</div>
</div>
)}
@ -183,9 +183,7 @@ export default function AdminUserVerifyPage() {
onSubmit={applyFilters}
className="bg-white rounded-2xl shadow-lg ring-1 ring-gray-100 px-8 py-8 flex flex-col gap-6 mb-8"
>
<h2 className="text-lg font-semibold text-blue-900">
Search & Filter Pending Users
</h2>
<h2 className="text-lg font-semibold text-blue-900">{t('autofix.k85c66f50')}</h2>
<div className="grid grid-cols-1 lg:grid-cols-7 gap-4">
<div className="lg:col-span-2">
<label className="block text-xs font-semibold text-blue-900 mb-1">Search</label>
@ -194,19 +192,19 @@ export default function AdminUserVerifyPage() {
<input
value={search}
onChange={e => setSearch(e.target.value)}
placeholder="Email, name, company..."
placeholder={t('autofix.k8b71f0c7')}
className="w-full rounded-lg border border-gray-300 pl-10 pr-3 py-3 text-sm focus:ring-2 focus:ring-blue-900 focus:border-transparent shadow"
/>
</div>
</div>
<div>
<label className="block text-xs font-semibold text-blue-900 mb-1">User Type</label>
<label className="block text-xs font-semibold text-blue-900 mb-1">{t('autofix.k577a012c')}</label>
<select
value={fType}
onChange={e => { setFType(e.target.value as any); setPage(1) }}
className="w-full rounded-lg border border-gray-300 px-4 py-3 text-sm text-blue-900 focus:ring-2 focus:ring-blue-900 focus:border-transparent shadow"
>
<option value="all">All Types</option>
<option value="all">{t('autofix.k10e2568f')}</option>
<option value="personal">Personal</option>
<option value="company">Company</option>
</select>
@ -218,21 +216,21 @@ export default function AdminUserVerifyPage() {
onChange={e => { setFRole(e.target.value as any); setPage(1) }}
className="w-full rounded-lg border border-gray-300 px-4 py-3 text-sm text-blue-900 focus:ring-2 focus:ring-blue-900 focus:border-transparent shadow"
>
<option value="all">All Roles</option>
<option value="all">{t('autofix.k110bae43')}</option>
<option value="user">User</option>
<option value="admin">Admin</option>
</select>
</div>
<div>
<label className="block text-xs font-semibold text-blue-900 mb-1">Verification Readiness</label>
<label className="block text-xs font-semibold text-blue-900 mb-1">{t('autofix.k0efd830c')}</label>
<select
value={fReady}
onChange={e => { setFReady(e.target.value as VerificationReadyFilter); setPage(1) }}
className="w-full rounded-lg border border-gray-300 px-4 py-3 text-sm text-blue-900 focus:ring-2 focus:ring-blue-900 focus:border-transparent shadow"
>
<option value="all">All Readiness</option>
<option value="ready">Ready to Verify</option>
<option value="not_ready">Not Ready</option>
<option value="all">{t('autofix.k7ab45054')}</option>
<option value="ready">{t('autofix.kf27e4502')}</option>
<option value="not_ready">{t('autofix.k4e0c889b')}</option>
</select>
</div>
<div>
@ -242,13 +240,13 @@ export default function AdminUserVerifyPage() {
onChange={e => { setFStatus(e.target.value as StatusFilter); setPage(1) }}
className="w-full rounded-lg border border-gray-300 px-4 py-3 text-sm text-blue-900 focus:ring-2 focus:ring-blue-900 focus:border-transparent shadow"
>
<option value="all">All Statuses</option>
<option value="all">{t('autofix.k0f1fc266')}</option>
<option value="pending">Pending</option>
<option value="active">Active</option>
</select>
</div>
<div>
<label className="block text-xs font-semibold text-blue-900 mb-1">Rows per page</label>
<label className="block text-xs font-semibold text-blue-900 mb-1">{t('autofix.kd2e35b08')}</label>
<select
value={perPage}
onChange={e => { setPerPage(parseInt(e.target.value, 10)); setPage(1) }}
@ -263,9 +261,7 @@ export default function AdminUserVerifyPage() {
{/* Pending Users Table */}
<div className="bg-white rounded-2xl shadow-lg ring-1 ring-gray-100 overflow-hidden mb-8">
<div className="px-8 py-6 border-b border-gray-100 flex items-center justify-between">
<div className="text-lg font-semibold text-blue-900">
Users Pending Verification
</div>
<div className="text-lg font-semibold text-blue-900">{t('autofix.k0da2c941')}</div>
<div className="text-xs text-gray-500">
Showing {current.length} of {filtered.length} users
</div>
@ -289,7 +285,7 @@ export default function AdminUserVerifyPage() {
<td colSpan={7} className="px-4 py-10 text-center">
<div className="flex items-center justify-center gap-2">
<div className="h-4 w-4 rounded-full border-2 border-blue-900 border-b-transparent animate-spin" />
<span className="text-sm text-blue-900">Loading users...</span>
<span className="text-sm text-blue-900">{t('autofix.k7fa2c4af')}</span>
</div>
</td>
</tr>
@ -343,9 +339,7 @@ export default function AdminUserVerifyPage() {
})}
{current.length === 0 && !loading && (
<tr>
<td colSpan={7} className="px-4 py-10 text-center text-sm text-blue-700">
No unverified users match current filters.
</td>
<td colSpan={7} className="px-4 py-10 text-center text-sm text-blue-700">{t('autofix.kb4aba3dc')}</td>
</tr>
)}
</tbody>
@ -361,16 +355,12 @@ export default function AdminUserVerifyPage() {
disabled={page === 1}
onClick={() => setPage(p => Math.max(1, p - 1))}
className="px-4 py-2 text-xs font-medium rounded-lg border border-gray-300 bg-white hover:bg-blue-50 disabled:opacity-40 disabled:cursor-not-allowed"
>
Previous
</button>
>{t('autofix.kdb27a82d')}</button>
<button
disabled={page === totalPages}
onClick={() => setPage(p => Math.min(totalPages, p + 1))}
className="px-4 py-2 text-xs font-medium rounded-lg border border-gray-300 bg-white hover:bg-blue-50 disabled:opacity-40 disabled:cursor-not-allowed"
>
Next
</button>
>{t('autofix.ka8ea17b8')}</button>
</div>
</div>
</div>

View File

@ -1,5 +1,8 @@
'use client'
import { useTranslation } from '../i18n/useTranslation';
import { useEffect, useState, useMemo } from 'react'
import PageLayout from '../components/PageLayout'
import Waves from '../components/background/waves'
@ -18,6 +21,7 @@ type Affiliate = {
const PLACEHOLDER_IMAGE = 'https://images.unsplash.com/photo-1557804506-669a67965ba0?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=3270&q=80'
export default function AffiliateLinksPage() {
const { t } = useTranslation();
const [affiliates, setAffiliates] = useState<Affiliate[]>([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState('')
@ -115,14 +119,12 @@ export default function AffiliateLinksPage() {
{/* Header (aligned with management pages) */}
<header className="flex flex-col gap-4 mb-8">
<div>
<h1 className="text-4xl font-extrabold text-blue-900 tracking-tight">Affiliate Partners</h1>
<p className="text-lg text-blue-700 mt-2">
Discover our trusted partners and earn commissions through affiliate links.
</p>
<h1 className="text-4xl font-extrabold text-blue-900 tracking-tight">{t('autofix.k1b9c46e5')}</h1>
<p className="text-lg text-blue-700 mt-2">{t('autofix.k633438a0')}</p>
</div>
{/* NEW: Category filter */}
<div className="flex items-center gap-2">
<label className="text-sm text-blue-900 font-medium">Filter by category:</label>
<label className="text-sm text-blue-900 font-medium">{t('autofix.kebf33594')}</label>
<select
value={selectedCategory}
onChange={(e) => setSelectedCategory(e.target.value)}
@ -139,7 +141,7 @@ export default function AffiliateLinksPage() {
{loading && (
<div className="mx-auto max-w-2xl text-center">
<div className="inline-block h-8 w-8 animate-spin rounded-full border-4 border-solid border-indigo-400 border-b-transparent" />
<p className="mt-4 text-sm text-gray-600">Loading affiliate partners...</p>
<p className="mt-4 text-sm text-gray-600">{t('autofix.k5834cbed')}</p>
</div>
)}
@ -150,9 +152,7 @@ export default function AffiliateLinksPage() {
)}
{!loading && !error && posts.length === 0 && (
<div className="mx-auto max-w-2xl text-center text-sm text-gray-600">
No affiliate partners available at the moment.
</div>
<div className="mx-auto max-w-2xl text-center text-sm text-gray-600">{t('autofix.k431328cf')}</div>
)}
{/* Cards (aligned to white panels, border, shadow) */}
@ -195,12 +195,8 @@ export default function AffiliateLinksPage() {
target="_blank"
rel="noopener noreferrer"
className="rounded-lg bg-indigo-600 hover:bg-indigo-500 text-white px-4 py-2 text-sm font-medium shadow transition"
>
Visit Affiliate Link
</a>
<span className="text-[11px] text-gray-500">
External partner website.
</span>
>{t('autofix.k7db4e5a9')}</a>
<span className="text-[11px] text-gray-500">{t('autofix.k8b89f863')}</span>
</div>
</div>
</article>

View File

@ -39,8 +39,43 @@ interface ScanResult {
uniqueKeyCount: number;
missingKeys: Array<{ key: string; files: string[] }>;
untranslatedLiterals: Array<{ text: string; files: string[] }>;
autoFixEligibleFiles: string[];
autoFixForceConvertibleFiles: string[];
}
interface AutoFixChange {
file: string;
replacements: number;
addedImport: boolean;
addedHook: boolean;
}
interface AutoFixResult {
changedFiles: AutoFixChange[];
skippedFiles: Array<{ file: string; reason: string }>;
createdKeys: string[];
debugEntries: AutoFixDebugEntry[];
}
interface AutoFixDebugEntry {
file: string;
status: 'changed' | 'skipped' | 'no-op';
beforeLiteralCount: number;
textReplacements: number;
attrReplacements: number;
afterLiteralCount: number;
reason?: string;
}
interface AutoFixOptions {
targetFiles?: Set<string>;
forceConvertToClient?: boolean;
}
const TRANSLATABLE_ATTRIBUTES = ['placeholder', 'title', 'alt', 'aria-label'] as const;
const USE_CLIENT_PREFIX_REGEX = /^\uFEFF?\s*(?:\/\/[^\n]*\n|\/\*[\s\S]*?\*\/\s*)*['\"]use client['\"]\s*;?\s*/;
const LEADING_PREAMBLE_REGEX = /^\uFEFF?\s*(?:\/\/[^\n]*\n|\/\*[\s\S]*?\*\/\s*)*/;
async function walk(dir: string, outFiles: string[], counters: { dirs: number }) {
counters.dirs += 1;
const entries = await fs.readdir(dir, { withFileTypes: true });
@ -84,6 +119,18 @@ function extractTranslationKeys(content: string): string[] {
return keys;
}
function isLikelyStaticTranslationKey(key: string): boolean {
const trimmed = key.trim();
if (!trimmed) return false;
// Dynamic template fragments cannot be validated as missing static keys.
if (trimmed.includes('${')) return false;
// Accept typical dotted key paths only.
if (!/^[A-Za-z0-9_.-]+$/.test(trimmed)) return false;
// Ignore placeholder examples like "..." that appear in help text.
if (!/[A-Za-z0-9]/.test(trimmed)) return false;
return true;
}
function isUiCodeFile(filePath: string): boolean {
const ext = path.extname(filePath).toLowerCase();
return ext === '.tsx' || ext === '.jsx';
@ -101,6 +148,14 @@ function extractPotentialUiLiterals(content: string): string[] {
literals.push(text);
}
// Simple quoted JSX attribute literals, e.g. placeholder="Search..."
const attrRegex = /\b(?:placeholder|title|alt|aria-label)\s*=\s*(['"])([^"'{}<>\n][^"'<>\n]*)\1/g;
while ((match = attrRegex.exec(content)) !== null) {
const text = match[2]?.replace(/\s+/g, ' ').trim();
if (!text) continue;
literals.push(text);
}
return literals;
}
@ -121,10 +176,421 @@ function shouldIgnoreLiteral(text: string): boolean {
if (/^(use client|true|false|null|undefined)$/i.test(trimmed)) return true;
if (/(className|onClick|href|src|aria-|data-)/.test(trimmed)) return true;
if (/^[A-Za-z0-9_.]+\s*:\s*[A-Za-z0-9_.]+$/.test(trimmed)) return true;
if (/\|/.test(trimmed) && /\b(void|Promise|string|number|boolean|any|unknown|never|null|undefined|Record|React)\b/.test(trimmed)) return true;
return false;
}
function escapeForTsString(value: string): string {
return value
.replace(/\\/g, '\\\\')
.replace(/'/g, "\\'")
.replace(/\r?\n/g, ' ')
.trim();
}
function hashText(input: string): string {
let hash = 5381;
for (let i = 0; i < input.length; i += 1) {
hash = ((hash << 5) + hash) ^ input.charCodeAt(i);
}
return Math.abs(hash >>> 0).toString(16).padStart(8, '0');
}
function toAutofixKey(text: string): string {
return `autofix.k${hashText(text)}`;
}
function hasUseClientDirective(content: string): boolean {
return USE_CLIENT_PREFIX_REGEX.test(content);
}
function isAutoFixCandidatePath(relPath: string): boolean {
if (!relPath.startsWith('src/app/')) return false;
if (relPath.startsWith('src/app/api/')) return false;
if (relPath.startsWith('src/app/i18n/')) return false;
return true;
}
function hasUseTranslationImport(content: string): boolean {
return /from\s+['\"][^'\"]*\/i18n\/useTranslation['\"];?/.test(content);
}
function hasTHook(content: string): boolean {
return /const\s*\{\s*t\s*\}\s*=\s*useTranslation\s*\(/.test(content);
}
function hasExistingTUsage(content: string): boolean {
return /\bt\(\s*['"`][^'"`]+['"`]\s*[,\)\]]/.test(content);
}
function hasServerOnlySignals(content: string): string | null {
if (/from\s+['\"]next\/headers['\"]/.test(content)) return 'uses next/headers';
if (/from\s+['\"]next\/server['\"]/.test(content)) return 'uses next/server';
if (/\bexport\s+const\s+metadata\b/.test(content)) return 'exports metadata';
if (/\bexport\s+async\s+function\s+generateMetadata\b/.test(content)) return 'uses generateMetadata';
if (/\bexport\s+default\s+async\s+function\b/.test(content)) return 'default component is async (server-style)';
return null;
}
function insertUseClientDirective(content: string): { content: string; added: boolean } {
if (hasUseClientDirective(content)) return { content, added: false };
const match = content.match(LEADING_PREAMBLE_REGEX);
const at = match ? match[0].length : 0;
const before = content.slice(0, at);
const after = content.slice(at);
const spacer = before.endsWith('\n') || before.length === 0 ? '' : '\n';
const next = `${before}${spacer}'use client';\n\n${after}`;
return { content: next, added: true };
}
function ensureUseTranslation(
content: string,
filePathAbs: string,
options: { forceConvertToClient?: boolean } = {}
): { content: string; addedImport: boolean; addedHook: boolean; reason?: string } {
let next = content;
if (!hasUseClientDirective(content)) {
if (hasTHook(content) || hasExistingTUsage(content)) {
return { content, addedImport: false, addedHook: false };
}
if (!options.forceConvertToClient) {
return { content, addedImport: false, addedHook: false, reason: "File is not a client component ('use client')." };
}
const signal = hasServerOnlySignals(content);
if (signal) {
return {
content,
addedImport: false,
addedHook: false,
reason: `Force convert skipped: file appears server-only (${signal}).`,
};
}
next = insertUseClientDirective(content).content;
}
let addedImport = false;
let addedHook = false;
if (!hasUseTranslationImport(next)) {
const fileDir = path.dirname(filePathAbs);
const importTarget = path.join(process.cwd(), 'src', 'app', 'i18n', 'useTranslation');
let rel = path.relative(fileDir, importTarget).split(path.sep).join('/');
if (!rel.startsWith('.')) rel = `./${rel}`;
const importLine = `import { useTranslation } from '${rel}';\n`;
const useClientMatch = next.match(USE_CLIENT_PREFIX_REGEX);
if (!useClientMatch || typeof useClientMatch.index !== 'number') {
return { content, addedImport: false, addedHook: false, reason: 'Could not place import statement.' };
}
const at = useClientMatch.index + useClientMatch[0].length;
next = `${next.slice(0, at)}\n\n${importLine}${next.slice(at)}`;
addedImport = true;
}
if (!hasTHook(next)) {
const fnPatterns = [
/export\s+default\s+function\s+\w+\s*\([\s\S]*?\)\s*\{/m,
/function\s+\w+\s*\([\s\S]*?\)\s*\{/m,
/const\s+\w+\s*=\s*\([\s\S]*?\)\s*=>\s*\{/m,
/const\s+\w+\s*:\s*[^=\n]+\s*=\s*\([\s\S]*?\)\s*=>\s*\{/m,
];
let inserted = false;
for (const pattern of fnPatterns) {
const match = next.match(pattern);
if (!match || typeof match.index !== 'number') continue;
const at = match.index + match[0].length;
next = `${next.slice(0, at)}\n const { t } = useTranslation();${next.slice(at)}`;
inserted = true;
addedHook = true;
break;
}
if (!inserted) {
return { content, addedImport: false, addedHook: false, reason: 'Could not locate a component function to inject useTranslation().' };
}
}
return { content: next, addedImport, addedHook };
}
function replaceJsxAttributeLiterals(content: string): { content: string; replacements: number; keyValueMap: Map<string, string> } {
const keyValueMap = new Map<string, string>();
let replacements = 0;
const attrsPattern = TRANSLATABLE_ATTRIBUTES.map((a) => a.replace('-', '\\-')).join('|');
const attrRegex = new RegExp(`\\b(${attrsPattern})\\s*=\\s*([\"'])([^\"'{}<>\\n][^\"'<>\\n]*)\\2`, 'g');
// Only rewrite inside JSX tag bodies, never in TS/JS function params or object literals.
const next = content.replace(/<[^>]+>/g, (tag) =>
tag.replace(attrRegex, (full, attrName: string, quote: string, captured: string) => {
const text = String(captured ?? '').replace(/\s+/g, ' ').trim();
if (shouldIgnoreLiteral(text)) return full;
const key = toAutofixKey(text);
keyValueMap.set(key, text);
replacements += 1;
return `${attrName}={t('${key}')}`;
})
);
return { content: next, replacements, keyValueMap };
}
function replaceJsxTextNodes(content: string): { content: string; replacements: number; keyValueMap: Map<string, string> } {
const keyValueMap = new Map<string, string>();
let replacements = 0;
const jsxTextRegex = />\s*([^<>{}\n][^<>{}\n]{1,})\s*</g;
const next = content.replace(jsxTextRegex, (full, captured: string) => {
const text = String(captured ?? '').replace(/\s+/g, ' ').trim();
if (shouldIgnoreLiteral(text)) return full;
const key = toAutofixKey(text);
keyValueMap.set(key, text);
replacements += 1;
return `>{t('${key}')}<`;
});
return { content: next, replacements, keyValueMap };
}
function upsertAutofixNamespace(content: string, entries: Map<string, string>): string {
if (entries.size === 0) return content;
const entryLines = Array.from(entries.entries())
.sort((a, b) => a[0].localeCompare(b[0]))
.map(([key, value]) => {
const shortKey = key.replace(/^autofix\./, '');
return ` ${shortKey}: '${escapeForTsString(value)}',`;
});
const autofixBlockRegex = /\n\s{2}autofix:\s*\{([\s\S]*?)\n\s{2}\},/m;
const match = content.match(autofixBlockRegex);
if (match) {
const blockBody = match[1] ?? '';
const existing = new Set<string>();
for (const m of blockBody.matchAll(/\n\s{4}([A-Za-z0-9_]+):\s*'/g)) {
existing.add(`autofix.${m[1]}`);
}
const missing = entryLines.filter((line) => {
const m = line.match(/^\s{4}([A-Za-z0-9_]+):/);
if (!m) return false;
return !existing.has(`autofix.${m[1]}`);
});
if (missing.length === 0) return content;
const replacement = `\n autofix: {${blockBody}\n${missing.join('\n')}\n },`;
return content.replace(autofixBlockRegex, replacement);
}
const toastsComment = /\n\s{2}\/\/\s*─+\s*Notifications\s*\/\s*Toasts[\s\S]*?\n\s{2}toasts:/m;
if (toastsComment.test(content)) {
const autofixBlock = `\n autofix: {\n${entryLines.join('\n')}\n },\n`;
return content.replace(toastsComment, `${autofixBlock}\n // ─── Notifications / Toasts ────────────────────────────\n toasts:`);
}
return content;
}
function upsertAutofixType(content: string): string {
// Already has a Record<string,string> entry (any indentation)
if (/\n\s*autofix:\s*Record<string,\s*string>;/.test(content)) {
return content;
}
// Already has a proper block entry
if (/\n\s*autofix:\s*\{/.test(content)) {
return content;
}
const insertBefore = /\n\s{2}\/\/\s*─+\s*Notifications\s*\/\s*Toasts[\s\S]*?\n\s{2}toasts:/m;
if (!insertBefore.test(content)) return content;
const block = `\n autofix: Record<string, string>;\n`;
return content.replace(insertBefore, `${block}\n // ─── Notifications / Toasts ────────────────────────────\n toasts:`);
}
async function runAutoFix(options: AutoFixOptions = {}): Promise<AutoFixResult> {
const workspaceRoot = process.cwd();
const files: string[] = [];
const counters = { dirs: 0 };
await walk(workspaceRoot, files, counters);
const uiFiles = files.filter((f) => {
const ext = path.extname(f).toLowerCase();
if (ext !== '.tsx' && ext !== '.jsx') return false;
const rel = toRelativeWorkspacePath(f);
if (!isAutoFixCandidatePath(rel)) return false;
if (options.targetFiles && options.targetFiles.size > 0 && !options.targetFiles.has(rel)) return false;
return true;
});
const changedFiles: AutoFixChange[] = [];
const skippedFiles: Array<{ file: string; reason: string }> = [];
const createdEntries = new Map<string, string>();
const debugEntries: AutoFixDebugEntry[] = [];
if (options.targetFiles && options.targetFiles.size > 0) {
const discoveredSet = new Set(files.map((f) => toRelativeWorkspacePath(f)));
const eligibleSet = new Set(uiFiles.map((f) => toRelativeWorkspacePath(f)));
for (const target of options.targetFiles) {
if (!discoveredSet.has(target)) {
skippedFiles.push({ file: target, reason: 'Target file was not found in workspace scan.' });
debugEntries.push({
file: target,
status: 'skipped',
beforeLiteralCount: 0,
textReplacements: 0,
attrReplacements: 0,
afterLiteralCount: 0,
reason: 'Target file was not found in workspace scan.',
});
continue;
}
if (!eligibleSet.has(target)) {
skippedFiles.push({ file: target, reason: 'Target file is not eligible for auto-fix (requires TSX/JSX under src/app).' });
debugEntries.push({
file: target,
status: 'skipped',
beforeLiteralCount: 0,
textReplacements: 0,
attrReplacements: 0,
afterLiteralCount: 0,
reason: 'Target file is not eligible for auto-fix (requires TSX/JSX under src/app).',
});
}
}
}
for (const absPath of uiFiles) {
const relPath = toRelativeWorkspacePath(absPath);
const raw = await fs.readFile(absPath, 'utf8');
const literalCheck = extractPotentialUiLiterals(raw).filter((text) => !shouldIgnoreLiteral(text));
if (literalCheck.length === 0) {
debugEntries.push({
file: relPath,
status: 'no-op',
beforeLiteralCount: 0,
textReplacements: 0,
attrReplacements: 0,
afterLiteralCount: 0,
reason: 'No potential untranslated literals remained in this file.',
});
continue;
}
const ensured = ensureUseTranslation(raw, absPath, { forceConvertToClient: options.forceConvertToClient });
if (ensured.reason) {
skippedFiles.push({ file: relPath, reason: ensured.reason });
debugEntries.push({
file: relPath,
status: 'skipped',
beforeLiteralCount: literalCheck.length,
textReplacements: 0,
attrReplacements: 0,
afterLiteralCount: literalCheck.length,
reason: ensured.reason,
});
continue;
}
const textReplaced = replaceJsxTextNodes(ensured.content);
const attrReplaced = replaceJsxAttributeLiterals(textReplaced.content);
const totalReplacements = textReplaced.replacements + attrReplaced.replacements;
const afterLiteralCount = extractPotentialUiLiterals(attrReplaced.content).filter((text) => !shouldIgnoreLiteral(text)).length;
if (totalReplacements === 0) {
const reason = 'No supported replacement patterns matched these literals (likely complex JSX/expression cases).';
skippedFiles.push({ file: relPath, reason });
debugEntries.push({
file: relPath,
status: 'skipped',
beforeLiteralCount: literalCheck.length,
textReplacements: 0,
attrReplacements: 0,
afterLiteralCount,
reason,
});
continue;
}
for (const [k, v] of textReplaced.keyValueMap.entries()) {
createdEntries.set(k, v);
}
for (const [k, v] of attrReplaced.keyValueMap.entries()) {
createdEntries.set(k, v);
}
if (attrReplaced.content !== raw) {
await fs.writeFile(absPath, attrReplaced.content, 'utf8');
changedFiles.push({
file: relPath,
replacements: totalReplacements,
addedImport: ensured.addedImport,
addedHook: ensured.addedHook,
});
debugEntries.push({
file: relPath,
status: 'changed',
beforeLiteralCount: literalCheck.length,
textReplacements: textReplaced.replacements,
attrReplacements: attrReplaced.replacements,
afterLiteralCount,
});
} else {
debugEntries.push({
file: relPath,
status: 'no-op',
beforeLiteralCount: literalCheck.length,
textReplacements: textReplaced.replacements,
attrReplacements: attrReplaced.replacements,
afterLiteralCount,
reason: 'Generated output matched input; no file write needed.',
});
}
}
if (createdEntries.size > 0) {
const enPath = path.join(process.cwd(), 'src', 'app', 'i18n', 'translations', 'en.ts');
const dePath = path.join(process.cwd(), 'src', 'app', 'i18n', 'translations', 'de.ts');
const typesPath = path.join(process.cwd(), 'src', 'app', 'i18n', 'types.ts');
const [enRaw, deRaw, typesRaw] = await Promise.all([
fs.readFile(enPath, 'utf8'),
fs.readFile(dePath, 'utf8'),
fs.readFile(typesPath, 'utf8'),
]);
const nextEn = upsertAutofixNamespace(enRaw, createdEntries);
const nextDe = upsertAutofixNamespace(deRaw, createdEntries);
const nextTypes = upsertAutofixType(typesRaw);
await Promise.all([
nextEn !== enRaw ? fs.writeFile(enPath, nextEn, 'utf8') : Promise.resolve(),
nextDe !== deRaw ? fs.writeFile(dePath, nextDe, 'utf8') : Promise.resolve(),
nextTypes !== typesRaw ? fs.writeFile(typesPath, nextTypes, 'utf8') : Promise.resolve(),
]);
}
return {
changedFiles,
skippedFiles,
createdKeys: Array.from(createdEntries.keys()).sort(),
debugEntries,
};
}
function toRelativeWorkspacePath(absPath: string): string {
const rel = path.relative(process.cwd(), absPath);
return rel.split(path.sep).join('/');
@ -141,6 +607,8 @@ async function runWorkspaceScan(): Promise<ScanResult> {
const uniqueUsedKeys = new Set<string>();
const missingKeyFiles: MissingKeyMap = new Map();
const untranslatedLiteralFiles: Map<string, Set<string>> = new Map();
const autoFixEligibleFilesSet = new Set<string>();
const autoFixForceConvertibleFilesSet = new Set<string>();
let translationCallCount = 0;
@ -150,9 +618,10 @@ async function runWorkspaceScan(): Promise<ScanResult> {
const usedKeys = extractTranslationKeys(raw);
if (usedKeys.length > 0) {
translationCallCount += usedKeys.length;
const staticKeys = usedKeys.filter(isLikelyStaticTranslationKey);
translationCallCount += staticKeys.length;
for (const key of usedKeys) {
for (const key of staticKeys) {
uniqueUsedKeys.add(key);
if (!englishKeys.has(key)) {
@ -172,6 +641,14 @@ async function runWorkspaceScan(): Promise<ScanResult> {
}
untranslatedLiteralFiles.get(text)?.add(relativePath);
}
if (literals.length > 0 && isAutoFixCandidatePath(relativePath) && (hasUseClientDirective(raw) || hasExistingTUsage(raw))) {
autoFixEligibleFilesSet.add(relativePath);
}
if (literals.length > 0 && isAutoFixCandidatePath(relativePath)) {
autoFixForceConvertibleFilesSet.add(relativePath);
}
}
}
@ -191,6 +668,8 @@ async function runWorkspaceScan(): Promise<ScanResult> {
uniqueKeyCount: uniqueUsedKeys.size,
missingKeys,
untranslatedLiterals,
autoFixEligibleFiles: Array.from(autoFixEligibleFilesSet).sort((a, b) => a.localeCompare(b)),
autoFixForceConvertibleFiles: Array.from(autoFixForceConvertibleFilesSet).sort((a, b) => a.localeCompare(b)),
};
}
@ -213,3 +692,62 @@ export async function GET() {
);
}
}
export async function POST(request: Request) {
try {
let targetFilesSet: Set<string> | undefined;
let forceConvertToClient = false;
try {
const body = await request.json();
const raw = Array.isArray(body?.targetFiles) ? body.targetFiles : [];
forceConvertToClient = Boolean(body?.forceConvertToClient);
const cleaned = raw
.filter((v: unknown) => typeof v === 'string')
.map((v: string) => v.trim())
.filter((v: string) => v.length > 0);
if (cleaned.length > 0) {
targetFilesSet = new Set(cleaned);
}
} catch {
// allow empty body (fix all eligible files)
}
const fixResult = await runAutoFix({ targetFiles: targetFilesSet, forceConvertToClient });
const scanResult = await runWorkspaceScan();
console.info('[i18n-autofix] Summary', {
targetFileCount: targetFilesSet ? targetFilesSet.size : null,
forceConvertToClient,
changedFileCount: fixResult.changedFiles.length,
skippedFileCount: fixResult.skippedFiles.length,
createdKeyCount: fixResult.createdKeys.length,
});
for (const entry of fixResult.debugEntries.slice(0, 200)) {
console.info('[i18n-autofix:file]', entry);
}
return NextResponse.json({
ok: true,
mode: 'autofix',
fixedAt: new Date().toISOString(),
forceConvertToClient,
targetFilesApplied: targetFilesSet ? Array.from(targetFilesSet).sort() : null,
changedFileCount: fixResult.changedFiles.length,
changedFiles: fixResult.changedFiles,
skippedFiles: fixResult.skippedFiles,
autoFixDebug: fixResult.debugEntries,
createdKeyCount: fixResult.createdKeys.length,
createdKeys: fixResult.createdKeys,
...scanResult,
});
} catch (error) {
console.error('Workspace i18n auto-fix failed:', error);
return NextResponse.json(
{
ok: false,
message: 'Workspace auto-fix failed.',
},
{ status: 500 }
);
}
}

View File

@ -0,0 +1,313 @@
import { NextResponse } from 'next/server';
import { promises as fs } from 'fs';
import path from 'path';
import { flattenObject, unflattenObject } from '@/app/i18n/dynamicTranslations';
const TRANSLATIONS_DIR = path.join(process.cwd(), 'src', 'app', 'i18n', 'translations');
const CORE_LANGUAGE_CODES = new Set(['en', 'de']);
type FlatTranslations = Record<string, string>;
type LanguageDescriptor = {
code: string;
name: string;
};
function isValidLanguageCode(code: string): boolean {
return /^[a-z]{2,5}(?:-[a-zA-Z]{2,4})?$/.test(code);
}
function escapeRegex(input: string): string {
return input.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
function normalizeLanguageName(input: string, code: string): string {
const trimmed = input.trim();
if (trimmed.length === 0) return code.toUpperCase();
return trimmed.replace(/[\r\n]+/g, ' ').trim();
}
function extractLanguageNameFromSource(source: string, code: string): string {
const nameMatch = source.match(/^\s*\/\/\s*Language name:\s*(.+)$/m);
if (nameMatch?.[1]) return normalizeLanguageName(nameMatch[1], code);
if (code === 'en') return 'English';
if (code === 'de') return 'Deutsch';
return code.toUpperCase();
}
function findObjectLiteralBounds(source: string, variableName: string): { start: number; end: number } {
const exportRegex = new RegExp(`export\\s+const\\s+${escapeRegex(variableName)}\\s*:\\s*Translations\\s*=`, 'm');
const match = exportRegex.exec(source);
if (!match) {
throw new Error(`Could not find exported translations object for language "${variableName}".`);
}
const afterExportIndex = match.index + match[0].length;
const objectStart = source.indexOf('{', afterExportIndex);
if (objectStart === -1) {
throw new Error(`Could not find object start for language "${variableName}".`);
}
let depth = 0;
let inSingle = false;
let inDouble = false;
let inBacktick = false;
let inLineComment = false;
let inBlockComment = false;
for (let i = objectStart; i < source.length; i += 1) {
const char = source[i];
const next = source[i + 1];
const prev = source[i - 1];
if (inLineComment) {
if (char === '\n') inLineComment = false;
continue;
}
if (inBlockComment) {
if (char === '*' && next === '/') {
inBlockComment = false;
i += 1;
}
continue;
}
if (!inSingle && !inDouble && !inBacktick) {
if (char === '/' && next === '/') {
inLineComment = true;
i += 1;
continue;
}
if (char === '/' && next === '*') {
inBlockComment = true;
i += 1;
continue;
}
}
if (!inDouble && !inBacktick && char === '\'' && prev !== '\\') {
inSingle = !inSingle;
continue;
}
if (!inSingle && !inBacktick && char === '"' && prev !== '\\') {
inDouble = !inDouble;
continue;
}
if (!inSingle && !inDouble && char === '`' && prev !== '\\') {
inBacktick = !inBacktick;
continue;
}
if (inSingle || inDouble || inBacktick) continue;
if (char === '{') {
depth += 1;
} else if (char === '}') {
depth -= 1;
if (depth === 0) {
return { start: objectStart, end: i + 1 };
}
}
}
throw new Error(`Could not find object end for language "${variableName}".`);
}
function parseTranslationObject(source: string, code: string): Record<string, unknown> {
const { start, end } = findObjectLiteralBounds(source, code);
const literal = source.slice(start, end);
try {
const parsed = Function(`"use strict"; return (${literal});`)() as Record<string, unknown>;
if (!parsed || typeof parsed !== 'object') {
throw new Error('Invalid object result.');
}
return parsed;
} catch (error) {
throw new Error(
`Failed to parse translation file for language "${code}": ${error instanceof Error ? error.message : 'unknown error'}`
);
}
}
function toTsObjectLiteral(value: unknown): string {
return JSON.stringify(value, null, 2);
}
function buildTranslationFileContent(code: string, languageName: string, nestedObject: Record<string, unknown>): string {
return [
"import { Translations } from '../types';",
'',
`// Language name: ${normalizeLanguageName(languageName, code)}`,
`export const ${code}: Translations = ${toTsObjectLiteral(nestedObject)};`,
'',
].join('\n');
}
async function readLanguageFile(code: string): Promise<{ source: string; filePath: string }> {
const filePath = path.join(TRANSLATIONS_DIR, `${code}.ts`);
const source = await fs.readFile(filePath, 'utf8');
return { source, filePath };
}
async function getLanguageFiles(): Promise<string[]> {
const entries = await fs.readdir(TRANSLATIONS_DIR, { withFileTypes: true });
return entries
.filter((entry) => entry.isFile() && entry.name.endsWith('.ts'))
.map((entry) => entry.name.slice(0, -3))
.sort((a, b) => a.localeCompare(b));
}
async function readAllTranslations(): Promise<{
languages: LanguageDescriptor[];
translations: Record<string, FlatTranslations>;
}> {
const codes = await getLanguageFiles();
const languages: LanguageDescriptor[] = [];
const translations: Record<string, FlatTranslations> = {};
for (const code of codes) {
const { source } = await readLanguageFile(code);
const parsed = parseTranslationObject(source, code);
languages.push({ code, name: extractLanguageNameFromSource(source, code) });
translations[code] = flattenObject(parsed as Record<string, unknown>);
}
return { languages, translations };
}
function normalizeFlatTranslations(value: unknown): FlatTranslations {
if (!value || typeof value !== 'object') return {};
return Object.fromEntries(
Object.entries(value as Record<string, unknown>).map(([k, v]) => [k, String(v ?? '')])
);
}
async function writeLanguageFile(
code: string,
languageName: string,
incomingFlat: FlatTranslations,
englishFlat: FlatTranslations
): Promise<void> {
const mergedFlat: FlatTranslations = {};
for (const key of Object.keys(englishFlat)) {
const candidate = incomingFlat[key];
mergedFlat[key] = candidate !== undefined ? String(candidate) : String(englishFlat[key] ?? '');
}
const nested = unflattenObject(mergedFlat);
const filePath = path.join(TRANSLATIONS_DIR, `${code}.ts`);
const content = buildTranslationFileContent(code, languageName, nested);
await fs.writeFile(filePath, content, 'utf8');
}
export async function GET() {
try {
const data = await readAllTranslations();
return NextResponse.json({ ok: true, ...data });
} catch (error) {
return NextResponse.json(
{ ok: false, message: error instanceof Error ? error.message : 'Failed to read translations.' },
{ status: 500 }
);
}
}
export async function POST(request: Request) {
try {
const body = await request.json().catch(() => ({}));
const code = String(body?.code ?? '').trim().toLowerCase();
const languageName = normalizeLanguageName(String(body?.name ?? ''), code);
if (!isValidLanguageCode(code)) {
return NextResponse.json({ ok: false, message: 'Invalid language code.' }, { status: 400 });
}
const existingCodes = new Set(await getLanguageFiles());
if (existingCodes.has(code)) {
return NextResponse.json({ ok: false, message: `Language "${code}" already exists.` }, { status: 409 });
}
const { translations } = await readAllTranslations();
const englishFlat = translations.en;
if (!englishFlat) {
return NextResponse.json({ ok: false, message: 'English base translation file is missing.' }, { status: 500 });
}
await writeLanguageFile(code, languageName, englishFlat, englishFlat);
const updated = await readAllTranslations();
return NextResponse.json({ ok: true, ...updated });
} catch (error) {
return NextResponse.json(
{ ok: false, message: error instanceof Error ? error.message : 'Failed to create language file.' },
{ status: 500 }
);
}
}
export async function PUT(request: Request) {
try {
const body = await request.json().catch(() => ({}));
const payload = body?.translations;
if (!payload || typeof payload !== 'object') {
return NextResponse.json({ ok: false, message: 'translations payload is required.' }, { status: 400 });
}
const allCurrent = await readAllTranslations();
const englishFlat = allCurrent.translations.en;
if (!englishFlat) {
return NextResponse.json({ ok: false, message: 'English base translation file is missing.' }, { status: 500 });
}
const languageNameByCode: Record<string, string> = Object.fromEntries(
allCurrent.languages.map((lang) => [lang.code, lang.name])
);
const incomingTranslations = payload as Record<string, unknown>;
const availableCodes = new Set(Object.keys(allCurrent.translations));
for (const [code, flatCandidate] of Object.entries(incomingTranslations)) {
if (!availableCodes.has(code)) continue;
const normalizedFlat = normalizeFlatTranslations(flatCandidate);
const languageName = languageNameByCode[code] ?? code.toUpperCase();
await writeLanguageFile(code, languageName, normalizedFlat, englishFlat);
}
const updated = await readAllTranslations();
return NextResponse.json({ ok: true, ...updated });
} catch (error) {
return NextResponse.json(
{ ok: false, message: error instanceof Error ? error.message : 'Failed to save translation files.' },
{ status: 500 }
);
}
}
export async function DELETE(request: Request) {
try {
const body = await request.json().catch(() => ({}));
const code = String(body?.code ?? '').trim().toLowerCase();
if (!isValidLanguageCode(code)) {
return NextResponse.json({ ok: false, message: 'Invalid language code.' }, { status: 400 });
}
if (CORE_LANGUAGE_CODES.has(code)) {
return NextResponse.json({ ok: false, message: `Language "${code}" cannot be deleted.` }, { status: 400 });
}
const filePath = path.join(TRANSLATIONS_DIR, `${code}.ts`);
await fs.unlink(filePath);
const updated = await readAllTranslations();
return NextResponse.json({ ok: true, ...updated });
} catch (error) {
return NextResponse.json(
{ ok: false, message: error instanceof Error ? error.message : 'Failed to delete language file.' },
{ status: 500 }
);
}
}

View File

@ -5,7 +5,10 @@ import { useRouter } from 'next/navigation';
import { useActiveCoffees } from './hooks/getActiveCoffees';
import { useShippingFees } from './hooks/useShippingFees';
import { useTranslation } from '../i18n/useTranslation';
export default function CoffeeAbonnementPage() {
const { t } = useTranslation();
const [selections, setSelections] = useState<Record<string, number>>({});
const [bump, setBump] = useState<Record<string, boolean>>({});
const [selectedPlanCapsules, setSelectedPlanCapsules] = useState<number>(60);
@ -117,7 +120,7 @@ export default function CoffeeAbonnementPage() {
<PageLayout>
<div className="mx-auto max-w-7xl px-4 py-10 space-y-10 bg-gradient-to-b from-white to-[#1C2B4A0D]">
<h1 className="text-3xl font-bold tracking-tight">
<span className="text-[#1C2B4A]">Configure Coffee Subscription</span>
<span className="text-[#1C2B4A]">{t('autofix.kb0b660e2')}</span>
</h1>
{/* Stepper */}
@ -135,7 +138,7 @@ export default function CoffeeAbonnementPage() {
{/* Section 1: Multi coffee selection + per-coffee quantity */}
<section>
<h2 className="text-xl font-semibold mb-4">1. Select subscription size</h2>
<h2 className="text-xl font-semibold mb-4">{t('autofix.k7f48f374')}</h2>
<div className="mb-6 rounded-xl border border-[#1C2B4A]/20 p-4 bg-white/80 backdrop-blur-sm shadow-lg">
<div className="flex items-center gap-4">
<button
@ -155,9 +158,9 @@ export default function CoffeeAbonnementPage() {
>+</button>
<div className="ml-4">
{shippingLoading ? (
<span className="inline-flex items-center rounded-full px-3 py-1 text-xs font-semibold bg-gray-100 text-gray-700">Shipping</span>
<span className="inline-flex items-center rounded-full px-3 py-1 text-xs font-semibold bg-gray-100 text-gray-700">{t('autofix.k12a86c71')}</span>
) : isFreeShippingSelected ? (
<span className="inline-flex items-center rounded-full px-3 py-1 text-xs font-semibold bg-emerald-50 text-emerald-700 ring-1 ring-inset ring-emerald-200">FREE SHIPPING</span>
<span className="inline-flex items-center rounded-full px-3 py-1 text-xs font-semibold bg-emerald-50 text-emerald-700 ring-1 ring-inset ring-emerald-200">{t('autofix.ke7f0a9e3')}</span>
) : (
<span className="inline-flex items-center rounded-full px-3 py-1 text-xs font-semibold bg-gray-100 text-gray-700 ring-1 ring-inset ring-gray-200">Shipping {selectedShippingFee.toFixed(2)}</span>
)}
@ -170,7 +173,7 @@ export default function CoffeeAbonnementPage() {
)}
</div>
<h2 className="text-xl font-semibold mb-4">2. Choose coffees & quantities</h2>
<h2 className="text-xl font-semibold mb-4">{t('autofix.k0b03e660')}</h2>
{error && (
<div className="mb-4 rounded-md border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">
@ -226,9 +229,7 @@ export default function CoffeeAbonnementPage() {
>
{coffee.pricePer10}
</span>
<span className="text-[10px] font-medium px-2 py-0.5 rounded-full bg-[#1C2B4A]/90 text-white border border-white/20 shadow-sm backdrop-blur-sm">
per 10 pcs
</span>
<span className="text-[10px] font-medium px-2 py-0.5 rounded-full bg-[#1C2B4A]/90 text-white border border-white/20 shadow-sm backdrop-blur-sm">{t('autofix.k83deba83')}</span>
</div>
</div>
<div className="flex items-start justify-between">
@ -317,10 +318,10 @@ export default function CoffeeAbonnementPage() {
{/* Section 2: Compact preview + next steps */}
<section>
<h2 className="text-xl font-semibold mb-4">3. Preview</h2>
<h2 className="text-xl font-semibold mb-4">{t('autofix.ke7b634f2')}</h2>
<div className="rounded-xl border border-[#1C2B4A]/20 p-6 bg-white/80 backdrop-blur-sm space-y-4 shadow-lg">
{selectedEntries.length === 0 && (
<p className="text-sm text-gray-600">No coffees selected yet.</p>
<p className="text-sm text-gray-600">{t('autofix.kec078e54')}</p>
)}
{selectedEntries.map((entry) => (
<div key={entry.coffee.id} className="flex justify-between text-sm border-b last:border-b-0 pb-2 last:pb-0">
@ -378,9 +379,7 @@ export default function CoffeeAbonnementPage() {
? 'bg-[#1C2B4A] text-white hover:bg-[#1C2B4A]/90 shadow-md hover:shadow-lg'
: 'bg-gray-200 text-gray-600 cursor-not-allowed'
}`}
>
Next steps
<svg
>{t('autofix.k02665163')}<svg
className={`ml-2 h-5 w-5 transition-transform ${
canProceed ? 'group-hover:translate-x-0.5' : ''
}`}

View File

@ -7,6 +7,8 @@ import { getStandardVatRate, getVatRates } from './hooks/getTaxRate';
import { subscribeAbo, type SubscribeAboInput } from './hooks/subscribeAbo';
import useAuthStore from '../../store/authStore'
import { useShippingFees } from '../hooks/useShippingFees';
import { useTranslation } from '../../i18n/useTranslation';
import { useAboContractTemplateHtml } from './hooks/useAboContractTemplateHtml'
import SignaturePad from './components/SignaturePad'
import { Dialog, DialogActions, DialogBody, DialogTitle } from '../../components/dialog'
@ -62,6 +64,7 @@ function pickFirstString(...values: unknown[]): string {
}
export default function SummaryPage() {
const { t } = useTranslation();
const router = useRouter();
const { coffees, loading, error } = useActiveCoffees();
const user = useAuthStore(state => state.user)
@ -640,14 +643,12 @@ export default function SummaryPage() {
<div className="mx-auto max-w-6xl px-4 py-10 space-y-8 bg-gradient-to-b from-white to-[#1C2B4A0D]">
<div className="flex items-center justify-between">
<h1 className="text-3xl font-bold tracking-tight">
<span className="text-[#1C2B4A]">Summary & Details</span>
<span className="text-[#1C2B4A]">{t('autofix.k21361e0d')}</span>
</h1>
<button
onClick={backToSelection}
className="rounded-md border border-gray-300 px-3 py-2 text-sm hover:bg-gray-100"
>
Back to selection
</button>
>{t('autofix.k96839795')}</button>
</div>
{/* Stepper */}
@ -669,9 +670,7 @@ export default function SummaryPage() {
<button
onClick={backToSelection}
className="rounded-md bg-[#1C2B4A] text-white px-4 py-2 font-semibold hover:bg-[#1C2B4A]/90"
>
Back to selection
</button>
>{t('autofix.k96839795')}</button>
</div>
)}
@ -688,35 +687,31 @@ export default function SummaryPage() {
</div>
) : selectedEntries.length === 0 ? (
<div className="rounded-xl border p-6 bg-white shadow-sm">
<p className="text-sm text-gray-600 mb-4">No selection found.</p>
<p className="text-sm text-gray-600 mb-4">{t('autofix.k20127e1c')}</p>
<button
onClick={backToSelection}
className="rounded-md bg-[#1C2B4A] text-white px-4 py-2 font-semibold hover:bg-[#1C2B4A]/90"
>
Back to selection
</button>
>{t('autofix.k96839795')}</button>
</div>
) : (
<div className="grid gap-8 lg:grid-cols-3">
{/* Left: Customer data */}
<section className="lg:col-span-2">
<h2 className="text-xl font-semibold mb-4">1. Your details</h2>
<h2 className="text-xl font-semibold mb-4">{t('autofix.kd6f8d7e9')}</h2>
<div className="rounded-xl border border-[#1C2B4A]/20 bg-white/80 backdrop-blur-sm p-6 shadow-lg">
<button
type="button"
onClick={fillFromLoggedInData}
className="mb-4 w-full rounded-md border border-[#1C2B4A] px-3 py-2 text-sm font-medium text-[#1C2B4A] hover:bg-[#1C2B4A]/5"
>
Fill fields with logged in data
</button>
>{t('autofix.k9c1a5ecc')}</button>
<div className="grid gap-4 sm:grid-cols-2">
{/* inputs translated */}
<div>
<label className="block text-sm font-medium mb-1">First name</label>
<label className="block text-sm font-medium mb-1">{t('autofix.kfe9527d8')}</label>
<input name="firstName" value={form.firstName} onChange={handleInput} className="w-full rounded border px-3 py-2 bg-white border-gray-300 focus:outline-none focus:ring-2 focus:ring-[#1C2B4A]" />
</div>
<div>
<label className="block text-sm font-medium mb-1">Last name</label>
<label className="block text-sm font-medium mb-1">{t('autofix.k6a2c64e8')}</label>
<input name="lastName" value={form.lastName} onChange={handleInput} className="w-full rounded border px-3 py-2 bg-white border-gray-300 focus:outline-none focus:ring-2 focus:ring-[#1C2B4A]" />
</div>
<div className="sm:col-span-2">
@ -724,7 +719,7 @@ export default function SummaryPage() {
<input type="email" name="email" value={form.email} onChange={handleInput} className="w-full rounded border px-3 py-2 bg-white border-gray-300 focus:outline-none focus:ring-2 focus:ring-[#1C2B4A]" />
</div>
<div className="sm:col-span-2">
<label className="block text-sm font-medium mb-1">Street & No.</label>
<label className="block text-sm font-medium mb-1">{t('autofix.kd1a2772d')}</label>
<input name="street" value={form.street} onChange={handleInput} className="w-full rounded border px-3 py-2 bg-white border-gray-300 focus:outline-none focus:ring-2 focus:ring-[#1C2B4A]" />
</div>
<div>
@ -751,7 +746,7 @@ export default function SummaryPage() {
{/* Payment method */}
<div className="mt-6 border-t border-gray-200 pt-4">
<h3 className="text-base font-semibold text-gray-900 mb-3">Payment method</h3>
<h3 className="text-base font-semibold text-gray-900 mb-3">{t('autofix.k3466b0e0')}</h3>
<div className="flex flex-wrap gap-3">
{(['sepa', 'card', 'sofort'] as const).map(method => (
<label key={method} className={`flex items-center gap-2 rounded-md border px-4 py-2 cursor-pointer transition ${form.paymentMethod === method ? 'border-[#1C2B4A] bg-[#1C2B4A]/5 font-medium' : 'border-gray-300 hover:bg-gray-50'}`}>
@ -761,23 +756,19 @@ export default function SummaryPage() {
))}
</div>
<label className="mt-3 flex items-center gap-2 text-sm">
<input type="checkbox" name="invoiceByEmail" checked={form.invoiceByEmail} onChange={handleCheckbox} className="accent-[#1C2B4A]" />
Send invoice by email
</label>
<input type="checkbox" name="invoiceByEmail" checked={form.invoiceByEmail} onChange={handleCheckbox} className="accent-[#1C2B4A]" />{t('autofix.ke33e6fbf')}</label>
</div>
{/* Invoice address */}
<div className="mt-6 border-t border-gray-200 pt-4">
<h3 className="text-base font-semibold text-gray-900 mb-3">Invoice address</h3>
<h3 className="text-base font-semibold text-gray-900 mb-3">{t('autofix.kce094582')}</h3>
{isCompanyCustomer && (
<div className="mb-3 rounded-lg border border-blue-200 bg-blue-50 px-3 py-2 text-xs text-blue-900">
Unternehmer mit gueltiger UID und Rechnungsland ausserhalb von {HOME_COUNTRY_CODE} werden per Reverse Charge ohne ausgewiesene MwSt verrechnet.
</div>
)}
<label className="flex items-center gap-2 text-sm mb-3">
<input type="checkbox" name="invoiceSameAsShipping" checked={form.invoiceSameAsShipping} onChange={handleCheckbox} className="accent-[#1C2B4A]" />
Same as shipping address
</label>
<input type="checkbox" name="invoiceSameAsShipping" checked={form.invoiceSameAsShipping} onChange={handleCheckbox} className="accent-[#1C2B4A]" />{t('autofix.k528eede9')}</label>
{isCompanyCustomer && (
<div className="mb-4">
<label className="block text-sm font-medium mb-1">UID Number (optional)</label>
@ -785,22 +776,20 @@ export default function SummaryPage() {
name="uidNumber"
value={form.uidNumber}
onChange={handleInput}
placeholder="z.B. SI12345678"
placeholder={t('autofix.kf1512f8f')}
className="w-full rounded border px-3 py-2 bg-white border-gray-300 uppercase focus:outline-none focus:ring-2 focus:ring-[#1C2B4A]"
/>
<p className="mt-1 text-xs text-gray-600">
Ohne gueltige UID wird die Rechnung mit normaler MwSt erstellt.
</p>
<p className="mt-1 text-xs text-gray-600">{t('autofix.kefe5f0dd')}</p>
</div>
)}
{!form.invoiceSameAsShipping && (
<div className="grid gap-4 sm:grid-cols-2">
<div className="sm:col-span-2">
<label className="block text-sm font-medium mb-1">Full name</label>
<label className="block text-sm font-medium mb-1">{t('autofix.k28f1a9b1')}</label>
<input name="invoiceFullName" value={form.invoiceFullName} onChange={handleInput} className="w-full rounded border px-3 py-2 bg-white border-gray-300 focus:outline-none focus:ring-2 focus:ring-[#1C2B4A]" />
</div>
<div className="sm:col-span-2">
<label className="block text-sm font-medium mb-1">Street & No.</label>
<label className="block text-sm font-medium mb-1">{t('autofix.kd1a2772d')}</label>
<input name="invoiceStreet" value={form.invoiceStreet} onChange={handleInput} className="w-full rounded border px-3 py-2 bg-white border-gray-300 focus:outline-none focus:ring-2 focus:ring-[#1C2B4A]" />
</div>
<div>
@ -826,14 +815,10 @@ export default function SummaryPage() {
{/* Contract preview + signature */}
<div className="mt-6 border-t border-gray-200 pt-6">
<h3 className="text-base font-semibold text-gray-900 mb-2">Contract preview (ABO)</h3>
<p className="text-xs text-gray-600 mb-3">
Contract variables are auto-populated from your form data.
</p>
<p className="text-xs text-gray-600 mb-3">{t('autofix.k155166db')}</p>
{contractLoading ? (
<div className="rounded-lg border border-gray-200 bg-gray-50 px-4 py-3 text-sm text-gray-700">
Loading contract preview
</div>
<div className="rounded-lg border border-gray-200 bg-gray-50 px-4 py-3 text-sm text-gray-700">{t('autofix.k0bbc633d')}</div>
) : contractError ? (
<div className="rounded-lg border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-900">
Contract preview could not be loaded: {contractError}
@ -843,21 +828,17 @@ export default function SummaryPage() {
type="button"
onClick={openContractPreview}
className="inline-flex items-center justify-center rounded-md bg-[#1C2B4A] text-white px-4 py-2 text-sm font-semibold hover:bg-[#1C2B4A]/90"
>
Open preview
</button>
>{t('autofix.kd379df9b')}</button>
) : (
<div className="rounded-lg border border-gray-200 bg-gray-50 px-4 py-3 text-sm text-gray-700">
Contract template is not available.
</div>
<div className="rounded-lg border border-gray-200 bg-gray-50 px-4 py-3 text-sm text-gray-700">{t('autofix.ke74b1adf')}</div>
)}
<div className="mt-4 space-y-3">
<div>
<label className="block text-sm font-medium mb-1">Ort (Signing City) *</label>
<input type="text" name="signingCity" value={form.signingCity} onChange={handleInput} className={`w-full max-w-xs rounded border px-3 py-2 bg-white focus:outline-none focus:ring-2 focus:ring-[#1C2B4A] ${!hasSigningCity && submitError ? 'border-red-400' : 'border-gray-300'}`} placeholder="z.B. Wien" />
<input type="text" name="signingCity" value={form.signingCity} onChange={handleInput} className={`w-full max-w-xs rounded border px-3 py-2 bg-white focus:outline-none focus:ring-2 focus:ring-[#1C2B4A] ${!hasSigningCity && submitError ? 'border-red-400' : 'border-gray-300'}`} placeholder={t('autofix.k1f0b2c48')} />
{!hasSigningCity && submitError && (
<p className="mt-1 text-xs text-red-700">Ort ist erforderlich.</p>
<p className="mt-1 text-xs text-red-700">{t('autofix.k516705dd')}</p>
)}
</div>
<SignaturePad value={signatureDataUrl} onChange={setSignatureDataUrl} required error={!hasSignature && submitError ? 'Signature is required.' : null} />
@ -872,21 +853,17 @@ export default function SummaryPage() {
PDF preview could not be generated: {contractPdfError}
</div>
) : contractPdfLoading ? (
<div className="rounded-lg border border-gray-200 bg-gray-50 px-4 py-3 text-sm text-gray-700">
Generating PDF preview
</div>
<div className="rounded-lg border border-gray-200 bg-gray-50 px-4 py-3 text-sm text-gray-700">{t('autofix.k0b2445d5')}</div>
) : contractPdfUrl ? (
<div className="rounded-lg border border-gray-300 bg-white overflow-hidden">
<iframe
title="ABO Contract PDF Preview"
title={t('autofix.kaa5e5363')}
className="w-full h-[75vh]"
src={contractPdfUrl}
/>
</div>
) : (
<div className="rounded-lg border border-gray-200 bg-gray-50 px-4 py-3 text-sm text-gray-700">
No PDF preview available.
</div>
<div className="rounded-lg border border-gray-200 bg-gray-50 px-4 py-3 text-sm text-gray-700">{t('autofix.ka56b7b2b')}</div>
)}
</DialogBody>
<DialogActions>
@ -913,16 +890,14 @@ export default function SummaryPage() {
</svg>
</button>
{!canSubmit && (
<p className="text-xs text-gray-500 mt-2">
Please select coffees and fill all required buyer fields, signing city, and signature.
</p>
<p className="text-xs text-gray-500 mt-2">{t('autofix.k1824f78d')}</p>
)}
</div>
</section>
{/* Right: Order summary */}
<section className="lg:col-span-1">
<h2 className="text-xl font-semibold mb-4">2. Your selection</h2>
<h2 className="text-xl font-semibold mb-4">{t('autofix.k4aeb8688')}</h2>
<div className="rounded-xl border border-[#1C2B4A]/20 bg-white/80 backdrop-blur-sm p-6 shadow-lg lg:sticky lg:top-6">
{selectedEntries.map(entry => (
<div key={entry.coffee.id} className="flex justify-between text-sm border-b last:border-b-0 pb-2 last:pb-0">
@ -965,13 +940,11 @@ export default function SummaryPage() {
<span className="text-sm font-medium">{taxAmountWithShipping.toFixed(2)}</span>
</div>
<div className="flex items-center justify-between">
<span className="text-sm font-semibold">Total incl. tax</span>
<span className="text-sm font-semibold">{t('autofix.k11438b4c')}</span>
<span className="text-xl font-extrabold text-[#1C2B4A]">{totalWithTax.toFixed(2)}</span>
</div>
{isReverseCharge && (
<div className="mt-2 rounded-md border border-blue-200 bg-blue-50 px-3 py-2 text-xs text-blue-900">
Reverse Charge aktiv: gueltige UID und auslaendisches Rechnungsland erkannt.
</div>
<div className="mt-2 rounded-md border border-blue-200 bg-blue-50 px-3 py-2 text-xs text-blue-900">{t('autofix.k74491338')}</div>
)}
{/* Validation summary (refined design) */}
<div className="mt-2 text-xs text-gray-700">
@ -1003,15 +976,11 @@ export default function SummaryPage() {
<path d="M20 6L9 17l-5-5" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</div>
<h3 className="text-2xl font-bold">Thanks for your subscription!</h3>
<p className="mt-1 text-sm text-gray-600">
Subscription created.
</p>
<h3 className="text-2xl font-bold">{t('autofix.k0853cfa6')}</h3>
<p className="mt-1 text-sm text-gray-600">{t('autofix.kd3092148')}</p>
<div className="mt-6 grid gap-3 sm:grid-cols-2">
<button onClick={() => { setShowThanks(false); backToSelection(); }} className="rounded-lg bg-[#1C2B4A] text-white px-4 py-2 font-semibold hover:bg-[#1C2B4A]/90">
Back to selection
</button>
<button onClick={() => { setShowThanks(false); backToSelection(); }} className="rounded-lg bg-[#1C2B4A] text-white px-4 py-2 font-semibold hover:bg-[#1C2B4A]/90">{t('autofix.k96839795')}</button>
<button onClick={() => setShowThanks(false)} className="rounded-lg border border-gray-300 px-4 py-2 font-semibold hover:bg-gray-50">
Close
</button>

View File

@ -1,5 +1,8 @@
'use client'
import { useTranslation } from '../i18n/useTranslation';
import { useEffect } from 'react'
import { useRouter } from 'next/navigation'
import useAuthStore from '../store/authStore'
@ -17,6 +20,7 @@ import {
} from '@heroicons/react/24/outline'
export default function CommunityPage() {
const { t } = useTranslation();
const router = useRouter()
const user = useAuthStore(state => state.user)
@ -113,12 +117,8 @@ export default function CommunityPage() {
<div className="max-w-7xl mx-auto">
{/* Header Section */}
<div className="text-center mb-12">
<h1 className="text-4xl font-bold text-gray-900 mb-4">
Welcome to Profit Planet Community 🌍
</h1>
<p className="text-xl text-gray-600 max-w-3xl mx-auto">
Connect with like-minded individuals, share sustainable practices, and make a positive impact together.
</p>
<h1 className="text-4xl font-bold text-gray-900 mb-4">{t('autofix.k08c92a12')}</h1>
<p className="text-xl text-gray-600 max-w-3xl mx-auto">{t('autofix.k3c32c87f')}</p>
</div>
{/* Community Stats */}
@ -139,12 +139,8 @@ export default function CommunityPage() {
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<div className="flex items-center justify-between mb-6">
<h2 className="text-xl font-semibold text-gray-900 flex items-center">
<TrophyIcon className="h-6 w-6 text-[#8D6B1D] mr-2" />
Trending Groups
</h2>
<button className="text-[#8D6B1D] hover:text-[#7A5E1A] text-sm font-medium flex items-center">
View All
<ArrowRightIcon className="h-4 w-4 ml-1" />
<TrophyIcon className="h-6 w-6 text-[#8D6B1D] mr-2" />{t('autofix.k5c598bc0')}</h2>
<button className="text-[#8D6B1D] hover:text-[#7A5E1A] text-sm font-medium flex items-center">{t('autofix.k16b60f69')}<ArrowRightIcon className="h-4 w-4 ml-1" />
</button>
</div>
@ -160,9 +156,7 @@ export default function CommunityPage() {
<p className="text-xs text-gray-500">{group.members} members</p>
</div>
</div>
<button className="w-full mt-3 px-3 py-2 bg-[#8D6B1D]/10 text-[#8D6B1D] rounded-lg text-sm font-medium hover:bg-[#8D6B1D]/20 transition-colors">
Join Group
</button>
<button className="w-full mt-3 px-3 py-2 bg-[#8D6B1D]/10 text-[#8D6B1D] rounded-lg text-sm font-medium hover:bg-[#8D6B1D]/20 transition-colors">{t('autofix.k15da24d8')}</button>
</div>
))}
</div>
@ -172,12 +166,8 @@ export default function CommunityPage() {
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<div className="flex items-center justify-between mb-6">
<h2 className="text-xl font-semibold text-gray-900 flex items-center">
<ChatBubbleLeftRightIcon className="h-6 w-6 text-[#8D6B1D] mr-2" />
Recent Discussions
</h2>
<button className="text-[#8D6B1D] hover:text-[#7A5E1A] text-sm font-medium">
Start Discussion
</button>
<ChatBubbleLeftRightIcon className="h-6 w-6 text-[#8D6B1D] mr-2" />{t('autofix.k70bcafbd')}</h2>
<button className="text-[#8D6B1D] hover:text-[#7A5E1A] text-sm font-medium">{t('autofix.k9c3db145')}</button>
</div>
<div className="space-y-6">
@ -220,41 +210,35 @@ export default function CommunityPage() {
<div className="space-y-6">
{/* Quick Actions */}
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<h3 className="font-semibold text-gray-900 mb-4">Quick Actions</h3>
<h3 className="font-semibold text-gray-900 mb-4">{t('autofix.k52af8b8d')}</h3>
<div className="space-y-3">
<button className="w-full flex items-center justify-center px-4 py-3 bg-[#8D6B1D] text-white rounded-lg hover:bg-[#7A5E1A] transition-colors">
<PlusIcon className="h-4 w-4 mr-2" />
Create Group
</button>
<PlusIcon className="h-4 w-4 mr-2" />{t('autofix.k6a486e3e')}</button>
<button className="w-full flex items-center justify-center px-4 py-3 border border-[#8D6B1D] text-[#8D6B1D] rounded-lg hover:bg-[#8D6B1D]/10 transition-colors">
<ChatBubbleLeftRightIcon className="h-4 w-4 mr-2" />
Start Discussion
</button>
<ChatBubbleLeftRightIcon className="h-4 w-4 mr-2" />{t('autofix.k9c3db145')}</button>
<button
onClick={() => router.push('/dashboard')}
className="w-full flex items-center justify-center px-4 py-3 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors"
>
Go to Dashboard
</button>
>{t('autofix.kd00443f2')}</button>
</div>
</div>
{/* My Groups */}
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<h3 className="font-semibold text-gray-900 mb-4">My Groups</h3>
<h3 className="font-semibold text-gray-900 mb-4">{t('autofix.k26ecadfd')}</h3>
<div className="space-y-3">
<div className="flex items-center space-x-3 p-2 rounded-lg hover:bg-gray-50 cursor-pointer">
<div className="text-lg">🌱</div>
<div>
<p className="text-sm font-medium text-gray-900">Eco Warriors</p>
<p className="text-xs text-gray-500">1,284 members</p>
<p className="text-sm font-medium text-gray-900">{t('autofix.k58424b1d')}</p>
<p className="text-xs text-gray-500">{t('autofix.kaf787fe5')}</p>
</div>
</div>
<div className="flex items-center space-x-3 p-2 rounded-lg hover:bg-gray-50 cursor-pointer">
<div className="text-lg"></div>
<div>
<p className="text-sm font-medium text-gray-900">Zero Waste Living</p>
<p className="text-xs text-gray-500">892 members</p>
<p className="text-sm font-medium text-gray-900">{t('autofix.k6de13000')}</p>
<p className="text-xs text-gray-500">{t('autofix.k258c3515')}</p>
</div>
</div>
</div>
@ -262,16 +246,14 @@ export default function CommunityPage() {
{/* Community Guidelines */}
<div className="bg-gradient-to-br from-[#8D6B1D]/10 to-[#C49225]/10 rounded-lg p-6 border border-[#8D6B1D]/20">
<h3 className="font-semibold text-gray-900 mb-2">Community Guidelines</h3>
<h3 className="font-semibold text-gray-900 mb-2">{t('autofix.k961ba411')}</h3>
<ul className="text-sm text-gray-700 space-y-1">
<li> Be respectful and kind</li>
<li> Stay on topic</li>
<li> Share authentic experiences</li>
<li> Help others learn and grow</li>
<li>{t('autofix.kccf7593a')}</li>
<li>{t('autofix.kf69154f8')}</li>
<li>{t('autofix.k483aa95a')}</li>
<li>{t('autofix.k75d83433')}</li>
</ul>
<button className="text-xs text-[#8D6B1D] hover:underline mt-3">
Read full guidelines
</button>
<button className="text-xs text-[#8D6B1D] hover:underline mt-3">{t('autofix.k6aa2d843')}</button>
</div>
</div>
</div>

View File

@ -4,28 +4,25 @@ import { Menu, MenuButton, MenuItem, MenuItems } from '@headlessui/react';
import { ChevronDownIcon } from '@heroicons/react/20/solid';
import { useTranslation } from '../i18n/useTranslation';
// Built-in language info (code → name + flag emoji)
const BUILTIN_LANG_INFO: Record<string, { name: string; flag: string }> = {
interface LangEntry { code: string; name: string; flag: string }
const FALLBACK_LANG_INFO: Record<string, { name: string; flag: string }> = {
en: { name: 'English', flag: '🇬🇧' },
de: { name: 'Deutsch', flag: '🇩🇪' },
};
interface LangEntry { code: string; name: string; flag: string }
interface LanguageSwitcherProps {
variant?: 'light' | 'dark';
}
export default function LanguageSwitcher({ variant = 'light' }: LanguageSwitcherProps) {
const { language, setLanguage, customI18n } = useTranslation();
const { language, setLanguage, languages } = useTranslation();
// Combine built-in + custom languages (deduplicated by code)
const allLangs: LangEntry[] = [
...Object.entries(BUILTIN_LANG_INFO).map(([code, info]) => ({ code, ...info })),
...customI18n.languages
.filter((l) => !BUILTIN_LANG_INFO[l.code])
.map((l) => ({ code: l.code, name: l.name, flag: l.flag ?? '🏳️' })),
];
const allLangs: LangEntry[] = languages.map((lang) => ({
code: lang.code,
name: lang.name,
flag: FALLBACK_LANG_INFO[lang.code]?.flag ?? '🏳️',
}));
const activeLang: LangEntry =
allLangs.find((l) => l.code === language) ?? { code: language, name: language, flag: '🏳️' };

View File

@ -6,6 +6,8 @@ import Header from './nav/Header';
import Footer from './Footer';
import PageTransitionEffect from './animation/pageTransitionEffect';
import { useTranslation } from '../i18n/useTranslation';
// Utility to detect mobile devices
function isMobileDevice() {
if (typeof navigator === 'undefined') return false;
@ -27,6 +29,7 @@ export default function PageLayout({
className = 'bg-white text-gray-900',
contentClassName = 'flex-1 relative z-10 w-full',
}: PageLayoutProps) {
const { t } = useTranslation();
const isMobile = isMobileDevice();
const [isLoggingOut, setIsLoggingOut] = useState(false); // NEW
const pathname = usePathname();
@ -72,7 +75,7 @@ export default function PageLayout({
<div className="fixed inset-0 z-[80] flex items-center justify-center bg-black/60 backdrop-blur-sm">
<div className="flex flex-col items-center gap-3 text-white">
<div className="h-10 w-10 rounded-full border-2 border-white/30 border-t-white animate-spin" />
<p className="text-sm font-medium">Logging you out...</p>
<p className="text-sm font-medium">{t('autofix.kb1c1c0e5')}</p>
</div>
</div>
)}

View File

@ -1,5 +1,8 @@
'use client'
import { useTranslation } from '../i18n/useTranslation';
import { Fragment } from 'react'
import { Dialog, Transition } from '@headlessui/react'
import {
@ -44,6 +47,7 @@ export default function TutorialModal({
onNext,
onPrevious
}: TutorialModalProps) {
const { t } = useTranslation();
const step = steps[currentStep - 1]
if (!step) return null
@ -194,17 +198,13 @@ export default function TutorialModal({
? 'text-slate-50 cursor-default'
: 'text-gray-500 hover:text-gray-700'
}`}
>
Go back
</button>
>{t('autofix.kccc13f16')}</button>
{!isLastStep && (
<button
type="button"
onClick={onNext}
className="text-xs font-semibold text-blue-600 hover:text-blue-700 transition-colors"
>
Continue
</button>
>{t('autofix.ka3cbb536')}</button>
)}
</div>
)}
@ -214,7 +214,7 @@ export default function TutorialModal({
<div className="relative hidden lg:flex lg:flex-1 mt-4 lg:mt-0 h-32 lg:h-full lg:min-h-[150px] items-end justify-end">
{/* <img
src="/images/misc/cow.png"
alt="Profit Planet Mascot"
alt={t('autofix.kcc1c5596')}
className="max-h-full max-w-full object-contain opacity-90 pl-30"
/> */}
</div>

View File

@ -467,21 +467,15 @@ export default function UserDetailModal({ isOpen, onClose, userId, onUserUpdated
</h3>
{missingIdOrContract && (
<div className="rounded-md border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-800 mb-4">
ID documents or a signed contract are missing for this user. The users verification status should be checked.
</div>
<div className="rounded-md border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-800 mb-4">{t('autofix.k6b0f4f70')}</div>
)}
{storageMissing && (
<div className="rounded-md border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-800 mb-4">
ID documents or a signed contract are missing from object storage. The users verification status should be checked.
</div>
<div className="rounded-md border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-800 mb-4">{t('autofix.kd4d50566')}</div>
)}
{missingIdOrContract && (
<div className="rounded-md border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-800 mb-4">
ID documents or a signed contract are missing for this user. The users verification status should be checked.
</div>
<div className="rounded-md border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-800 mb-4">{t('autofix.k6b0f4f70')}</div>
)}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">

View File

@ -1,5 +1,8 @@
'use client'
import { useTranslation } from '../i18n/useTranslation';
import { Fragment, useState, useEffect } from 'react'
import { Dialog, Transition } from '@headlessui/react'
import {
@ -30,6 +33,7 @@ interface UserDetailModalProps {
}
export default function UserDetailModal({ isOpen, onClose, userId, onUserUpdated }: UserDetailModalProps) {
const { t } = useTranslation();
const [userDetails, setUserDetails] = useState<DetailedUserInfo | null>(null)
const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
@ -259,16 +263,14 @@ export default function UserDetailModal({ isOpen, onClose, userId, onUserUpdated
User Details
{isEditing && (
<span className="inline-flex items-center gap-1 rounded-full bg-blue-100 px-2 py-1 text-xs font-medium text-blue-700">
<PencilSquareIcon className="h-3 w-3" />
Edit Mode
</span>
<PencilSquareIcon className="h-3 w-3" />{t('autofix.k73d110fa')}</span>
)}
</Dialog.Title>
{loading && (
<div className="flex items-center justify-center py-12">
<div className="h-8 w-8 rounded-full border-2 border-blue-500 border-b-transparent animate-spin" />
<span className="ml-3 text-gray-600">Loading user details...</span>
<span className="ml-3 text-gray-600">{t('autofix.k4a9e1ebe')}</span>
</div>
)}
@ -284,28 +286,28 @@ export default function UserDetailModal({ isOpen, onClose, userId, onUserUpdated
<div className="bg-gray-50 rounded-lg p-4">
<div className="flex items-center gap-3 mb-4">
<UserIcon className="h-5 w-5 text-gray-600" />
<h4 className="text-sm font-medium text-gray-900">Basic Information</h4>
<h4 className="text-sm font-medium text-gray-900">{t('autofix.k1d178b73')}</h4>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 text-sm">
<div>
<span className="font-medium text-gray-700">Email:</span>
<span className="font-medium text-gray-700">{t('autofix.k8bb1c673')}</span>
<span className="ml-2 text-gray-600">{userDetails.user.email}</span>
</div>
<div>
<span className="font-medium text-gray-700">Type:</span>
<span className="font-medium text-gray-700">{t('autofix.k0d8cb427')}</span>
<span className="ml-2 text-gray-600 capitalize">{userDetails.user.user_type}</span>
</div>
<div>
<span className="font-medium text-gray-700">Role:</span>
<span className="font-medium text-gray-700">{t('autofix.k0dba4c6b')}</span>
<span className="ml-2 text-gray-600 capitalize">{userDetails.user.role}</span>
</div>
<div>
<span className="font-medium text-gray-700">Created:</span>
<span className="font-medium text-gray-700">{t('autofix.kf971ea7f')}</span>
<span className="ml-2 text-gray-600">{formatDate(userDetails.user.created_at)}</span>
</div>
{userDetails.user.last_login_at && (
<div>
<span className="font-medium text-gray-700">Last Login:</span>
<span className="font-medium text-gray-700">{t('autofix.kfb37e056')}</span>
<span className="ml-2 text-gray-600">{formatDate(userDetails.user.last_login_at)}</span>
</div>
)}
@ -317,7 +319,7 @@ export default function UserDetailModal({ isOpen, onClose, userId, onUserUpdated
<div className="bg-blue-50 rounded-lg p-4">
<div className="flex items-center gap-3 mb-4">
<ShieldCheckIcon className="h-5 w-5 text-blue-600" />
<h4 className="text-sm font-medium text-gray-900">Verification Status</h4>
<h4 className="text-sm font-medium text-gray-900">{t('autofix.k4e532c48')}</h4>
</div>
<div className="grid grid-cols-2 sm:grid-cols-3 gap-4">
<div className="flex items-center justify-between">
@ -337,7 +339,7 @@ export default function UserDetailModal({ isOpen, onClose, userId, onUserUpdated
<StatusBadge status={userDetails.userStatus.contract_signed === 1} />
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-gray-700">Admin Verified</span>
<span className="text-sm text-gray-700">{t('autofix.k022df6ac')}</span>
<StatusBadge
status={userDetails.userStatus.is_admin_verified === 1}
verified={userDetails.userStatus.is_admin_verified === 1}
@ -356,7 +358,7 @@ export default function UserDetailModal({ isOpen, onClose, userId, onUserUpdated
) : (
<BuildingOfficeIcon className="h-5 w-5 text-green-600" />
)}
<h4 className="text-sm font-medium text-gray-900">Profile Information</h4>
<h4 className="text-sm font-medium text-gray-900">{t('autofix.k228929e2')}</h4>
</div>
{isEditing && editedProfile ? (
@ -365,7 +367,7 @@ export default function UserDetailModal({ isOpen, onClose, userId, onUserUpdated
{userDetails.personalProfile && (
<>
<div>
<label className="block font-medium text-gray-700 mb-1">First Name</label>
<label className="block font-medium text-gray-700 mb-1">{t('autofix.kfe8083f8')}</label>
<input
type="text"
value={editedProfile.first_name || ''}
@ -374,7 +376,7 @@ export default function UserDetailModal({ isOpen, onClose, userId, onUserUpdated
/>
</div>
<div>
<label className="block font-medium text-gray-700 mb-1">Last Name</label>
<label className="block font-medium text-gray-700 mb-1">{t('autofix.k6a4108c8')}</label>
<input
type="text"
value={editedProfile.last_name || ''}
@ -392,7 +394,7 @@ export default function UserDetailModal({ isOpen, onClose, userId, onUserUpdated
/>
</div>
<div>
<label className="block font-medium text-gray-700 mb-1">Date of Birth</label>
<label className="block font-medium text-gray-700 mb-1">{t('autofix.kbc368b5d')}</label>
<input
type="date"
value={editedProfile.date_of_birth || ''}
@ -419,7 +421,7 @@ export default function UserDetailModal({ isOpen, onClose, userId, onUserUpdated
/>
</div>
<div>
<label className="block font-medium text-gray-700 mb-1">Postal Code</label>
<label className="block font-medium text-gray-700 mb-1">{t('autofix.kc9d9d15d')}</label>
<input
type="text"
value={editedProfile.zip_code || ''}
@ -442,7 +444,7 @@ export default function UserDetailModal({ isOpen, onClose, userId, onUserUpdated
{userDetails.companyProfile && (
<>
<div>
<label className="block font-medium text-gray-700 mb-1">Company Name</label>
<label className="block font-medium text-gray-700 mb-1">{t('autofix.k33918465')}</label>
<input
type="text"
value={editedProfile.company_name || ''}
@ -451,7 +453,7 @@ export default function UserDetailModal({ isOpen, onClose, userId, onUserUpdated
/>
</div>
<div>
<label className="block font-medium text-gray-700 mb-1">Tax ID</label>
<label className="block font-medium text-gray-700 mb-1">{t('autofix.kbfa5b4c5')}</label>
<input
type="text"
value={editedProfile.tax_id || ''}
@ -460,7 +462,7 @@ export default function UserDetailModal({ isOpen, onClose, userId, onUserUpdated
/>
</div>
<div>
<label className="block font-medium text-gray-700 mb-1">Registration Number</label>
<label className="block font-medium text-gray-700 mb-1">{t('autofix.kc0d718d7')}</label>
<input
type="text"
value={editedProfile.registration_number || ''}
@ -496,7 +498,7 @@ export default function UserDetailModal({ isOpen, onClose, userId, onUserUpdated
/>
</div>
<div>
<label className="block font-medium text-gray-700 mb-1">Postal Code</label>
<label className="block font-medium text-gray-700 mb-1">{t('autofix.kc9d9d15d')}</label>
<input
type="text"
value={editedProfile.zip_code || ''}
@ -522,26 +524,26 @@ export default function UserDetailModal({ isOpen, onClose, userId, onUserUpdated
{userDetails.personalProfile && (
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 text-sm">
<div>
<span className="font-medium text-gray-700">Name:</span>
<span className="font-medium text-gray-700">{t('autofix.k0cdde8f8')}</span>
<span className="ml-2 text-gray-600">
{userDetails.personalProfile.first_name} {userDetails.personalProfile.last_name}
</span>
</div>
{userDetails.personalProfile.phone && (
<div>
<span className="font-medium text-gray-700">Phone:</span>
<span className="font-medium text-gray-700">{t('autofix.kca04f5e3')}</span>
<span className="ml-2 text-gray-600">{userDetails.personalProfile.phone}</span>
</div>
)}
{userDetails.personalProfile.date_of_birth && (
<div>
<span className="font-medium text-gray-700">Date of Birth:</span>
<span className="font-medium text-gray-700">{t('autofix.k4307f6c7')}</span>
<span className="ml-2 text-gray-600">{formatDate(userDetails.personalProfile.date_of_birth)}</span>
</div>
)}
{userDetails.personalProfile.address && (
<div className="sm:col-span-2">
<span className="font-medium text-gray-700">Address:</span>
<span className="font-medium text-gray-700">{t('autofix.k71d565c9')}</span>
<span className="ml-2 text-gray-600">
{userDetails.personalProfile.address}, {userDetails.personalProfile.zip_code} {userDetails.personalProfile.city}, {userDetails.personalProfile.country}
</span>
@ -553,30 +555,30 @@ export default function UserDetailModal({ isOpen, onClose, userId, onUserUpdated
{userDetails.companyProfile && (
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 text-sm">
<div>
<span className="font-medium text-gray-700">Company Name:</span>
<span className="font-medium text-gray-700">{t('autofix.ka5c2113f')}</span>
<span className="ml-2 text-gray-600">{userDetails.companyProfile.company_name}</span>
</div>
{userDetails.companyProfile.tax_id && (
<div>
<span className="font-medium text-gray-700">Tax ID:</span>
<span className="font-medium text-gray-700">{t('autofix.kb45c4d5f')}</span>
<span className="ml-2 text-gray-600">{userDetails.companyProfile.tax_id}</span>
</div>
)}
{userDetails.companyProfile.registration_number && (
<div>
<span className="font-medium text-gray-700">Registration Number:</span>
<span className="font-medium text-gray-700">{t('autofix.kdbba338d')}</span>
<span className="ml-2 text-gray-600">{userDetails.companyProfile.registration_number}</span>
</div>
)}
{userDetails.companyProfile.phone && (
<div>
<span className="font-medium text-gray-700">Phone:</span>
<span className="font-medium text-gray-700">{t('autofix.kca04f5e3')}</span>
<span className="ml-2 text-gray-600">{userDetails.companyProfile.phone}</span>
</div>
)}
{userDetails.companyProfile.address && (
<div className="sm:col-span-2">
<span className="font-medium text-gray-700">Address:</span>
<span className="font-medium text-gray-700">{t('autofix.k71d565c9')}</span>
<span className="ml-2 text-gray-600">
{userDetails.companyProfile.address}, {userDetails.companyProfile.zip_code} {userDetails.companyProfile.city}, {userDetails.companyProfile.country}
</span>
@ -600,7 +602,7 @@ export default function UserDetailModal({ isOpen, onClose, userId, onUserUpdated
{/* Regular Documents */}
{userDetails.documents.length > 0 && (
<div className="mb-4">
<h5 className="text-xs font-medium text-gray-700 mb-2">Uploaded Documents</h5>
<h5 className="text-xs font-medium text-gray-700 mb-2">{t('autofix.k93b6dc1b')}</h5>
<div className="space-y-2">
{userDetails.documents.map((doc) => (
<div key={doc.id} className="flex items-center justify-between bg-white p-2 rounded border">
@ -636,7 +638,7 @@ export default function UserDetailModal({ isOpen, onClose, userId, onUserUpdated
{/* ID Documents */}
{userDetails.idDocuments.length > 0 && (
<div>
<h5 className="text-xs font-medium text-gray-700 mb-2">ID Documents</h5>
<h5 className="text-xs font-medium text-gray-700 mb-2">{t('autofix.k63115bb4')}</h5>
<div className="space-y-4">
{userDetails.idDocuments.map((idDoc) => (
<div key={idDoc.id} className="bg-white p-3 rounded border">
@ -648,20 +650,20 @@ export default function UserDetailModal({ isOpen, onClose, userId, onUserUpdated
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
{idDoc.frontUrl && (
<div>
<p className="text-xs text-gray-700 mb-1">Front:</p>
<p className="text-xs text-gray-700 mb-1">{t('autofix.k972cee5e')}</p>
<img
src={idDoc.frontUrl}
alt="ID Front"
alt={t('autofix.k4a055849')}
className="max-w-full h-32 object-contain border rounded"
/>
</div>
)}
{idDoc.backUrl && (
<div>
<p className="text-xs text-gray-700 mb-1">Back:</p>
<p className="text-xs text-gray-700 mb-1">{t('autofix.k0c95a1b4')}</p>
<img
src={idDoc.backUrl}
alt="ID Back"
alt={t('autofix.kbc6a6543')}
className="max-w-full h-32 object-contain border rounded"
/>
</div>
@ -765,9 +767,7 @@ export default function UserDetailModal({ isOpen, onClose, userId, onUserUpdated
</>
) : (
<>
<CheckCircleIcon className="h-4 w-4" />
Save Changes
</>
<CheckCircleIcon className="h-4 w-4" />{t('autofix.k5a489751')}</>
)}
</button>
<button
@ -796,9 +796,7 @@ export default function UserDetailModal({ isOpen, onClose, userId, onUserUpdated
disabled={saving}
className="inline-flex items-center justify-center gap-2 rounded-md bg-blue-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-blue-500 focus-visible:outline focus-visible:outline-offset-2 focus-visible:outline-blue-600 disabled:opacity-50 disabled:cursor-not-allowed"
>
<PencilSquareIcon className="h-4 w-4" />
Edit Profile
</button>
<PencilSquareIcon className="h-4 w-4" />{t('autofix.k70972912')}</button>
{userDetails?.userStatus && (
<button

View File

@ -1,5 +1,8 @@
'use client';
import { useTranslation } from '../../i18n/useTranslation';
import { motion, AnimatePresence } from 'framer-motion';
import { usePathname } from 'next/navigation';
import Image from 'next/image';
@ -7,6 +10,7 @@ import React, { useEffect, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
const PageTransitionEffect = ({ children }: { children: React.ReactNode }) => {
const { t } = useTranslation();
const pathname = usePathname();
const DELAY_MS = 200;
const EXIT_DURATION = 0.7; // slow the fade/slide-out a bit
@ -82,7 +86,7 @@ const PageTransitionEffect = ({ children }: { children: React.ReactNode }) => {
<div className="flex flex-col items-center">
<Image
src="/images/logos/pp_logo_gold_transparent.png"
alt="Profit Planet"
alt={t('autofix.k788633d1')}
width={160}
height={160}
className="w-32 h-32 object-contain"

View File

@ -1,5 +1,8 @@
'use client'
import { useTranslation } from '../../i18n/useTranslation';
import { useState, useEffect } from 'react'
import {
EnvelopeIcon,
@ -26,6 +29,7 @@ interface QuickAction {
// UserStatus interface is now imported from useUserStatus hook
export default function QuickActions() {
const { t } = useTranslation();
const { userStatus, loading, error, refreshStatus } = useUserStatus()
const [showEmailVerification, setShowEmailVerification] = useState(false)
const [showDocumentUpload, setShowDocumentUpload] = useState(false)
@ -54,7 +58,7 @@ export default function QuickActions() {
return (
<div className="mb-8">
<div className="flex items-center justify-between mb-4">
<h2 className="text-xl font-semibold text-gray-900">Account Setup</h2>
<h2 className="text-xl font-semibold text-gray-900">{t('autofix.kbfd13a03')}</h2>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
{[1, 2, 3, 4].map((i) => (
@ -178,12 +182,12 @@ export default function QuickActions() {
if (error && !userStatus) {
return (
<div className="mb-8">
<h2 className="text-xl font-semibold text-gray-900 mb-4">Account Setup</h2>
<h2 className="text-xl font-semibold text-gray-900 mb-4">{t('autofix.kbfd13a03')}</h2>
<div className="bg-red-50 border border-red-200 rounded-lg p-4">
<div className="flex">
<ExclamationTriangleIcon className="h-5 w-5 text-red-400" />
<div className="ml-3">
<h3 className="text-sm font-medium text-red-800">Error loading account status</h3>
<h3 className="text-sm font-medium text-red-800">{t('autofix.k3b03502e')}</h3>
<div className="mt-2 text-sm text-red-700">
<p>{error}</p>
</div>
@ -192,9 +196,7 @@ export default function QuickActions() {
type="button"
onClick={refreshStatus}
className="bg-red-100 px-2 py-1 text-xs font-semibold text-red-800 hover:bg-red-200 rounded"
>
Try again
</button>
>{t('autofix.k3b7dd87a')}</button>
</div>
</div>
</div>
@ -207,12 +209,10 @@ export default function QuickActions() {
<>
<div className="mb-8">
<div className="flex items-center justify-between mb-4">
<h2 className="text-xl font-semibold text-gray-900">Account Setup</h2>
<h2 className="text-xl font-semibold text-gray-900">{t('autofix.kbfd13a03')}</h2>
{loading && (
<div className="flex items-center text-sm text-gray-500">
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-[#8D6B1D] mr-2"></div>
Updating status...
</div>
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-[#8D6B1D] mr-2"></div>{t('autofix.kf2d8db2b')}</div>
)}
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
@ -402,7 +402,7 @@ function EmailVerificationModal({ onClose, onSuccess }: { onClose: () => void, o
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white rounded-lg p-6 w-full max-w-md mx-4">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-gray-900">Email Verification</h3>
<h3 className="text-lg font-semibold text-gray-900">{t('autofix.k3c3e6850')}</h3>
<button onClick={onClose} className="text-gray-400 hover:text-gray-600">
<XCircleIcon className="h-6 w-6" />
</button>
@ -410,9 +410,7 @@ function EmailVerificationModal({ onClose, onSuccess }: { onClose: () => void, o
<div className="space-y-4">
<div>
<p className="text-sm text-gray-600 mb-4">
We'll send a verification code to your email address.
</p>
<p className="text-sm text-gray-600 mb-4">{t('autofix.k20ab2fc7')}</p>
<button
onClick={sendVerificationEmail}
@ -424,16 +422,14 @@ function EmailVerificationModal({ onClose, onSuccess }: { onClose: () => void, o
</div>
<div>
<label htmlFor="code" className="block text-sm font-medium text-gray-700 mb-1">
Verification Code
</label>
<label htmlFor="code" className="block text-sm font-medium text-gray-700 mb-1">{t('autofix.kace2fe51')}</label>
<input
type="text"
id="code"
value={verificationCode}
onChange={(e) => setVerificationCode(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-[#8D6B1D] focus:border-transparent"
placeholder="Enter 6-digit code"
placeholder={t('autofix.k8eb2524c')}
maxLength={6}
/>
</div>
@ -568,19 +564,19 @@ function DocumentUploadModal({ userType, onClose, onSuccess }: {
onChange={(e) => setIdType(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-[#8D6B1D] focus:border-transparent"
>
<option value="">Select Document Type</option>
<option value="">{t('autofix.kb846955a')}</option>
{userType === 'company' ? (
<>
<option value="business_registration">Business Registration</option>
<option value="tax_certificate">Tax Certificate</option>
<option value="business_license">Business License</option>
<option value="business_registration">{t('autofix.ke17859b2')}</option>
<option value="tax_certificate">{t('autofix.k97abed7d')}</option>
<option value="business_license">{t('autofix.kbe9355f8')}</option>
<option value="other">Other</option>
</>
) : (
<>
<option value="passport">Passport</option>
<option value="driver_license">Driver's License</option>
<option value="national_id">National ID</option>
<option value="driver_license">{t('autofix.k5d85b354')}</option>
<option value="national_id">{t('autofix.k8eab7c16')}</option>
<option value="other">Other</option>
</>
)}
@ -601,8 +597,7 @@ function DocumentUploadModal({ userType, onClose, onSuccess }: {
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Expiry Date <span className="text-red-500">*</span>
<label className="block text-sm font-medium text-gray-700 mb-2">{t('autofix.k1ddc749e')}<span className="text-red-500">*</span>
</label>
<input
type="date"
@ -685,7 +680,7 @@ function ProfileCompletionModal({ userType, onClose, onSuccess }: {
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white rounded-lg p-6 w-full max-w-md mx-4">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-gray-900">Complete Profile</h3>
<h3 className="text-lg font-semibold text-gray-900">{t('autofix.k91f24187')}</h3>
<button onClick={onClose} className="text-gray-400 hover:text-gray-600">
<XCircleIcon className="h-6 w-6" />
</button>
@ -695,7 +690,7 @@ function ProfileCompletionModal({ userType, onClose, onSuccess }: {
{userType === 'company' ? (
<>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Company Name</label>
<label className="block text-sm font-medium text-gray-700 mb-1">{t('autofix.k33918465')}</label>
<input
type="text"
value={formData.companyName || ''}
@ -716,7 +711,7 @@ function ProfileCompletionModal({ userType, onClose, onSuccess }: {
) : (
<>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Phone Number</label>
<label className="block text-sm font-medium text-gray-700 mb-1">{t('autofix.k2a2fe15a')}</label>
<input
type="tel"
value={formData.phone || ''}
@ -812,7 +807,7 @@ function ContractSigningModal({ userType, onClose, onSuccess }: {
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white rounded-lg p-6 w-full max-w-md mx-4">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-gray-900">Sign Contract</h3>
<h3 className="text-lg font-semibold text-gray-900">{t('autofix.kbd8b3364')}</h3>
<button onClick={onClose} className="text-gray-400 hover:text-gray-600">
<XCircleIcon className="h-6 w-6" />
</button>
@ -820,13 +815,9 @@ function ContractSigningModal({ userType, onClose, onSuccess }: {
<div className="space-y-4">
<div>
<p className="text-sm text-gray-600 mb-4">
Please review and upload your signed service agreement.
</p>
<p className="text-sm text-gray-600 mb-4">{t('autofix.k9860434f')}</p>
<label className="block text-sm font-medium text-gray-700 mb-2">
Signed Contract Document
</label>
<label className="block text-sm font-medium text-gray-700 mb-2">{t('autofix.k93f03bca')}</label>
<input
type="file"
accept="image/*,.pdf"
@ -843,9 +834,7 @@ function ContractSigningModal({ userType, onClose, onSuccess }: {
onChange={(e) => setAgreed(e.target.checked)}
className="mt-1 h-4 w-4 text-[#8D6B1D] focus:ring-[#8D6B1D] border-gray-300 rounded"
/>
<label htmlFor="agreement" className="ml-2 text-sm text-gray-600">
I have read, understood, and agree to the terms and conditions of this service agreement.
</label>
<label htmlFor="agreement" className="ml-2 text-sm text-gray-600">{t('autofix.k98519a5e')}</label>
</div>
<button

View File

@ -1,3 +1,6 @@
'use client';
import { useTranslation } from '../../i18n/useTranslation';
import React from "react";
import ConfirmActionModal from "../modals/ConfirmActionModal";
@ -15,7 +18,7 @@ type DeleteConfirmationModalProps = {
export default function DeleteConfirmationModal({
open,
title = "Delete Item",
title,
description = "Are you sure you want to delete this item? This action cannot be undone.",
confirmText = "Delete",
cancelText = "Cancel",
@ -24,12 +27,15 @@ export default function DeleteConfirmationModal({
onCancel,
children,
}: DeleteConfirmationModalProps) {
const { t } = useTranslation();
const resolvedTitle = title ?? t('autofix.k74914369');
return (
<ConfirmActionModal
open={open}
pending={loading}
intent="danger"
title={title}
title={resolvedTitle}
description={description}
confirmText={confirmText}
cancelText={cancelText}

View File

@ -450,7 +450,7 @@ export default function Header({ setGlobalLoggingOut }: HeaderProps) {
<span className="sr-only">ProfitPlanet</span>
<Image
src="/images/logos/pp_logo_gold_transparent.png"
alt="ProfitPlanet Logo"
alt={t('autofix.k91eb415a')}
width={280}
height={84}
className="h-14 w-auto flex-shrink-0 sm:h-16 lg:h-[4.5rem]"
@ -513,9 +513,7 @@ export default function Header({ setGlobalLoggingOut }: HeaderProps) {
<button
onClick={() => router.push('/personal-matrix')}
className="text-sm/6 font-semibold text-gray-900 dark:text-white hover:text-indigo-600 dark:hover:text-indigo-400 transition-colors"
>
Personal Matrix
</button>
>{t('autofix.k73831c06')}</button>
)}
</>
)}
@ -524,9 +522,7 @@ export default function Header({ setGlobalLoggingOut }: HeaderProps) {
<button
onClick={() => router.push('/coffee-abonnements')}
className="text-sm/6 font-semibold text-gray-900 dark:text-white hover:text-indigo-600 dark:hover:text-indigo-400 transition-colors"
>
Coffee Abonnements
</button>
>{t('autofix.k4e168c01')}</button>
)}
{/* Information dropdown already removed here */}
@ -612,21 +608,19 @@ export default function Header({ setGlobalLoggingOut }: HeaderProps) {
<span className="sr-only">ProfitPlanet</span>
<Image
src="/images/logos/pp_logo_gold_transparent.png"
alt="ProfitPlanet Logo"
alt={t('autofix.k91eb415a')}
width={190}
height={60}
className="h-12 w-auto flex-shrink-0"
/>
<span className="text-xl font-bold tracking-tight text-[#D4AF37]">
Profit Planet
</span>
<span className="text-xl font-bold tracking-tight text-[#D4AF37]">{t('autofix.k788633d1')}</span>
</button>
<button
type="button"
onClick={() => setMobileMenuOpen(false)}
className="rounded-md p-2.5 text-gray-400 hover:text-gray-100 hover:bg-white/10 transition-transform duration-300 hover:scale-110"
>
<span className="sr-only">Close menu</span>
<span className="sr-only">{t('autofix.kd4eb7ee0')}</span>
<XMarkIcon aria-hidden="true" className="size-6" />
</button>
</div>

View File

@ -1,3 +1,8 @@
'use client';
import { useTranslation } from '../i18n/useTranslation';
import clsx from 'clsx'
import type React from 'react'
import { Button } from './button'
@ -7,6 +12,7 @@ export function Pagination({
className,
...props
}: React.ComponentPropsWithoutRef<'nav'>) {
const { t } = useTranslation();
return <nav aria-label={ariaLabel} {...props} className={clsx(className, 'flex gap-x-2')} />
}
@ -17,7 +23,7 @@ export function PaginationPrevious({
}: React.PropsWithChildren<{ href?: string | null; className?: string }>) {
return (
<span className={clsx(className, 'grow basis-0')}>
<Button {...(href === null ? { disabled: true } : { href })} plain aria-label="Previous page">
<Button {...(href === null ? { disabled: true } : { href })} plain aria-label={t('autofix.k5b7042c7')}>
<svg className="stroke-current" data-slot="icon" viewBox="0 0 16 16" fill="none" aria-hidden="true">
<path
d="M2.75 8H13.25M2.75 8L5.25 5.5M2.75 8L5.25 10.5"
@ -39,7 +45,7 @@ export function PaginationNext({
}: React.PropsWithChildren<{ href?: string | null; className?: string }>) {
return (
<span className={clsx(className, 'flex grow basis-0 justify-end')}>
<Button {...(href === null ? { disabled: true } : { href })} plain aria-label="Next page">
<Button {...(href === null ? { disabled: true } : { href })} plain aria-label={t('autofix.k17581b31')}>
{children}
<svg className="stroke-current" data-slot="icon" viewBox="0 0 16 16" fill="none" aria-hidden="true">
<path

View File

@ -1,10 +1,14 @@
'use client'
import { useTranslation } from '../i18n/useTranslation';
import * as Headless from '@headlessui/react'
import React, { useState } from 'react'
import { NavbarItem } from './navbar'
function OpenMenuIcon() {
const { t } = useTranslation();
return (
<svg data-slot="icon" viewBox="0 0 20 20" aria-hidden="true">
<path d="M2 6.75C2 6.33579 2.33579 6 2.75 6H17.25C17.6642 6 18 6.33579 18 6.75C18 7.16421 17.6642 7.5 17.25 7.5H2.75C2.33579 7.5 2 7.16421 2 6.75ZM2 13.25C2 12.8358 2.33579 12.5 2.75 12.5H17.25C17.6642 12.5 18 12.8358 18 13.25C18 13.6642 17.6642 14 17.25 14H2.75C2.33579 14 2 13.6642 2 13.25Z" />
@ -33,7 +37,7 @@ function MobileSidebar({ open, close, children }: React.PropsWithChildren<{ open
>
<div className="flex h-full flex-col rounded-lg bg-zinc-900 shadow-xs ring-1 ring-white/10">
<div className="-mb-3 px-4 pt-3">
<Headless.CloseButton as={NavbarItem} aria-label="Close navigation">
<Headless.CloseButton as={NavbarItem} aria-label={t('autofix.k91912619')}>
<CloseMenuIcon />
</Headless.CloseButton>
</div>
@ -64,7 +68,7 @@ export function SidebarLayout({
{/* Navbar on mobile */}
<header className="flex items-center px-4 lg:hidden">
<div className="py-2.5">
<NavbarItem onClick={() => setShowSidebar(true)} aria-label="Open navigation">
<NavbarItem onClick={() => setShowSidebar(true)} aria-label={t('autofix.k6af9037b')}>
<OpenMenuIcon />
</NavbarItem>
</div>

View File

@ -1,10 +1,14 @@
'use client'
import { useTranslation } from '../i18n/useTranslation';
import * as Headless from '@headlessui/react'
import React, { useState } from 'react'
import { NavbarItem } from './navbar'
function OpenMenuIcon() {
const { t } = useTranslation();
return (
<svg data-slot="icon" viewBox="0 0 20 20" aria-hidden="true">
<path d="M2 6.75C2 6.33579 2.33579 6 2.75 6H17.25C17.6642 6 18 6.33579 18 6.75C18 7.16421 17.6642 7.5 17.25 7.5H2.75C2.33579 7.5 2 7.16421 2 6.75ZM2 13.25C2 12.8358 2.33579 12.5 2.75 12.5H17.25C17.6642 12.5 18 12.8358 18 13.25C18 13.6642 17.6642 14 17.25 14H2.75C2.33579 14 2 13.6642 2 13.25Z" />
@ -33,7 +37,7 @@ function MobileSidebar({ open, close, children }: React.PropsWithChildren<{ open
>
<div className="flex h-full flex-col rounded-lg bg-zinc-900 shadow-xs ring-1 ring-white/10">
<div className="-mb-3 px-4 pt-3">
<Headless.CloseButton as={NavbarItem} aria-label="Close navigation">
<Headless.CloseButton as={NavbarItem} aria-label={t('autofix.k91912619')}>
<CloseMenuIcon />
</Headless.CloseButton>
</div>
@ -61,7 +65,7 @@ export function StackedLayout({
{/* Navbar */}
<header className="flex items-center px-4">
<div className="py-2.5 lg:hidden">
<NavbarItem onClick={() => setShowSidebar(true)} aria-label="Open navigation">
<NavbarItem onClick={() => setShowSidebar(true)} aria-label={t('autofix.k6af9037b')}>
<OpenMenuIcon />
</NavbarItem>
</div>

View File

@ -1,5 +1,8 @@
'use client'
import { useTranslation } from '../../i18n/useTranslation';
import React, {
createContext,
useCallback,
@ -45,6 +48,7 @@ let toastPortalRoot: ReturnType<typeof createRoot> | null = null
let toastPortalMounted = false
function notifyToastListeners() {
const { t } = useTranslation();
for (const listener of toastListeners) {
listener(globalToasts)
}
@ -271,7 +275,7 @@ function ToastItem({ toast, onClose }: ToastItemProps) {
type="button"
onClick={onClose}
className="ml-1 mt-0.5 inline-flex h-6 w-6 items-center justify-center rounded-full text-xs text-slate-300 hover:bg-slate-800/70 hover:text-white focus:outline-none focus:ring-2 focus:ring-slate-500"
aria-label="Close notification"
aria-label={t('autofix.k77767b9e')}
>
×
</button>

View File

@ -1,10 +1,14 @@
'use client'
import { useTranslation } from '../i18n/useTranslation';
import { useEffect, useState } from 'react'
import useAuthStore from '../store/authStore'
import { useUserStatus } from '../hooks/useUserStatus'
export default function DebugAuthPage() {
const { t } = useTranslation();
const [debugInfo, setDebugInfo] = useState<any>({})
const { accessToken, user, isAuthReady, refreshAuthToken, getAuthState } = useAuthStore()
const { userStatus, loading, error } = useUserStatus()
@ -58,12 +62,12 @@ export default function DebugAuthPage() {
return (
<div className="min-h-screen bg-gray-100 p-8">
<div className="max-w-4xl mx-auto">
<h1 className="text-3xl font-bold mb-8">Auth Debug Page</h1>
<h1 className="text-3xl font-bold mb-8">{t('autofix.kadc6abcf')}</h1>
<div className="grid gap-6 md:grid-cols-2">
{/* Auth Store State */}
<div className="bg-white rounded-lg p-6 shadow">
<h2 className="text-xl font-semibold mb-4">Auth Store State</h2>
<h2 className="text-xl font-semibold mb-4">{t('autofix.ka15f5ec5')}</h2>
<pre className="text-xs bg-gray-100 p-4 rounded overflow-auto">
{JSON.stringify(debugInfo, null, 2)}
</pre>
@ -71,25 +75,21 @@ export default function DebugAuthPage() {
<button
onClick={handleRefreshToken}
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
>
Refresh Token
</button>
>{t('autofix.k54c06343')}</button>
<button
onClick={handleTestApiCall}
className="px-4 py-2 bg-green-600 text-white rounded hover:bg-green-700"
>
Test API Call
</button>
>{t('autofix.k7f57b169')}</button>
</div>
</div>
{/* User Status */}
<div className="bg-white rounded-lg p-6 shadow">
<h2 className="text-xl font-semibold mb-4">User Status Hook</h2>
<h2 className="text-xl font-semibold mb-4">{t('autofix.k1eedcda3')}</h2>
<div className="space-y-2 text-sm">
<p><strong>Loading:</strong> {loading ? 'Yes' : 'No'}</p>
<p><strong>Error:</strong> {error || 'None'}</p>
<p><strong>Status:</strong></p>
<p><strong>{t('autofix.k8323a7d9')}</strong> {loading ? 'Yes' : 'No'}</p>
<p><strong>{t('autofix.k8be14d47')}</strong> {error || 'None'}</p>
<p><strong>{t('autofix.k81c0b74b')}</strong></p>
<pre className="text-xs bg-gray-100 p-4 rounded overflow-auto">
{JSON.stringify(userStatus, null, 2)}
</pre>
@ -100,9 +100,9 @@ export default function DebugAuthPage() {
<div className="bg-white rounded-lg p-6 shadow md:col-span-2">
<h2 className="text-xl font-semibold mb-4">Environment</h2>
<div className="text-sm space-y-1">
<p><strong>API Base URL:</strong> {process.env.NEXT_PUBLIC_API_BASE_URL}</p>
<p><strong>Node Env:</strong> {process.env.NODE_ENV}</p>
<p><strong>Current URL:</strong> {typeof window !== 'undefined' ? window.location.href : 'SSR'}</p>
<p><strong>{t('autofix.k811fbc99')}</strong> {process.env.NEXT_PUBLIC_API_BASE_URL}</p>
<p><strong>{t('autofix.kfce271a2')}</strong> {process.env.NODE_ENV}</p>
<p><strong>{t('autofix.k49f254bd')}</strong> {typeof window !== 'undefined' ? window.location.href : 'SSR'}</p>
</div>
</div>
</div>

View File

@ -32,52 +32,3 @@ export function unflattenObject(flat: Record<string, string>): Record<string, an
return result;
}
export interface CustomLanguageEntry {
code: string;
name: string;
flag?: string; // emoji flag, e.g. '🇫🇷'
}
export interface CustomI18nData {
/** Extra languages added by admins (does not include built-in en/de) */
languages: CustomLanguageEntry[];
/** Flat translation overrides per language code (includes overrides for built-in langs too) */
translations: Record<string, Record<string, string>>;
}
const STORAGE_KEY = 'pp_i18n_custom';
export function loadCustomI18n(): CustomI18nData {
if (typeof window === 'undefined') return { languages: [], translations: {} };
try {
const raw = localStorage.getItem(STORAGE_KEY);
if (!raw) return { languages: [], translations: {} };
const parsed = JSON.parse(raw);
return {
languages: Array.isArray(parsed.languages) ? parsed.languages : [],
translations:
parsed.translations && typeof parsed.translations === 'object'
? parsed.translations
: {},
};
} catch {
return { languages: [], translations: {} };
}
}
export function saveCustomI18n(data: CustomI18nData): void {
if (typeof window === 'undefined') return;
localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
}
/** Resolve a flat translation value for a given language, with fallback to English flat map */
export function resolveKey(
key: string,
langCode: string,
customTranslations: Record<string, Record<string, string>>,
enFlat: Record<string, string>
): string {
const langOverride = customTranslations[langCode]?.[key];
if (langOverride !== undefined && langOverride !== '') return langOverride;
return enFlat[key] ?? key;
}

View File

@ -163,6 +163,36 @@ export const de: Translations = {
sessionDetectedMessage: 'Du bist bereits angemeldet. Möchtest du dich abmelden und ein neues Konto registrieren?',
sessionContinue: 'Zum Dashboard',
sessionLogout: 'Abmelden und registrieren',
formTitle: 'Registrierung für Profit Planet',
guestRegistration: 'Gäste-Registrierung',
registerNow: 'Jetzt registrieren',
guestDescription: 'Als Gast registrieren, um auf dein Kaffee-Abonnement zuzugreifen.',
personalDescription: 'Erstelle dein persönliches oder Firmenkonto bei Profit Planet.',
invitedBy: 'Du wurdest eingeladen von',
tabIndividual: 'Privat',
submitCompany: 'Unternehmen registrieren',
submitGuest: 'Als Gast registrieren',
successRedirecting: 'Registrierung erfolgreich Weiterleitung...',
guestNote: 'Du registrierst dich als Gast. Du hast nur Zugang zu deinen Kaffee-Abonnements.',
errorBothCountryCodes: 'Bitte wähle Ländervorwahlen (Dropdown) für Unternehmens- und Kontakttelefonnummern.',
successCompanyMessage: 'Du kannst dich jetzt mit deinem neuen Unternehmenskonto anmelden.',
successGuestMessage: 'Du kannst dich jetzt anmelden, um dein Kaffee-Abonnement zu sehen.',
failedTitle: 'Registrierung fehlgeschlagen',
failedMessage: 'Registrierung fehlgeschlagen. Bitte versuche es erneut.',
networkErrorGeneric: 'Netzwerkfehler. Bitte versuche es später erneut.',
passwordRequirements: 'Passwortanforderungen:',
pwdMinLength: 'Mindestens 8 Zeichen',
pwdLowercase: 'Kleinbuchstaben (a-z)',
pwdUppercase: 'Großbuchstaben (A-Z)',
pwdDigits: 'Ziffern (0-9)',
pwdSpecial: 'Sonderzeichen (!@#$...)',
invalidLinkTitle: 'Ungültiger Einladungslink',
invalidLinkMessage: 'Dieser Registrierungslink ist ungültig oder nicht mehr aktiv. Bitte fordere einen neuen Link an.',
tokenLabel: 'Token',
sessionDescription: 'Du bist bereits angemeldet. Um dich zu registrieren, musst du dich zuerst abmelden oder zum Dashboard gehen.',
goToDashboard: 'Zum Dashboard',
goToHomepage: 'Zur Startseite',
loginHere: 'Hier anmelden',
},
passwordReset: {
@ -398,16 +428,111 @@ export const de: Translations = {
referralManagement: {
title: 'Empfehlungsverwaltung',
subtitle: 'Verwalte deine Empfehlungslinks.',
description: 'Erstelle und verwalte deine Empfehlungslinks. Verfolge die Performance auf einen Blick.',
createLink: 'Empfehlungslink erstellen',
copyLink: 'Link kopieren',
copy: 'Kopieren',
copyMobile: 'Link kopieren',
copied: 'Kopiert',
copiedToClipboard: 'In die Zwischenablage kopiert!',
linkExpiry: 'Läuft ab',
noLinks: 'Noch keine Empfehlungslinks.',
noLinks: 'Keine Empfehlungslinks gefunden.',
generating: 'Wird erstellt…',
generateLink: 'Link generieren',
usesRemaining: 'verbleibende Nutzungen',
unlimited: 'Unbegrenzt',
never: 'Nie',
createSuccess: 'Empfehlungslink erfolgreich erstellt.',
createError: 'Empfehlungslink konnte nicht erstellt werden.',
deactivate: 'Deaktivieren',
deactivated: 'Link deaktiviert',
deactivatedMessage: 'Der Empfehlungslink wurde erfolgreich deaktiviert.',
deactivateFailed: 'Deaktivierung fehlgeschlagen',
deactivateFailedMessage: 'Der Empfehlungslink konnte nicht deaktiviert werden.',
deactivateNetworkError: 'Netzwerkfehler beim Deaktivieren des Empfehlungslinks.',
deactivateModalTitle: 'Empfehlungslink deaktivieren?',
deactivateModalDescription: 'Dies deaktiviert den ausgewählten Empfehlungslink sofort, sodass er nicht mehr verwendet werden kann.',
linkLabel: 'Link',
accessCheckFailed: 'Zugriffsprüfung fehlgeschlagen',
userIdMissing: 'Benutzer-ID fehlt. Weiterleitung…',
accessDenied: 'Zugriff verweigert',
accessDeniedMessage: 'Du hast keine Berechtigung für die Empfehlungsverwaltung.',
permCheckFailed: 'Berechtigungsprüfung fehlgeschlagen',
permCheckFailedMessage: 'Berechtigungen konnten nicht verifiziert werden. Weiterleitung…',
loadFailed: 'Laden fehlgeschlagen',
loadStatsError: 'Empfehlungsstatistiken konnten nicht geladen werden.',
loadLinksError: 'Empfehlungslinks konnten nicht geladen werden.',
copyFailed: 'Kopieren fehlgeschlagen',
copyFailedMessage: 'Link konnte nicht in die Zwischenablage kopiert werden.',
copiedMessage: 'Link in die Zwischenablage kopiert.',
allLinks: 'Alle Empfehlungslinks',
allLinksSubtitle: 'Verwalte deine Links und sieh deren Status.',
colLink: 'Link',
colCreated: 'Erstellt',
colExpires: 'Läuft ab',
colUsage: 'Nutzung',
colStatus: 'Status',
generateTitle: 'Empfehlungslink generieren',
maxUsesLabel: 'Max. Nutzungen',
expiresIn: 'Läuft ab in',
lockedByNeverExpires: 'Gesperrt durch „Läuft nie ab".',
lockedByUnlimited: 'Gesperrt durch „Unbegrenzte Nutzungen".',
statsActiveLinks: 'Aktive Links',
statsLinksUsed: 'Genutzte Links',
statsPersonalUsers: 'Privatnutzer',
statsCompanyUsers: 'Firmennutzer',
statsTotalLinks: 'Links gesamt',
levelStarter: 'Starter',
levelNovice: 'Novize',
levelHustler: 'Hustler',
levelEntrepreneur: 'Unternehmer',
levelPrestige: 'Prestige',
levelMax: 'MAX',
levelLabel: 'Level',
referrals: 'Empfehlungen',
of: 'von',
maxLevelReached: 'Maximales Level erreicht',
nextMilestone: 'Nächster Meilenstein',
registeredUsersTitle: 'Registrierte Nutzer über deine Empfehlung',
totalRefBadge: 'GESAMT REGISTRIERTE NUTZER MIT DEINEM REF-LINK',
registeredUsersSubtitle: 'Nutzer, die sich über einen deiner Empfehlungslinks registriert haben.',
showingLatest5: 'Zeigt die neuesten 5 Nutzer. Klicke auf „Alle anzeigen" für die vollständige Liste.',
viewAll: 'Alle anzeigen',
colUser: 'Nutzer',
colEmail: 'E-Mail',
colType: 'Typ',
colRegistered: 'Registriert',
noRegisteredUsers: 'Keine registrierten Nutzer gefunden.',
typeCompany: 'Unternehmen',
typePersonal: 'Privat',
allRegisteredUsersTitle: 'Alle registrierten Nutzer über deine Empfehlung',
allRegisteredUsersSubtitle: 'Suchen, filtern, blättern oder die vollständige Liste exportieren.',
exportCsv: 'CSV exportieren',
searchPlaceholder: 'Name oder E-Mail suchen…',
filterAllTypes: 'Alle Typen',
filterAllStatus: 'Alle Status',
filterActive: 'Aktiv',
filterInactive: 'Inaktiv',
filterPending: 'Ausstehend',
filterBlocked: 'Gesperrt',
noUsersMatchFilters: 'Keine Nutzer entsprechen deinen Filtern.',
showing: 'Zeige',
pagePrev: 'Zurück',
pageNext: 'Weiter',
pageOf: 'von',
expiry1Day: '1 Tag',
expiry2Days: '2 Tage',
expiry3Days: '3 Tage',
expiry4Days: '4 Tage',
expiry5Days: '5 Tage',
expiry6Days: '6 Tage',
expiry7Days: '7 Tage',
expiryNever: 'Läuft nie ab',
maxUses1: '1 Nutzung',
maxUses5: '5 Nutzungen',
maxUses10: '10 Nutzungen',
maxUses50: '50 Nutzungen',
maxUsesUnlimited: 'Unbegrenzt',
},
quickactionDashboard: {
@ -793,6 +918,7 @@ export const de: Translations = {
codeDuplicate: 'Sprache existiert bereits.',
codeRequired: 'Sprachcode ist erforderlich.',
nameRequired: 'Sprachname ist erforderlich.',
wizardInputPlaceholder: 'Übersetzung eingeben',
},
contractManagement: {
@ -926,6 +1052,646 @@ export const de: Translations = {
reasonNoActivePools: 'Keine aktiven System-Pools gefunden',
},
autofix: {
k02665163: 'Next steps',
k027bd82e: 'Edit the shipping prices for 60 and 120 pieces.',
k047a175d: 'No contracts found.',
k06d4487f: 'Cancel editing',
k0853cfa6: 'Thanks for your subscription!',
k096f4013: 'Manage your company stamps. One active at a time.',
k0af6c6be: 'Create & Activate',
k0affa826: 'Shown to users in the shop and checkout.',
k0b03e660: '2. Choose coffees & quantities',
k0b2445d5: 'Generating PDF preview…',
k0bbc633d: 'Loading contract preview…',
k0d9c63c5: 'Scanning workspace files and component subdirectories...',
k11438b4c: 'Total incl. tax',
k12a86c71: 'Shipping…',
k14eb468b: 'Potential untranslated UI text detected',
k155166db: 'Contract variables are auto-populated from your form data.',
k15bea9bb: 'Address details used on invoices.',
k1824f78d: 'Please select coffees and fill all required buyer fields, signing city, and signature.',
k18872b63: 'No image',
k1bf4ffa4: 'Untranslated literals',
k20127e1c: 'No selection found.',
k21361e0d: 'Summary & Details',
k221fa311: 'Invoice template variables',
k22c8f7f1: 'Create Template',
k28f1a9b1: 'Full name',
k2d0798a6: 'Loading subscription…',
k2e43a9c4: 'Click or drag and drop a new image here',
k3466b0e0: 'Payment method',
k346a2c64: 'Language Management',
k39791457: 'Manage contract templates, company stamp, and create new templates.',
k41ab9eb6: 'You\'ll be able to crop and adjust the image after uploading',
k41afd863: 'Editing:',
k4aeb8688: '2. Your selection',
k4be6f631: 'Save changes',
k516705dd: 'Ort ist erforderlich.',
k528eede9: 'Same as shipping address',
k56717603: 'no image',
k56a52520: 'Skipped files',
k5a489751: 'Save Changes',
k5ad4d864: 'Auto-fixed files',
k6070f6e3: 'Add New Stamp',
k60874ea3: 'Keys auto-created',
k67cb36a4: 'Contract Management',
k6a2c64e8: 'Last name',
k6a892262: 'No keys match your search.',
k6ee0a1b6: 'Click or drag and drop an image here',
k73d1d7d7: 'Edit Crop',
k74491338: 'Reverse Charge aktiv: gueltige UID und auslaendisches Rechnungsland erkannt.',
k7775eddb: 'Your Company Stamps',
k788633d1: 'Profit Planet',
k7a3a6ea3: 'to render invoice line items.',
k7f48f374: '1. Select subscription size',
k7fe72eff: 'No platforms available.',
k80ac9651: 'PNG, JPG, WebP up to 10MB',
k825359ab: 'Accepted types: PNG, JPEG, WebP, SVG. Max size: 5MB.',
k832387c5: 'Loading…',
k83deba83: 'per 10 pcs',
k875f4054: 'Manage all coffees.',
k8c75468c: 'No subscriptions found.',
k8cf40180: 'Missing keys in en.ts',
k90a6e795: 'Unique keys used',
k91052e3f: 'Translation calls',
k92639a9a: 'Language code',
k926966d0: 'Language name',
k96839795: 'Back to selection',
k99bffb65: 'Fill all fields to proceed.',
k9b173204: 'Files auto-fixed',
k9c1a5ecc: 'Fill fields with logged in data',
ka3ee9ded: 'Subscription Billing',
ka56b7b2b: 'No PDF preview available.',
ka5f38d19: 'Company Stamp',
ka802064d: 'Applying i18n auto-fixes to client components and updating translation files...',
kaa30f0cd: 'Create Coffee',
kaa8bbc8e: 'Company Information',
kac6cedc7: 'Saving…',
kae63e46a: 'Missing translation keys detected in workspace',
kb06fa395: 'Edit Coffee',
kb0b660e2: 'Configure Coffee Subscription',
kb1c1c0e5: 'Logging you out...',
kb2217bdf: 'Translation Coverage Scan',
kb791958e: 'Use these placeholders in your HTML: invoiceNumber, customerName, issuedAt, totalNet, totalTax, totalGross, itemsHtml.',
kb8f33873: 'Translation progress',
kb9e483c4: 'Update details of the coffee.',
kba6bd6f3: 'or click to browse',
kcc4adbcc: 'Navigation shortcuts',
kce094582: 'Invoice address',
kd1a2772d: 'Street & No.',
kd2a00802: 'Image removed - Click to upload a new one',
kd3092148: 'Subscription created.',
kd379df9b: 'Open preview',
kd63c8219: 'You have unsaved changes.',
kd6f8d7e9: '1. Your details',
kd8a5ad17: 'Back to list',
kda5f982e: 'Delete Language',
kddd4832f: 'Delete coffee?',
kde5c689e: 'Pick a platform to continue.',
ke33e6fbf: 'Send invoice by email',
ke58b7627: 'Drag and drop your stamp here',
ke74b1adf: 'Contract template is not available.',
ke7b634f2: '3. Preview',
ke7f0a9e3: 'FREE SHIPPING',
kea7cde7a: 'Back to Admin',
kec078e54: 'No coffees selected yet.',
kefe5f0dd: 'Ohne gueltige UID wird die Rechnung mit normaler MwSt erstellt.',
kf1a9384b: 'Auto-applied to documents where applicable.',
kf4e45236: 'Add Language',
kf72d41db: 'Add a new coffee.',
kfe9527d8: 'First name',
kfeac3f7e: 'Choose file',
k0c51fa85: 'Activate template now?',
k134e3932: 'Active stamp',
k1f0b2c48: 'z.B. Wien',
k2fac9ff2: 'Template name',
k3477c83a: 'Describe the product',
k35ac864e: 'Search templates…',
ka8f53660: 'Delete Company Stamp',
kaa5e5363: 'ABO Contract PDF Preview',
kcb65c692: 'e.g., Company Seal 2025',
kd9e4bcbd: 'Contract Preview',
kf1512f8f: 'z.B. SI12345678',
k00016501: '🧪 Token Refresh Test',
k002455d8: 'Total Gross / Brutto',
k00394342: 'Welcome back! Log in to continue.',
k01ad6d49: 'Overview of taxes, revenue, and invoices.',
k022df6ac: 'Admin Verified',
k039e629b: 'Overview meta',
k03cd9b72: 'Commission:',
k04b5cbca: 'Loose Files',
k051e8ac8: 'Confirm password',
k055bba0c: 'Are you sure you want to delete',
k05626798: 'Click to upload logo',
k0778fa87: 'Make sure JWT_EXPIRES_IN=2m in backend .env for fast testing',
k07fe11b2: 'Company / Holder name',
k088d8f6c: 'Delete news?',
k08c92a12: 'Welcome to Profit Planet Community 🌍',
k0925e287: 'e.g., VIP Members',
k098ec0b9: 'Manage the “Platforms” cards shown on the user dashboard.',
k09def344: 'Edit Affiliate',
k09f4290f: 'Reset password',
k0ac84efe: '← Back',
k0c838ec3: 'Min €',
k0c87d75d: 'Max €',
k0c95a1b4: 'Back:',
k0cc2a3ba: 'Versuche andere Suchbegriffe oder Filter',
k0cdde8f8: 'Name:',
k0d6626e3: '👤 User Info',
k0d8cb427: 'Type:',
k0da2c941: 'Users Pending Verification',
k0dba4c6b: 'Role:',
k0dca1445: 'Sort:',
k0dcb69ea: 'No matrices found.',
k0dd01c1c: 'Show:',
k0efd830c: 'Verification Readiness',
k0f0395ca: 'Multi-statement SQL and dump files are supported. Use with caution.',
k0f1fc266: 'All Statuses',
k0fbaa1a9: 'Jetzt registrieren',
k0fe28e0b: 'Affiliate Management',
k10ccb626: 'All Users',
k10e2568f: 'All Types',
k110bae43: 'All Roles',
k111c49d8: 'Users count respects each matrixs max depth policy.',
k11974e0f: 'Advanced: choose parent manually',
k12a7170a: 'No ghost directories found. Run Refresh to scan again.',
k1387f81e: 'Export all filtered users to CSV',
k1405afab: 'Login and watch the countdown timer',
k14a4b43e: 'Refreshing…',
k1521a376: 'Export all users as CSV',
k15843a06: 'Active Pools',
k15da24d8: 'Join Group',
k16b60f69: 'View All',
k17ba59ff: 'Community Hands - Profit Planet',
k17f65c37: 'Example: /shop or https://example.com',
k1882bd75: 'Max Mustermann',
k199db5f1: 'your.email@example.com',
k19f2c5dc: 'No affiliates found',
k1a1ca621: 'e.g. DE89 3704 0044 0532 0130 00',
k1af107a4: 'Logo preview',
k1af97a07: 'User Management',
k1b9c46e5: 'Affiliate Partners',
k1d178b73: 'Basic Information',
k1db0c7cd: 'No loose files found. Run Refresh to scan again.',
k1ddc749e: 'Expiry Date',
k1df74994: 'All subscriptions',
k1e5d5139: 'No users match your filters.',
k1e62338a: 'Commission Rate',
k1eedcda3: 'User Status Hook',
k1f269263: 'DE89 3704 0044 0532 0130 00',
k209ba561: 'Create New Pool',
k20ab2fc7: 'We\'ll send a verification code to your email address.',
k21440f8a: 'Pool Management',
k21db276a: 'Auf Lager',
k228929e2: 'Profile Information',
k23c9f0ff: 'No results yet. Import a SQL dump to see output.',
k258c3515: '892 members',
k26ecadfd: 'My Groups',
k26fbc186: 'Access Denied',
k2786bc5f: 'Signing in...',
k27e93fd7: 'Stay informed with our latest announcements and insights',
k27f56959: 'State change will affect add/remove operations.',
k290e3aab: 'tt.mm jjjj',
k2a2fe15a: 'Phone Number',
k2a37c394: 'Brief description of the affiliate partner...',
k2af2916f: 'Your account is fully submitted. Our team will verify your account shortly.',
k2cd79a3d: 'Browse all favorites',
k2e8f3110: 'All Status',
k2f176a63: 'No news articles available yet.',
k2f4ebc32: 'Rows per page:',
k2f78fabe: 'Go to User Verification',
k31cadca6: 'Partner Name *',
k31d46514: 'Top node:',
k33918465: 'Company Name',
k354a026b: 'Subscription ID',
k35f67931: 'Changes apply from your next billing cycle.',
k3777e830: 'Our team',
k37d7b9c4: 'Checking pool inflow...',
k383672e3: 'owner@example.com',
k39437388: 'Core Pool — 1¢ per capsule per member',
k39e2c5db: 'Add Platform',
k3ac8ca10: 'Only SQL dump files are supported.',
k3b03502e: 'Error loading account status',
k3b7dd87a: 'Try again',
k3b8e0964: 'Subscription details',
k3c32c87f: 'Connect with like-minded individuals, share sustainable practices, and make a positive impact together.',
k3c3e6850: 'Email Verification',
k3d01de91: 'PROFIT PLANET',
k3d5fe74a: 'Expires At:',
k3def5ebf: 'Category *',
k3ee27b4f: 'Top-node Email',
k3f833ce6: 'e.g., Platinum Matrix',
k40f4552a: 'Level 2+',
k410ff9a9: 'Total Affiliates',
k416bfe70: 'No further status changes are available for this subscription.',
k4191cdba: 'Edit VAT',
k41f7c81d: 'Delete Account',
k4307f6c7: 'Date of Birth:',
k431328cf: 'No affiliate partners available at the moment.',
k471ba099: 'News Manager',
k47b952de: 'Has Token:',
k47bbd37e: 'Sign in to Profit Planet',
k483aa95a: '• Share authentic experiences',
k48852b8d: 'Customer Email',
k49568342: 'Manage your affiliate partners and tracking links',
k4968eb2a: 'Abonement:',
k49f254bd: 'Current URL:',
k4a055849: 'ID Front',
k4a9e1ebe: 'Loading user details...',
k4b6c7681: 'Open subscriptions',
k4c5e8e87: 'Export CSV',
k4c5ecd73: 'Export PDF',
k4cb62cff: 'Keine Produkte gefunden',
k4db68c96: 'SQL Import',
k4e0c889b: 'Not Ready',
k4e168c01: 'Coffee Abonnements',
k4e532c48: 'Verification Status',
k4e61bc77: 'Root not yet loaded.',
k4ed7f4d1: 'Time Left:',
k502a0057: 'Last 7 days',
k5122ab54: 'Request again',
k51ee3aae: 'email@example.com',
k52af8b8d: 'Quick Actions',
k533db977: 'Your new password',
k54c06343: 'Refresh Token',
k54f49724: 'No users match your search.',
k55aba973: 'Produkte gefunden',
k5614c806: 'Review and verify all users who need admin approval. Users must complete all steps before verification.',
k56435c9b: 'Verfügbarkeit',
k5738c039: 'Matrix created successfully.',
k577a012c: 'User Type',
k578dcc0b: 'PNG, JPG, WebP, SVG up to 5MB',
k58344b74: 'No direct children.',
k5834cbed: 'Loading affiliate partners...',
k58424b1d: 'Eco Warriors',
k5857ef79: 'Existing Pools',
k59422f07: 'Linked Subscription',
k5aae8706: 'New password',
k5c598bc0: 'Trending Groups',
k5d4d494e: 'Loading members...',
k5d85b354: 'Driver\'s License',
k5e580e3f: 'Filter zurücksetzen',
k5ef19112: 'Join our team',
k5f74c123: 'Last 30 days',
k5fb70267: 'Shop wird geladen...',
k5fbf1824: 'Masked names for deeper descendants.',
k61c2a732: 'Angemeldet bleiben',
k61f6cd4e: 'Token Preview:',
k6285753a: 'Back to Pool Management',
k62bc3c59: 'e.g. Berlin',
k62d12fab: 'Error loading data',
k63115bb4: 'ID Documents',
k633438a0: 'Discover our trusted partners and earn commissions through affiliate links.',
k63458f03: 'Produkte durchsuchen...',
k65b67dc3: 'Back to matrices',
k65e33378: 'Total users fetched',
k661c032b: 'You need admin privileges to access this page.',
k664072a1: 'Dev Management',
k67391c88: 'Manage system pools and members.',
k678d2b40: 'Super reduced',
k67cace8b: 'Profile Settings',
k67dd8a82: 'Contact name',
k6828cdd9: 'Affiliate URL *',
k6838438d: 'Ghost Directories',
k6a4108c8: 'Last Name',
k6a486e3e: 'Create Group',
k6aa2d843: 'Read full guidelines',
k6af9037b: 'Open navigation',
k6b0f4f70: 'ID documents or a signed contract are missing for this user. The users verification status should be checked.',
k6b76bd0e: 'Willkommen bei Profit Planet',
k6c6e5c0f: 'Use with caution',
k6ca85cda: 'Trending right now',
k6d85810b: 'Your password',
k6de13000: 'Zero Waste Living',
k6e4a6069: 'Import SQL dump files to run database migrations.',
k70972912: 'Edit Profile',
k70bcafbd: 'Recent Discussions',
k71d565c9: 'Address:',
k72428656: 'Highest full level:',
k73831c06: 'Personal Matrix',
k73cf4fb6: 'Edit News',
k73d110fa: 'Edit Mode',
k73d4a156: 'Check Network tab for /api/refresh requests',
k744fda01: 'My Subscriptions',
k748bf541: 'No users match current filters.',
k75078d0b: 'Add News',
k750c1eb5: 'Add User',
k7572cceb: 'Edit VAT rates',
k75cb45a7: 'This value is stored as net price.',
k75d83433: '• Help others learn and grow',
k77049179: '• Reason:',
k77444d5b: 'Exoscale directories that do not have a matching user in the database.',
k776b751c: 'Policy Max Depth',
k777299de: 'Finance Management',
k77767b9e: 'Close notification',
k77a56aae: 'News & Updates',
k77d5ecd9: 'Copy referral link',
k7938d4fd: 'Result Sets',
k79e1c459: 'Manage all users, view statistics, and handle verification.',
k7ab45054: 'All Readiness',
k7bed84a7: 'Member Since',
k7c19388f: 'e.g., 10%',
k7c740cd5: 'Ready to search. Click the Search button to fetch candidates.',
k7db4e5a9: 'Visit Affiliate Link',
k7f57b169: 'Test API Call',
k7f9568ec: 'Highest full level',
k7fa2c4af: 'Loading users...',
k811fbc99: 'API Base URL:',
k815ca9ba: 'A matrix configuration already exists for this selection.',
k8193b7a2: 'Loading loose files...',
k81a1b900: 'Loading settings…',
k81b056f2: 'See our job postings',
k81c0b74b: 'Status:',
k81c7c2f2: 'Musterstraße 1',
k8323a7d9: 'Loading:',
k832a032b: 'Search affiliates...',
k8358f1d1: 'Loading folder issues...',
k84d5cfcb: 'View overview',
k85446b89: 'Next billing',
k85682289: 'e.g. FN123456a',
k85c66f50: 'Search & Filter Pending Users',
k867f8265: 'Due Date',
k86aa4f9c: 'Current Month',
k87e4b9a2: 'Core Pool',
k883ea8c5: 'Loading ghost directories...',
k88d8bb9d: 'Passwort vergessen?',
k890ff52f: 'e.g., Coffee Equipment Co.',
k8a35cc53: 'SQL dumps run immediately and can modify production data.',
k8a59b156: 'Import SQL',
k8b71f0c7: 'Email, name, company...',
k8b89f863: 'External partner website.',
k8bb1c673: 'Email:',
k8be14d47: 'Error:',
k8c3085f4: 'Users ↓',
k8c3085f6: 'Users ↑',
k8d84b4c5: 'Add New Affiliate',
k8dda5201: 'you@example.com',
k8eaa7b3b: 'e.g. +43 1 234567',
k8eab7c16: 'National ID',
k8eb2524c: 'Enter 6-digit code',
k8f46c81e: 'e.g., 5',
k8f528877: 'Crop & Adjust Image',
k915115a9: 'Last 90 days',
k91912619: 'Close navigation',
k91e69df1: 'ProfitPlanet GmbH',
k91eb415a: 'ProfitPlanet Logo',
k91f24187: 'Complete Profile',
k9213db6e: '📋 Testing Instructions',
k93165aea: '12345 Berlin',
k93b6dc1b: 'Uploaded Documents',
k93f03bca: 'Signed Contract Document',
k941fd092: 'Last Folder Structure Action',
k955b1cbe: 'No results',
k959fb1a6: 'Remove member from pool?',
k961ba411: 'Community Guidelines',
k9683262f: 'Fill %',
k96dbbe05: 'Street & House Number',
k972cee5e: 'Front:',
k9772afa4: 'No included items were returned for this subscription.',
k97abed7d: 'Tax Certificate',
k981b1f1a: 'SQL Dump Import',
k98519a5e: 'I have read, understood, and agree to the terms and conditions of this service agreement.',
k9860434f: 'Please review and upload your signed service agreement.',
k9b3266b5: 'Pending backend wiring to MatrixController.listVacancies. This section will surface empty slots and allow reassignment.',
k9bc83f50: 'Change coffees for next month',
k9c3db145: 'Start Discussion',
k9d0c063d: 'Password saved. Redirecting to login...',
k9e609523: 'No missing folders found. Run Refresh to scan again.',
k9f29dbfb: 'Entdecke nachhaltige Produkte und verdiene dabei. Deine Plattform für bewussten Konsum und finanzielle Vorteile.',
k9f56d4ac: 'e.g. +43 676 1234567',
ka00fc5db: 'Manage your account information and preferences',
ka15f5ec5: 'Auth Store State',
ka1d0b6ff: 'No deeper descendants.',
ka29ac729: 'Saved successfully',
ka3c41ff8: 'View details →',
ka3cbb536: 'Continue →',
ka4ecb6cd: 'Search…',
ka5bf342b: 'Select…',
ka5c2113f: 'Company Name:',
ka5d50257: 'Loading VAT rates…',
ka6be28d2: 'Add user to pool',
ka7073aee: 'Profit Planet Store',
ka72e833f: 'Policy filter:',
ka8ea17b8: 'Next ',
ka991f523: 'Loading affiliates...',
ka9d6e905: 'Total users under me',
kaa656770: 'You are not part of any matrix yet.',
kaa8231ec: 'This Year',
kab4f5159: 'Each node can hold up to 5 direct children. Depth unbounded.',
kab99811e: 'Disabled message',
kace2fe51: 'Verification Code',
kadc6abcf: 'Auth Debug Page',
kadd80fbc: 'Clear selection',
kaf787fe5: '1,284 members',
kb0031873: 'Browse all trending',
kb01addda: 'Token should automatically renew without user action',
kb1341138: 'Ensures both contract and gdpr folders exist for each user.',
kb24782ec: 'Last Login',
kb2dfe482: 'Read More',
kb324fb25: 'Total Users',
kb337d94e: 'No entries found.',
kb343460d: 'Rogue users',
kb35549bb: 'Search name or email…',
kb383a3e8: 'Included in your subscription',
kb45c4d5f: 'Tax ID:',
kb4675362: 'Import Results',
kb4aba3dc: 'No unverified users match current filters.',
kb573897d: 'Short description of the pool',
kb5e0b861: 'Inactive Pools',
kb6b367b7: 'When time left ≤ 3 minutes, auto-refresh should trigger',
kb6eacc9d: 'Select a .sql dump file using Import SQL.',
kb74d7c51: '🔧 Manual Controls',
kb7849a5a: 'Create Matrix',
kb846955a: 'Select Document Type',
kb87eb38b: 'Enter at least 3 characters and click Search.',
kb8cd2810: 'Account Status',
kb8d6f3f7: 'Shop Collection',
kbbefb159: 'Error loading users',
kbc368b5d: 'Date of Birth',
kbc6a6543: 'ID Back',
kbce9fbea: 'No platforms configured.',
kbd8b3364: 'Sign Contract',
kbd979e13: 'We are a community',
kbdb02e32: 'Keine Rechnungen gefunden.',
kbe9355f8: 'Business License',
kbf4b7789: 'You are already logged in. Redirecting...',
kbf7bde57: 'Select any subscription to view details and included items.',
kbfa5b4c5: 'Tax ID',
kbfd13a03: 'Account Setup',
kbff01823: 'Shows files directly under the user folder that are not in contract or gdpr.',
kc0d718d7: 'Registration Number',
kc0e3b03d: 'Total:',
kc3d181e2: 'Forgot password?',
kc4315932: 'Dashboard Management',
kc4d7816e: 'Levels filled',
kc7bb0c06: 'Filter by country or code',
kc7c429a6: 'Add users to matrix',
kc813a103: 'Loading available coffees…',
kc8652e34: 'You dont have any subscriptions yet.',
kc9d9d15d: 'Postal Code',
kca04f5e3: 'Phone:',
kcada239b: 'View your active subscriptions, included items and subscription details on a dedicated page.',
kcb491706: 'Folder Structure',
kcbc17bbd: 'No users in this pool yet.',
kcc15636b: 'Coffee content can only be changed while a subscription is issued, ongoing, or paused.',
kcc1c5596: 'Profit Planet Mascot',
kccbc54c1: 'Delete Affiliate',
kccc13f16: '← Go back',
kccde6d86: 'User Verification Center',
kccf7593a: '• Be respectful and kind',
kcd7a1625: 'deine@email.com',
kcd9890e5: 'PNG, JPG, WebP up to 5MB',
kcdfef775: 'Loading subscriptions…',
kce0ab46c: 'Dein Passwort',
kcf4ba87d: 'Crop Affiliate Logo',
kcf61fc9e: 'Last Loose Files Action',
kd00443f2: 'Go to Dashboard',
kd04a7c59: 'Matrix Name',
kd058bb7b: 'Missing:',
kd09be3cd: 'Matrix Management',
kd1c17b3f: 'Alle Marken',
kd1f35ccf: 'Search & Filter Users',
kd2e35b08: 'Rows per page',
kd2e5e813: '• Already booked:',
kd304af2e: 'Global search...',
kd40c4f86: 'Activate this pool',
kd49dc1e1: 'Pool Type',
kd4a0fd1e: 'Pool Name',
kd4af6368: 'Issue Date',
kd4d50566: 'ID documents or a signed contract are missing from object storage. The users verification status should be checked.',
kd4eb7ee0: 'Close menu',
kd51f320c: 'Exoscale Folder Structure',
kd56a13f2: 'Recipient Email',
kd5cca6e9: '? This action cannot be undone.',
kd6024811: 'PDF File',
kd642e230: 'Search by name or email. Minimum 3 characters. Existing matrix members are hidden.',
kd68da70d: 'Nachhaltige Produkte für deinen Erfolg',
kd89474fa: 'Back to News',
kda96f5b3: 'Matrix Depth',
kdb27a82d: ' Previous',
kdbba338d: 'Registration Number:',
kdc22ad8a: 'Manage matrices, see stats, and create new ones.',
kdc47630b: 'Matrix fill:',
kdca959c3: 'Discover a curated selection of high-quality products that cater to your every need.',
kde1c3c69: 'Logo Image',
kde2b4fa0: 'Result Summary',
ke0a3528a: 'Passwords do not match.',
ke17859b2: 'Business Registration',
ke19afb3d: 'Archive this pool',
ke1abc7d9: 'Add Affiliate',
ke24abf9c: 'Edit coffee content',
ke3889dc2: 'Loading news...',
ke4c4a858: 'Min. 3 characters',
ke697b8cb: 'Set Active',
ke8b9f33c: 'Total in Pool',
ke9e71971: 'Oder weiter mit',
kebf33594: 'Filter by category:',
kec5a5357: 'Upload Invoice',
keccee79f: 'Email address',
ked60db76: 'Back to login',
kee28b8c6: '🔑 Token Status',
kef1656df: 'Apply Crop',
kefd5231d: 'Depth 5',
kf0646f35: 'Our values',
kf0d33884: 'Check browser console for detailed logs',
kf0eef57e: 'Total members:',
kf2147f07: 'Not provided',
kf2180ff6: 'Manage VAT rates',
kf27e4502: 'Ready to Verify',
kf2a1257e: 'Back to profile',
kf2b5c1a6: 'Customer Name',
kf2d8db2b: 'Updating status...',
kf340aa10: 'Loose files:',
kf3557acd: '5ary Tree',
kf3b81ba3: 'Used in the URL. Auto-generated from title unless edited.',
kf4868273: 'Click to upload',
kf4f44e2f: 'e.g. ATU12345678',
kf530c357: 'Anmeldung läuft...',
kf663ef67: 'Shop with an infinite variety of products',
kf69154f8: '• Stay on topic',
kf70b9896: 'e.g. 12345',
kf7189e80: '🔄 Manual Refresh Token',
kf78c9087: 'Immediate children',
kf7a91674: 'Policy ↑',
kf7a91676: 'Policy ↓',
kf823daf7: 'Fallback to root if referral parent not in matrix',
kf8c220d3: 'kunde@example.com',
kf971ea7f: 'Created:',
kfaa8fc4a: '• Will book:',
kfb1676b0: 'Phone number *',
kfb37e056: 'Last Login:',
kfb92efe9: 'Description *',
kfce271a2: 'Node Env:',
kfdcad59b: 'Send Email Report',
kfe8083f8: 'First Name',
k17581b31: 'Next page',
k2108b5a0: 'No invoices found for this subscription.',
k34a0a2e4: 'Select the files where you want to run i18n auto-fix.',
k41f3daea: 'Billed monthly',
k43218db0: 'Fix Targets',
k49e51b5f: 'Current plan',
k4bfb4f28: 'Feature comparison',
k4c6eb72c: 'Select all',
k4f209a66: 'You currently dont have an active subscription.',
k5b7042c7: 'Previous page',
k60b1e339: 'No media or documents found.',
k6569783c: 'Use this to include server-style files. Files with server-only Next.js APIs are skipped for safety.',
k68c88f41: 'Force convert selected files to client components before auto-fix',
k74914369: 'Delete Item',
k772cc77b: 'Complete your profile to unlock all features',
k7fa55432: 'My Subscription',
k86b03343: 'Billed annually',
k8953de89: 'Finance & Invoices',
k947d8777: 'Invoice #',
ka5603827: 'Loading invoices…',
ka86bdc9b: 'Payment frequency',
kb3243742: 'No file',
kc48b877b: 'No subscription selected. Invoices will appear once you have an active subscription.',
kd08b698a: 'Profile Completion',
ke3480838: 'No fixable hardcoded UI text detected in eligible components.',
ked7d533b: 'Media & Documents',
kf5ac16fb: 'Pricing that grows with you',
kf9f94d5e: 'Buy this plan',
kfd632d02: 'Export all invoices',
k5d4f6b2f: 'Bank Information',
k9dafde30: 'Contact Person',
kada9d61c: 'Account Holder',
kde6d477f: 'Email Address',
kfc6b6a29: 'Editing disabled',
k03538639: 'e.g. fr, es, zh-TW',
k5fcc9b0e: 'Delete language',
k9bd0812b: 'Shows why a file was changed, skipped, or left untouched after a fix attempt.',
ka019b3c0: 'e.g. Français',
kbe30c353: 'Coverage by namespace',
kbf49d59b: 'Search keys or English text…',
kc8034db6: 'Auto-fix debug logs',
k0cdc3ee9: 'Assign namespace...',
k1db86f96: 'Create tab',
k505ebdae: 'Unassigned namespaces',
k5f978731: 'Category tabs',
k66edf1eb: 'Drag into a tab card',
k741a01f7: 'All namespaces are categorized.',
ka6791a02: 'Remove from tab',
kb7a30760: 'Filter namespaces quickly or open manager to create and assign tabs.',
kc4671abe: 'Create tabs and assign namespaces by drag-and-drop or dropdown.',
kd6e42900: 'Manage tabs',
ke52ed6e9: 'New tab name',
kef9de7f0: 'Manage category tabs',
kf3c3223a: 'Drop a namespace here.',
k0700b1f2: 'No global keys yet. Mark keys as global in the translation wizard, or add one from the dropdown above.',
k47bce570: 'Select key to add to global',
k5e5e8744: 'Translation wizard available',
k6aba2cb0: 'Back to panels',
k6cfeedd3: 'Global terms',
k725dd1d6: 'Start wizard',
kad7d8c49: 'Use this list for cross-context terms like IBAN.',
kc02b17c3: 'Remove from global terms',
kc518ff5c: 'English reference',
kcd190bdd: 'Translation wizard',
kfd1e0089: 'Auto-scroll on panel open',
},
// ─── Notifications / Toasts ────────────────────────────
toasts: {
loginSuccess: 'Anmeldung erfolgreich',

View File

@ -163,6 +163,36 @@ export const en: Translations = {
sessionDetectedMessage: 'You are already logged in. Do you want to log out and register a new account?',
sessionContinue: 'Continue to dashboard',
sessionLogout: 'Log out and register',
formTitle: 'Registration for Profit Planet',
guestRegistration: 'Guest Registration',
registerNow: 'Register now',
guestDescription: 'Register as a guest to access your coffee abonnement.',
personalDescription: 'Create your personal or company account with Profit Planet.',
invitedBy: 'You were invited by',
tabIndividual: 'Individual',
submitCompany: 'Register company',
submitGuest: 'Register as Guest',
successRedirecting: 'Registration successful redirecting...',
guestNote: 'You are registering as a guest. You will have access to your coffee abonnements only.',
errorBothCountryCodes: 'Please select country codes (dropdown) for both company and contact phone numbers.',
successCompanyMessage: 'You can now log in with your new company account.',
successGuestMessage: 'You can now log in to view your coffee abonnement.',
failedTitle: 'Registration failed',
failedMessage: 'Registration failed. Please try again.',
networkErrorGeneric: 'Network error. Please try again later.',
passwordRequirements: 'Password requirements:',
pwdMinLength: 'At least 8 characters',
pwdLowercase: 'Lowercase letters (a-z)',
pwdUppercase: 'Uppercase letters (A-Z)',
pwdDigits: 'Digits (0-9)',
pwdSpecial: 'Special characters (!@#$...)',
invalidLinkTitle: 'Invalid invitation link',
invalidLinkMessage: 'This registration link is invalid or no longer active. Please request a new link.',
tokenLabel: 'Token',
sessionDescription: 'You are already logged in. To register, you must first log out or you can go to the dashboard.',
goToDashboard: 'Go to dashboard',
goToHomepage: 'Go to homepage',
loginHere: 'Login here',
},
passwordReset: {
@ -398,16 +428,111 @@ export const en: Translations = {
referralManagement: {
title: 'Referral Management',
subtitle: 'Manage your referral links.',
description: 'Create and manage your referral links. Track performance at a glance.',
createLink: 'Create referral link',
copyLink: 'Copy link',
copy: 'Copy',
copyMobile: 'Copy link',
copied: 'Copied',
copiedToClipboard: 'Copied to clipboard!',
linkExpiry: 'Expires',
noLinks: 'No referral links yet.',
noLinks: 'No referral links found.',
generating: 'Generating…',
generateLink: 'Generate Link',
usesRemaining: 'uses remaining',
unlimited: 'Unlimited',
never: 'Never',
createSuccess: 'Referral link created successfully.',
createError: 'Could not create referral link.',
deactivate: 'Deactivate',
deactivated: 'Link deactivated',
deactivatedMessage: 'The referral link has been deactivated successfully.',
deactivateFailed: 'Deactivate failed',
deactivateFailedMessage: 'Could not deactivate the referral link.',
deactivateNetworkError: 'Network error while deactivating the referral link.',
deactivateModalTitle: 'Deactivate referral link?',
deactivateModalDescription: 'This will immediately deactivate the selected referral link so it can no longer be used.',
linkLabel: 'Link',
accessCheckFailed: 'Access check failed',
userIdMissing: 'User id is missing. Redirecting…',
accessDenied: 'Access denied',
accessDeniedMessage: 'You do not have permission to access Referral Management.',
permCheckFailed: 'Permission check failed',
permCheckFailedMessage: 'Could not verify permissions. Redirecting…',
loadFailed: 'Load failed',
loadStatsError: 'Could not load referral statistics.',
loadLinksError: 'Could not load referral links.',
copyFailed: 'Copy failed',
copyFailedMessage: 'Could not copy link to clipboard.',
copiedMessage: 'Link copied to clipboard.',
allLinks: 'All Referral Links',
allLinksSubtitle: 'Manage your links and see their status.',
colLink: 'Link',
colCreated: 'Created',
colExpires: 'Expires',
colUsage: 'Usage',
colStatus: 'Status',
generateTitle: 'Generate Referral Link',
maxUsesLabel: 'Max Uses',
expiresIn: 'Expires In',
lockedByNeverExpires: 'Locked by "Never expires".',
lockedByUnlimited: 'Locked by "Unlimited uses".',
statsActiveLinks: 'Active Links',
statsLinksUsed: 'Links Used',
statsPersonalUsers: 'Personal Users',
statsCompanyUsers: 'Company Users',
statsTotalLinks: 'Total Links',
levelStarter: 'Starter',
levelNovice: 'Novice',
levelHustler: 'Hustler',
levelEntrepreneur: 'Entrepreneur',
levelPrestige: 'Prestige',
levelMax: 'MAX',
levelLabel: 'Level',
referrals: 'referrals',
of: 'of',
maxLevelReached: 'Max level reached',
nextMilestone: 'Next milestone',
registeredUsersTitle: 'Registered Users via Your Referral',
totalRefBadge: 'TOTAL REGISTERED USER WITH YOUR REF LINK',
registeredUsersSubtitle: 'Users who signed up using one of your referral links.',
showingLatest5: 'Showing the latest 5 users. Use “View all” to see the complete list.',
viewAll: 'View all',
colUser: 'User',
colEmail: 'Email',
colType: 'Type',
colRegistered: 'Registered',
noRegisteredUsers: 'No registered users found.',
typeCompany: 'Company',
typePersonal: 'Personal',
allRegisteredUsersTitle: 'All Registered Users via Your Referral',
allRegisteredUsersSubtitle: 'Search, filter, paginate, or export the full list.',
exportCsv: 'Export CSV',
searchPlaceholder: 'Search name or email…',
filterAllTypes: 'All Types',
filterAllStatus: 'All Status',
filterActive: 'Active',
filterInactive: 'Inactive',
filterPending: 'Pending',
filterBlocked: 'Blocked',
noUsersMatchFilters: 'No users match your filters.',
showing: 'Showing',
pagePrev: 'Previous',
pageNext: 'Next',
pageOf: 'of',
expiry1Day: '1 day',
expiry2Days: '2 days',
expiry3Days: '3 days',
expiry4Days: '4 days',
expiry5Days: '5 days',
expiry6Days: '6 days',
expiry7Days: '7 days',
expiryNever: 'Never expires',
maxUses1: '1 use',
maxUses5: '5 uses',
maxUses10: '10 uses',
maxUses50: '50 uses',
maxUsesUnlimited: 'Unlimited',
},
quickactionDashboard: {
@ -793,6 +918,7 @@ export const en: Translations = {
codeDuplicate: 'Language already exists.',
codeRequired: 'Language code is required.',
nameRequired: 'Language name is required.',
wizardInputPlaceholder: 'Enter translated text',
},
contractManagement: {
@ -926,6 +1052,646 @@ export const en: Translations = {
reasonNoActivePools: 'No active system pools found',
},
autofix: {
k02665163: 'Next steps',
k027bd82e: 'Edit the shipping prices for 60 and 120 pieces.',
k047a175d: 'No contracts found.',
k06d4487f: 'Cancel editing',
k0853cfa6: 'Thanks for your subscription!',
k096f4013: 'Manage your company stamps. One active at a time.',
k0af6c6be: 'Create & Activate',
k0affa826: 'Shown to users in the shop and checkout.',
k0b03e660: '2. Choose coffees & quantities',
k0b2445d5: 'Generating PDF preview…',
k0bbc633d: 'Loading contract preview…',
k0d9c63c5: 'Scanning workspace files and component subdirectories...',
k11438b4c: 'Total incl. tax',
k12a86c71: 'Shipping…',
k14eb468b: 'Potential untranslated UI text detected',
k155166db: 'Contract variables are auto-populated from your form data.',
k15bea9bb: 'Address details used on invoices.',
k1824f78d: 'Please select coffees and fill all required buyer fields, signing city, and signature.',
k18872b63: 'No image',
k1bf4ffa4: 'Untranslated literals',
k20127e1c: 'No selection found.',
k21361e0d: 'Summary & Details',
k221fa311: 'Invoice template variables',
k22c8f7f1: 'Create Template',
k28f1a9b1: 'Full name',
k2d0798a6: 'Loading subscription…',
k2e43a9c4: 'Click or drag and drop a new image here',
k3466b0e0: 'Payment method',
k346a2c64: 'Language Management',
k39791457: 'Manage contract templates, company stamp, and create new templates.',
k41ab9eb6: 'You\'ll be able to crop and adjust the image after uploading',
k41afd863: 'Editing:',
k4aeb8688: '2. Your selection',
k4be6f631: 'Save changes',
k516705dd: 'Ort ist erforderlich.',
k528eede9: 'Same as shipping address',
k56717603: 'no image',
k56a52520: 'Skipped files',
k5a489751: 'Save Changes',
k5ad4d864: 'Auto-fixed files',
k6070f6e3: 'Add New Stamp',
k60874ea3: 'Keys auto-created',
k67cb36a4: 'Contract Management',
k6a2c64e8: 'Last name',
k6a892262: 'No keys match your search.',
k6ee0a1b6: 'Click or drag and drop an image here',
k73d1d7d7: 'Edit Crop',
k74491338: 'Reverse Charge aktiv: gueltige UID und auslaendisches Rechnungsland erkannt.',
k7775eddb: 'Your Company Stamps',
k788633d1: 'Profit Planet',
k7a3a6ea3: 'to render invoice line items.',
k7f48f374: '1. Select subscription size',
k7fe72eff: 'No platforms available.',
k80ac9651: 'PNG, JPG, WebP up to 10MB',
k825359ab: 'Accepted types: PNG, JPEG, WebP, SVG. Max size: 5MB.',
k832387c5: 'Loading…',
k83deba83: 'per 10 pcs',
k875f4054: 'Manage all coffees.',
k8c75468c: 'No subscriptions found.',
k8cf40180: 'Missing keys in en.ts',
k90a6e795: 'Unique keys used',
k91052e3f: 'Translation calls',
k92639a9a: 'Language code',
k926966d0: 'Language name',
k96839795: 'Back to selection',
k99bffb65: 'Fill all fields to proceed.',
k9b173204: 'Files auto-fixed',
k9c1a5ecc: 'Fill fields with logged in data',
ka3ee9ded: 'Subscription Billing',
ka56b7b2b: 'No PDF preview available.',
ka5f38d19: 'Company Stamp',
ka802064d: 'Applying i18n auto-fixes to client components and updating translation files...',
kaa30f0cd: 'Create Coffee',
kaa8bbc8e: 'Company Information',
kac6cedc7: 'Saving…',
kae63e46a: 'Missing translation keys detected in workspace',
kb06fa395: 'Edit Coffee',
kb0b660e2: 'Configure Coffee Subscription',
kb1c1c0e5: 'Logging you out...',
kb2217bdf: 'Translation Coverage Scan',
kb791958e: 'Use these placeholders in your HTML: invoiceNumber, customerName, issuedAt, totalNet, totalTax, totalGross, itemsHtml.',
kb8f33873: 'Translation progress',
kb9e483c4: 'Update details of the coffee.',
kba6bd6f3: 'or click to browse',
kcc4adbcc: 'Navigation shortcuts',
kce094582: 'Invoice address',
kd1a2772d: 'Street & No.',
kd2a00802: 'Image removed - Click to upload a new one',
kd3092148: 'Subscription created.',
kd379df9b: 'Open preview',
kd63c8219: 'You have unsaved changes.',
kd6f8d7e9: '1. Your details',
kd8a5ad17: 'Back to list',
kda5f982e: 'Delete Language',
kddd4832f: 'Delete coffee?',
kde5c689e: 'Pick a platform to continue.',
ke33e6fbf: 'Send invoice by email',
ke58b7627: 'Drag and drop your stamp here',
ke74b1adf: 'Contract template is not available.',
ke7b634f2: '3. Preview',
ke7f0a9e3: 'FREE SHIPPING',
kea7cde7a: 'Back to Admin',
kec078e54: 'No coffees selected yet.',
kefe5f0dd: 'Ohne gueltige UID wird die Rechnung mit normaler MwSt erstellt.',
kf1a9384b: 'Auto-applied to documents where applicable.',
kf4e45236: 'Add Language',
kf72d41db: 'Add a new coffee.',
kfe9527d8: 'First name',
kfeac3f7e: 'Choose file',
k0c51fa85: 'Activate template now?',
k134e3932: 'Active stamp',
k1f0b2c48: 'z.B. Wien',
k2fac9ff2: 'Template name',
k3477c83a: 'Describe the product',
k35ac864e: 'Search templates…',
ka8f53660: 'Delete Company Stamp',
kaa5e5363: 'ABO Contract PDF Preview',
kcb65c692: 'e.g., Company Seal 2025',
kd9e4bcbd: 'Contract Preview',
kf1512f8f: 'z.B. SI12345678',
k00016501: '🧪 Token Refresh Test',
k002455d8: 'Total Gross / Brutto',
k00394342: 'Welcome back! Log in to continue.',
k01ad6d49: 'Overview of taxes, revenue, and invoices.',
k022df6ac: 'Admin Verified',
k039e629b: 'Overview meta',
k03cd9b72: 'Commission:',
k04b5cbca: 'Loose Files',
k051e8ac8: 'Confirm password',
k055bba0c: 'Are you sure you want to delete',
k05626798: 'Click to upload logo',
k0778fa87: 'Make sure JWT_EXPIRES_IN=2m in backend .env for fast testing',
k07fe11b2: 'Company / Holder name',
k088d8f6c: 'Delete news?',
k08c92a12: 'Welcome to Profit Planet Community 🌍',
k0925e287: 'e.g., VIP Members',
k098ec0b9: 'Manage the “Platforms” cards shown on the user dashboard.',
k09def344: 'Edit Affiliate',
k09f4290f: 'Reset password',
k0ac84efe: '← Back',
k0c838ec3: 'Min €',
k0c87d75d: 'Max €',
k0c95a1b4: 'Back:',
k0cc2a3ba: 'Versuche andere Suchbegriffe oder Filter',
k0cdde8f8: 'Name:',
k0d6626e3: '👤 User Info',
k0d8cb427: 'Type:',
k0da2c941: 'Users Pending Verification',
k0dba4c6b: 'Role:',
k0dca1445: 'Sort:',
k0dcb69ea: 'No matrices found.',
k0dd01c1c: 'Show:',
k0efd830c: 'Verification Readiness',
k0f0395ca: 'Multi-statement SQL and dump files are supported. Use with caution.',
k0f1fc266: 'All Statuses',
k0fbaa1a9: 'Jetzt registrieren',
k0fe28e0b: 'Affiliate Management',
k10ccb626: 'All Users',
k10e2568f: 'All Types',
k110bae43: 'All Roles',
k111c49d8: 'Users count respects each matrixs max depth policy.',
k11974e0f: 'Advanced: choose parent manually',
k12a7170a: 'No ghost directories found. Run Refresh to scan again.',
k1387f81e: 'Export all filtered users to CSV',
k1405afab: 'Login and watch the countdown timer',
k14a4b43e: 'Refreshing…',
k1521a376: 'Export all users as CSV',
k15843a06: 'Active Pools',
k15da24d8: 'Join Group',
k16b60f69: 'View All',
k17ba59ff: 'Community Hands - Profit Planet',
k17f65c37: 'Example: /shop or https://example.com',
k1882bd75: 'Max Mustermann',
k199db5f1: 'your.email@example.com',
k19f2c5dc: 'No affiliates found',
k1a1ca621: 'e.g. DE89 3704 0044 0532 0130 00',
k1af107a4: 'Logo preview',
k1af97a07: 'User Management',
k1b9c46e5: 'Affiliate Partners',
k1d178b73: 'Basic Information',
k1db0c7cd: 'No loose files found. Run Refresh to scan again.',
k1ddc749e: 'Expiry Date',
k1df74994: 'All subscriptions',
k1e5d5139: 'No users match your filters.',
k1e62338a: 'Commission Rate',
k1eedcda3: 'User Status Hook',
k1f269263: 'DE89 3704 0044 0532 0130 00',
k209ba561: 'Create New Pool',
k20ab2fc7: 'We\'ll send a verification code to your email address.',
k21440f8a: 'Pool Management',
k21db276a: 'Auf Lager',
k228929e2: 'Profile Information',
k23c9f0ff: 'No results yet. Import a SQL dump to see output.',
k258c3515: '892 members',
k26ecadfd: 'My Groups',
k26fbc186: 'Access Denied',
k2786bc5f: 'Signing in...',
k27e93fd7: 'Stay informed with our latest announcements and insights',
k27f56959: 'State change will affect add/remove operations.',
k290e3aab: 'tt.mm jjjj',
k2a2fe15a: 'Phone Number',
k2a37c394: 'Brief description of the affiliate partner...',
k2af2916f: 'Your account is fully submitted. Our team will verify your account shortly.',
k2cd79a3d: 'Browse all favorites',
k2e8f3110: 'All Status',
k2f176a63: 'No news articles available yet.',
k2f4ebc32: 'Rows per page:',
k2f78fabe: 'Go to User Verification',
k31cadca6: 'Partner Name *',
k31d46514: 'Top node:',
k33918465: 'Company Name',
k354a026b: 'Subscription ID',
k35f67931: 'Changes apply from your next billing cycle.',
k3777e830: 'Our team',
k37d7b9c4: 'Checking pool inflow...',
k383672e3: 'owner@example.com',
k39437388: 'Core Pool — 1¢ per capsule per member',
k39e2c5db: 'Add Platform',
k3ac8ca10: 'Only SQL dump files are supported.',
k3b03502e: 'Error loading account status',
k3b7dd87a: 'Try again',
k3b8e0964: 'Subscription details',
k3c32c87f: 'Connect with like-minded individuals, share sustainable practices, and make a positive impact together.',
k3c3e6850: 'Email Verification',
k3d01de91: 'PROFIT PLANET',
k3d5fe74a: 'Expires At:',
k3def5ebf: 'Category *',
k3ee27b4f: 'Top-node Email',
k3f833ce6: 'e.g., Platinum Matrix',
k40f4552a: 'Level 2+',
k410ff9a9: 'Total Affiliates',
k416bfe70: 'No further status changes are available for this subscription.',
k4191cdba: 'Edit VAT',
k41f7c81d: 'Delete Account',
k4307f6c7: 'Date of Birth:',
k431328cf: 'No affiliate partners available at the moment.',
k471ba099: 'News Manager',
k47b952de: 'Has Token:',
k47bbd37e: 'Sign in to Profit Planet',
k483aa95a: '• Share authentic experiences',
k48852b8d: 'Customer Email',
k49568342: 'Manage your affiliate partners and tracking links',
k4968eb2a: 'Abonement:',
k49f254bd: 'Current URL:',
k4a055849: 'ID Front',
k4a9e1ebe: 'Loading user details...',
k4b6c7681: 'Open subscriptions',
k4c5e8e87: 'Export CSV',
k4c5ecd73: 'Export PDF',
k4cb62cff: 'Keine Produkte gefunden',
k4db68c96: 'SQL Import',
k4e0c889b: 'Not Ready',
k4e168c01: 'Coffee Abonnements',
k4e532c48: 'Verification Status',
k4e61bc77: 'Root not yet loaded.',
k4ed7f4d1: 'Time Left:',
k502a0057: 'Last 7 days',
k5122ab54: 'Request again',
k51ee3aae: 'email@example.com',
k52af8b8d: 'Quick Actions',
k533db977: 'Your new password',
k54c06343: 'Refresh Token',
k54f49724: 'No users match your search.',
k55aba973: 'Produkte gefunden',
k5614c806: 'Review and verify all users who need admin approval. Users must complete all steps before verification.',
k56435c9b: 'Verfügbarkeit',
k5738c039: 'Matrix created successfully.',
k577a012c: 'User Type',
k578dcc0b: 'PNG, JPG, WebP, SVG up to 5MB',
k58344b74: 'No direct children.',
k5834cbed: 'Loading affiliate partners...',
k58424b1d: 'Eco Warriors',
k5857ef79: 'Existing Pools',
k59422f07: 'Linked Subscription',
k5aae8706: 'New password',
k5c598bc0: 'Trending Groups',
k5d4d494e: 'Loading members...',
k5d85b354: 'Driver\'s License',
k5e580e3f: 'Filter zurücksetzen',
k5ef19112: 'Join our team',
k5f74c123: 'Last 30 days',
k5fb70267: 'Shop wird geladen...',
k5fbf1824: 'Masked names for deeper descendants.',
k61c2a732: 'Angemeldet bleiben',
k61f6cd4e: 'Token Preview:',
k6285753a: 'Back to Pool Management',
k62bc3c59: 'e.g. Berlin',
k62d12fab: 'Error loading data',
k63115bb4: 'ID Documents',
k633438a0: 'Discover our trusted partners and earn commissions through affiliate links.',
k63458f03: 'Produkte durchsuchen...',
k65b67dc3: 'Back to matrices',
k65e33378: 'Total users fetched',
k661c032b: 'You need admin privileges to access this page.',
k664072a1: 'Dev Management',
k67391c88: 'Manage system pools and members.',
k678d2b40: 'Super reduced',
k67cace8b: 'Profile Settings',
k67dd8a82: 'Contact name',
k6828cdd9: 'Affiliate URL *',
k6838438d: 'Ghost Directories',
k6a4108c8: 'Last Name',
k6a486e3e: 'Create Group',
k6aa2d843: 'Read full guidelines',
k6af9037b: 'Open navigation',
k6b0f4f70: 'ID documents or a signed contract are missing for this user. The users verification status should be checked.',
k6b76bd0e: 'Willkommen bei Profit Planet',
k6c6e5c0f: 'Use with caution',
k6ca85cda: 'Trending right now',
k6d85810b: 'Your password',
k6de13000: 'Zero Waste Living',
k6e4a6069: 'Import SQL dump files to run database migrations.',
k70972912: 'Edit Profile',
k70bcafbd: 'Recent Discussions',
k71d565c9: 'Address:',
k72428656: 'Highest full level:',
k73831c06: 'Personal Matrix',
k73cf4fb6: 'Edit News',
k73d110fa: 'Edit Mode',
k73d4a156: 'Check Network tab for /api/refresh requests',
k744fda01: 'My Subscriptions',
k748bf541: 'No users match current filters.',
k75078d0b: 'Add News',
k750c1eb5: 'Add User',
k7572cceb: 'Edit VAT rates',
k75cb45a7: 'This value is stored as net price.',
k75d83433: '• Help others learn and grow',
k77049179: '• Reason:',
k77444d5b: 'Exoscale directories that do not have a matching user in the database.',
k776b751c: 'Policy Max Depth',
k777299de: 'Finance Management',
k77767b9e: 'Close notification',
k77a56aae: 'News & Updates',
k77d5ecd9: 'Copy referral link',
k7938d4fd: 'Result Sets',
k79e1c459: 'Manage all users, view statistics, and handle verification.',
k7ab45054: 'All Readiness',
k7bed84a7: 'Member Since',
k7c19388f: 'e.g., 10%',
k7c740cd5: 'Ready to search. Click the Search button to fetch candidates.',
k7db4e5a9: 'Visit Affiliate Link',
k7f57b169: 'Test API Call',
k7f9568ec: 'Highest full level',
k7fa2c4af: 'Loading users...',
k811fbc99: 'API Base URL:',
k815ca9ba: 'A matrix configuration already exists for this selection.',
k8193b7a2: 'Loading loose files...',
k81a1b900: 'Loading settings…',
k81b056f2: 'See our job postings',
k81c0b74b: 'Status:',
k81c7c2f2: 'Musterstraße 1',
k8323a7d9: 'Loading:',
k832a032b: 'Search affiliates...',
k8358f1d1: 'Loading folder issues...',
k84d5cfcb: 'View overview',
k85446b89: 'Next billing',
k85682289: 'e.g. FN123456a',
k85c66f50: 'Search & Filter Pending Users',
k867f8265: 'Due Date',
k86aa4f9c: 'Current Month',
k87e4b9a2: 'Core Pool',
k883ea8c5: 'Loading ghost directories...',
k88d8bb9d: 'Passwort vergessen?',
k890ff52f: 'e.g., Coffee Equipment Co.',
k8a35cc53: 'SQL dumps run immediately and can modify production data.',
k8a59b156: 'Import SQL',
k8b71f0c7: 'Email, name, company...',
k8b89f863: 'External partner website.',
k8bb1c673: 'Email:',
k8be14d47: 'Error:',
k8c3085f4: 'Users ↓',
k8c3085f6: 'Users ↑',
k8d84b4c5: 'Add New Affiliate',
k8dda5201: 'you@example.com',
k8eaa7b3b: 'e.g. +43 1 234567',
k8eab7c16: 'National ID',
k8eb2524c: 'Enter 6-digit code',
k8f46c81e: 'e.g., 5',
k8f528877: 'Crop & Adjust Image',
k915115a9: 'Last 90 days',
k91912619: 'Close navigation',
k91e69df1: 'ProfitPlanet GmbH',
k91eb415a: 'ProfitPlanet Logo',
k91f24187: 'Complete Profile',
k9213db6e: '📋 Testing Instructions',
k93165aea: '12345 Berlin',
k93b6dc1b: 'Uploaded Documents',
k93f03bca: 'Signed Contract Document',
k941fd092: 'Last Folder Structure Action',
k955b1cbe: 'No results',
k959fb1a6: 'Remove member from pool?',
k961ba411: 'Community Guidelines',
k9683262f: 'Fill %',
k96dbbe05: 'Street & House Number',
k972cee5e: 'Front:',
k9772afa4: 'No included items were returned for this subscription.',
k97abed7d: 'Tax Certificate',
k981b1f1a: 'SQL Dump Import',
k98519a5e: 'I have read, understood, and agree to the terms and conditions of this service agreement.',
k9860434f: 'Please review and upload your signed service agreement.',
k9b3266b5: 'Pending backend wiring to MatrixController.listVacancies. This section will surface empty slots and allow reassignment.',
k9bc83f50: 'Change coffees for next month',
k9c3db145: 'Start Discussion',
k9d0c063d: 'Password saved. Redirecting to login...',
k9e609523: 'No missing folders found. Run Refresh to scan again.',
k9f29dbfb: 'Entdecke nachhaltige Produkte und verdiene dabei. Deine Plattform für bewussten Konsum und finanzielle Vorteile.',
k9f56d4ac: 'e.g. +43 676 1234567',
ka00fc5db: 'Manage your account information and preferences',
ka15f5ec5: 'Auth Store State',
ka1d0b6ff: 'No deeper descendants.',
ka29ac729: 'Saved successfully',
ka3c41ff8: 'View details →',
ka3cbb536: 'Continue →',
ka4ecb6cd: 'Search…',
ka5bf342b: 'Select…',
ka5c2113f: 'Company Name:',
ka5d50257: 'Loading VAT rates…',
ka6be28d2: 'Add user to pool',
ka7073aee: 'Profit Planet Store',
ka72e833f: 'Policy filter:',
ka8ea17b8: 'Next ',
ka991f523: 'Loading affiliates...',
ka9d6e905: 'Total users under me',
kaa656770: 'You are not part of any matrix yet.',
kaa8231ec: 'This Year',
kab4f5159: 'Each node can hold up to 5 direct children. Depth unbounded.',
kab99811e: 'Disabled message',
kace2fe51: 'Verification Code',
kadc6abcf: 'Auth Debug Page',
kadd80fbc: 'Clear selection',
kaf787fe5: '1,284 members',
kb0031873: 'Browse all trending',
kb01addda: 'Token should automatically renew without user action',
kb1341138: 'Ensures both contract and gdpr folders exist for each user.',
kb24782ec: 'Last Login',
kb2dfe482: 'Read More',
kb324fb25: 'Total Users',
kb337d94e: 'No entries found.',
kb343460d: 'Rogue users',
kb35549bb: 'Search name or email…',
kb383a3e8: 'Included in your subscription',
kb45c4d5f: 'Tax ID:',
kb4675362: 'Import Results',
kb4aba3dc: 'No unverified users match current filters.',
kb573897d: 'Short description of the pool',
kb5e0b861: 'Inactive Pools',
kb6b367b7: 'When time left ≤ 3 minutes, auto-refresh should trigger',
kb6eacc9d: 'Select a .sql dump file using Import SQL.',
kb74d7c51: '🔧 Manual Controls',
kb7849a5a: 'Create Matrix',
kb846955a: 'Select Document Type',
kb87eb38b: 'Enter at least 3 characters and click Search.',
kb8cd2810: 'Account Status',
kb8d6f3f7: 'Shop Collection',
kbbefb159: 'Error loading users',
kbc368b5d: 'Date of Birth',
kbc6a6543: 'ID Back',
kbce9fbea: 'No platforms configured.',
kbd8b3364: 'Sign Contract',
kbd979e13: 'We are a community',
kbdb02e32: 'Keine Rechnungen gefunden.',
kbe9355f8: 'Business License',
kbf4b7789: 'You are already logged in. Redirecting...',
kbf7bde57: 'Select any subscription to view details and included items.',
kbfa5b4c5: 'Tax ID',
kbfd13a03: 'Account Setup',
kbff01823: 'Shows files directly under the user folder that are not in contract or gdpr.',
kc0d718d7: 'Registration Number',
kc0e3b03d: 'Total:',
kc3d181e2: 'Forgot password?',
kc4315932: 'Dashboard Management',
kc4d7816e: 'Levels filled',
kc7bb0c06: 'Filter by country or code',
kc7c429a6: 'Add users to matrix',
kc813a103: 'Loading available coffees…',
kc8652e34: 'You dont have any subscriptions yet.',
kc9d9d15d: 'Postal Code',
kca04f5e3: 'Phone:',
kcada239b: 'View your active subscriptions, included items and subscription details on a dedicated page.',
kcb491706: 'Folder Structure',
kcbc17bbd: 'No users in this pool yet.',
kcc15636b: 'Coffee content can only be changed while a subscription is issued, ongoing, or paused.',
kcc1c5596: 'Profit Planet Mascot',
kccbc54c1: 'Delete Affiliate',
kccc13f16: '← Go back',
kccde6d86: 'User Verification Center',
kccf7593a: '• Be respectful and kind',
kcd7a1625: 'deine@email.com',
kcd9890e5: 'PNG, JPG, WebP up to 5MB',
kcdfef775: 'Loading subscriptions…',
kce0ab46c: 'Dein Passwort',
kcf4ba87d: 'Crop Affiliate Logo',
kcf61fc9e: 'Last Loose Files Action',
kd00443f2: 'Go to Dashboard',
kd04a7c59: 'Matrix Name',
kd058bb7b: 'Missing:',
kd09be3cd: 'Matrix Management',
kd1c17b3f: 'Alle Marken',
kd1f35ccf: 'Search & Filter Users',
kd2e35b08: 'Rows per page',
kd2e5e813: '• Already booked:',
kd304af2e: 'Global search...',
kd40c4f86: 'Activate this pool',
kd49dc1e1: 'Pool Type',
kd4a0fd1e: 'Pool Name',
kd4af6368: 'Issue Date',
kd4d50566: 'ID documents or a signed contract are missing from object storage. The users verification status should be checked.',
kd4eb7ee0: 'Close menu',
kd51f320c: 'Exoscale Folder Structure',
kd56a13f2: 'Recipient Email',
kd5cca6e9: '? This action cannot be undone.',
kd6024811: 'PDF File',
kd642e230: 'Search by name or email. Minimum 3 characters. Existing matrix members are hidden.',
kd68da70d: 'Nachhaltige Produkte für deinen Erfolg',
kd89474fa: 'Back to News',
kda96f5b3: 'Matrix Depth',
kdb27a82d: ' Previous',
kdbba338d: 'Registration Number:',
kdc22ad8a: 'Manage matrices, see stats, and create new ones.',
kdc47630b: 'Matrix fill:',
kdca959c3: 'Discover a curated selection of high-quality products that cater to your every need.',
kde1c3c69: 'Logo Image',
kde2b4fa0: 'Result Summary',
ke0a3528a: 'Passwords do not match.',
ke17859b2: 'Business Registration',
ke19afb3d: 'Archive this pool',
ke1abc7d9: 'Add Affiliate',
ke24abf9c: 'Edit coffee content',
ke3889dc2: 'Loading news...',
ke4c4a858: 'Min. 3 characters',
ke697b8cb: 'Set Active',
ke8b9f33c: 'Total in Pool',
ke9e71971: 'Oder weiter mit',
kebf33594: 'Filter by category:',
kec5a5357: 'Upload Invoice',
keccee79f: 'Email address',
ked60db76: 'Back to login',
kee28b8c6: '🔑 Token Status',
kef1656df: 'Apply Crop',
kefd5231d: 'Depth 5',
kf0646f35: 'Our values',
kf0d33884: 'Check browser console for detailed logs',
kf0eef57e: 'Total members:',
kf2147f07: 'Not provided',
kf2180ff6: 'Manage VAT rates',
kf27e4502: 'Ready to Verify',
kf2a1257e: 'Back to profile',
kf2b5c1a6: 'Customer Name',
kf2d8db2b: 'Updating status...',
kf340aa10: 'Loose files:',
kf3557acd: '5ary Tree',
kf3b81ba3: 'Used in the URL. Auto-generated from title unless edited.',
kf4868273: 'Click to upload',
kf4f44e2f: 'e.g. ATU12345678',
kf530c357: 'Anmeldung läuft...',
kf663ef67: 'Shop with an infinite variety of products',
kf69154f8: '• Stay on topic',
kf70b9896: 'e.g. 12345',
kf7189e80: '🔄 Manual Refresh Token',
kf78c9087: 'Immediate children',
kf7a91674: 'Policy ↑',
kf7a91676: 'Policy ↓',
kf823daf7: 'Fallback to root if referral parent not in matrix',
kf8c220d3: 'kunde@example.com',
kf971ea7f: 'Created:',
kfaa8fc4a: '• Will book:',
kfb1676b0: 'Phone number *',
kfb37e056: 'Last Login:',
kfb92efe9: 'Description *',
kfce271a2: 'Node Env:',
kfdcad59b: 'Send Email Report',
kfe8083f8: 'First Name',
k17581b31: 'Next page',
k2108b5a0: 'No invoices found for this subscription.',
k34a0a2e4: 'Select the files where you want to run i18n auto-fix.',
k41f3daea: 'Billed monthly',
k43218db0: 'Fix Targets',
k49e51b5f: 'Current plan',
k4bfb4f28: 'Feature comparison',
k4c6eb72c: 'Select all',
k4f209a66: 'You currently dont have an active subscription.',
k5b7042c7: 'Previous page',
k60b1e339: 'No media or documents found.',
k6569783c: 'Use this to include server-style files. Files with server-only Next.js APIs are skipped for safety.',
k68c88f41: 'Force convert selected files to client components before auto-fix',
k74914369: 'Delete Item',
k772cc77b: 'Complete your profile to unlock all features',
k7fa55432: 'My Subscription',
k86b03343: 'Billed annually',
k8953de89: 'Finance & Invoices',
k947d8777: 'Invoice #',
ka5603827: 'Loading invoices…',
ka86bdc9b: 'Payment frequency',
kb3243742: 'No file',
kc48b877b: 'No subscription selected. Invoices will appear once you have an active subscription.',
kd08b698a: 'Profile Completion',
ke3480838: 'No fixable hardcoded UI text detected in eligible components.',
ked7d533b: 'Media & Documents',
kf5ac16fb: 'Pricing that grows with you',
kf9f94d5e: 'Buy this plan',
kfd632d02: 'Export all invoices',
k5d4f6b2f: 'Bank Information',
k9dafde30: 'Contact Person',
kada9d61c: 'Account Holder',
kde6d477f: 'Email Address',
kfc6b6a29: 'Editing disabled',
k03538639: 'e.g. fr, es, zh-TW',
k5fcc9b0e: 'Delete language',
k9bd0812b: 'Shows why a file was changed, skipped, or left untouched after a fix attempt.',
ka019b3c0: 'e.g. Français',
kbe30c353: 'Coverage by namespace',
kbf49d59b: 'Search keys or English text…',
kc8034db6: 'Auto-fix debug logs',
k0cdc3ee9: 'Assign namespace...',
k1db86f96: 'Create tab',
k505ebdae: 'Unassigned namespaces',
k5f978731: 'Category tabs',
k66edf1eb: 'Drag into a tab card',
k741a01f7: 'All namespaces are categorized.',
ka6791a02: 'Remove from tab',
kb7a30760: 'Filter namespaces quickly or open manager to create and assign tabs.',
kc4671abe: 'Create tabs and assign namespaces by drag-and-drop or dropdown.',
kd6e42900: 'Manage tabs',
ke52ed6e9: 'New tab name',
kef9de7f0: 'Manage category tabs',
kf3c3223a: 'Drop a namespace here.',
k0700b1f2: 'No global keys yet. Mark keys as global in the translation wizard, or add one from the dropdown above.',
k47bce570: 'Select key to add to global',
k5e5e8744: 'Translation wizard available',
k6aba2cb0: 'Back to panels',
k6cfeedd3: 'Global terms',
k725dd1d6: 'Start wizard',
kad7d8c49: 'Use this list for cross-context terms like IBAN.',
kc02b17c3: 'Remove from global terms',
kc518ff5c: 'English reference',
kcd190bdd: 'Translation wizard',
kfd1e0089: 'Auto-scroll on panel open',
},
// ─── Notifications / Toasts ────────────────────────────
toasts: {
loginSuccess: 'Login successful',

View File

@ -145,6 +145,36 @@ export interface Translations {
sessionDetectedMessage: string;
sessionContinue: string;
sessionLogout: string;
formTitle: string;
guestRegistration: string;
registerNow: string;
guestDescription: string;
personalDescription: string;
invitedBy: string;
tabIndividual: string;
submitCompany: string;
submitGuest: string;
successRedirecting: string;
guestNote: string;
errorBothCountryCodes: string;
successCompanyMessage: string;
successGuestMessage: string;
failedTitle: string;
failedMessage: string;
networkErrorGeneric: string;
passwordRequirements: string;
pwdMinLength: string;
pwdLowercase: string;
pwdUppercase: string;
pwdDigits: string;
pwdSpecial: string;
invalidLinkTitle: string;
invalidLinkMessage: string;
tokenLabel: string;
sessionDescription: string;
goToDashboard: string;
goToHomepage: string;
loginHere: string;
};
passwordReset: {
@ -368,16 +398,111 @@ export interface Translations {
referralManagement: {
title: string;
subtitle: string;
description: string;
createLink: string;
copyLink: string;
copy: string;
copyMobile: string;
copied: string;
copiedToClipboard: string;
linkExpiry: string;
noLinks: string;
generating: string;
generateLink: string;
usesRemaining: string;
unlimited: string;
never: string;
createSuccess: string;
createError: string;
deactivate: string;
deactivated: string;
deactivatedMessage: string;
deactivateFailed: string;
deactivateFailedMessage: string;
deactivateNetworkError: string;
deactivateModalTitle: string;
deactivateModalDescription: string;
linkLabel: string;
accessCheckFailed: string;
userIdMissing: string;
accessDenied: string;
accessDeniedMessage: string;
permCheckFailed: string;
permCheckFailedMessage: string;
loadFailed: string;
loadStatsError: string;
loadLinksError: string;
copyFailed: string;
copyFailedMessage: string;
copiedMessage: string;
allLinks: string;
allLinksSubtitle: string;
colLink: string;
colCreated: string;
colExpires: string;
colUsage: string;
colStatus: string;
generateTitle: string;
maxUsesLabel: string;
expiresIn: string;
lockedByNeverExpires: string;
lockedByUnlimited: string;
statsActiveLinks: string;
statsLinksUsed: string;
statsPersonalUsers: string;
statsCompanyUsers: string;
statsTotalLinks: string;
levelStarter: string;
levelNovice: string;
levelHustler: string;
levelEntrepreneur: string;
levelPrestige: string;
levelMax: string;
levelLabel: string;
referrals: string;
of: string;
maxLevelReached: string;
nextMilestone: string;
registeredUsersTitle: string;
totalRefBadge: string;
registeredUsersSubtitle: string;
showingLatest5: string;
viewAll: string;
colUser: string;
colEmail: string;
colType: string;
colRegistered: string;
noRegisteredUsers: string;
typeCompany: string;
typePersonal: string;
allRegisteredUsersTitle: string;
allRegisteredUsersSubtitle: string;
exportCsv: string;
searchPlaceholder: string;
filterAllTypes: string;
filterAllStatus: string;
filterActive: string;
filterInactive: string;
filterPending: string;
filterBlocked: string;
noUsersMatchFilters: string;
showing: string;
pagePrev: string;
pageNext: string;
pageOf: string;
expiry1Day: string;
expiry2Days: string;
expiry3Days: string;
expiry4Days: string;
expiry5Days: string;
expiry6Days: string;
expiry7Days: string;
expiryNever: string;
maxUses1: string;
maxUses5: string;
maxUses10: string;
maxUses50: string;
maxUsesUnlimited: string;
};
quickactionDashboard: {
@ -759,6 +884,7 @@ export interface Translations {
codeDuplicate: string;
codeRequired: string;
nameRequired: string;
wizardInputPlaceholder: string;
};
contractManagement: {
@ -892,6 +1018,8 @@ export interface Translations {
reasonNoActivePools: string;
};
autofix: Record<string, string>;
// ─── Notifications / Toasts ────────────────────────────
toasts: {
loginSuccess: string;

View File

@ -1,22 +1,47 @@
'use client';
import { createContext, useContext, useState, useEffect, useCallback, ReactNode } from 'react';
import { Language, DEFAULT_LANGUAGE } from './config';
import { DEFAULT_LANGUAGE } from './config';
import { en } from './translations/en';
import { de } from './translations/de';
import { flattenObject, loadCustomI18n, type CustomI18nData } from './dynamicTranslations';
import { flattenObject } from './dynamicTranslations';
const builtInTranslations: Record<string, Record<string, any>> = { en, de };
type LanguageEntry = {
code: string;
name: string;
};
type TranslationFilesPayload = {
languages: LanguageEntry[];
translations: Record<string, Record<string, string>>;
};
const builtInTranslations: Record<string, Record<string, unknown>> = {
en: en as unknown as Record<string, unknown>,
de: de as unknown as Record<string, unknown>,
};
// Flat map of English keys used as canonical key list and fallback
const enFlat = flattenObject(en as Record<string, any>);
const enFlat = flattenObject(en as unknown as Record<string, unknown>);
function getNestedValue(root: Record<string, unknown>, key: string): string | null {
const segments = key.split('.');
let current: unknown = root;
for (const segment of segments) {
if (!current || typeof current !== 'object') return null;
current = (current as Record<string, unknown>)[segment];
}
return typeof current === 'string' ? current : null;
}
interface I18nContextType {
language: string;
setLanguage: (lang: string) => void;
t: (key: string) => string;
customI18n: CustomI18nData;
reloadCustomI18n: () => void;
languages: LanguageEntry[];
reloadTranslations: () => Promise<void>;
}
const I18nContext = createContext<I18nContextType | undefined>(undefined);
@ -27,38 +52,84 @@ interface I18nProviderProps {
export function I18nProvider({ children }: I18nProviderProps) {
const [language, setLanguage] = useState<string>(DEFAULT_LANGUAGE);
const [customI18n, setCustomI18n] = useState<CustomI18nData>({ languages: [], translations: {} });
const [translationFiles, setTranslationFiles] = useState<TranslationFilesPayload>({
languages: [
{ code: 'en', name: 'English' },
{ code: 'de', name: 'Deutsch' },
],
translations: {
en: enFlat,
de: flattenObject(de as unknown as Record<string, unknown>),
},
});
const reloadCustomI18n = useCallback(() => {
setCustomI18n(loadCustomI18n());
const reloadTranslations = useCallback(async () => {
try {
const response = await fetch('/api/i18n/translations', { cache: 'no-store' });
const result = await response.json();
if (!response.ok || !result?.ok) return;
const languages = Array.isArray(result.languages)
? result.languages.filter((lang: unknown): lang is LanguageEntry => {
if (!lang || typeof lang !== 'object') return false;
const entry = lang as LanguageEntry;
return typeof entry.code === 'string' && typeof entry.name === 'string';
})
: [];
const translations = result.translations && typeof result.translations === 'object'
? result.translations as Record<string, Record<string, string>>
: {};
if (languages.length > 0) {
setTranslationFiles({ languages, translations });
}
} catch {
// Keep built-in fallback translations if API is unavailable.
}
}, []);
useEffect(() => {
reloadCustomI18n();
}, [reloadCustomI18n]);
void reloadTranslations();
}, [reloadTranslations]);
useEffect(() => {
if (translationFiles.languages.length === 0) return;
if (translationFiles.languages.some((entry) => entry.code === language)) return;
const fallback = translationFiles.languages.find((entry) => entry.code === DEFAULT_LANGUAGE)?.code
?? translationFiles.languages[0]?.code
?? DEFAULT_LANGUAGE;
setLanguage(fallback);
}, [translationFiles.languages, language]);
const t = useCallback((key: string): string => {
// 1. Check custom translation overrides for this language
const customOverride = customI18n.translations[language]?.[key];
if (customOverride !== undefined && customOverride !== '') return customOverride;
// 1. Check translation loaded from translation files API.
const fileValue = translationFiles.translations[language]?.[key];
if (fileValue !== undefined && fileValue !== '') return fileValue;
// 2. Check built-in translations (nested lookup)
// 2. Check built-in static imports (works even before API load).
const builtIn = builtInTranslations[language];
if (builtIn) {
const keys = key.split('.');
let value: any = builtIn;
for (const k of keys) {
value = value?.[k];
}
if (typeof value === 'string') return value;
const value = getNestedValue(builtIn, key);
if (value !== null) return value;
}
// 3. Fallback to English (flat map)
// 3. Fallback to English.
return enFlat[key] ?? key;
}, [language, customI18n]);
}, [language, translationFiles.translations]);
return (
<I18nContext.Provider value={{ language, setLanguage, t, customI18n, reloadCustomI18n }}>
<I18nContext.Provider
value={{
language,
setLanguage,
t,
languages: translationFiles.languages,
reloadTranslations,
}}
>
{children}
</I18nContext.Provider>
);
@ -81,10 +152,3 @@ export function getAllTranslationKeys(): string[] {
export function getEnglishValue(key: string): string {
return enFlat[key] ?? key;
}
/** Returns flat translations for a built-in language */
export function getBuiltInFlatTranslations(langCode: string): Record<string, string> {
const builtIn = builtInTranslations[langCode];
if (!builtIn) return {};
return flattenObject(builtIn);
}

View File

@ -1,5 +1,8 @@
'use client'
import { useTranslation } from '../../i18n/useTranslation';
import { useState, useEffect } from 'react'
import { useRouter } from 'next/navigation'
import { EyeIcon, EyeSlashIcon } from '@heroicons/react/24/outline'
@ -9,6 +12,7 @@ import { useToast } from '../../components/toast/toastComponent'
const GLASS_BG = 'rgba(255,255,255,0.55)'
export default function LoginForm() {
const { t } = useTranslation();
const [showPassword, setShowPassword] = useState(false)
const [formData, setFormData] = useState({
email: '',
@ -158,12 +162,8 @@ export default function LoginForm() {
>
{/* Title + Subtitle (restored) */}
<div className="mb-6 text-center">
<h1 className="text-2xl md:text-3xl font-extrabold tracking-tight text-[#0F172A] drop-shadow-sm">
PROFIT PLANET
</h1>
<p className="mt-1 text-sm md:text-base text-slate-700/90">
Welcome back! Log in to continue.
</p>
<h1 className="text-2xl md:text-3xl font-extrabold tracking-tight text-[#0F172A] drop-shadow-sm">{t('autofix.k3d01de91')}</h1>
<p className="mt-1 text-sm md:text-base text-slate-700/90">{t('autofix.k00394342')}</p>
</div>
<form
@ -182,9 +182,7 @@ export default function LoginForm() {
fontSize: isMobile ? '0.875rem' : isTablet ? '0.9rem' : undefined,
marginBottom: isMobile ? '0.25rem' : undefined,
}}
>
Email address
</label>
>{t('autofix.keccee79f')}</label>
<input
id="email"
name="email"
@ -197,7 +195,7 @@ export default function LoginForm() {
fontSize: isMobile ? '0.95rem' : isTablet ? '1rem' : '1rem',
padding: isMobile ? '0.5rem 0.75rem' : isTablet ? '0.6rem 0.875rem' : '0.7rem 1rem',
}}
placeholder="you@example.com"
placeholder={t('autofix.k8dda5201')}
required
/>
</div>
@ -231,7 +229,7 @@ export default function LoginForm() {
? '0.6rem 2.75rem 0.6rem 0.875rem'
: '0.7rem 3rem 0.7rem 1rem',
}}
placeholder="Your password"
placeholder={t('autofix.k6d85810b')}
required
/>
<button
@ -272,9 +270,7 @@ export default function LoginForm() {
>
{loading ? (
<div className="flex items-center justify-center gap-2">
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-current"></div>
Signing in...
</div>
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-current"></div>{t('autofix.k2786bc5f')}</div>
) : (
'Sign in'
)}
@ -287,9 +283,7 @@ export default function LoginForm() {
type="button"
className="text-[#8D6B1D] hover:text-[#7A5E1A] hover:underline text-sm font-medium transition-colors"
onClick={() => router.push("/password-reset")}
>
Forgot password?
</button>
>{t('autofix.kc3d181e2')}</button>
</div>
</form>
</div>

View File

@ -1,5 +1,8 @@
'use client'
import { useTranslation } from '../../i18n/useTranslation';
import { useState } from 'react'
import { useRouter } from 'next/navigation'
import Image from 'next/image'
@ -7,6 +10,7 @@ import { EyeIcon, EyeSlashIcon } from '@heroicons/react/24/outline'
import { useLogin } from '../hooks/useLogin'
export default function LoginForm() {
const { t } = useTranslation();
const [showPassword, setShowPassword] = useState(false)
const [formData, setFormData] = useState({
email: '',
@ -68,14 +72,10 @@ export default function LoginForm() {
<div className="flex flex-1 flex-col justify-center px-4 py-2 sm:px-6 lg:flex-none lg:px-20 xl:px-24 bg-[#0F172A]">
<div className="mx-auto w-full max-w-sm lg:w-96">
<div>
<h2 className="mt-8 text-2xl/9 font-bold tracking-tight text-white">
Sign in to Profit Planet
</h2>
<h2 className="mt-8 text-2xl/9 font-bold tracking-tight text-white">{t('autofix.k47bbd37e')}</h2>
<p className="mt-2 text-sm/6 text-gray-400">
Noch kein Mitglied?{' '}
<a href="/register" className="font-semibold text-[#8D6B1D] hover:text-[#A67C20]">
Jetzt registrieren
</a>
<a href="/register" className="font-semibold text-[#8D6B1D] hover:text-[#A67C20]">{t('autofix.k0fbaa1a9')}</a>
</p>
</div>
@ -96,7 +96,7 @@ export default function LoginForm() {
autoComplete="email"
value={formData.email}
onChange={handleInputChange}
placeholder="deine@email.com"
placeholder={t('autofix.kcd7a1625')}
className="block w-full rounded-md bg-white/5 px-3 py-1.5 text-base text-white outline-1 -outline-offset-1 outline-white/10 placeholder:text-gray-500 focus:outline-2 focus:-outline-offset-2 focus:outline-[#8D6B1D] sm:text-sm/6"
/>
</div>
@ -116,7 +116,7 @@ export default function LoginForm() {
autoComplete="current-password"
value={formData.password}
onChange={handleInputChange}
placeholder="Dein Passwort"
placeholder={t('autofix.kce0ab46c')}
className="block w-full rounded-md bg-white/5 px-3 py-1.5 pr-10 text-base text-white outline-1 -outline-offset-1 outline-white/10 placeholder:text-gray-500 focus:outline-2 focus:-outline-offset-2 focus:outline-[#8D6B1D] sm:text-sm/6"
/>
<button
@ -161,9 +161,7 @@ export default function LoginForm() {
</svg>
</div>
</div>
<label htmlFor="rememberMe" className="block text-sm/6 text-gray-300">
Angemeldet bleiben
</label>
<label htmlFor="rememberMe" className="block text-sm/6 text-gray-300">{t('autofix.k61c2a732')}</label>
</div>
<div className="text-sm/6">
@ -171,9 +169,7 @@ export default function LoginForm() {
type="button"
onClick={() => router.push("/password-reset")}
className="font-semibold text-[#8D6B1D] hover:text-[#A67C20]"
>
Passwort vergessen?
</button>
>{t('autofix.k88d8bb9d')}</button>
</div>
</div>
@ -193,9 +189,7 @@ export default function LoginForm() {
>
{loading ? (
<div className="flex items-center">
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
Anmeldung läuft...
</div>
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>{t('autofix.kf530c357')}</div>
) : (
'Anmelden'
)}
@ -211,7 +205,7 @@ export default function LoginForm() {
<div className="w-full border-t border-gray-700" />
</div>
<div className="relative flex justify-center text-sm/6 font-medium">
<span className="bg-[#0F172A] px-6 text-gray-300">Oder weiter mit</span>
<span className="bg-[#0F172A] px-6 text-gray-300">{t('autofix.ke9e71971')}</span>
</div>
</div>
@ -263,19 +257,15 @@ export default function LoginForm() {
{/* Right Side Image */}
<div className="relative hidden w-0 flex-1 lg:block overflow-hidden bg-[#0F172A]">
<img
alt="Community Hands - Profit Planet"
alt={t('autofix.k17ba59ff')}
src="/images/misc/community_hands.jpg"
className="absolute inset-0 size-full object-cover rounded-l-3xl"
/>
{/* Overlay with branding */}
<div className="absolute inset-0 bg-gradient-to-br from-[#8D6B1D]/20 via-transparent to-[#8D6B1D]/10" />
<div className="absolute bottom-8 left-8 right-8">
<h3 className="text-3xl font-bold text-white mb-4 drop-shadow-lg">
Willkommen bei Profit Planet
</h3>
<p className="text-lg text-white/90 drop-shadow-md">
Entdecke nachhaltige Produkte und verdiene dabei. Deine Plattform für bewussten Konsum und finanzielle Vorteile.
</p>
<h3 className="text-3xl font-bold text-white mb-4 drop-shadow-lg">{t('autofix.k6b76bd0e')}</h3>
<p className="text-lg text-white/90 drop-shadow-md">{t('autofix.k9f29dbfb')}</p>
</div>
</div>
</div>

View File

@ -1,5 +1,8 @@
'use client'
import { useTranslation } from '../i18n/useTranslation';
import { useState, useEffect } from 'react'
import { useRouter } from 'next/navigation'
import LoginForm from './components/LoginForm'
@ -12,6 +15,7 @@ import BlueBlurryBackground from '../components/background/blueblurry' // NEW
import CurvedLoop from '../components/curvedLoop'
export default function LoginPage() {
const { t } = useTranslation();
const [hasHydrated, setHasHydrated] = useState(false)
const [isMobile, setIsMobile] = useState(false)
const router = useRouter()
@ -50,7 +54,7 @@ export default function LoginPage() {
<div className="min-h-screen flex items-center justify-center">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-[#8D6B1D] mx-auto mb-4"></div>
<p className="text-slate-700">You are already logged in. Redirecting...</p>
<p className="text-slate-700">{t('autofix.kbf4b7789')}</p>
</div>
</div>
</PageLayout>

View File

@ -1,3 +1,8 @@
'use client';
import { useTranslation } from '../i18n/useTranslation';
import { CheckIcon, XMarkIcon } from '@heroicons/react/20/solid'
import PageLayout from '../components/PageLayout'
@ -76,21 +81,20 @@ function classNames(...classes: (string | undefined | null | false)[]): string {
}
export default function MembershipsPage() {
const { t } = useTranslation();
return (
<PageLayout>
<form className="group/tiers isolate bg-gray-900 pb-24">
<div className="flow-root border-b border-b-white/5 bg-gray-800/25 pt-24 pb-16 sm:pt-32 lg:pb-0">
<div className="mx-auto max-w-7xl px-6 lg:px-8">
<div className="relative z-10">
<h2 className="mx-auto max-w-4xl text-center text-5xl font-semibold tracking-tight text-balance text-white sm:text-6xl">
Pricing that grows with you
</h2>
<h2 className="mx-auto max-w-4xl text-center text-5xl font-semibold tracking-tight text-balance text-white sm:text-6xl">{t('autofix.kf5ac16fb')}</h2>
<p className="mx-auto mt-6 max-w-2xl text-center text-lg font-medium text-pretty text-gray-400 sm:text-xl/8">
Choose an affordable plan thats packed with the best features for engaging your audience, creating
customer loyalty, and driving sales.
</p>
<div className="mt-16 flex justify-center">
<fieldset aria-label="Payment frequency">
<fieldset aria-label={t('autofix.ka86bdc9b')}>
<div className="grid grid-cols-2 gap-x-1 rounded-full bg-white/5 p-1 text-center text-xs/5 font-semibold text-white">
<label className="group relative rounded-full px-2.5 py-1 has-checked:bg-indigo-500">
<input
@ -158,12 +162,8 @@ export default function MembershipsPage() {
</p>
<div className="text-sm">
<p className="text-white">USD</p>
<p className="text-gray-400 group-not-has-[[name=frequency][value=monthly]:checked]/tiers:hidden">
Billed monthly
</p>
<p className="text-gray-400 group-not-has-[[name=frequency][value=annually]:checked]/tiers:hidden">
Billed annually
</p>
<p className="text-gray-400 group-not-has-[[name=frequency][value=monthly]:checked]/tiers:hidden">{t('autofix.k41f3daea')}</p>
<p className="text-gray-400 group-not-has-[[name=frequency][value=annually]:checked]/tiers:hidden">{t('autofix.k86b03343')}</p>
</div>
</div>
<button
@ -172,9 +172,7 @@ export default function MembershipsPage() {
type="submit"
aria-describedby={`tier-${tier.id}`}
className="w-full rounded-md bg-white/10 px-3 py-2 text-center text-sm/6 font-semibold text-white not-group-data-featured:inset-ring not-group-data-featured:inset-ring-white/5 group-data-featured/tier:bg-indigo-500 hover:bg-white/20 group-data-featured/tier:hover:bg-indigo-400 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-white/75 group-data-featured/tier:focus-visible:outline-indigo-500"
>
Buy this plan
</button>
>{t('autofix.kf9f94d5e')}</button>
</div>
<div className="mt-8 flow-root sm:mt-10">
<ul
@ -202,9 +200,7 @@ export default function MembershipsPage() {
<div className="mx-auto max-w-7xl px-6 py-24 sm:py-32 lg:px-8">
{/* Feature comparison (up to lg) */}
<section aria-labelledby="mobile-comparison-heading" className="lg:hidden">
<h2 id="mobile-comparison-heading" className="sr-only">
Feature comparison
</h2>
<h2 id="mobile-comparison-heading" className="sr-only">{t('autofix.k4bfb4f28')}</h2>
<div className="mx-auto max-w-2xl space-y-16">
{tiers.map((tier) => (
@ -293,9 +289,7 @@ export default function MembershipsPage() {
{/* Feature comparison (lg+) */}
<section aria-labelledby="comparison-heading" className="hidden lg:block">
<h2 id="comparison-heading" className="sr-only">
Feature comparison
</h2>
<h2 id="comparison-heading" className="sr-only">{t('autofix.k4bfb4f28')}</h2>
<div className="grid grid-cols-4 gap-x-8 border-t border-white/10 before:block">
{tiers.map((tier) => (

View File

@ -1,5 +1,8 @@
'use client'
import { useTranslation } from '../../i18n/useTranslation';
import React from 'react'
import Link from 'next/link'
import { useParams } from 'next/navigation'
@ -25,6 +28,7 @@ function estimateReadingTime(text: string) {
}
export default function NewsDetailPage() {
const { t } = useTranslation();
const params = useParams()
const slug = typeof params?.slug === 'string' ? params.slug : Array.isArray(params?.slug) ? params.slug[0] : ''
@ -90,9 +94,7 @@ export default function NewsDetailPage() {
{error && !loading && (
<div className="rounded-2xl bg-white shadow-lg border border-red-100 p-8">
<div className="text-red-700 font-medium mb-2">{error}</div>
<Link href="/news" className="text-blue-900 hover:text-blue-700 font-semibold">
Back to News
</Link>
<Link href="/news" className="text-blue-900 hover:text-blue-700 font-semibold">{t('autofix.kd89474fa')}</Link>
</div>
)}

View File

@ -1,5 +1,8 @@
'use client'
import { useTranslation } from '../i18n/useTranslation';
import React from 'react'
import Link from 'next/link'
import Header from '../components/nav/Header'
@ -18,6 +21,7 @@ type PublicNewsItem = {
}
export default function NewsPage() {
const { t } = useTranslation();
const [items, setItems] = React.useState<PublicNewsItem[]>([])
const [loading, setLoading] = React.useState(false)
const [error, setError] = React.useState<string | null>(null)
@ -48,8 +52,8 @@ export default function NewsPage() {
{/* Hero Section */}
<div className="bg-blue-900 text-white py-16">
<div className="mx-auto max-w-7xl px-6">
<h1 className="text-5xl font-bold mb-4">News & Updates</h1>
<p className="text-xl text-blue-100">Stay informed with our latest announcements and insights</p>
<h1 className="text-5xl font-bold mb-4">{t('autofix.k77a56aae')}</h1>
<p className="text-xl text-blue-100">{t('autofix.k27e93fd7')}</p>
</div>
</div>
@ -61,12 +65,12 @@ export default function NewsPage() {
)}
{loading && (
<div className="text-center py-12 text-gray-500">Loading news...</div>
<div className="text-center py-12 text-gray-500">{t('autofix.ke3889dc2')}</div>
)}
{!loading && items.length === 0 && !error && (
<div className="text-center py-12">
<p className="text-gray-500 text-lg">No news articles available yet.</p>
<p className="text-gray-500 text-lg">{t('autofix.k2f176a63')}</p>
</div>
)}
@ -106,9 +110,7 @@ export default function NewsPage() {
{item.summary && (
<p className="mt-3 text-gray-700 line-clamp-3">{item.summary}</p>
)}
<div className="mt-4 inline-flex items-center text-blue-900 font-semibold group-hover:text-blue-700">
Read More
<svg className="ml-2 h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<div className="mt-4 inline-flex items-center text-blue-900 font-semibold group-hover:text-blue-700">{t('autofix.kb2dfe482')}<svg className="ml-2 h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
</div>

View File

@ -15,7 +15,10 @@ import {
import type { ComponentType, SVGProps } from 'react';
import type { DashboardPlatformIconName } from './utils/dashboardPlatforms';
import { useTranslation } from './i18n/useTranslation';
export default function HomePage() {
const { t } = useTranslation();
const containerRef = useRef<HTMLDivElement | null>(null);
const [isMobile, setIsMobile] = useState(() => {
if (typeof window === 'undefined') return false;
@ -85,19 +88,15 @@ export default function HomePage() {
<div className="flex flex-col sm:flex-row sm:items-center gap-5 sm:gap-6">
<img
src="/images/logos/PP_Logo_BW_round.png"
alt="Profit Planet"
alt={t('autofix.k788633d1')}
className="h-16 w-16 sm:h-20 sm:w-20 object-contain"
/>
<div className="min-w-0 flex-1">
<div className="inline-flex items-center rounded-full border border-gray-200 bg-white/60 px-3 py-1 text-xs font-semibold text-gray-700">
Welcome
</div>
<h1 className="mt-3 text-5xl sm:text-6xl md:text-7xl font-black tracking-tight leading-none text-transparent bg-clip-text bg-gradient-to-r from-gray-900 via-gray-700 to-amber-700">
Profit Planet
</h1>
<p className="mt-3 text-sm sm:text-base text-gray-700">
Pick a platform to continue.
</p>
<h1 className="mt-3 text-5xl sm:text-6xl md:text-7xl font-black tracking-tight leading-none text-transparent bg-clip-text bg-gradient-to-r from-gray-900 via-gray-700 to-amber-700">{t('autofix.k788633d1')}</h1>
<p className="mt-3 text-sm sm:text-base text-gray-700">{t('autofix.kde5c689e')}</p>
</div>
</div>
</div>
@ -106,15 +105,13 @@ export default function HomePage() {
<div className="flex items-start justify-between gap-4">
<div>
<h2 className="text-xl sm:text-2xl font-bold text-gray-900">Platforms</h2>
<p className="mt-1 text-sm text-gray-700">Navigation shortcuts</p>
<p className="mt-1 text-sm text-gray-700">{t('autofix.kcc4adbcc')}</p>
</div>
</div>
<div className="mt-6">
{loading && (
<div className="rounded-2xl border border-gray-200 bg-white p-6 text-center text-sm text-gray-600">
Loading
</div>
<div className="rounded-2xl border border-gray-200 bg-white p-6 text-center text-sm text-gray-600">{t('autofix.k832387c5')}</div>
)}
{error && (
@ -185,9 +182,7 @@ export default function HomePage() {
})}
{platforms.length === 0 && (
<div className="sm:col-span-2 lg:col-span-3 rounded-2xl border border-gray-200 bg-white p-8 text-center text-sm text-gray-600">
No platforms available.
</div>
<div className="sm:col-span-2 lg:col-span-3 rounded-2xl border border-gray-200 bg-white p-8 text-center text-sm text-gray-600">{t('autofix.k7fe72eff')}</div>
)}
</div>
)}

View File

@ -1,5 +1,8 @@
'use client'
import { useTranslation } from '../i18n/useTranslation';
import { useState, useEffect, Suspense } from 'react' // CHANGED: add Suspense
import { useSearchParams, useRouter } from 'next/navigation'
import PageLayout from '../components/PageLayout'
@ -169,9 +172,7 @@ function PasswordResetPageInner() {
<div className="absolute -top-24 -right-24 size-56 rounded-full bg-gradient-to-br from-indigo-500/10 to-fuchsia-500/10 blur-3xl pointer-events-none" />
<div className="relative">
<div className="mx-auto max-w-2xl text-center mb-8">
<h1 className="text-3xl sm:text-4xl font-extrabold tracking-tight text-[#0F172A]">
Reset password
</h1>
<h1 className="text-3xl sm:text-4xl font-extrabold tracking-tight text-[#0F172A]">{t('autofix.k09f4290f')}</h1>
<p className="mt-3 text-slate-700 text-base sm:text-lg">
{!token
? 'Request a link to reset your password.'
@ -181,16 +182,14 @@ function PasswordResetPageInner() {
{!token && (
<form onSubmit={handleRequestSubmit} className="space-y-6">
<div>
<label className="block text-sm font-semibold text-[#0F172A] mb-2" htmlFor="email">
Email address
</label>
<label className="block text-sm font-semibold text-[#0F172A] mb-2" htmlFor="email">{t('autofix.keccee79f')}</label>
<input
id="email"
type="email"
value={email}
onChange={e => { setEmail(e.target.value); setRequestError(''); setRequestSuccess(false)}}
className="w-full rounded-xl border border-white/40 bg-white/60 px-4 py-3 text-[#0F172A] placeholder-slate-600/80 shadow-sm focus:outline-none focus:ring-2 focus:ring-[#8D6B1D]/80 focus:border-[#8D6B1D] transition"
placeholder="your.email@example.com"
placeholder={t('autofix.k199db5f1')}
required
/>
</div>
@ -232,9 +231,7 @@ function PasswordResetPageInner() {
type="button"
onClick={() => router.push('/login')}
className="text-[#8D6B1D] hover:text-[#7A5E1A] hover:underline font-medium"
>
Back to login
</button>
>{t('autofix.ked60db76')}</button>
</div>
</form>
)}
@ -243,9 +240,7 @@ function PasswordResetPageInner() {
<form onSubmit={handleResetSubmit} className="space-y-6">
<div className="grid gap-6 sm:grid-cols-2">
<div className="sm:col-span-2">
<label className="block text-sm font-medium text-gray-900 mb-2" htmlFor="password">
New password
</label>
<label className="block text-sm font-medium text-gray-900 mb-2" htmlFor="password">{t('autofix.k5aae8706')}</label>
<div className="relative">
<input
id="password"
@ -253,7 +248,7 @@ function PasswordResetPageInner() {
value={password}
onChange={e => { setPassword(e.target.value); setResetError(''); setResetSuccess(false)}}
className="w-full rounded-xl border border-white/40 bg-white/60 px-4 py-3 pr-12 text-[#0F172A] placeholder-slate-600/80 shadow-sm focus:outline-none focus:ring-2 focus:ring-[#8D6B1D]/80 focus:border-[#8D6B1D] transition"
placeholder="Your new password"
placeholder={t('autofix.k533db977')}
required
/>
<button
@ -280,20 +275,18 @@ function PasswordResetPageInner() {
</div>
<div className="sm:col-span-2">
<label className="block text-sm font-medium text-gray-900 mb-2" htmlFor="confirm">
Confirm password
</label>
<label className="block text-sm font-medium text-gray-900 mb-2" htmlFor="confirm">{t('autofix.k051e8ac8')}</label>
<input
id="confirm"
type={showPassword ? 'text' : 'password'}
value={confirmPassword}
onChange={e => { setConfirmPassword(e.target.value); setResetError(''); setResetSuccess(false)}}
className="w-full rounded-xl border border-white/40 bg-white/60 px-4 py-3 text-[#0F172A] placeholder-slate-600/80 shadow-sm focus:outline-none focus:ring-2 focus:ring-[#8D6B1D]/80 focus:border-[#8D6B1D] transition"
placeholder="Confirm password"
placeholder={t('autofix.k051e8ac8')}
required
/>
{confirmPassword && password !== confirmPassword && (
<p className="mt-2 text-xs text-red-500">Passwords do not match.</p>
<p className="mt-2 text-xs text-red-500">{t('autofix.ke0a3528a')}</p>
)}
</div>
</div>
@ -304,9 +297,7 @@ function PasswordResetPageInner() {
</div>
)}
{resetSuccess && (
<div className="rounded-lg border border-green-300 bg-green-50 px-4 py-3 text-sm text-green-700">
Password saved. Redirecting to login...
</div>
<div className="rounded-lg border border-green-300 bg-green-50 px-4 py-3 text-sm text-green-700">{t('autofix.k9d0c063d')}</div>
)}
<button
@ -334,9 +325,7 @@ function PasswordResetPageInner() {
type="button"
onClick={() => router.push('/password-reset')}
className="text-indigo-600 dark:text-indigo-400 hover:underline font-medium"
>
Request again
</button>
>{t('autofix.k5122ab54')}</button>
</div>
</form>
)}
@ -350,6 +339,7 @@ function PasswordResetPageInner() {
}
export default function PasswordResetPage() {
const { t } = useTranslation();
return (
<ToastProvider>
<Suspense

View File

@ -1,4 +1,7 @@
'use client'
import { useTranslation } from '../i18n/useTranslation';
import React, { useEffect, useMemo, useState } from 'react'
import useAuthStore from '../store/authStore'
import { useRouter } from 'next/navigation'
@ -7,6 +10,7 @@ import { UsersIcon, CalendarDaysIcon, ArrowLeftIcon } from '@heroicons/react/24/
import { usePersonalMatrixOverview, useMyMatrices } from './hooks/getStats'
export default function PersonalMatrixPage() {
const { t } = useTranslation();
const router = useRouter()
const user = useAuthStore(s => s.user)
@ -88,9 +92,7 @@ export default function PersonalMatrixPage() {
</div>
)}
{!loadingMatrices && matrices.length === 0 && (
<div className="rounded-md border border-yellow-200 bg-yellow-50 px-4 py-3 text-sm text-yellow-800">
You are not part of any matrix yet.
</div>
<div className="rounded-md border border-yellow-200 bg-yellow-50 px-4 py-3 text-sm text-yellow-800">{t('autofix.kaa656770')}</div>
)}
{!loadingMatrices && matrices.length > 0 && (
<ul className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
@ -116,15 +118,15 @@ export default function PersonalMatrixPage() {
<div className="text-lg font-semibold text-blue-900">{m.name}</div>
<div className="mt-3 grid grid-cols-1 gap-2 text-xs text-gray-700">
<div>
<span className="font-medium text-gray-800">Total members:</span>{' '}
<span className="font-medium text-gray-800">{t('autofix.kf0eef57e')}</span>{' '}
{totalUsersDisplay != null ? totalUsersDisplay : 'N/A'}
</div>
<div>
<span className="font-medium text-gray-800">Highest full level:</span>{' '}
<span className="font-medium text-gray-800">{t('autofix.k72428656')}</span>{' '}
{m.highestFullLevel != null ? m.highestFullLevel : 'N/A'}
</div>
<div className="flex flex-col gap-1">
<span className="font-medium text-gray-800">Matrix fill:</span>
<span className="font-medium text-gray-800">{t('autofix.kdc47630b')}</span>
<div className="h-2 w-full rounded bg-gray-200">
<div
className="h-2 rounded bg-blue-600"
@ -137,7 +139,7 @@ export default function PersonalMatrixPage() {
</div>
{m.createdAt && (
<div>
<span className="font-medium text-gray-800">Created:</span>{' '}
<span className="font-medium text-gray-800">{t('autofix.kf971ea7f')}</span>{' '}
{new Date(m.createdAt).toLocaleDateString()}
</div>
)}
@ -151,9 +153,7 @@ export default function PersonalMatrixPage() {
<button
onClick={() => setSelectedId(m.id)}
className="inline-flex items-center rounded-md bg-blue-800 px-3 py-1.5 text-sm font-semibold text-white hover:bg-blue-900"
>
View overview
</button>
>{t('autofix.k84d5cfcb')}</button>
</div>
</div>
</li>
@ -170,19 +170,19 @@ export default function PersonalMatrixPage() {
{/* Stats cards */}
<div className="mb-8 grid grid-cols-1 sm:grid-cols-3 gap-6">
<div className="rounded-xl border border-gray-100 bg-white p-5 shadow">
<div className="text-xs text-gray-500 mb-1">Total users under me</div>
<div className="text-xs text-gray-500 mb-1">{t('autofix.ka9d6e905')}</div>
<div className="text-xl font-semibold text-blue-900">
{data?.totalUsersUnderMe ?? (loading ? '…' : 0)}
</div>
</div>
<div className="rounded-xl border border-gray-100 bg-white p-5 shadow">
<div className="text-xs text-gray-500 mb-1">Immediate children</div>
<div className="text-xs text-gray-500 mb-1">{t('autofix.kf78c9087')}</div>
<div className="text-xl font-semibold text-blue-900">
{data?.immediateChildrenCount ?? (loading ? '…' : 0)}
</div>
</div>
<div className="rounded-xl border border-gray-100 bg-white p-5 shadow">
<div className="text-xs text-gray-500 mb-1">Levels filled</div>
<div className="text-xs text-gray-500 mb-1">{t('autofix.kc4d7816e')}</div>
<div className="text-xl font-semibold text-blue-900">
{data?.levelsFilled ?? (loading ? '…' : 0)}
</div>
@ -198,9 +198,9 @@ export default function PersonalMatrixPage() {
</p>
</div>
<div className="px-8 py-6">
{loading && <div className="text-xs text-gray-500">Loading</div>}
{loading && <div className="text-xs text-gray-500">{t('autofix.k832387c5')}</div>}
{!loading && (data?.level1?.length ?? 0) === 0 && (
<div className="text-xs text-gray-500 italic">No direct children.</div>
<div className="text-xs text-gray-500 italic">{t('autofix.k58344b74')}</div>
)}
<ul className="grid grid-cols-1 md:grid-cols-2 gap-4">
{data?.level1.map((c) => (
@ -221,13 +221,13 @@ export default function PersonalMatrixPage() {
{/* Level 2+ */}
<div className="rounded-2xl bg-white border border-gray-100 shadow-lg overflow-hidden mb-8">
<div className="px-8 py-6 border-b border-gray-100">
<h2 className="text-xl font-semibold text-blue-900">Level 2+</h2>
<p className="text-xs text-blue-700">Masked names for deeper descendants.</p>
<h2 className="text-xl font-semibold text-blue-900">{t('autofix.k40f4552a')}</h2>
<p className="text-xs text-blue-700">{t('autofix.k5fbf1824')}</p>
</div>
<div className="px-8 py-6">
{loading && <div className="text-xs text-gray-500">Loading</div>}
{loading && <div className="text-xs text-gray-500">{t('autofix.k832387c5')}</div>}
{!loading && (data?.level2Plus?.length ?? 0) === 0 && (
<div className="text-xs text-gray-500 italic">No deeper descendants.</div>
<div className="text-xs text-gray-500 italic">{t('autofix.ka1d0b6ff')}</div>
)}
<ul className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
{data?.level2Plus.map((x) => (
@ -244,7 +244,7 @@ export default function PersonalMatrixPage() {
{/* Meta */}
<div className="rounded-xl border border-gray-100 bg-white p-5 shadow">
<div className="text-xs text-gray-500 mb-1">Overview meta</div>
<div className="text-xs text-gray-500 mb-1">{t('autofix.k039e629b')}</div>
<div className="text-xs text-gray-700">
Matrix instance: {data?.matrixInstanceId ?? (loading ? '…' : '—')}{' '}
Level1: {meta.countL1} Level2+: {meta.countL2Plus}

View File

@ -1,3 +1,8 @@
'use client';
import { useTranslation } from '../../i18n/useTranslation';
import React from 'react'
export default function BankInformation({
@ -17,6 +22,7 @@ export default function BankInformation({
setBankInfo: (v: { accountHolder: string, iban: string }) => void,
onEdit?: () => void
}) {
const { t } = useTranslation();
// editing disabled for now; keep props to avoid refactors
const accountHolder = profileData.accountHolder || ''
const iban = profileData.iban || ''
@ -24,21 +30,21 @@ export default function BankInformation({
return (
<div className="rounded-lg bg-white/70 backdrop-blur-md border border-white/60 shadow-lg p-4 sm:p-6">
<div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-semibold text-gray-900">Bank Information</h2>
<span className="text-xs text-gray-500">Editing disabled</span>
<h2 className="text-lg font-semibold text-gray-900">{t('autofix.k5d4f6b2f')}</h2>
<span className="text-xs text-gray-500">{t('autofix.kfc6b6a29')}</span>
</div>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Account Holder</label>
<label className="block text-sm font-medium text-gray-700 mb-1">{t('autofix.kada9d61c')}</label>
<input
type="text"
className="w-full p-2 border border-white/60 rounded-lg bg-white/50 text-gray-900"
value={accountHolder}
disabled
placeholder="Not provided"
placeholder={t('autofix.kf2147f07')}
/>
{!accountHolder && <div className="mt-1 text-sm italic text-gray-400">Not provided</div>}
{!accountHolder && <div className="mt-1 text-sm italic text-gray-400">{t('autofix.kf2147f07')}</div>}
</div>
<div>
@ -48,9 +54,9 @@ export default function BankInformation({
className="w-full p-2 border border-white/60 rounded-lg bg-white/50 text-gray-900"
value={iban}
disabled
placeholder="Not provided"
placeholder={t('autofix.kf2147f07')}
/>
{!iban && <div className="mt-1 text-sm italic text-gray-400">Not provided</div>}
{!iban && <div className="mt-1 text-sm italic text-gray-400">{t('autofix.kf2147f07')}</div>}
</div>
</div>
</div>

View File

@ -1,3 +1,8 @@
'use client';
import { useTranslation } from '../../i18n/useTranslation';
import React from 'react'
import { UserCircleIcon, EnvelopeIcon, PhoneIcon, MapPinIcon, PencilIcon, CheckCircleIcon } from '@heroicons/react/24/outline'
@ -6,10 +11,11 @@ export default function BasicInformation({ profileData, HighlightIfMissing, onEd
HighlightIfMissing: React.FC<{ value: any, children: React.ReactNode }>
onEdit?: () => void
}) {
const { t } = useTranslation();
return (
<div className="rounded-lg bg-white/70 backdrop-blur-md border border-white/60 shadow-lg p-4 sm:p-6">
<div className="flex items-center justify-between mb-6">
<h2 className="text-lg font-semibold text-gray-900">Basic Information</h2>
<h2 className="text-lg font-semibold text-gray-900">{t('autofix.k1d178b73')}</h2>
<button
className="flex items-center text-[#8D6B1D] hover:text-[#7A5E1A] transition-colors"
onClick={onEdit}
@ -22,9 +28,7 @@ export default function BasicInformation({ profileData, HighlightIfMissing, onEd
{profileData.userType === 'personal' && (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
First Name
</label>
<label className="block text-sm font-medium text-gray-700 mb-1">{t('autofix.kfe8083f8')}</label>
<div className="flex items-center p-3 bg-white/50 border border-white/60 rounded-lg">
<UserCircleIcon className="h-5 w-5 text-gray-400 mr-3" />
<HighlightIfMissing value={profileData.firstName}>
@ -33,9 +37,7 @@ export default function BasicInformation({ profileData, HighlightIfMissing, onEd
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Last Name
</label>
<label className="block text-sm font-medium text-gray-700 mb-1">{t('autofix.k6a4108c8')}</label>
<div className="flex items-center p-3 bg-white/50 border border-white/60 rounded-lg">
<UserCircleIcon className="h-5 w-5 text-gray-400 mr-3" />
<HighlightIfMissing value={profileData.lastName}>
@ -47,9 +49,7 @@ export default function BasicInformation({ profileData, HighlightIfMissing, onEd
)}
{profileData.userType === 'company' && (
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Contact Person
</label>
<label className="block text-sm font-medium text-gray-700 mb-1">{t('autofix.k9dafde30')}</label>
<div className="flex items-center p-3 bg-white/50 border border-white/60 rounded-lg">
<UserCircleIcon className="h-5 w-5 text-gray-400 mr-3" />
<HighlightIfMissing value={profileData.contactPersonName}>
@ -59,9 +59,7 @@ export default function BasicInformation({ profileData, HighlightIfMissing, onEd
</div>
)}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Email Address
</label>
<label className="block text-sm font-medium text-gray-700 mb-1">{t('autofix.kde6d477f')}</label>
<div className="flex items-center p-3 bg-white/50 border border-white/60 rounded-lg">
<EnvelopeIcon className="h-5 w-5 text-gray-400 mr-3" />
<HighlightIfMissing value={profileData.email}>
@ -71,9 +69,7 @@ export default function BasicInformation({ profileData, HighlightIfMissing, onEd
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Phone Number
</label>
<label className="block text-sm font-medium text-gray-700 mb-1">{t('autofix.k2a2fe15a')}</label>
<div className="flex items-center p-3 bg-white/50 border border-white/60 rounded-lg">
<PhoneIcon className="h-5 w-5 text-gray-400 mr-3" />
<HighlightIfMissing value={profileData.phone}>

View File

@ -1,3 +1,8 @@
'use client';
import { useTranslation } from '../../i18n/useTranslation';
import React from 'react'
import { authFetch } from '../../utils/authFetch'
import { AboInvoice, useAboInvoices } from '../hooks/getAboInvoices'
@ -78,6 +83,7 @@ function downloadBlob(content: Blob, fileName: string) {
}
export default function FinanceInvoices({ abonementId }: Props) {
const { t } = useTranslation();
const { data: invoices, loading, error } = useAboInvoices(abonementId)
const [busyId, setBusyId] = React.useState<string | number | null>(null)
const [actionError, setActionError] = React.useState<string | null>(null)
@ -154,39 +160,31 @@ export default function FinanceInvoices({ abonementId }: Props) {
return (
<section className="space-y-4">
<div className="flex items-center justify-between gap-3">
<h2 className="text-lg font-semibold text-gray-900">Finance & Invoices</h2>
<h2 className="text-lg font-semibold text-gray-900">{t('autofix.k8953de89')}</h2>
<button
onClick={onExportAll}
disabled={!invoices.length || loading}
className="rounded-md border border-gray-300 px-3 py-1.5 text-xs text-gray-700 hover:bg-gray-50 disabled:opacity-60 disabled:cursor-not-allowed"
>
Export all invoices
</button>
>{t('autofix.kfd632d02')}</button>
</div>
{!abonementId ? (
<div className="rounded-md border border-white/60 bg-white/70 backdrop-blur-md p-4 text-sm text-gray-600 shadow-lg">
No subscription selected. Invoices will appear once you have an active subscription.
</div>
<div className="rounded-md border border-white/60 bg-white/70 backdrop-blur-md p-4 text-sm text-gray-600 shadow-lg">{t('autofix.kc48b877b')}</div>
) : loading ? (
<div className="rounded-md border border-white/60 bg-white/70 backdrop-blur-md p-4 text-sm text-gray-600 shadow-lg">
Loading invoices
</div>
<div className="rounded-md border border-white/60 bg-white/70 backdrop-blur-md p-4 text-sm text-gray-600 shadow-lg">{t('autofix.ka5603827')}</div>
) : error ? (
<div className="rounded-md border border-red-200 bg-red-50/80 backdrop-blur-md p-4 text-sm text-red-700">
{error}
</div>
) : invoices.length === 0 ? (
<div className="rounded-md border border-white/60 bg-white/70 backdrop-blur-md p-4 text-sm text-gray-600 shadow-lg">
No invoices found for this subscription.
</div>
<div className="rounded-md border border-white/60 bg-white/70 backdrop-blur-md p-4 text-sm text-gray-600 shadow-lg">{t('autofix.k2108b5a0')}</div>
) : (
<div className="overflow-x-auto rounded-lg border border-white/60 bg-white/70 backdrop-blur-md shadow-lg">
<table className="min-w-full text-sm">
<thead className="bg-white/80">
<tr className="text-left text-gray-700">
<th className="px-4 py-3 font-semibold">Date</th>
<th className="px-4 py-3 font-semibold">Invoice #</th>
<th className="px-4 py-3 font-semibold">{t('autofix.k947d8777')}</th>
<th className="px-4 py-3 font-semibold">Status</th>
<th className="px-4 py-3 font-semibold">Total</th>
<th className="px-4 py-3 font-semibold">Actions</th>

View File

@ -1,10 +1,16 @@
'use client';
import { useTranslation } from '../../i18n/useTranslation';
import React from 'react'
export default function MediaSection({ documents }: { documents: any[] }) {
const { t } = useTranslation();
const hasDocuments = Array.isArray(documents) && documents.length > 0;
return (
<div className="rounded-lg bg-white/70 backdrop-blur-md border border-white/60 shadow-lg p-4 sm:p-6">
<h2 className="text-lg font-semibold text-gray-900 mb-4">Media & Documents</h2>
<h2 className="text-lg font-semibold text-gray-900 mb-4">{t('autofix.ked7d533b')}</h2>
<div className="overflow-x-auto">
{hasDocuments ? (
<table className="min-w-full divide-y divide-gray-200">
@ -29,7 +35,7 @@ export default function MediaSection({ documents }: { documents: any[] }) {
<a href={doc.signedUrl} target="_blank" rel="noopener noreferrer" className="px-3 py-1 text-xs bg-gray-200 text-gray-700 rounded hover:bg-gray-300 transition">Preview</a>
</>
) : (
<span className="text-xs text-gray-400 italic">No file</span>
<span className="text-xs text-gray-400 italic">{t('autofix.kb3243742')}</span>
)}
</td>
</tr>
@ -37,7 +43,7 @@ export default function MediaSection({ documents }: { documents: any[] }) {
</tbody>
</table>
) : (
<div className="text-gray-500 italic py-6 text-center">No media or documents found.</div>
<div className="text-gray-500 italic py-6 text-center">{t('autofix.k60b1e339')}</div>
)}
</div>
</div>

View File

@ -1,10 +1,16 @@
'use client';
import { useTranslation } from '../../i18n/useTranslation';
import React from 'react';
export default function ProfileCompletion({ profileComplete }: { profileComplete: number }) {
const { t } = useTranslation();
return (
<div className="rounded-lg bg-white/70 backdrop-blur-md border border-white/60 shadow-lg p-4 sm:p-6 mb-8">
<div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-semibold text-gray-900">Profile Completion</h2>
<h2 className="text-lg font-semibold text-gray-900">{t('autofix.kd08b698a')}</h2>
<span className="text-sm font-medium text-[#8D6B1D]">
{profileComplete}%
</span>
@ -15,9 +21,7 @@ export default function ProfileCompletion({ profileComplete }: { profileComplete
style={{ width: `${profileComplete}%` }}
></div>
</div>
<p className="text-sm text-gray-600 mt-2">
Complete your profile to unlock all features
</p>
<p className="text-sm text-gray-600 mt-2">{t('autofix.k772cc77b')}</p>
</div>
);
}

View File

@ -1,3 +1,8 @@
'use client';
import { useTranslation } from '../../i18n/useTranslation';
import React from 'react'
import { useMyAboStatus } from '../hooks/getAbo'
@ -6,6 +11,7 @@ type Props = {
}
export default function UserAbo({ onAboChange }: Props) {
const { t } = useTranslation();
const { hasAbo, abonement, loading, error } = useMyAboStatus()
React.useEffect(() => {
@ -16,10 +22,8 @@ export default function UserAbo({ onAboChange }: Props) {
if (loading) {
return (
<section className="space-y-4">
<h2 className="text-lg font-semibold text-gray-900">My Subscriptions</h2>
<div className="rounded-md border border-white/60 bg-white/70 backdrop-blur-md p-4 text-sm text-gray-600 shadow-lg">
Loading subscriptions
</div>
<h2 className="text-lg font-semibold text-gray-900">{t('autofix.k744fda01')}</h2>
<div className="rounded-md border border-white/60 bg-white/70 backdrop-blur-md p-4 text-sm text-gray-600 shadow-lg">{t('autofix.kcdfef775')}</div>
</section>
)
}
@ -27,7 +31,7 @@ export default function UserAbo({ onAboChange }: Props) {
if (error) {
return (
<section className="space-y-4">
<h2 className="text-lg font-semibold text-gray-900">My Subscriptions</h2>
<h2 className="text-lg font-semibold text-gray-900">{t('autofix.k744fda01')}</h2>
<div className="rounded-md border border-red-200 bg-red-50/80 backdrop-blur-md p-4 text-sm text-red-700">
{error}
</div>
@ -37,11 +41,9 @@ export default function UserAbo({ onAboChange }: Props) {
return (
<section className="space-y-4">
<h2 className="text-lg font-semibold text-gray-900">My Subscription</h2>
<h2 className="text-lg font-semibold text-gray-900">{t('autofix.k7fa55432')}</h2>
{(!hasAbo || !abonement) ? (
<div className="rounded-md border border-white/60 bg-white/70 backdrop-blur-md p-4 text-sm text-gray-600 shadow-lg">
You currently dont have an active subscription.
</div>
<div className="rounded-md border border-white/60 bg-white/70 backdrop-blur-md p-4 text-sm text-gray-600 shadow-lg">{t('autofix.k4f209a66')}</div>
) : (
<div className="grid gap-3 sm:gap-4">
{(() => {
@ -92,9 +94,7 @@ export default function UserAbo({ onAboChange }: Props) {
</div>
</div>
<div className="mt-3 flex gap-2">
<button className="rounded-md border border-gray-300 px-3 py-1.5 text-xs text-gray-700 hover:bg-gray-50">
Current plan
</button>
<button className="rounded-md border border-gray-300 px-3 py-1.5 text-xs text-gray-700 hover:bg-gray-50">{t('autofix.k49e51b5f')}</button>
</div>
</div>
)

View File

@ -1,5 +1,8 @@
'use client'
import { useTranslation } from '../i18n/useTranslation';
import React, { useEffect } from 'react'
import { useRouter } from 'next/navigation'
import useAuthStore from '../store/authStore'
@ -21,9 +24,7 @@ import { authFetch } from '../utils/authFetch'
function HighlightIfMissing({ value, children }: { value: any, children: React.ReactNode }) {
if (value === null || value === undefined || value === '') {
return (
<span className="italic text-gray-400">
Not provided
</span>
<span className="italic text-gray-400">{t('autofix.kf2147f07')}</span>
);
}
return <>{children}</>;
@ -69,6 +70,7 @@ const bankFields = [
const BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:3001'
export default function ProfilePage() {
const { t } = useTranslation();
const router = useRouter()
const user = useAuthStore(state => state.user)
const isAuthReady = useAuthStore(state => state.isAuthReady)
@ -328,17 +330,13 @@ export default function ProfilePage() {
<div className="rounded-3xl bg-white/60 backdrop-blur-md border border-white/50 shadow-xl p-4 sm:p-6 lg:p-8">
{/* Page Header */}
<div className="mb-8">
<h1 className="text-3xl font-bold text-gray-900">Profile Settings</h1>
<p className="text-gray-600 mt-2">
Manage your account information and preferences
</p>
<h1 className="text-3xl font-bold text-gray-900">{t('autofix.k67cace8b')}</h1>
<p className="text-gray-600 mt-2">{t('autofix.ka00fc5db')}</p>
</div>
{/* Pending admin verification notice (above progress) */}
{profileDataApi?.userStatus && profileDataApi.userStatus.is_admin_verified === 0 && (
<div className="rounded-md bg-yellow-50/80 backdrop-blur border border-yellow-200 p-3 text-sm text-yellow-800 mb-2">
Your account is fully submitted. Our team will verify your account shortly.
</div>
<div className="rounded-md bg-yellow-50/80 backdrop-blur border border-yellow-200 p-3 text-sm text-yellow-800 mb-2">{t('autofix.k2af2916f')}</div>
)}
<ProfileCompletion profileComplete={profileData.profileComplete} />
@ -363,11 +361,11 @@ export default function ProfilePage() {
<div className="space-y-6">
{/* Account Status (make translucent) */}
<div className="rounded-lg bg-white/70 backdrop-blur-md border border-white/60 shadow-lg p-4 sm:p-6">
<h3 className="font-semibold text-gray-900 mb-4">Account Status</h3>
<h3 className="font-semibold text-gray-900 mb-4">{t('autofix.kb8cd2810')}</h3>
<div className="space-y-3">
<div className="flex items-center justify-between">
<span className="text-sm text-gray-600">Member Since</span>
<span className="text-sm text-gray-600">{t('autofix.k7bed84a7')}</span>
<span className="text-sm font-medium text-gray-900">{profileData.joinDate}</span>
</div>
@ -386,15 +384,13 @@ export default function ProfilePage() {
</div>
{/* Quick Actions (make translucent) */}
<div className="rounded-lg bg-white/70 backdrop-blur-md border border-white/60 shadow-lg p-4 sm:p-6">
<h3 className="font-semibold text-gray-900 mb-4">Quick Actions</h3>
<h3 className="font-semibold text-gray-900 mb-4">{t('autofix.k52af8b8d')}</h3>
<div className="space-y-3">
<button
onClick={() => router.push('/dashboard')}
className="w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-50 rounded-lg transition-colors"
>
Go to Dashboard
</button>
>{t('autofix.kd00443f2')}</button>
<button
onClick={handleDownloadAccountData}
disabled={downloadLoading}
@ -402,9 +398,7 @@ export default function ProfilePage() {
>
{downloadLoading ? 'Preparing download...' : 'Download Account Data'}
</button>
<button className="w-full text-left px-4 py-2 text-sm text-red-600 hover:bg-red-50 rounded-lg transition-colors">
Delete Account
</button>
<button className="w-full text-left px-4 py-2 text-sm text-red-600 hover:bg-red-50 rounded-lg transition-colors">{t('autofix.k41f7c81d')}</button>
</div>
{downloadError && (
<p className="mt-2 text-xs text-red-600">{downloadError}</p>
@ -418,26 +412,22 @@ export default function ProfilePage() {
<section className="rounded-lg bg-white/70 backdrop-blur-md border border-white/60 shadow-lg p-4 sm:p-6">
<div className="flex items-start justify-between gap-4 flex-wrap">
<div>
<h2 className="text-lg font-semibold text-gray-900">My Subscriptions</h2>
<p className="text-sm text-gray-600 mt-1">
View your active subscriptions, included items and subscription details on a dedicated page.
</p>
<h2 className="text-lg font-semibold text-gray-900">{t('autofix.k744fda01')}</h2>
<p className="text-sm text-gray-600 mt-1">{t('autofix.kcada239b')}</p>
</div>
<button
onClick={() => router.push('/profile/subscriptions')}
className="rounded-md border border-gray-300 px-4 py-2 text-sm text-gray-700 hover:bg-gray-50"
>
Open subscriptions
</button>
>{t('autofix.k4b6c7681')}</button>
</div>
<div className="mt-4 rounded-md border border-white/60 bg-white/70 p-3">
{subscriptionsLoading ? (
<p className="text-sm text-gray-600">Loading subscriptions</p>
<p className="text-sm text-gray-600">{t('autofix.kcdfef775')}</p>
) : subscriptionsError ? (
<p className="text-sm text-red-700">{subscriptionsError}</p>
) : subscriptions.length === 0 ? (
<p className="text-sm text-gray-600">You dont have any subscriptions yet.</p>
<p className="text-sm text-gray-600">{t('autofix.kc8652e34')}</p>
) : (
<ul className="space-y-2">
{subscriptions.map((subscription) => {

View File

@ -1,5 +1,8 @@
'use client'
import { useTranslation } from '../../i18n/useTranslation';
import React, { useEffect } from 'react'
import { useRouter } from 'next/navigation'
import useAuthStore from '../../store/authStore'
@ -55,6 +58,7 @@ const formatMoney = (value?: string | number | null, currency?: string | null) =
}
export default function ProfileSubscriptionsPage() {
const { t } = useTranslation();
const router = useRouter()
const user = useAuthStore(state => state.user)
const isAuthReady = useAuthStore(state => state.isAuthReady)
@ -230,34 +234,28 @@ export default function ProfileSubscriptionsPage() {
<div className="rounded-3xl bg-white/60 backdrop-blur-md border border-white/50 shadow-xl p-4 sm:p-6 lg:p-8 space-y-6 sm:space-y-8">
<div className="flex items-center justify-between gap-3 flex-wrap">
<div>
<h1 className="text-3xl font-bold text-gray-900">My Subscriptions</h1>
<p className="text-gray-600 mt-2">Select any subscription to view details and included items.</p>
<h1 className="text-3xl font-bold text-gray-900">{t('autofix.k744fda01')}</h1>
<p className="text-gray-600 mt-2">{t('autofix.kbf7bde57')}</p>
</div>
<button
onClick={() => router.push('/profile')}
className="rounded-md border border-gray-300 px-4 py-2 text-sm text-gray-700 hover:bg-gray-50"
>
Back to profile
</button>
>{t('autofix.kf2a1257e')}</button>
</div>
{loading ? (
<div className="rounded-md border border-white/60 bg-white/70 backdrop-blur-md p-4 text-sm text-gray-600 shadow-lg">
Loading subscriptions
</div>
<div className="rounded-md border border-white/60 bg-white/70 backdrop-blur-md p-4 text-sm text-gray-600 shadow-lg">{t('autofix.kcdfef775')}</div>
) : error ? (
<div className="rounded-md border border-red-200 bg-red-50/80 backdrop-blur-md p-4 text-sm text-red-700">
{error}
</div>
) : subscriptions.length === 0 ? (
<div className="rounded-md border border-white/60 bg-white/70 backdrop-blur-md p-4 text-sm text-gray-600 shadow-lg">
You dont have any subscriptions yet.
</div>
<div className="rounded-md border border-white/60 bg-white/70 backdrop-blur-md p-4 text-sm text-gray-600 shadow-lg">{t('autofix.kc8652e34')}</div>
) : (
<div className="space-y-6">
<section className="rounded-lg border border-white/60 bg-white/70 backdrop-blur-md p-4 sm:p-6 shadow-lg">
<div className="flex items-center justify-between gap-3 flex-wrap mb-3">
<h2 className="text-lg font-semibold text-gray-900">All subscriptions</h2>
<h2 className="text-lg font-semibold text-gray-900">{t('autofix.k1df74994')}</h2>
<p className="text-xs text-gray-600">{subscriptions.length} total</p>
</div>
@ -306,7 +304,7 @@ export default function ProfileSubscriptionsPage() {
<section className="rounded-lg border border-white/60 bg-white/70 backdrop-blur-md p-4 sm:p-6 shadow-lg">
<div className="flex items-start justify-between gap-3 flex-wrap">
<div>
<h2 className="text-lg font-semibold text-gray-900">Subscription details</h2>
<h2 className="text-lg font-semibold text-gray-900">{t('autofix.k3b8e0964')}</h2>
<p className="text-sm text-gray-600 mt-1">{selectedAbo.name || 'Coffee Subscription'}</p>
</div>
<span
@ -345,7 +343,7 @@ export default function ProfileSubscriptionsPage() {
</button>
)}
{(status === 'finished' || status === 'cancelled') && (
<p className="text-xs text-gray-600">No further status changes are available for this subscription.</p>
<p className="text-xs text-gray-600">{t('autofix.k416bfe70')}</p>
)}
</div>
{statusError && (
@ -354,7 +352,7 @@ export default function ProfileSubscriptionsPage() {
<div className="mt-4 grid grid-cols-1 sm:grid-cols-2 gap-3 text-sm">
<div className="rounded-md bg-white/80 border border-gray-200 px-3 py-2">
<p className="text-gray-500">Subscription ID</p>
<p className="text-gray-500">{t('autofix.k354a026b')}</p>
<p className="font-medium text-gray-900">{selectedAbo.id}</p>
</div>
<div className="rounded-md bg-white/80 border border-gray-200 px-3 py-2">
@ -374,7 +372,7 @@ export default function ProfileSubscriptionsPage() {
<p className="font-medium text-gray-900">{formatDate(selectedAbo.startedAt || selectedAbo.createdAt)}</p>
</div>
<div className="rounded-md bg-white/80 border border-gray-200 px-3 py-2">
<p className="text-gray-500">Next billing</p>
<p className="text-gray-500">{t('autofix.k85446b89')}</p>
<p className="font-medium text-gray-900">{formatDate(selectedAbo.nextBillingAt)}</p>
</div>
</div>
@ -383,30 +381,24 @@ export default function ProfileSubscriptionsPage() {
<section className="rounded-lg border border-white/60 bg-white/70 backdrop-blur-md p-4 sm:p-6 shadow-lg">
<div className="flex items-center justify-between gap-3 flex-wrap">
<div>
<h2 className="text-lg font-semibold text-gray-900">Included in your subscription</h2>
<h2 className="text-lg font-semibold text-gray-900">{t('autofix.kb383a3e8')}</h2>
<p className="text-sm text-gray-600 mt-1">{includedItems.length} item(s), {totalPacks} total pack(s)</p>
<p className="text-xs text-gray-500 mt-1">Changes apply from your next billing cycle.</p>
<p className="text-xs text-gray-500 mt-1">{t('autofix.k35f67931')}</p>
</div>
{!editingContent && canChangeContent && (
<button
onClick={startEditingContent}
className="rounded-md border border-gray-300 px-3 py-1.5 text-xs text-gray-700 hover:bg-gray-50"
>
Change coffees for next month
</button>
>{t('autofix.k9bc83f50')}</button>
)}
</div>
{!canChangeContent && (
<p className="mt-3 text-xs text-gray-600">
Coffee content can only be changed while a subscription is issued, ongoing, or paused.
</p>
<p className="mt-3 text-xs text-gray-600">{t('autofix.kcc15636b')}</p>
)}
{includedItems.length === 0 ? (
<div className="mt-4 rounded-md bg-white/80 border border-gray-200 p-3 text-sm text-gray-600">
No included items were returned for this subscription.
</div>
<div className="mt-4 rounded-md bg-white/80 border border-gray-200 p-3 text-sm text-gray-600">{t('autofix.k9772afa4')}</div>
) : (
<div className="mt-4 grid grid-cols-1 md:grid-cols-2 gap-3">
{includedItems.map((item, index) => {
@ -438,12 +430,12 @@ export default function ProfileSubscriptionsPage() {
{editingContent && canChangeContent && (
<div className="mt-4 rounded-md border border-gray-200 bg-white/90 p-4">
<div className="flex items-center justify-between gap-2 flex-wrap mb-3">
<h3 className="text-sm font-semibold text-gray-900">Edit coffee content</h3>
<h3 className="text-sm font-semibold text-gray-900">{t('autofix.ke24abf9c')}</h3>
<p className="text-xs text-gray-600">Selected packs: {draftTotalPacks} (minimum 6)</p>
</div>
{coffeesLoading ? (
<p className="text-sm text-gray-600">Loading available coffees</p>
<p className="text-sm text-gray-600">{t('autofix.kc813a103')}</p>
) : coffeesError ? (
<p className="text-sm text-red-600">{coffeesError}</p>
) : (

View File

@ -62,7 +62,7 @@ const init: CompanyProfileData = {
function ModernSelect({
label,
placeholder = 'Select…',
placeholder={t('autofix.ka5bf342b')},
value,
onChange,
options,
@ -145,7 +145,7 @@ function ModernSelect({
<input
value={query}
onChange={e => setQuery(e.target.value)}
placeholder="Search…"
placeholder={t('autofix.ka4ecb6cd')}
className="w-full rounded-lg border border-gray-200 px-3 py-2 text-sm
focus:outline-none focus:ring-2 focus:ring-[#8D6B1D] focus:border-transparent"
autoFocus
@ -153,7 +153,7 @@ function ModernSelect({
</div>
<div className="max-h-[42vh] overflow-auto p-1">
{filtered.length === 0 ? (
<div className="px-3 py-2 text-sm text-gray-500">No results</div>
<div className="px-3 py-2 text-sm text-gray-500">{t('autofix.k955b1cbe')}</div>
) : (
filtered.map(o => {
const active = o.value === value
@ -554,7 +554,7 @@ export default function CompanyAdditionalInformationPage() {
value={form.companyPhone}
onChange={handleChange}
onInput={handlePhoneInput}
placeholder="e.g. +43 1 234567"
placeholder={t('autofix.k8eaa7b3b')}
ref={companyPhoneRef}
required
/>
@ -580,7 +580,7 @@ export default function CompanyAdditionalInformationPage() {
value={form.contactPersonPhone}
onChange={handleChange}
onInput={handlePhoneInput}
placeholder="e.g. +43 676 1234567"
placeholder={t('autofix.k9f56d4ac')}
ref={contactPhoneRef}
required
/>
@ -593,7 +593,7 @@ export default function CompanyAdditionalInformationPage() {
name="registrationNumber"
value={form.registrationNumber}
onChange={handleChange}
placeholder="e.g. FN123456a"
placeholder={t('autofix.k85682289')}
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm uppercase focus:ring-2 focus:ring-[#8D6B1D] focus:border-transparent"
/>
</div>
@ -605,7 +605,7 @@ export default function CompanyAdditionalInformationPage() {
name="uidNumber"
value={form.uidNumber}
onChange={handleChange}
placeholder="e.g. ATU12345678"
placeholder={t('autofix.kf4f44e2f')}
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm uppercase focus:ring-2 focus:ring-[#8D6B1D] focus:border-transparent"
/>
</div>
@ -673,7 +673,7 @@ export default function CompanyAdditionalInformationPage() {
name="accountHolder"
value={form.accountHolder}
onChange={handleChange}
placeholder="Company / Holder name"
placeholder={t('autofix.k07fe11b2')}
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-[#8D6B1D] focus:border-transparent"
required
/>
@ -686,7 +686,7 @@ export default function CompanyAdditionalInformationPage() {
name="iban"
value={form.iban}
onChange={handleChange}
placeholder="DE89 3704 0044 0532 0130 00"
placeholder={t('autofix.k1f269263')}
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm tracking-wide uppercase focus:ring-2 focus:ring-[#8D6B1D] focus:border-transparent"
required
/>
@ -735,7 +735,7 @@ export default function CompanyAdditionalInformationPage() {
name="emergencyName"
value={form.emergencyName}
onChange={handleChange}
placeholder="Contact name"
placeholder={t('autofix.k67dd8a82')}
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-[#8D6B1D] focus:border-transparent"
/>
</div>

View File

@ -80,7 +80,7 @@ type SelectOption = { value: string; label: string }
function ModernSelect({
label,
placeholder = 'Select…',
placeholder={t('autofix.ka5bf342b')},
searchPlaceholder = 'Search…',
noResults = 'No results',
value,
@ -651,7 +651,7 @@ export default function PersonalAdditionalInformationPage() {
value={form.phone}
onChange={handleChange}
onInput={handlePhoneInput}
placeholder="e.g. +43 676 1234567"
placeholder={t('autofix.k9f56d4ac')}
ref={phoneRef}
required
/>
@ -690,7 +690,7 @@ export default function PersonalAdditionalInformationPage() {
name="street"
value={form.street}
onChange={handleChange}
placeholder="Street & House Number"
placeholder={t('autofix.k96dbbe05')}
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-[#8D6B1D] focus:border-transparent"
required
/>
@ -703,7 +703,7 @@ export default function PersonalAdditionalInformationPage() {
name="postalCode"
value={form.postalCode}
onChange={handleChange}
placeholder="e.g. 12345"
placeholder={t('autofix.kf70b9896')}
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-[#8D6B1D] focus:border-transparent"
required
/>
@ -716,7 +716,7 @@ export default function PersonalAdditionalInformationPage() {
name="city"
value={form.city}
onChange={handleChange}
placeholder="e.g. Berlin"
placeholder={t('autofix.k62bc3c59')}
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-[#8D6B1D] focus:border-transparent"
required
/>
@ -751,7 +751,7 @@ export default function PersonalAdditionalInformationPage() {
name="accountHolder"
value={form.accountHolder}
onChange={handleChange}
placeholder="Full name"
placeholder={t('autofix.k28f1a9b1')}
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-[#8D6B1D] focus:border-transparent"
required
/>
@ -764,7 +764,7 @@ export default function PersonalAdditionalInformationPage() {
name="iban"
value={form.iban}
onChange={handleChange}
placeholder="e.g. DE89 3704 0044 0532 0130 00"
placeholder={t('autofix.k1a1ca621')}
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm tracking-wide uppercase focus:ring-2 focus:ring-[#8D6B1D] focus:border-transparent"
required
/>
@ -801,7 +801,7 @@ export default function PersonalAdditionalInformationPage() {
name="emergencyName"
value={form.emergencyName}
onChange={handleChange}
placeholder="Contact name"
placeholder={t('autofix.k67dd8a82')}
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-[#8D6B1D] focus:border-transparent"
/>
</div>

View File

@ -159,7 +159,7 @@ export default function PersonalIdUploadPage() {
type="date"
value={expiry}
onChange={e => setExpiry(e.target.value)}
placeholder="tt.mm jjjj"
placeholder={t('autofix.k290e3aab')}
className={`${inputBase} ${expiry ? 'text-gray-900' : 'text-gray-700'} appearance-none [&::-webkit-calendar-picker-indicator]:opacity-80 focus:ring-[#8D6B1D] focus:border-transparent`}
required
/>

View File

@ -1,6 +1,7 @@
'use client'
import ConfirmActionModal from '../../components/modals/ConfirmActionModal'
import { useTranslation } from '../../i18n/useTranslation'
interface DeactivateReferralLinkModalProps {
open: boolean
@ -19,20 +20,21 @@ export default function DeactivateReferralLinkModal({
onClose,
onConfirm,
}: DeactivateReferralLinkModalProps) {
const { t } = useTranslation()
return (
<ConfirmActionModal
open={open}
pending={pending}
intent="danger"
title="Deactivate referral link?"
description="This will immediately deactivate the selected referral link so it can no longer be used."
confirmText="Deactivate"
title={t('referralManagement.deactivateModalTitle')}
description={t('referralManagement.deactivateModalDescription')}
confirmText={t('referralManagement.deactivate')}
onClose={onClose}
onConfirm={onConfirm}
extraContent={
linkPreview ? (
<div>
<span className="text-xs uppercase text-gray-500">Link</span>
<span className="text-xs uppercase text-gray-500">{t('referralManagement.linkLabel')}</span>
<div title={fullUrl} className="mt-1 inline-flex items-center rounded bg-gray-100 px-2 py-1 text-xs font-mono text-gray-800">
{linkPreview}
</div>

View File

@ -3,12 +3,14 @@
import React, { useMemo, useState } from 'react'
import { ClipboardDocumentIcon } from '@heroicons/react/24/outline'
import { createReferralLink } from '../hooks/generateReferralLink'
import { useTranslation } from '../../i18n/useTranslation'
interface Props {
onCreated?: () => void | Promise<void>
}
export default function GenerateReferralLinkWidget({ onCreated }: Props) {
const { t } = useTranslation()
// Defaults: Unlimited + Never expires
const [maxUses, setMaxUses] = useState<string>('-1')
const [expiresInDays, setExpiresInDays] = useState<string>('-1')
@ -21,27 +23,27 @@ export default function GenerateReferralLinkWidget({ onCreated }: Props) {
const expiryOptions = useMemo(
() => [
{ value: '1', label: '1 day' },
{ value: '2', label: '2 days' },
{ value: '3', label: '3 days' },
{ value: '4', label: '4 days' },
{ value: '5', label: '5 days' },
{ value: '6', label: '6 days' },
{ value: '7', label: '7 days' },
{ value: '-1', label: 'Never expires' },
{ value: '1', label: t('referralManagement.expiry1Day') },
{ value: '2', label: t('referralManagement.expiry2Days') },
{ value: '3', label: t('referralManagement.expiry3Days') },
{ value: '4', label: t('referralManagement.expiry4Days') },
{ value: '5', label: t('referralManagement.expiry5Days') },
{ value: '6', label: t('referralManagement.expiry6Days') },
{ value: '7', label: t('referralManagement.expiry7Days') },
{ value: '-1', label: t('referralManagement.expiryNever') },
],
[]
[t]
)
const maxUsesOptions = useMemo(
() => [
{ value: '1', label: '1 use' },
{ value: '5', label: '5 uses' },
{ value: '10', label: '10 uses' },
{ value: '50', label: '50 uses' },
{ value: '-1', label: 'Unlimited' },
{ value: '1', label: t('referralManagement.maxUses1') },
{ value: '5', label: t('referralManagement.maxUses5') },
{ value: '10', label: t('referralManagement.maxUses10') },
{ value: '50', label: t('referralManagement.maxUses50') },
{ value: '-1', label: t('referralManagement.maxUsesUnlimited') },
],
[]
[t]
)
// Handlers that enforce coupling
@ -117,10 +119,10 @@ export default function GenerateReferralLinkWidget({ onCreated }: Props) {
return (
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<h2 className="text-xl font-semibold text-gray-900 mb-4">Generate Referral Link</h2>
<h2 className="text-xl font-semibold text-gray-900 mb-4">{t('referralManagement.generateTitle')}</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Max Uses</label>
<label className="block text-sm font-medium text-gray-700 mb-1">{t('referralManagement.maxUsesLabel')}</label>
<select
value={maxUses}
onChange={(e) => onChangeMaxUses(e.target.value)}
@ -131,11 +133,11 @@ export default function GenerateReferralLinkWidget({ onCreated }: Props) {
<option key={opt.value} value={opt.value}>{opt.label}</option>
))}
</select>
{disableMaxUses && <p className="mt-1 text-xs text-gray-500">Locked by Never expires.</p>}
{disableMaxUses && <p className="mt-1 text-xs text-gray-500">{t('referralManagement.lockedByNeverExpires')}</p>}
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Expires In</label>
<label className="block text-sm font-medium text-gray-700 mb-1">{t('referralManagement.expiresIn')}</label>
<select
value={expiresInDays}
onChange={(e) => onChangeExpires(e.target.value)}
@ -146,7 +148,7 @@ export default function GenerateReferralLinkWidget({ onCreated }: Props) {
<option key={opt.value} value={opt.value}>{opt.label}</option>
))}
</select>
{disableExpires && <p className="mt-1 text-xs text-gray-500">Locked by Unlimited uses.</p>}
{disableExpires && <p className="mt-1 text-xs text-gray-500">{t('referralManagement.lockedByUnlimited')}</p>}
</div>
</div>
@ -156,7 +158,7 @@ export default function GenerateReferralLinkWidget({ onCreated }: Props) {
disabled={isGenerating}
className="inline-flex items-center gap-2 rounded-md bg-[#8D6B1D] px-4 py-2 text-white hover:bg-[#7A5E1A] disabled:opacity-60"
>
{isGenerating ? 'Generating...' : 'Generate Link'}
{isGenerating ? t('referralManagement.generating') : t('referralManagement.generateLink')}
</button>
{generatedLink && (
<div className="flex items-center gap-2">
@ -166,7 +168,7 @@ export default function GenerateReferralLinkWidget({ onCreated }: Props) {
className="inline-flex items-center gap-1 rounded-md border border-gray-300 px-3 py-2 text-sm text-gray-800 hover:bg-gray-50"
>
<ClipboardDocumentIcon className="h-4 w-4" />
{isCopying ? 'Copied' : 'Copy'}
{isCopying ? t('referralManagement.copied') : t('referralManagement.copy')}
</button>
</div>
)}

View File

@ -1,6 +1,7 @@
'use client'
import React, { useEffect, useMemo, useState } from 'react'
import { useTranslation } from '../../i18n/useTranslation'
interface Props {
// total points = total registered users via your referral
@ -11,11 +12,11 @@ interface Props {
// NEW: thresholds with names (Level 1+)
// Level 0 (<5) will be handled separately as "Starter"
const LEVELS = [
{ threshold: 5, name: 'Novice' }, // Level 1
{ threshold: 25, name: 'Hustler' }, // Level 2
{ threshold: 125, name: 'Entrepreneur' },// Level 3
{ threshold: 625, name: 'Prestige' }, // Level 4
{ threshold: 3125, name: 'MAX' }, // Level 5+
{ threshold: 5, nameKey: 'levelNovice' },
{ threshold: 25, nameKey: 'levelHustler' },
{ threshold: 125, nameKey: 'levelEntrepreneur' },
{ threshold: 625, nameKey: 'levelPrestige' },
{ threshold: 3125, nameKey: 'levelMax' },
]
// ...existing calc helpers...
@ -38,6 +39,7 @@ function nextThreshold(points: number) {
}
export default function LevelTrackerWidget({ points = 3, className = '' }: Props) {
const { t } = useTranslation()
const safePoints = Math.max(0, Math.floor(points))
// NEW: derive level index and name
@ -54,8 +56,8 @@ export default function LevelTrackerWidget({ points = 3, className = '' }: Props
const level = useMemo(() => calcLevel(safePoints), [safePoints])
const displayLevel = Math.min(level, LEVELS.length) // cap at 5 for display
const levelName = level === 0
? 'Starter'
: LEVELS[Math.min(level - 1, LEVELS.length - 1)].name
? t('referralManagement.levelStarter')
: t(`referralManagement.${LEVELS[Math.min(level - 1, LEVELS.length - 1)].nameKey}` as any)
// For progress to next target: keep growing in powers of 5, even after MAX (name stays MAX)
const target = useMemo(() => nextThreshold(safePoints), [safePoints])
@ -77,7 +79,7 @@ export default function LevelTrackerWidget({ points = 3, className = '' }: Props
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2">
<div className="flex items-baseline gap-2">
<span className="text-sm font-semibold text-gray-900">Level</span>
<span className="text-sm font-semibold text-gray-900">{t('referralManagement.levelLabel')}</span>
<span className="text-lg font-bold text-indigo-700">#{displayLevel}</span>
</div>
{/* NEW: level name badge */}
@ -86,7 +88,7 @@ export default function LevelTrackerWidget({ points = 3, className = '' }: Props
</span>
</div>
<div className="text-xs text-gray-600">
{targetProgress} of {target} referrals
{targetProgress} {t('referralManagement.of')} {target} {t('referralManagement.referrals')}
</div>
</div>
@ -101,8 +103,8 @@ export default function LevelTrackerWidget({ points = 3, className = '' }: Props
<div className="mt-2 flex items-center justify-between">
<span className="text-[11px] font-medium text-gray-600">
{displayLevel >= LEVELS.length
? 'Max level reached'
: `Next milestone: ${target} total referrals`}
? t('referralManagement.maxLevelReached')
: `${t('referralManagement.nextMilestone')}: ${target} ${t('referralManagement.referrals')}`}
</span>
<span className="text-[11px] font-semibold text-indigo-700">{animPercent}%</span>
</div>

View File

@ -3,6 +3,7 @@
import React, { useState } from 'react'
import { ClipboardDocumentIcon } from '@heroicons/react/24/outline'
import { useToast } from '../../components/toast/toastComponent'
import { useTranslation } from '../../i18n/useTranslation'
interface ReferralLink {
id?: string | number
@ -41,6 +42,7 @@ function shortLink(href?: string) {
export default function ReferralLinksListWidget({ links, onDeactivate }: Props) {
const { showToast } = useToast()
const { t } = useTranslation()
// Local floating tooltip (fixed) so table doesn't scroll to show it
const [tooltip, setTooltip] = useState<{ visible: boolean; text: string; x: number; y: number }>({
visible: false,
@ -61,15 +63,15 @@ export default function ReferralLinksListWidget({ links, onDeactivate }: Props)
await navigator.clipboard.writeText(text)
showToast({
variant: 'success',
title: 'Copied',
message: 'Link copied to Zwischenablage.',
title: t('referralManagement.copied'),
message: t('referralManagement.copiedMessage'),
duration: 2500,
})
} catch {
showToast({
variant: 'error',
title: 'Copy failed',
message: 'Could not copy link to clipboard.',
title: t('referralManagement.copyFailed'),
message: t('referralManagement.copyFailedMessage'),
})
}
}
@ -78,19 +80,19 @@ export default function ReferralLinksListWidget({ links, onDeactivate }: Props)
<>
<div className="mt-8 bg-white rounded-lg shadow-sm border border-gray-200">
<div className="p-6 border-b border-gray-100">
<h2 className="text-xl font-semibold text-gray-900">All Referral Links</h2>
<p className="text-sm text-gray-600 mt-1">Manage your links and see their status.</p>
<h2 className="text-xl font-semibold text-gray-900">{t('referralManagement.allLinks')}</h2>
<p className="text-sm text-gray-600 mt-1">{t('referralManagement.allLinksSubtitle')}</p>
</div>
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Link</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Created</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Expires</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Usage</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Status</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">{t('referralManagement.colLink')}</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">{t('referralManagement.colCreated')}</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">{t('referralManagement.colExpires')}</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">{t('referralManagement.colUsage')}</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">{t('referralManagement.colStatus')}</th>
<th className="px-6 py-3" />
</tr>
</thead>
@ -98,7 +100,7 @@ export default function ReferralLinksListWidget({ links, onDeactivate }: Props)
{links.length === 0 ? (
<tr>
<td colSpan={6} className="px-6 py-6 text-sm text-gray-500">
No referral links found.
{t('referralManagement.noLinks')}
</td>
</tr>
) : (
@ -106,7 +108,7 @@ export default function ReferralLinksListWidget({ links, onDeactivate }: Props)
const createdDate = l.createdAt ? new Date(l.createdAt) : null
const created = createdDate ? createdDate.toLocaleString() : '—'
const expiresDate = l.expiresAt ? new Date(l.expiresAt) : null
const expires = expiresDate ? expiresDate.toLocaleString() : 'Never'
const expires = expiresDate ? expiresDate.toLocaleString() : t('referralManagement.never')
const unlimited = !!(l.isUnlimited || l.maxUses === -1)
// Usage text and badge color
@ -115,7 +117,7 @@ export default function ReferralLinksListWidget({ links, onDeactivate }: Props)
: 0
const usage =
unlimited
? 'Unlimited'
? t('referralManagement.unlimited')
: (typeof l.uses === 'number' && typeof l.maxUses === 'number' && l.maxUses > 0
? `${l.uses} / ${l.maxUses}`
: (typeof l.uses === 'number' ? String(l.uses) : '—'))
@ -172,16 +174,16 @@ export default function ReferralLinksListWidget({ links, onDeactivate }: Props)
transition-all duration-150 hover:bg-gray-50 hover:shadow-sm hover:-translate-y-0.5 active:translate-y-0"
>
<ClipboardDocumentIcon className="h-4 w-4" />
Copy
{t('referralManagement.copy')}
</button>
{/* Mobile: only copy button */}
<button
onClick={() => copyToClipboard(l.url || String(l.code || ''))}
className="inline-flex md:hidden items-center gap-2 rounded border border-gray-300 px-3 py-2 text-xs text-gray-700 hover:bg-gray-50"
aria-label="Copy referral link"
aria-label={t('autofix.k77d5ecd9')}
>
<ClipboardDocumentIcon className="h-4 w-4" />
Copy link
{t('referralManagement.copyMobile')}
</button>
</div>
</td>
@ -226,7 +228,7 @@ export default function ReferralLinksListWidget({ links, onDeactivate }: Props)
disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-transparent disabled:shadow-none
"
>
Deactivate
{t('referralManagement.deactivate')}
</button>
</td>
</tr>

View File

@ -9,6 +9,7 @@ import {
BuildingOffice2Icon,
} from '@heroicons/react/24/outline'
import LevelTrackerWidget from './levelTrackerWidget' // NEW
import { useTranslation } from '../../i18n/useTranslation'
type Stats = {
activeLinks: number
@ -39,14 +40,15 @@ const renderStatCard = (
)
export default function ReferralStatisticWidget({ stats, totalReferredFromBackend }: Props) {
const { t } = useTranslation()
const topStats = [
{ label: 'Active Links', value: stats.activeLinks, icon: CheckCircleIcon, color: 'bg-green-500' },
{ label: 'Links Used', value: stats.linksUsed, icon: ChartBarIcon, color: 'bg-indigo-500' },
{ label: t('referralManagement.statsActiveLinks'), value: stats.activeLinks, icon: CheckCircleIcon, color: 'bg-green-500' },
{ label: t('referralManagement.statsLinksUsed'), value: stats.linksUsed, icon: ChartBarIcon, color: 'bg-indigo-500' },
]
const bottomStats = [
{ label: 'Personal Users', value: stats.personalUsersReferred, icon: UsersIcon, color: 'bg-blue-500' },
{ label: 'Company Users', value: stats.companyUsersReferred, icon: BuildingOffice2Icon, color: 'bg-amber-600' },
{ label: 'Total Links', value: stats.totalLinks, icon: LinkIcon, color: 'bg-yellow-500' },
{ label: t('referralManagement.statsPersonalUsers'), value: stats.personalUsersReferred, icon: UsersIcon, color: 'bg-blue-500' },
{ label: t('referralManagement.statsCompanyUsers'), value: stats.companyUsersReferred, icon: BuildingOffice2Icon, color: 'bg-amber-600' },
{ label: t('referralManagement.statsTotalLinks'), value: stats.totalLinks, icon: LinkIcon, color: 'bg-yellow-500' },
]
// NEW: prefer backend total_referred_users if provided

View File

@ -2,6 +2,7 @@
import React, { useMemo, useState } from 'react'
import { UsersIcon } from '@heroicons/react/24/outline'
import { useTranslation } from '../../i18n/useTranslation'
type UserType = 'personal' | 'company'
type UserStatus = 'active' | 'inactive' | 'pending' | 'blocked'
@ -59,6 +60,7 @@ function exportCsv(rows: RegisteredUser[]) {
}
export default function RegisteredUserList({ users, loading }: Props) {
const { t } = useTranslation()
// Normalize backend shape to local RegisteredUser shape
const normalizedUsers = useMemo<RegisteredUser[]>(() => {
if (!users || users.length === 0) return []
@ -129,28 +131,28 @@ export default function RegisteredUserList({ users, loading }: Props) {
<div className="mt-8 mb-8 bg-white rounded-lg shadow-sm border border-gray-200">
<div className="p-6 border-b border-gray-100 flex items-start justify-between gap-4">
<div>
<h2 className="text-xl font-semibold text-gray-900">Registered Users via Your Referral</h2>
<h2 className="text-xl font-semibold text-gray-900">{t('referralManagement.registeredUsersTitle')}</h2>
<div className="mt-2 inline-flex items-center gap-2">
<span className="inline-flex items-center gap-2 rounded-full bg-violet-100 text-violet-800 px-3 py-1 text-[11px] font-semibold tracking-wide">
<UsersIcon className="h-4 w-4" />
TOTAL REGISTERED USER WITH YOUR REF LINK
{t('referralManagement.totalRefBadge')}
</span>
<span className="inline-flex items-center rounded-full bg-violet-600 text-white px-2 py-1 text-[11px] font-bold">
{totalRegistered}
</span>
</div>
<p className="text-sm text-gray-600 mt-2">
Users who signed up using one of your referral links.
{t('referralManagement.registeredUsersSubtitle')}
</p>
<p className="text-xs text-gray-500 mt-2">
Showing the latest 5 users. Use View all to see the complete list.
{t('referralManagement.showingLatest5')}
</p>
</div>
<button
onClick={resetAndOpen}
className="inline-flex items-center rounded-md bg-indigo-600 px-3 py-2 text-sm font-medium text-white hover:bg-indigo-500"
>
View all
{t('referralManagement.viewAll')}
</button>
</div>
@ -158,11 +160,11 @@ export default function RegisteredUserList({ users, loading }: Props) {
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">User</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Email</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Type</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Registered</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Status</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">{t('referralManagement.colUser')}</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">{t('referralManagement.colEmail')}</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">{t('referralManagement.colType')}</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">{t('referralManagement.colRegistered')}</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">{t('referralManagement.colStatus')}</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-100 bg-white">
@ -175,7 +177,7 @@ export default function RegisteredUserList({ users, loading }: Props) {
) : rows.length === 0 ? (
<tr>
<td className="px-6 py-6 text-sm text-gray-500" colSpan={5}>
No registered users found.
{t('referralManagement.noRegisteredUsers')}
</td>
</tr>
) : (
@ -187,7 +189,7 @@ export default function RegisteredUserList({ users, loading }: Props) {
<td className="px-6 py-4 text-sm text-gray-700">{u.email}</td>
<td className="px-6 py-4 text-sm">
<span className={`inline-flex items-center rounded px-2 py-1 text-xs font-medium ${typeBadgeClass(u.userType)}`}>
{u.userType === 'company' ? 'Company' : 'Personal'}
{u.userType === 'company' ? t('referralManagement.typeCompany') : t('referralManagement.typePersonal')}
</span>
</td>
<td className="px-6 py-4 text-sm text-gray-700">{date}</td>
@ -213,21 +215,21 @@ export default function RegisteredUserList({ users, loading }: Props) {
<div className="w-full max-w-6xl bg-white rounded-xl shadow-2xl ring-1 ring-black/10 overflow-hidden">
<div className="px-6 py-4 border-b border-gray-200 flex items-center justify-between gap-3">
<div>
<h3 className="text-lg font-semibold text-gray-900">All Registered Users via Your Referral</h3>
<p className="text-xs text-gray-600">Search, filter, paginate, or export the full list.</p>
<h3 className="text-lg font-semibold text-gray-900">{t('referralManagement.allRegisteredUsersTitle')}</h3>
<p className="text-xs text-gray-600">{t('referralManagement.allRegisteredUsersSubtitle')}</p>
</div>
<div className="flex items-center gap-2">
<button
onClick={() => exportCsv(filtered)}
className="inline-flex items-center rounded-md border border-gray-300 px-3 py-1.5 text-sm text-gray-700 hover:bg-gray-50"
>
Export CSV
{t('referralManagement.exportCsv')}
</button>
<button
onClick={() => setOpen(false)}
className="inline-flex items-center rounded-md bg-gray-900 px-3 py-1.5 text-sm text-white hover:bg-gray-800"
>
Close
{t('common.close')}
</button>
</div>
</div>
@ -236,7 +238,7 @@ export default function RegisteredUserList({ users, loading }: Props) {
<input
value={query}
onChange={e => { setQuery(e.target.value); setPage(1) }}
placeholder="Search name or email…"
placeholder={t('referralManagement.searchPlaceholder')}
className="md:col-span-2 w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent text-gray-900 placeholder:text-gray-700 placeholder:opacity-100"
/>
<select
@ -244,20 +246,20 @@ export default function RegisteredUserList({ users, loading }: Props) {
onChange={e => { setTypeFilter(e.target.value as any); setPage(1) }}
className="w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent text-gray-900"
>
<option value="all">All Types</option>
<option value="personal">Personal</option>
<option value="company">Company</option>
<option value="all">{t('referralManagement.filterAllTypes')}</option>
<option value="personal">{t('referralManagement.typePersonal')}</option>
<option value="company">{t('referralManagement.typeCompany')}</option>
</select>
<select
value={statusFilter}
onChange={e => { setStatusFilter(e.target.value as any); setPage(1) }}
className="w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent text-gray-900"
>
<option value="all">All Status</option>
<option value="active">Active</option>
<option value="inactive">Inactive</option>
<option value="pending">Pending</option>
<option value="blocked">Blocked</option>
<option value="all">{t('referralManagement.filterAllStatus')}</option>
<option value="active">{t('referralManagement.filterActive')}</option>
<option value="inactive">{t('referralManagement.filterInactive')}</option>
<option value="pending">{t('referralManagement.filterPending')}</option>
<option value="blocked">{t('referralManagement.filterBlocked')}</option>
</select>
</div>
@ -265,18 +267,18 @@ export default function RegisteredUserList({ users, loading }: Props) {
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">User</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Email</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Type</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Registered</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Status</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">{t('referralManagement.colUser')}</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">{t('referralManagement.colEmail')}</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">{t('referralManagement.colType')}</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">{t('referralManagement.colRegistered')}</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">{t('referralManagement.colStatus')}</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-100 bg-white">
{pageRows.length === 0 ? (
<tr>
<td className="px-6 py-6 text-sm text-gray-500" colSpan={5}>
No users match your filters.
{t('referralManagement.noUsersMatchFilters')}
</td>
</tr>
) : (
@ -288,7 +290,7 @@ export default function RegisteredUserList({ users, loading }: Props) {
<td className="px-6 py-3 text-sm text-gray-700">{u.email}</td>
<td className="px-6 py-3 text-sm">
<span className={`inline-flex items-center rounded px-2 py-1 text-xs font-medium ${typeBadgeClass(u.userType)}`}>
{u.userType === 'company' ? 'Company' : 'Personal'}
{u.userType === 'company' ? t('referralManagement.typeCompany') : t('referralManagement.typePersonal')}
</span>
</td>
<td className="px-6 py-3 text-sm text-gray-700">{date}</td>
@ -307,7 +309,7 @@ export default function RegisteredUserList({ users, loading }: Props) {
<div className="px-6 py-4 flex items-center justify-between gap-3">
<span className="text-xs text-gray-600">
Showing {pageRows.length} of {filtered.length} users
{t('referralManagement.showing')} {pageRows.length} {t('referralManagement.of')} {filtered.length} {t('referralManagement.colUser').toLowerCase()}s
</span>
<div className="flex items-center gap-2">
<button
@ -315,17 +317,17 @@ export default function RegisteredUserList({ users, loading }: Props) {
onClick={() => setPage(p => Math.max(1, p - 1))}
className="rounded-md border border-gray-300 px-3 py-1.5 text-sm text-gray-700 disabled:opacity-50 hover:bg-gray-50"
>
Previous
{t('referralManagement.pagePrev')}
</button>
<span className="text-sm text-gray-700">
Page {page} of {totalPages}
{t('referralManagement.pageOf').replace('{page}', String(page)).replace('{total}', String(totalPages))}
</span>
<button
disabled={page >= totalPages}
onClick={() => setPage(p => Math.min(totalPages, p + 1))}
className="rounded-md border border-gray-300 px-3 py-1.5 text-sm text-gray-700 disabled:opacity-50 hover:bg-gray-50"
>
Next
{t('referralManagement.pageNext')}
</button>
</div>
</div>

View File

@ -13,9 +13,11 @@ import { useRouter } from 'next/navigation'
import useAuthStore from '../store/authStore'
import { useRegisteredUsers } from './hooks/registeredUsers'
import { ToastProvider, useToast } from '../components/toast/toastComponent' // NEW
import { useTranslation } from '../i18n/useTranslation'
function ReferralManagementPageInner() {
const { showToast } = useToast() // NEW
const { t } = useTranslation()
const router = useRouter()
const user = useAuthStore(s => s.user)
const isAuthReady = useAuthStore(s => s.isAuthReady)
@ -56,14 +58,14 @@ function ReferralManagementPageInner() {
if (res?.ok) {
showToast({
variant: 'success',
title: 'Link deactivated',
message: 'The referral link has been deactivated successfully.',
title: t('referralManagement.deactivated'),
message: t('referralManagement.deactivatedMessage'),
})
} else {
showToast({
variant: 'error',
title: 'Deactivate failed',
message: (res as any)?.body?.message || 'Could not deactivate the referral link.',
title: t('referralManagement.deactivateFailed'),
message: (res as any)?.body?.message || t('referralManagement.deactivateFailedMessage'),
})
}
@ -71,8 +73,8 @@ function ReferralManagementPageInner() {
} catch (e: any) {
showToast({
variant: 'error',
title: 'Deactivate failed',
message: 'Network error while deactivating the referral link.',
title: t('referralManagement.deactivateFailed'),
message: t('referralManagement.deactivateNetworkError'),
})
} finally {
setDeactivatePending(false)
@ -105,8 +107,8 @@ function ReferralManagementPageInner() {
}
showToast({
variant: 'error',
title: 'Access check failed',
message: 'User id is missing. Redirecting…',
title: t('referralManagement.accessCheckFailed'),
message: t('referralManagement.userIdMissing'),
})
router.replace('/dashboard')
return
@ -158,8 +160,8 @@ function ReferralManagementPageInner() {
if (!can) {
showToast({
variant: 'warning',
title: 'Access denied',
message: 'You do not have permission to access Referral Management.',
title: t('referralManagement.accessDenied'),
message: t('referralManagement.accessDeniedMessage'),
})
router.replace('/dashboard')
}
@ -170,8 +172,8 @@ function ReferralManagementPageInner() {
}
showToast({
variant: 'error',
title: 'Permission check failed',
message: 'Could not verify permissions. Redirecting…',
title: t('referralManagement.permCheckFailed'),
message: t('referralManagement.permCheckFailedMessage'),
})
router.replace('/dashboard')
}
@ -222,15 +224,15 @@ function ReferralManagementPageInner() {
if (!statsRes.ok) {
showToast({
variant: 'error',
title: 'Load failed',
message: 'Could not load referral statistics.',
title: t('referralManagement.loadFailed'),
message: t('referralManagement.loadStatsError'),
})
}
if (!listRes.ok) {
showToast({
variant: 'error',
title: 'Load failed',
message: 'Could not load referral links.',
title: t('referralManagement.loadFailed'),
message: t('referralManagement.loadLinksError'),
})
}
@ -276,7 +278,7 @@ function ReferralManagementPageInner() {
<div className="min-h-screen flex items-center justify-center">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-[#8D6B1D] mx-auto mb-4"></div>
<p className="text-slate-700">Loading...</p>
<p className="text-slate-700">{t('common.loading')}</p>
</div>
</div>
</PageLayout>
@ -289,9 +291,9 @@ function ReferralManagementPageInner() {
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
{/* Title */}
<div className="mb-8">
<h1 className="text-3xl font-bold text-gray-900">Referral Management</h1>
<h1 className="text-3xl font-bold text-gray-900">{t('referralManagement.title')}</h1>
<p className="text-gray-600 mt-2">
Create and manage your referral links. Track performance at a glance.
{t('referralManagement.description')}
</p>
</div>

View File

@ -5,6 +5,7 @@ import { EyeIcon, EyeSlashIcon } from '@heroicons/react/24/outline'
import { useRegister } from '../hooks/useRegister'
import { useToast } from '../../components/toast/toastComponent'
import TelephoneInput, { TelephoneInputHandle } from '../../components/phone/telephoneInput'
import { useTranslation } from '../../i18n/useTranslation'
interface RegisterFormProps {
mode: 'personal' | 'company' | 'guest'
@ -80,6 +81,7 @@ export default function RegisterForm({
// Hook for backend calls
const { registerPersonalReferral, registerCompanyReferral, registerGuest, error: regError } = useRegister()
const { showToast } = useToast()
const { t } = useTranslation()
// Guest form state
const [guestForm, setGuestForm] = useState({
@ -132,22 +134,22 @@ export default function RegisterForm({
!personalForm.email.trim() || !personalForm.confirmEmail.trim() ||
!personalForm.password.trim() || !personalForm.confirmPassword.trim()
) {
setError('All fields are required')
setError(t('register.errorAllRequired'))
return false
}
if (personalForm.email !== personalForm.confirmEmail) {
setError('Email addresses do not match')
setError(t('register.errorEmailMismatch'))
return false
}
if (personalForm.password !== personalForm.confirmPassword) {
setError('Passwords do not match')
setError(t('register.errorPasswordMismatch'))
return false
}
if (!/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[\W_]).{8,}$/.test(personalForm.password)) {
setError('Password must be at least 8 characters long and contain uppercase and lowercase letters, numbers and special characters')
setError(t('register.errorPasswordWeak'))
return false
}
@ -157,15 +159,15 @@ export default function RegisterForm({
const valid = phoneApi?.isValid() ?? false
if (!dialCode) {
setError('Please select a country code from the dropdown before continuing.')
setError(t('register.errorSelectCountryCode'))
return false
}
if (!intlNumber) {
setError('Please enter your phone number.')
setError(t('register.errorPhoneRequired'))
return false
}
if (!valid) {
setError('Please enter a valid mobile phone number.')
setError(t('register.errorPhoneInvalid'))
return false
}
@ -178,22 +180,22 @@ export default function RegisterForm({
!companyForm.confirmCompanyEmail.trim() || !companyForm.contactPersonName.trim() ||
!companyForm.password.trim() || !companyForm.confirmPassword.trim()
) {
setError('All fields are required')
setError(t('register.errorAllRequired'))
return false
}
if (companyForm.companyEmail !== companyForm.confirmCompanyEmail) {
setError('Email addresses do not match')
setError(t('register.errorEmailMismatch'))
return false
}
if (companyForm.password !== companyForm.confirmPassword) {
setError('Passwords do not match')
setError(t('register.errorPasswordMismatch'))
return false
}
if (!/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[\W_]).{8,}$/.test(companyForm.password)) {
setError('Password must be at least 8 characters long and contain uppercase and lowercase letters, numbers and special characters')
setError(t('register.errorPasswordWeak'))
return false
}
@ -209,15 +211,15 @@ export default function RegisterForm({
const contactValid = contactApi?.isValid() ?? false
if (!companyDialCode || !contactDialCode) {
setError('Please select country codes (dropdown) for both company and contact phone numbers.')
setError(t('register.errorBothCountryCodes'))
return false
}
if (!companyNumber || !contactNumber) {
setError('Please enter both company and contact phone numbers.')
setError(t('register.errorBothPhonesRequired'))
return false
}
if (!companyValid || !contactValid) {
setError('Please enter valid phone numbers for company and contact person.')
setError(t('register.errorBothPhonesInvalid'))
return false
}
@ -254,25 +256,25 @@ export default function RegisterForm({
if (res.ok) {
showToast({
variant: 'success',
title: 'Registration successful',
message: 'You can now log in with your new account.'
title: t('register.successTitle'),
message: t('register.successMessage')
})
onRegistered()
} else {
const msg = res.message || 'Registration failed. Please try again.'
const msg = res.message || t('register.failedMessage')
setError(msg)
showToast({
variant: 'error',
title: 'Registration failed',
title: t('register.failedTitle'),
message: msg
})
}
} catch (error) {
const msg = 'Registration failed. Please try again.'
const msg = t('register.failedMessage')
setError(msg)
showToast({
variant: 'error',
title: 'Registration failed',
title: t('register.failedTitle'),
message: msg
})
} finally {
@ -312,25 +314,25 @@ export default function RegisterForm({
if (res.ok) {
showToast({
variant: 'success',
title: 'Registration successful',
message: 'You can now log in with your new company account.'
title: t('register.successTitle'),
message: t('register.successCompanyMessage')
})
onRegistered()
} else {
const msg = res.message || 'Registration failed. Please try again.'
const msg = res.message || t('register.failedMessage')
setError(msg)
showToast({
variant: 'error',
title: 'Registration failed',
title: t('register.failedTitle'),
message: msg
})
}
} catch (error) {
const msg = 'Registration failed. Please try again.'
const msg = t('register.failedMessage')
setError(msg)
showToast({
variant: 'error',
title: 'Registration failed',
title: t('register.failedTitle'),
message: msg
})
} finally {
@ -344,7 +346,7 @@ export default function RegisterForm({
setError(regError)
showToast({
variant: 'error',
title: 'Registration failed',
title: t('register.failedTitle'),
message: regError
})
}
@ -371,19 +373,19 @@ export default function RegisterForm({
!guestForm.email.trim() || !guestForm.confirmEmail.trim() ||
!guestForm.password.trim() || !guestForm.confirmPassword.trim()
) {
setError('All fields are required')
setError(t('register.errorAllRequired'))
return false
}
if (guestForm.email !== guestForm.confirmEmail) {
setError('Email addresses do not match')
setError(t('register.errorEmailMismatch'))
return false
}
if (guestForm.password !== guestForm.confirmPassword) {
setError('Passwords do not match')
setError(t('register.errorPasswordMismatch'))
return false
}
if (!/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[\W_]).{8,}$/.test(guestForm.password)) {
setError('Password must be at least 8 characters long and contain uppercase and lowercase letters, numbers and special characters')
setError(t('register.errorPasswordWeak'))
return false
}
setError('')
@ -408,19 +410,19 @@ export default function RegisterForm({
if (res.ok) {
showToast({
variant: 'success',
title: 'Registration successful',
message: 'You can now log in to view your coffee abonnement.'
title: t('register.successTitle'),
message: t('register.successGuestMessage')
})
onRegistered()
} else {
const msg = res.message || 'Registration failed. Please try again.'
const msg = res.message || t('register.failedMessage')
setError(msg)
showToast({ variant: 'error', title: 'Registration failed', message: msg })
showToast({ variant: 'error', title: t('register.failedTitle'), message: msg })
}
} catch {
const msg = 'Registration failed. Please try again.'
const msg = t('register.failedMessage')
setError(msg)
showToast({ variant: 'error', title: 'Registration failed', message: msg })
showToast({ variant: 'error', title: t('register.failedTitle'), message: msg })
} finally {
setLoading(false)
}
@ -440,16 +442,16 @@ export default function RegisterForm({
const renderPasswordStrength = (password: string) => {
const strength = getPasswordStrength(password)
const rules = [
{ test: password.length >= 8, text: 'At least 8 characters' },
{ test: /[a-z]/.test(password), text: 'Lowercase letters (a-z)' },
{ test: /[A-Z]/.test(password), text: 'Uppercase letters (A-Z)' },
{ test: /\d/.test(password), text: 'Digits (0-9)' },
{ test: /[\W_]/.test(password), text: 'Special characters (!@#$...)' }
{ test: password.length >= 8, text: t('register.pwdMinLength') },
{ test: /[a-z]/.test(password), text: t('register.pwdLowercase') },
{ test: /[A-Z]/.test(password), text: t('register.pwdUppercase') },
{ test: /\d/.test(password), text: t('register.pwdDigits') },
{ test: /[\W_]/.test(password), text: t('register.pwdSpecial') }
]
return (
<div className="mt-2">
<div className="text-sm text-slate-700 mb-2">Password requirements:</div>
<div className="text-sm text-slate-700 mb-2">{t('register.passwordRequirements')}</div>
<ul className="text-sm space-y-1">
{rules.map((rule, index) => (
<li
@ -471,11 +473,11 @@ export default function RegisterForm({
{/* Header */}
<div className="mb-6 text-center">
<h2 className="text-2xl sm:text-3xl font-extrabold text-[#0F172A] mb-2">
Registration for Profit Planet
{t('register.formTitle')}
</h2>
{referrerEmail && (
<p className="text-base sm:text-sm text-[#8D6B1D] font-medium">
You were invited by <span className="font-semibold">{referrerEmail}</span>!
{t('register.invitedBy')} <span className="font-semibold">{referrerEmail}</span>!
</p>
)}
</div>
@ -488,7 +490,7 @@ export default function RegisterForm({
className="px-6 py-2 rounded-md font-semibold text-sm bg-[#8D6B1D] text-white shadow-sm cursor-default"
type="button"
>
Guest
{t('register.tabGuest')}
</button>
) : (
<>
@ -501,7 +503,7 @@ export default function RegisterForm({
onClick={() => setMode('personal')}
type="button"
>
Individual
{t('register.tabIndividual')}
</button>
<button
className={`px-6 py-2 rounded-md font-semibold text-sm transition-all duration-200 ${
@ -512,7 +514,7 @@ export default function RegisterForm({
onClick={() => setMode('company')}
type="button"
>
Company
{t('register.tabCompany')}
</button>
</>
)}
@ -533,7 +535,7 @@ export default function RegisterForm({
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label htmlFor="firstName" className="block text-sm font-medium text-[#0F172A] mb-2">
First name *
{t('register.firstName')} *
</label>
<input
type="text"
@ -548,7 +550,7 @@ export default function RegisterForm({
<div>
<label htmlFor="lastName" className="block text-sm font-medium text-[#0F172A] mb-2">
Last name *
{t('register.lastName')} *
</label>
<input
type="text"
@ -565,7 +567,7 @@ export default function RegisterForm({
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label htmlFor="email" className="block text-sm font-medium text-[#0F172A] mb-2">
Email address *
{t('register.email')} *
</label>
<input
type="email"
@ -580,7 +582,7 @@ export default function RegisterForm({
<div>
<label htmlFor="confirmEmail" className="block text-sm font-medium text-[#0F172A] mb-2">
Confirm email *
{t('register.confirmEmail')} *
</label>
<input
type="email"
@ -595,15 +597,13 @@ export default function RegisterForm({
</div>
<div>
<label htmlFor="phoneNumber" className="block text-sm font-medium text-[#0F172A] mb-2">
Phone number *
</label>
<label htmlFor="phoneNumber" className="block text-sm font-medium text-[#0F172A] mb-2">{t('autofix.kfb1676b0')}</label>
<TelephoneInput
id="phoneNumber"
name="phoneNumber"
ref={personalPhoneRef}
autoComplete="tel"
placeholder="e.g. +43 676 1234567"
placeholder={t('autofix.k9f56d4ac')}
required
onChange={e =>
setPersonalForm(prev => ({ ...prev, phoneNumber: (e.target as HTMLInputElement).value }))
@ -614,7 +614,7 @@ export default function RegisterForm({
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label htmlFor="password" className="block text-sm font-medium text-[#0F172A] mb-2">
Password *
{t('register.password')} *
</label>
<div className="relative">
<input
@ -644,7 +644,7 @@ export default function RegisterForm({
<div>
<label htmlFor="confirmPassword" className="block text-sm font-medium text-[#0F172A] mb-2">
Confirm password *
{t('register.confirmPassword')} *
</label>
<input
type={showPersonalPassword ? 'text' : 'password'}
@ -671,10 +671,10 @@ export default function RegisterForm({
{loading ? (
<>
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
Registration in progress...
{t('register.submitting')}
</>
) : (
'Register now'
t('register.registerNow')
)}
</button>
</form>
@ -683,7 +683,7 @@ export default function RegisterForm({
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label htmlFor="companyName" className="block text-sm font-medium text-[#0F172A] mb-2">
Company name *
{t('register.companyName')} *
</label>
<input
type="text"
@ -698,7 +698,7 @@ export default function RegisterForm({
<div>
<label htmlFor="contactPersonName" className="block text-sm font-medium text-[#0F172A] mb-2">
Contact person *
{t('register.contactPersonName')} *
</label>
<input
type="text"
@ -715,7 +715,7 @@ export default function RegisterForm({
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label htmlFor="companyEmail" className="block text-sm font-medium text-[#0F172A] mb-2">
Company email *
{t('register.companyEmail')} *
</label>
<input
type="email"
@ -730,7 +730,7 @@ export default function RegisterForm({
<div>
<label htmlFor="confirmCompanyEmail" className="block text-sm font-medium text-[#0F172A] mb-2">
Confirm email *
{t('register.confirmEmail')} *
</label>
<input
type="email"
@ -747,14 +747,14 @@ export default function RegisterForm({
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label htmlFor="companyPhone" className="block text-sm font-medium text-[#0F172A] mb-2">
Company phone *
{t('register.companyPhone')} *
</label>
<TelephoneInput
id="companyPhone"
name="companyPhone"
ref={companyPhoneRef}
autoComplete="tel"
placeholder="e.g. +43 1 234567"
placeholder={t('autofix.k8eaa7b3b')}
required
onChange={e =>
setCompanyForm(prev => ({ ...prev, companyPhone: (e.target as HTMLInputElement).value }))
@ -764,14 +764,14 @@ export default function RegisterForm({
<div>
<label htmlFor="contactPersonPhone" className="block text-sm font-medium text-[#0F172A] mb-2">
Contact person phone *
{t('register.contactPersonPhone')} *
</label>
<TelephoneInput
id="contactPersonPhone"
name="contactPersonPhone"
ref={contactPhoneRef}
autoComplete="tel"
placeholder="e.g. +43 676 1234567"
placeholder={t('autofix.k9f56d4ac')}
required
onChange={e =>
setCompanyForm(prev => ({
@ -786,7 +786,7 @@ export default function RegisterForm({
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label htmlFor="password" className="block text-sm font-medium text-[#0F172A] mb-2">
Password *
{t('register.password')} *
</label>
<div className="relative">
<input
@ -816,7 +816,7 @@ export default function RegisterForm({
<div>
<label htmlFor="confirmPassword" className="block text-sm font-medium text-[#0F172A] mb-2">
Confirm password *
{t('register.confirmPassword')} *
</label>
<input
type={showCompanyPassword ? 'text' : 'password'}
@ -843,10 +843,10 @@ export default function RegisterForm({
{loading ? (
<>
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
Registration in progress...
{t('register.submitting')}
</>
) : (
'Register company'
t('register.submitCompany')
)}
</button>
</form>
@ -854,14 +854,14 @@ export default function RegisterForm({
<form onSubmit={handleGuestSubmit} className="space-y-6">
<div className="p-4 bg-amber-50/70 backdrop-blur-[18px] border border-amber-200/70 rounded-lg mb-2">
<p className="text-amber-800 text-sm font-medium">
You are registering as a guest. You will have access to your coffee abonnements only.
{t('register.guestNote')}
</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label htmlFor="guestFirstName" className="block text-sm font-medium text-[#0F172A] mb-2">
First name *
{t('register.firstName')} *
</label>
<input
type="text"
@ -875,7 +875,7 @@ export default function RegisterForm({
</div>
<div>
<label htmlFor="guestLastName" className="block text-sm font-medium text-[#0F172A] mb-2">
Last name *
{t('register.lastName')} *
</label>
<input
type="text"
@ -892,7 +892,7 @@ export default function RegisterForm({
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label htmlFor="guestEmail" className="block text-sm font-medium text-[#0F172A] mb-2">
Email *
{t('register.email')} *
</label>
<input
type="email"
@ -906,7 +906,7 @@ export default function RegisterForm({
</div>
<div>
<label htmlFor="guestConfirmEmail" className="block text-sm font-medium text-[#0F172A] mb-2">
Confirm email *
{t('register.confirmEmail')} *
</label>
<input
type="email"
@ -923,7 +923,7 @@ export default function RegisterForm({
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label htmlFor="guestPassword" className="block text-sm font-medium text-[#0F172A] mb-2">
Password *
{t('register.password')} *
</label>
<div className="relative">
<input
@ -951,7 +951,7 @@ export default function RegisterForm({
</div>
<div>
<label htmlFor="guestConfirmPassword" className="block text-sm font-medium text-[#0F172A] mb-2">
Confirm password *
{t('register.confirmPassword')} *
</label>
<input
type="password"
@ -977,10 +977,10 @@ export default function RegisterForm({
{loading ? (
<>
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
Registration in progress...
{t('register.submitting')}
</>
) : (
'Register as Guest'
t('register.submitGuest')
)}
</button>
</form>
@ -990,12 +990,12 @@ export default function RegisterForm({
{/* Login Link */}
<div className="mt-8 text-center">
<p className="text-slate-700">
Already registered?{' '}
{t('register.alreadyHaveAccount')}{' '}
<a
href="/login"
className="text-[#8D6B1D] hover:text-[#7A5E1A] font-medium transition-colors"
>
Login here
{t('register.loginHere')}
</a>
</p>
</div>

View File

@ -3,6 +3,7 @@
import { Fragment } from 'react'
import { Dialog, Transition } from '@headlessui/react'
import { ExclamationTriangleIcon } from '@heroicons/react/24/outline'
import { useTranslation } from '../../i18n/useTranslation'
interface SessionDetectedModalProps {
open: boolean
@ -16,8 +17,7 @@ export default function SessionDetectedModal({
onLogout,
onCancel,
inline = false
}: SessionDetectedModalProps) {
// Make inline + non-inline consistent
}: SessionDetectedModalProps) { const { t } = useTranslation() // Make inline + non-inline consistent
if (!open) return null
if (inline) {
@ -30,10 +30,10 @@ export default function SessionDetectedModal({
</div>
<div>
<h3 className="text-base font-semibold leading-6 text-[#0F172A]">
Active session detected
{t('register.sessionDetectedTitle')}
</h3>
<p className="mt-2 text-sm text-[#4A4A4A]">
You are already logged in. To register, you must first log out or you can go to the dashboard.
{t('register.sessionDescription')}
</p>
<div className="mt-5 flex flex-col sm:flex-row gap-3 sm:justify-end">
<button
@ -41,14 +41,14 @@ export default function SessionDetectedModal({
onClick={onCancel}
className="inline-flex justify-center rounded-md bg-white/80 px-4 py-2 text-sm font-medium text-[#4A4A4A] shadow-sm ring-1 ring-gray-300/70 hover:bg-gray-50 transition-colors"
>
Go to dashboard
{t('register.goToDashboard')}
</button>
<button
type="button"
onClick={onLogout}
className="inline-flex justify-center rounded-md bg-[#8D6B1D] px-4 py-2 text-sm font-semibold text-white shadow-sm hover:bg-[#7A5E1A] transition-colors"
>
Log out and register
{t('register.sessionLogout')}
</button>
</div>
</div>
@ -98,11 +98,11 @@ export default function SessionDetectedModal({
as="h3"
className="text-base font-semibold leading-6 text-[#0F172A]"
>
Active session detected
{t('register.sessionDetectedTitle')}
</Dialog.Title>
<div className="mt-2">
<p className="text-sm text-[#4A4A4A]">
You are already logged in. To register, you must first log out or you can go to the dashboard.
{t('register.sessionDescription')}
</p>
</div>
</div>
@ -113,14 +113,14 @@ export default function SessionDetectedModal({
className="inline-flex w-full justify-center rounded-md bg-[#8D6B1D] px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-[#7A5E1A] sm:ml-3 sm:w-auto transition-colors"
onClick={onLogout}
>
Log out and register
{t('register.sessionLogout')}
</button>
<button
type="button"
className="mt-3 inline-flex w-full justify-center rounded-md bg-white px-3 py-2 text-sm font-semibold text-[#4A4A4A] shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50 sm:mt-0 sm:w-auto transition-colors"
onClick={onCancel}
>
Go to dashboard
{t('register.goToDashboard')}
</button>
</div>
</Dialog.Panel>

View File

@ -2,6 +2,7 @@
import React from 'react'
import { ExclamationTriangleIcon } from '@heroicons/react/24/outline'
import { useTranslation } from '../../i18n/useTranslation'
interface InvalidRefLinkModalProps {
open: boolean
@ -18,6 +19,7 @@ export default function InvalidRefLinkModal({
onClose,
onGoHome
}: InvalidRefLinkModalProps) {
const { t } = useTranslation()
if (!open) return null
const Content = (
@ -27,13 +29,13 @@ export default function InvalidRefLinkModal({
<ExclamationTriangleIcon className="h-6 w-6 text-red-600" />
</div>
<div>
<h3 className="text-lg font-semibold text-[#0F172A]">Invalid invitation link</h3>
<h3 className="text-lg font-semibold text-[#0F172A]">{t('register.invalidLinkTitle')}</h3>
<p className="mt-1 text-sm text-slate-700">
This registration link is invalid or no longer active. Please request a new link.
{t('register.invalidLinkMessage')}
</p>
{token && (
<p className="mt-2 text-xs text-slate-500">
Token: <span className="font-mono break-all">{token}</span>
{t('register.tokenLabel')}: <span className="font-mono break-all">{token}</span>
</p>
)}
<div className="mt-4 flex flex-wrap items-center gap-2">
@ -41,14 +43,14 @@ export default function InvalidRefLinkModal({
onClick={onGoHome}
className="inline-flex items-center rounded-md bg-[#8D6B1D] px-3.5 py-2 text-sm font-semibold text-white shadow-sm hover:bg-[#7A5E1A] transition-colors"
>
Go to homepage
{t('register.goToHomepage')}
</button>
{onClose && (
<button
onClick={onClose}
className="inline-flex items-center rounded-md border border-slate-300/80 bg-white/70 px-3.5 py-2 text-sm font-semibold text-slate-800 hover:bg-slate-50/90 transition-colors"
>
Close
{t('common.close')}
</button>
)}
</div>

View File

@ -10,9 +10,11 @@ import InvalidRefLinkModal from './components/invalidRefLinkModal'
import { ToastProvider, useToast } from '../components/toast/toastComponent'
import Waves from '../components/background/waves'
import BlueBlurryBackground from '../components/background/blueblurry' // NEW
import { useTranslation } from '../i18n/useTranslation'
// NEW: inner component that actually uses useToast and all the logic
function RegisterPageInner() {
const { t } = useTranslation()
const searchParams = useSearchParams()
const refToken = searchParams.get('ref')
const isGuestInvite = searchParams.get('guest') === 'true'
@ -59,8 +61,8 @@ function RegisterPageInner() {
}
showToast({
variant: 'error',
title: 'Invitation error',
message: 'No invitation token found in the link.'
title: t('register.invalidInvitationTitle'),
message: t('register.noInvitationToken')
})
return
}
@ -91,16 +93,16 @@ function RegisterPageInner() {
setInvalidRef(false)
showToast({
variant: 'success',
title: 'Invitation verified',
message: 'Your invitation link is valid. You can register now.'
title: t('register.invitationVerifiedTitle'),
message: t('register.invitationVerifiedMessage')
})
} else {
const reason = body?.reason || `HTTP ${res.status}`
setInvalidRef(true)
showToast({
variant: 'error',
title: 'Invalid invitation',
message: `Reason: ${reason}. This invitation link is invalid or no longer active.`
title: t('register.invalidInvitationTitle'),
message: `${reason ? `${reason}. ` : ''}${t('register.invalidInvitationMessage')}`
})
}
setIsRefChecked(true)
@ -113,8 +115,8 @@ function RegisterPageInner() {
}
showToast({
variant: 'error',
title: 'Network error',
message: 'Could not reach the server. Is the backend running?'
title: t('common.error'),
message: t('register.networkError')
})
}
}
@ -214,7 +216,7 @@ function RegisterPageInner() {
<div className="absolute -top-24 -right-24 size-56 rounded-full bg-gradient-to-br from-indigo-500/10 to-fuchsia-500/10 blur-3xl pointer-events-none" />
<div className="relative text-center">
<div className="animate-spin rounded-full h-10 w-10 border-2 border-b-transparent border-[#8D6B1D] mx-auto mb-4" />
<p className="text-slate-700">Checking invitation link</p>
<p className="text-slate-700">{t('register.checkingInvitation')}</p>
</div>
</div>
</div>
@ -282,12 +284,12 @@ function RegisterPageInner() {
<div className="relative">
<div className="mx-auto max-w-2xl text-center mb-8">
<h1 className="text-3xl sm:text-4xl font-extrabold tracking-tight text-[#0F172A]">
{mode === 'guest' ? 'Guest Registration' : 'Register now'}
{mode === 'guest' ? t('register.guestRegistration') : t('register.registerNow')}
</h1>
<p className="mt-3 text-slate-700 text-base sm:text-lg">
{mode === 'guest'
? 'Register as a guest to access your coffee abonnement.'
: 'Create your personal or company account with Profit Planet.'}
? t('register.guestDescription')
: t('register.personalDescription')}
</p>
</div>
@ -314,7 +316,7 @@ function RegisterPageInner() {
)}
{registered && (
<div className="mt-6 mx-auto text-center text-sm text-gray-700">
Registration successful redirecting...
{t('register.successRedirecting')}
</div>
)}
</>
@ -329,22 +331,26 @@ function RegisterPageInner() {
)
}
// NEW: default export only provides the ToastProvider wrapper
function RegisterSuspenseFallback() {
const { t } = useTranslation()
return (
<PageLayout>
<div className="min-h-screen flex items-center justify-center">
<div className="text-center">
<div className="animate-spin rounded-full h-10 w-10 border-2 border-b-transparent border-[#8D6B1D] mx-auto mb-3" />
<p className="text-[#4A4A4A]">{t('autofix.k832387c5')}</p>
</div>
</div>
</PageLayout>
)
}
export default function RegisterPage() {
return (
<ToastProvider>
<Suspense
fallback={
<PageLayout>
<div className="min-h-screen flex items-center justify-center">
<div className="text-center">
<div className="animate-spin rounded-full h-10 w-10 border-2 border-b-transparent border-[#8D6B1D] mx-auto mb-3" />
<p className="text-[#4A4A4A]">Loading...</p>
</div>
</div>
</PageLayout>
}
>
<Suspense fallback={<RegisterSuspenseFallback />}>
<RegisterPageInner />
</Suspense>
</ToastProvider>

View File

@ -1,5 +1,8 @@
'use client'
import { useTranslation } from '../i18n/useTranslation';
import { useState } from 'react'
import { notFound } from 'next/navigation'
import { MagnifyingGlassIcon, FunnelIcon, Squares2X2Icon, ListBulletIcon, HeartIcon, StarIcon, ShoppingCartIcon } from '@heroicons/react/20/solid'
@ -131,6 +134,7 @@ const sampleProducts = [
]
export default function StorePage() {
const { t } = useTranslation();
const SHOW_SHOP = 'true'
if (!SHOW_SHOP) notFound()
const [selectedCategory, setSelectedCategory] = useState('Alle Kategorien')
@ -236,12 +240,8 @@ export default function StorePage() {
<div className="mx-auto max-w-7xl">
{/* Store Title */}
<div className="text-center mb-8">
<h1 className="text-3xl font-bold tracking-tight text-white sm:text-4xl">
Profit Planet Store
</h1>
<p className="mt-2 text-lg text-gray-300">
Nachhaltige Produkte für deinen Erfolg
</p>
<h1 className="text-3xl font-bold tracking-tight text-white sm:text-4xl">{t('autofix.ka7073aee')}</h1>
<p className="mt-2 text-lg text-gray-300">{t('autofix.kd68da70d')}</p>
</div>
{/* Search & Navigation Section */}
@ -257,7 +257,7 @@ export default function StorePage() {
name="search"
id="search"
className="block w-full rounded-lg border-0 bg-white/90 backdrop-blur-sm py-3 pl-10 pr-3 text-gray-900 placeholder-gray-500 shadow-lg ring-1 ring-inset ring-white/10 focus:ring-2 focus:ring-inset focus:ring-[#8D6B1D] sm:text-sm sm:leading-6"
placeholder="Produkte durchsuchen..."
placeholder={t('autofix.k63458f03')}
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
@ -298,8 +298,7 @@ export default function StorePage() {
{/* Left: Results Count & Filters */}
<div className="flex items-center space-x-4">
<p className="text-sm text-gray-700">
<span className="font-medium">{sortedProducts.length}</span> Produkte gefunden
</p>
<span className="font-medium">{sortedProducts.length}</span>{t('autofix.k55aba973')}</p>
<button
type="button"
@ -372,14 +371,14 @@ export default function StorePage() {
<div className="flex space-x-3">
<input
type="number"
placeholder="Min €"
placeholder={t('autofix.k0c838ec3')}
value={priceRange.min}
onChange={(e) => setPriceRange({...priceRange, min: e.target.value})}
className="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-[#8D6B1D] sm:text-sm sm:leading-6"
/>
<input
type="number"
placeholder="Max €"
placeholder={t('autofix.k0c87d75d')}
value={priceRange.max}
onChange={(e) => setPriceRange({...priceRange, max: e.target.value})}
className="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-[#8D6B1D] sm:text-sm sm:leading-6"
@ -418,7 +417,7 @@ export default function StorePage() {
{/* Availability */}
<div>
<h3 className="text-sm font-medium text-gray-900">Verfügbarkeit</h3>
<h3 className="text-sm font-medium text-gray-900">{t('autofix.k56435c9b')}</h3>
<div className="mt-2 space-y-2">
<label className="flex items-center">
<input
@ -433,7 +432,7 @@ export default function StorePage() {
}}
className="h-4 w-4 rounded border-gray-300 text-[#8D6B1D] focus:ring-[#8D6B1D]"
/>
<span className="ml-2 text-sm text-gray-600">Auf Lager</span>
<span className="ml-2 text-sm text-gray-600">{t('autofix.k21db276a')}</span>
</label>
<label className="flex items-center">
<input
@ -462,7 +461,7 @@ export default function StorePage() {
onChange={(e) => setSelectedBrand(e.target.value)}
className="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-inset focus:ring-[#8D6B1D] sm:text-sm sm:leading-6"
>
<option>Alle Marken</option>
<option>{t('autofix.kd1c17b3f')}</option>
<option>EcoTech</option>
<option>GreenLife</option>
<option>SustainableStyle</option>
@ -477,9 +476,7 @@ export default function StorePage() {
type="button"
onClick={resetFilters}
className="rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50 transition-colors duration-200"
>
Filter zurücksetzen
</button>
>{t('autofix.k5e580e3f')}</button>
</div>
</div>
</div>
@ -489,10 +486,8 @@ export default function StorePage() {
<div className="mx-auto max-w-7xl px-4 py-8 sm:px-6 lg:px-8">
{sortedProducts.length === 0 ? (
<div className="text-center py-16">
<p className="text-lg text-gray-500">Keine Produkte gefunden</p>
<p className="mt-2 text-sm text-gray-400">
Versuche andere Suchbegriffe oder Filter
</p>
<p className="text-lg text-gray-500">{t('autofix.k4cb62cff')}</p>
<p className="mt-2 text-sm text-gray-400">{t('autofix.k0cc2a3ba')}</p>
</div>
) : (
<>

View File

@ -1,5 +1,8 @@
'use client'
import { useTranslation } from '../../i18n/useTranslation';
import { useState } from 'react'
import { notFound } from 'next/navigation'
import { MagnifyingGlassIcon, FunnelIcon, Squares2X2Icon, ListBulletIcon, HeartIcon, StarIcon, ShoppingCartIcon } from '@heroicons/react/20/solid'
@ -132,6 +135,7 @@ const sampleProducts = [
]
export default function StorePage() {
const { t } = useTranslation();
const SHOW_SHOP = process.env.NEXT_PUBLIC_SHOW_SHOP === 'true'
if (!SHOW_SHOP) notFound()
const [selectedCategory, setSelectedCategory] = useState('Alle Kategorien')
@ -237,12 +241,8 @@ export default function StorePage() {
<div className="mx-auto max-w-7xl">
{/* Store Title */}
<div className="text-center mb-8">
<h1 className="text-3xl font-bold tracking-tight text-white sm:text-4xl">
Profit Planet Store
</h1>
<p className="mt-2 text-lg text-gray-300">
Nachhaltige Produkte für deinen Erfolg
</p>
<h1 className="text-3xl font-bold tracking-tight text-white sm:text-4xl">{t('autofix.ka7073aee')}</h1>
<p className="mt-2 text-lg text-gray-300">{t('autofix.kd68da70d')}</p>
</div>
{/* Search & Navigation Section */}
@ -258,7 +258,7 @@ export default function StorePage() {
name="search"
id="search"
className="block w-full rounded-lg border-0 bg-white/90 backdrop-blur-sm py-3 pl-10 pr-3 text-gray-900 placeholder-gray-500 shadow-lg ring-1 ring-inset ring-white/10 focus:ring-2 focus:ring-inset focus:ring-[#8D6B1D] sm:text-sm sm:leading-6"
placeholder="Produkte durchsuchen..."
placeholder={t('autofix.k63458f03')}
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
@ -299,8 +299,7 @@ export default function StorePage() {
{/* Left: Results Count & Filters */}
<div className="flex items-center space-x-4">
<p className="text-sm text-gray-700">
<span className="font-medium">{sortedProducts.length}</span> Produkte gefunden
</p>
<span className="font-medium">{sortedProducts.length}</span>{t('autofix.k55aba973')}</p>
<button
type="button"
@ -373,14 +372,14 @@ export default function StorePage() {
<div className="flex space-x-3">
<input
type="number"
placeholder="Min €"
placeholder={t('autofix.k0c838ec3')}
value={priceRange.min}
onChange={(e) => setPriceRange({...priceRange, min: e.target.value})}
className="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-[#8D6B1D] sm:text-sm sm:leading-6"
/>
<input
type="number"
placeholder="Max €"
placeholder={t('autofix.k0c87d75d')}
value={priceRange.max}
onChange={(e) => setPriceRange({...priceRange, max: e.target.value})}
className="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-[#8D6B1D] sm:text-sm sm:leading-6"
@ -419,7 +418,7 @@ export default function StorePage() {
{/* Availability */}
<div>
<h3 className="text-sm font-medium text-gray-900">Verfügbarkeit</h3>
<h3 className="text-sm font-medium text-gray-900">{t('autofix.k56435c9b')}</h3>
<div className="mt-2 space-y-2">
<label className="flex items-center">
<input
@ -434,7 +433,7 @@ export default function StorePage() {
}}
className="h-4 w-4 rounded border-gray-300 text-[#8D6B1D] focus:ring-[#8D6B1D]"
/>
<span className="ml-2 text-sm text-gray-600">Auf Lager</span>
<span className="ml-2 text-sm text-gray-600">{t('autofix.k21db276a')}</span>
</label>
<label className="flex items-center">
<input
@ -463,7 +462,7 @@ export default function StorePage() {
onChange={(e) => setSelectedBrand(e.target.value)}
className="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-inset focus:ring-[#8D6B1D] sm:text-sm sm:leading-6"
>
<option>Alle Marken</option>
<option>{t('autofix.kd1c17b3f')}</option>
<option>EcoTech</option>
<option>GreenLife</option>
<option>SustainableStyle</option>
@ -478,9 +477,7 @@ export default function StorePage() {
type="button"
onClick={resetFilters}
className="rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50 transition-colors duration-200"
>
Filter zurücksetzen
</button>
>{t('autofix.k5e580e3f')}</button>
</div>
</div>
</div>
@ -490,10 +487,8 @@ export default function StorePage() {
<div className="mx-auto max-w-7xl px-4 py-8 sm:px-6 lg:px-8">
{sortedProducts.length === 0 ? (
<div className="text-center py-16">
<p className="text-lg text-gray-500">Keine Produkte gefunden</p>
<p className="mt-2 text-sm text-gray-400">
Versuche andere Suchbegriffe oder Filter
</p>
<p className="text-lg text-gray-500">{t('autofix.k4cb62cff')}</p>
<p className="mt-2 text-sm text-gray-400">{t('autofix.k0cc2a3ba')}</p>
</div>
) : (
<>

View File

@ -1,5 +1,8 @@
"use client"
import { useTranslation } from '../../i18n/useTranslation';
import { useState, useEffect } from 'react'
import { notFound } from 'next/navigation'
import { StarIcon } from '@heroicons/react/20/solid'
@ -30,6 +33,7 @@ function classNames(...classes: (string | undefined | null | boolean)[]): string
}
export default function VipShopPage() {
const { t } = useTranslation();
const SHOW_SHOP = process.env.NEXT_PUBLIC_SHOW_SHOP === 'true'
if (!SHOW_SHOP) notFound()
@ -67,7 +71,7 @@ export default function VipShopPage() {
<div className="min-h-screen flex items-center justify-center">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-[#8D6B1D] mx-auto mb-4"></div>
<p className="text-[#4A4A4A]">Shop wird geladen...</p>
<p className="text-[#4A4A4A]">{t('autofix.k5fb70267')}</p>
</div>
</div>
</PageLayout>
@ -81,12 +85,8 @@ export default function VipShopPage() {
<div className="pt-16 pb-80 sm:pt-24 sm:pb-40 lg:pt-40 lg:pb-48">
<div className="relative mx-auto max-w-7xl px-4 sm:static sm:px-6 lg:px-8">
<div className="sm:max-w-lg">
<h1 className="text-4xl font-bold tracking-tight text-gray-900 sm:text-6xl">
Shop with an infinite variety of products
</h1>
<p className="mt-4 text-xl text-gray-500">
Discover a curated selection of high-quality products that cater to your every need.
</p>
<h1 className="text-4xl font-bold tracking-tight text-gray-900 sm:text-6xl">{t('autofix.kf663ef67')}</h1>
<p className="mt-4 text-xl text-gray-500">{t('autofix.kdca959c3')}</p>
</div>
<div>
<div className="mt-10">
@ -124,9 +124,7 @@ export default function VipShopPage() {
</div>
</div>
<a href="#" className="inline-block rounded-md border border-transparent bg-indigo-600 px-8 py-3 text-center font-medium text-white hover:bg-indigo-700">
Shop Collection
</a>
<a href="#" className="inline-block rounded-md border border-transparent bg-indigo-600 px-8 py-3 text-center font-medium text-white hover:bg-indigo-700">{t('autofix.kb8d6f3f7')}</a>
</div>
</div>
</div>
@ -136,10 +134,8 @@ export default function VipShopPage() {
<div className="bg-white">
<div className="mx-auto max-w-7xl px-4 py-16 sm:px-6 sm:py-24 lg:px-8">
<div className="sm:flex sm:items-baseline sm:justify-between">
<h2 className="text-2xl font-bold tracking-tight text-gray-900">Trending right now</h2>
<a href="#" className="hidden text-sm font-semibold text-indigo-600 hover:text-indigo-500 sm:block">
Browse all trending
<span aria-hidden="true"> &rarr;</span>
<h2 className="text-2xl font-bold tracking-tight text-gray-900">{t('autofix.k6ca85cda')}</h2>
<a href="#" className="hidden text-sm font-semibold text-indigo-600 hover:text-indigo-500 sm:block">{t('autofix.kb0031873')}<span aria-hidden="true"> &rarr;</span>
</a>
</div>
@ -159,9 +155,7 @@ export default function VipShopPage() {
</div>
<div className="mt-6 sm:hidden">
<a href="#" className="block text-sm font-semibold text-indigo-600 hover:text-indigo-500">
Browse all favorites
<span aria-hidden="true"> &rarr;</span>
<a href="#" className="block text-sm font-semibold text-indigo-600 hover:text-indigo-500">{t('autofix.k2cd79a3d')}<span aria-hidden="true"> &rarr;</span>
</a>
</div>
</div>

View File

@ -3,25 +3,27 @@
import Link from 'next/link'
import PageLayout from '../components/PageLayout'
import PageTransitionEffect from '../components/animation/pageTransitionEffect'
import { useTranslation } from '../i18n/useTranslation'
export default function SuspendedPage() {
const { t } = useTranslation()
return (
<PageTransitionEffect>
<PageLayout showFooter={true} className="bg-[#F5F5F0] text-slate-900">
<div className="min-h-[70vh] flex items-center justify-center px-6">
<div className="max-w-xl w-full rounded-3xl border border-slate-200 bg-white/80 shadow-xl p-8 text-center">
<div className="mx-auto mb-4 h-12 w-12 rounded-full bg-rose-100 text-rose-700 flex items-center justify-center text-2xl">!</div>
<h1 className="text-2xl md:text-3xl font-bold text-slate-900">Account suspended</h1>
<h1 className="text-2xl md:text-3xl font-bold text-slate-900">{t('suspended.title')}</h1>
<p className="mt-3 text-slate-700">
Your account has been suspended. For more information, contact
{' '}<a className="text-[#8D6B1D] font-semibold" href="mailto:office@profit-planet.com">office@profit-planet.com</a>.
{t('suspended.message')}
</p>
<div className="mt-6">
<Link
href="/login"
className="inline-flex items-center justify-center rounded-xl border border-slate-300 px-5 py-2.5 text-sm font-semibold text-slate-800 hover:bg-slate-50"
>
Back to login
{t('suspended.backToLogin')}
</Link>
</div>
</div>

View File

@ -1,5 +1,8 @@
'use client'
import { useTranslation } from '../i18n/useTranslation';
import { useState, useEffect } from 'react'
import useAuthStore from '../store/authStore'
@ -16,6 +19,7 @@ function getTokenExpiry(token: string | null): Date | null {
}
export default function TestRefreshPage() {
const { t } = useTranslation();
const { accessToken, refreshAuthToken, user } = useAuthStore()
const [tokenInfo, setTokenInfo] = useState<any>({})
const [refreshStatus, setRefreshStatus] = useState<string>('')
@ -59,25 +63,25 @@ export default function TestRefreshPage() {
return (
<div className="min-h-screen bg-gray-50 p-8 text-black">
<div className="max-w-4xl mx-auto">
<h1 className="text-3xl font-bold mb-8 text-black">🧪 Token Refresh Test</h1>
<h1 className="text-3xl font-bold mb-8 text-black">{t('autofix.k00016501')}</h1>
<div className="grid gap-6">
{/* Token Status */}
<div className="bg-white p-6 rounded-lg shadow text-black">
<h2 className="text-xl font-semibold mb-4 text-black">🔑 Token Status</h2>
<h2 className="text-xl font-semibold mb-4 text-black">{t('autofix.kee28b8c6')}</h2>
<div className="space-y-2 font-mono text-sm text-black">
<div>Has Token: <span className={tokenInfo.hasToken ? 'text-green-600' : 'text-red-600'}>
<div>{t('autofix.k47b952de')}<span className={tokenInfo.hasToken ? 'text-green-600' : 'text-red-600'}>
{tokenInfo.hasToken ? '✅ Yes' : '❌ No'}
</span></div>
{tokenInfo.hasToken && (
<>
<div>Token Preview: <span className="text-blue-600">{tokenInfo.tokenPrefix}</span></div>
<div>Expires At: <span className="text-purple-600">{tokenInfo.expiresAt}</span></div>
<div>Time Left: <span className={tokenInfo.timeLeftSec <= 180 ? 'text-red-600' : 'text-green-600'}>
<div>{t('autofix.k61f6cd4e')}<span className="text-blue-600">{tokenInfo.tokenPrefix}</span></div>
<div>{t('autofix.k3d5fe74a')}<span className="text-purple-600">{tokenInfo.expiresAt}</span></div>
<div>{t('autofix.k4ed7f4d1')}<span className={tokenInfo.timeLeftSec <= 180 ? 'text-red-600' : 'text-green-600'}>
{tokenInfo.timeLeftMin}m {tokenInfo.timeLeftSec % 60}s
</span></div>
<div>Status: <span className={tokenInfo.isExpired ? 'text-red-600' : 'text-green-600'}>
<div>{t('autofix.k81c0b74b')}<span className={tokenInfo.isExpired ? 'text-red-600' : 'text-green-600'}>
{tokenInfo.isExpired ? '💀 Expired' : '✅ Valid'}
</span></div>
</>
@ -87,7 +91,7 @@ export default function TestRefreshPage() {
{/* User Info */}
<div className="bg-white p-6 rounded-lg shadow text-black">
<h2 className="text-xl font-semibold mb-4 text-black">👤 User Info</h2>
<h2 className="text-xl font-semibold mb-4 text-black">{t('autofix.k0d6626e3')}</h2>
<pre className="text-sm bg-gray-100 p-4 rounded overflow-auto text-black">
{JSON.stringify(user, null, 2)}
</pre>
@ -95,14 +99,12 @@ export default function TestRefreshPage() {
{/* Manual Controls */}
<div className="bg-white p-6 rounded-lg shadow text-black">
<h2 className="text-xl font-semibold mb-4 text-black">🔧 Manual Controls</h2>
<h2 className="text-xl font-semibold mb-4 text-black">{t('autofix.kb74d7c51')}</h2>
<div className="space-y-4">
<button
onClick={handleManualRefresh}
className="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600"
>
🔄 Manual Refresh Token
</button>
>{t('autofix.kf7189e80')}</button>
{refreshStatus && (
<div className="text-sm font-mono text-black">{refreshStatus}</div>
)}
@ -111,14 +113,14 @@ export default function TestRefreshPage() {
{/* Instructions */}
<div className="bg-yellow-50 border border-yellow-200 p-6 rounded-lg text-black">
<h2 className="text-xl font-semibold mb-4 text-black">📋 Testing Instructions</h2>
<h2 className="text-xl font-semibold mb-4 text-black">{t('autofix.k9213db6e')}</h2>
<ol className="list-decimal list-inside space-y-2 text-sm text-black">
<li>Make sure JWT_EXPIRES_IN=2m in backend .env for fast testing</li>
<li>Login and watch the countdown timer</li>
<li>When time left 3 minutes, auto-refresh should trigger</li>
<li>Check browser console for detailed logs</li>
<li>Check Network tab for /api/refresh requests</li>
<li>Token should automatically renew without user action</li>
<li>{t('autofix.k0778fa87')}</li>
<li>{t('autofix.k1405afab')}</li>
<li>{t('autofix.kb6b367b7')}</li>
<li>{t('autofix.kf0d33884')}</li>
<li>{t('autofix.k73d4a156')}</li>
<li>{t('autofix.kb01addda')}</li>
</ol>
</div>
</div>