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

View File

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

View File

@ -1,5 +1,8 @@
'use client' 'use client'
import { useTranslation } from '../../i18n/useTranslation';
import React, { useState } from 'react' import React, { useState } from 'react'
import Header from '../../components/nav/Header' import Header from '../../components/nav/Header'
import Footer from '../../components/Footer' import Footer from '../../components/Footer'
@ -40,6 +43,7 @@ const AFFILIATE_CATEGORIES = [
] as const ] as const
export default function AffiliateManagementPage() { export default function AffiliateManagementPage() {
const { t } = useTranslation();
const router = useRouter() const router = useRouter()
const user = useAuthStore(s => s.user) const user = useAuthStore(s => s.user)
const isAdmin = !!user && ( const isAdmin = !!user && (
@ -133,9 +137,7 @@ export default function AffiliateManagementPage() {
<button <button
onClick={refresh} onClick={refresh}
className="mt-2 text-sm text-red-600 hover:text-red-800 font-medium" className="mt-2 text-sm text-red-600 hover:text-red-800 font-medium"
> >{t('autofix.k3b7dd87a')}</button>
Try again
</button>
</div> </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"> <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="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"> <div className="min-w-0">
<h1 className="text-2xl md:text-4xl font-extrabold text-blue-900 tracking-tight"> <h1 className="text-2xl md:text-4xl font-extrabold text-blue-900 tracking-tight">{t('autofix.k0fe28e0b')}</h1>
Affiliate Management <p className="text-sm md:text-lg text-blue-700 mt-1 md:mt-2">{t('autofix.k49568342')}</p>
</h1>
<p className="text-sm md:text-lg text-blue-700 mt-1 md:mt-2">
Manage your affiliate partners and tracking links
</p>
</div> </div>
<button <button
onClick={() => setShowCreateModal(true)} 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" 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" /> <PlusIcon className="h-5 w-5" />{t('autofix.ke1abc7d9')}</button>
Add Affiliate
</button>
</div> </div>
{/* Search and Filter */} {/* 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" /> <MagnifyingGlassIcon className="absolute left-3 top-1/2 -translate-y-1/2 h-5 w-5 text-gray-400" />
<input <input
type="text" type="text"
placeholder="Search affiliates..." placeholder={t('autofix.k832a032b')}
value={searchQuery} value={searchQuery}
onChange={e => setSearchQuery(e.target.value)} 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" 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" /> <LinkIcon className="h-6 w-6 text-blue-900" />
</div> </div>
<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> <p className="text-2xl font-bold text-gray-900">{affiliates.length}</p>
</div> </div>
</div> </div>
@ -231,7 +227,7 @@ export default function AffiliateManagementPage() {
{loading && ( {loading && (
<div className="col-span-full text-center py-12"> <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> <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> </div>
)} )}
@ -276,7 +272,7 @@ export default function AffiliateManagementPage() {
{affiliate.commissionRate && ( {affiliate.commissionRate && (
<div className="mb-4 flex items-center gap-2"> <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> <span className="text-sm font-semibold text-blue-900">{affiliate.commissionRate}</span>
</div> </div>
)} )}
@ -350,7 +346,7 @@ export default function AffiliateManagementPage() {
{!loading && filteredAffiliates.length === 0 && ( {!loading && filteredAffiliates.length === 0 && (
<div className="col-span-full text-center py-12"> <div className="col-span-full text-center py-12">
<PhotoIcon className="mx-auto h-12 w-12 text-gray-400" /> <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"> <p className="mt-1 text-sm text-gray-500">
{searchQuery || categoryFilter !== 'all' {searchQuery || categoryFilter !== 'all'
? 'Try adjusting your search or filter' ? '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="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="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"> <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"> <button onClick={onClose} className="text-gray-500 hover:text-gray-700 transition">
<XMarkIcon className="h-6 w-6" /> <XMarkIcon className="h-6 w-6" />
</button> </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"> <form onSubmit={handleSubmit} className="p-4 sm:p-6 space-y-5 sm:space-y-6">
<div> <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 <input
required required
value={name} value={name}
onChange={e => setName(e.target.value)} 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" 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>
<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 <textarea
required required
value={description} value={description}
onChange={e => setDescription(e.target.value)} onChange={e => setDescription(e.target.value)}
rows={3} 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" 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>
<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 <input
required required
type="url" type="url"
@ -594,7 +590,7 @@ function CreateAffiliateModal({ onClose, onCreate }: { onClose: () => void; onCr
</div> </div>
<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 <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" 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' }} style={{ minHeight: '200px' }}
@ -608,16 +604,16 @@ function CreateAffiliateModal({ onClose, onCreate }: { onClose: () => void; onCr
<div className="text-center w-full px-6 py-10"> <div className="text-center w-full px-6 py-10">
<PhotoIcon aria-hidden="true" className="mx-auto h-12 w-12 text-gray-400" /> <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"> <div className="mt-4 text-sm font-medium text-gray-700">
<span>Click to upload logo</span> <span>{t('autofix.k05626798')}</span>
</div> </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> </div>
)} )}
{previewUrl && ( {previewUrl && (
<div className="relative w-full h-full min-h-[200px] flex items-center justify-center bg-white p-4"> <div className="relative w-full h-full min-h-[200px] flex items-center justify-center bg-white p-4">
<img <img
src={previewUrl} src={previewUrl}
alt="Logo preview" alt={t('autofix.k1af107a4')}
className="max-h-[180px] max-w-full object-contain" className="max-h-[180px] max-w-full object-contain"
/> />
<button <button
@ -644,7 +640,7 @@ function CreateAffiliateModal({ onClose, onCreate }: { onClose: () => void; onCr
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
<div> <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 <select
value={category} value={category}
onChange={e => setCategory(e.target.value)} onChange={e => setCategory(e.target.value)}
@ -657,12 +653,12 @@ function CreateAffiliateModal({ onClose, onCreate }: { onClose: () => void; onCr
</div> </div>
<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 <input
value={commissionRate} value={commissionRate}
onChange={e => setCommissionRate(e.target.value)} 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" 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>
</div> </div>
@ -691,9 +687,7 @@ function CreateAffiliateModal({ onClose, onCreate }: { onClose: () => void; onCr
<button <button
type="submit" 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" className="px-5 py-2.5 text-sm font-semibold text-white bg-blue-900 rounded-lg hover:bg-blue-800 shadow transition"
> >{t('autofix.ke1abc7d9')}</button>
Add Affiliate
</button>
</div> </div>
</form> </form>
</div> </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="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="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"> <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"> <button onClick={onClose} className="text-gray-500 hover:text-gray-700 transition">
<XMarkIcon className="h-6 w-6" /> <XMarkIcon className="h-6 w-6" />
</button> </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"> <form onSubmit={handleSubmit} className="p-4 sm:p-6 space-y-5 sm:space-y-6">
<div> <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 <input
required required
value={name} value={name}
@ -833,7 +827,7 @@ function EditAffiliateModal({ affiliate, onClose, onUpdate }: {
</div> </div>
<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 <textarea
required required
value={description} value={description}
@ -844,7 +838,7 @@ function EditAffiliateModal({ affiliate, onClose, onUpdate }: {
</div> </div>
<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 <input
required required
type="url" type="url"
@ -855,7 +849,7 @@ function EditAffiliateModal({ affiliate, onClose, onUpdate }: {
</div> </div>
<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 <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" 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' }} style={{ minHeight: '200px' }}
@ -869,9 +863,9 @@ function EditAffiliateModal({ affiliate, onClose, onUpdate }: {
<div className="text-center w-full px-6 py-10"> <div className="text-center w-full px-6 py-10">
<PhotoIcon aria-hidden="true" className="mx-auto h-12 w-12 text-gray-400" /> <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"> <div className="mt-4 text-sm font-medium text-gray-700">
<span>Click to upload logo</span> <span>{t('autofix.k05626798')}</span>
</div> </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> </div>
)} )}
{displayLogoUrl && ( {displayLogoUrl && (
@ -908,7 +902,7 @@ function EditAffiliateModal({ affiliate, onClose, onUpdate }: {
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
<div> <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 <select
value={category} value={category}
onChange={e => setCategory(e.target.value)} onChange={e => setCategory(e.target.value)}
@ -921,7 +915,7 @@ function EditAffiliateModal({ affiliate, onClose, onUpdate }: {
</div> </div>
<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 <input
value={commissionRate} value={commissionRate}
onChange={e => setCommissionRate(e.target.value)} onChange={e => setCommissionRate(e.target.value)}
@ -954,9 +948,7 @@ function EditAffiliateModal({ affiliate, onClose, onUpdate }: {
<button <button
type="submit" 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" className="px-5 py-2.5 text-sm font-semibold text-white bg-blue-900 rounded-lg hover:bg-blue-800 shadow transition"
> >{t('autofix.k5a489751')}</button>
Save Changes
</button>
</div> </div>
</form> </form>
</div> </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"> <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" /> <TrashIcon className="h-6 w-6 text-red-600" />
</div> </div>
<h3 className="mt-4 text-lg font-semibold text-center text-gray-900">Delete Affiliate</h3> <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"> <p className="mt-2 text-sm text-center text-gray-600">{t('autofix.k055bba0c')}<span className="font-semibold">{affiliateName}</span>{t('autofix.kd5cca6e9')}</p>
Are you sure you want to delete <span className="font-semibold">{affiliateName}</span>? This action cannot be undone.
</p>
</div> </div>
<div className="flex flex-col sm:flex-row items-stretch sm:items-center gap-3"> <div className="flex flex-col sm:flex-row items-stretch sm:items-center gap-3">

View File

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

View File

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

View File

@ -4,6 +4,8 @@ import React, { useEffect, useMemo, useState } from 'react';
import useContractManagement, { DocumentTemplate } from '../hooks/useContractManagement'; import useContractManagement, { DocumentTemplate } from '../hooks/useContractManagement';
import ConfirmActionModal from '../../../components/modals/ConfirmActionModal'; import ConfirmActionModal from '../../../components/modals/ConfirmActionModal';
import { useTranslation } from '../../../i18n/useTranslation';
type Props = { type Props = {
refreshKey?: number; refreshKey?: number;
onEdit?: (id: string) => void; onEdit?: (id: string) => void;
@ -345,6 +347,7 @@ function StatusBadge({ status }: { status: string }) {
} }
export default function ContractTemplateList({ refreshKey = 0, onEdit }: Props) { export default function ContractTemplateList({ refreshKey = 0, onEdit }: Props) {
const { t } = useTranslation();
const [items, setItems] = useState<ContractTemplate[]>([]); const [items, setItems] = useState<ContractTemplate[]>([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [q, setQ] = useState(''); 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"> <div className="mt-5 flex flex-col gap-3 lg:flex-row lg:items-center">
<input <input
placeholder="Search by name, language, version or status" placeholder={t('autofix.k35ac864e')}
value={q} value={q}
onChange={(e) => setQ(e.target.value)} 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" 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"> <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 {items.length === 0
? 'No templates available yet. Create the first template to populate this workspace.' ? 'No templates available yet. Create the first template to populate this workspace.'
: 'No templates match the current search.'} : t('autofix.k047a175d')}
</div> </div>
)} )}
</div> </div>
@ -854,7 +857,7 @@ export default function ContractTemplateList({ refreshKey = 0, onEdit }: Props)
<ConfirmActionModal <ConfirmActionModal
open={Boolean(pendingToggle?.requiresConfirm)} open={Boolean(pendingToggle?.requiresConfirm)}
title="Activate template now?" title={t('autofix.k0c51fa85')}
description={pendingToggle?.message || 'This action will update template activation status.'} description={pendingToggle?.message || 'This action will update template activation status.'}
confirmText="Activate" confirmText="Activate"
onClose={() => setPendingToggle(null)} onClose={() => setPendingToggle(null)}

View File

@ -4,11 +4,14 @@ import React, { useEffect, useMemo, useRef, useState } from 'react';
import useContractManagement, { CompanyStamp } from '../hooks/useContractManagement'; import useContractManagement, { CompanyStamp } from '../hooks/useContractManagement';
import DeleteConfirmationModal from '../../../components/delete/deleteConfirmationModal'; import DeleteConfirmationModal from '../../../components/delete/deleteConfirmationModal';
import { useTranslation } from '../../../i18n/useTranslation';
type Props = { type Props = {
onUploaded?: () => void; onUploaded?: () => void;
}; };
export default function ContractUploadCompanyStamp({ onUploaded }: Props) { export default function ContractUploadCompanyStamp({ onUploaded }: Props) {
const { t } = useTranslation();
const [file, setFile] = useState<File | null>(null); const [file, setFile] = useState<File | null>(null);
const [label, setLabel] = useState<string>(''); const [label, setLabel] = useState<string>('');
const [uploading, setUploading] = useState(false); const [uploading, setUploading] = useState(false);
@ -215,15 +218,13 @@ export default function ContractUploadCompanyStamp({ onUploaded }: Props) {
<div className="space-y-6"> <div className="space-y-6">
{/* Header with Add New Stamp modal trigger */} {/* Header with Add New Stamp modal trigger */}
<div className="flex items-center justify-between"> <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 <button
type="button" type="button"
onClick={openModal} 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" 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> <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>
Add New Stamp
</button>
</div> </div>
{/* Emphasized Active stamp */} {/* Emphasized Active stamp */}
@ -234,13 +235,11 @@ export default function ContractUploadCompanyStamp({ onUploaded }: Props) {
{activeStamp.base64 ? ( {activeStamp.base64 ? (
<img <img
src={toImgSrc(activeStamp)} 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" 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"> <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>
no image
</div>
)} )}
<span className="absolute -top-2 -right-2 rounded-full bg-green-600 text-white text-xs px-3 py-1 shadow font-bold"> <span className="absolute -top-2 -right-2 rounded-full bg-green-600 text-white text-xs px-3 py-1 shadow font-bold">
Active Active
@ -248,7 +247,7 @@ export default function ContractUploadCompanyStamp({ onUploaded }: Props) {
</div> </div>
<div className="min-w-0"> <div className="min-w-0">
<p className="text-base font-semibold text-gray-900 truncate">{activeStamp.label || activeStamp.id}</p> <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>
<div className="ml-auto flex items-center gap-2"> <div className="ml-auto flex items-center gap-2">
<button <button
@ -265,7 +264,7 @@ export default function ContractUploadCompanyStamp({ onUploaded }: Props) {
{/* Stamps list */} {/* Stamps list */}
{!!stamps.length && ( {!!stamps.length && (
<div className="mt-2"> <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"> <ul className="grid grid-cols-1 sm:grid-cols-2 gap-3">
{stamps.map((s) => { {stamps.map((s) => {
const src = toImgSrc(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" 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"> <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>
no image
</div>
)} )}
<div className="flex flex-col"> <div className="flex flex-col">
<span className="text-sm text-gray-900">{s.label || s.id}</span> <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="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="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"> <div className="p-6 border-b border-gray-100">
<h3 className="text-lg font-bold text-indigo-700">Add New Stamp</h3> <h3 className="text-lg font-bold text-indigo-700">{t('autofix.k6070f6e3')}</h3>
<p className="mt-1 text-xs text-gray-500"> <p className="mt-1 text-xs text-gray-500">{t('autofix.k825359ab')}</p>
Accepted types: PNG, JPEG, WebP, SVG. Max size: 5MB.
</p>
</div> </div>
<div className="p-6 space-y-4"> <div className="p-6 space-y-4">
<div> <div>
@ -347,7 +342,7 @@ export default function ContractUploadCompanyStamp({ onUploaded }: Props) {
type="text" type="text"
value={modalLabel} value={modalLabel}
onChange={(e) => setModalLabel(e.target.value)} 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" 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> </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" 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"> <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>
No image
</div>
)} )}
<div className="min-w-0"> <div className="min-w-0">
<p className="text-sm text-gray-900">Drag and drop your stamp here</p> <p className="text-sm text-gray-900">{t('autofix.ke58b7627')}</p>
<p className="text-xs text-gray-500">or click to browse</p> <p className="text-xs text-gray-500">{t('autofix.kba6bd6f3')}</p>
<div className="mt-2"> <div className="mt-2">
<label className="inline-block"> <label className="inline-block">
<input <input
@ -381,9 +374,7 @@ export default function ContractUploadCompanyStamp({ onUploaded }: Props) {
className="hidden" 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"> <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> <svg width="14" height="14" fill="currentColor"><path d="M12 10v2H2v-2H0v4h14v-4h-2zM7 0l4 4H9v5H5V4H3l4-4z"/></svg>{t('autofix.kfeac3f7e')}</span>
Choose file
</span>
</label> </label>
</div> </div>
</div> </div>
@ -418,7 +409,7 @@ export default function ContractUploadCompanyStamp({ onUploaded }: Props) {
{/* Delete Confirmation Modal */} {/* Delete Confirmation Modal */}
<DeleteConfirmationModal <DeleteConfirmationModal
open={deleteModal.open} open={deleteModal.open}
title="Delete Company Stamp" title={t('autofix.ka8f53660')}
description={ description={
deleteModal.active deleteModal.active
? `This stamp (${deleteModal.label || deleteModal.id}) is currently active. Are you sure you want to delete it? This action cannot be undone.` ? `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 useAuthStore from '../../store/authStore';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useTranslation } from '../../i18n/useTranslation';
const NAV = [ 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: '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> }, { 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() { export default function ContractManagementPage() {
const { t } = useTranslation();
const [refreshKey, setRefreshKey] = useState(0); const [refreshKey, setRefreshKey] = useState(0);
const user = useAuthStore((s) => s.user); const user = useAuthStore((s) => s.user);
const mounted = useSyncExternalStore( const mounted = useSyncExternalStore(
@ -89,9 +92,9 @@ export default function ContractManagementPage() {
Admin workspace Admin workspace
</div> </div>
<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"> <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> </p>
</div> </div>
<div className="flex flex-wrap gap-2 text-xs text-slate-600"> <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"> <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"> <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> <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> </h2>
<ContractUploadCompanyStamp onUploaded={bumpRefresh} /> <ContractUploadCompanyStamp onUploaded={bumpRefresh} />
<div className="mt-8 border-t border-slate-200 pt-6"> <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"> <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> <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> </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 /> <CompanySettingsPanel />
</div> </div>
</section> </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"> <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"> <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> <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> </h2>
<ContractEditor <ContractEditor
key={`${editorKey}-${editingTemplateId ?? 'new'}`} key={`${editorKey}-${editingTemplateId ?? 'new'}`}

View File

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

View File

@ -1,5 +1,8 @@
'use client' 'use client'
import { useTranslation } from '../../i18n/useTranslation';
import React from 'react' import React from 'react'
import Header from '../../components/nav/Header' import Header from '../../components/nav/Header'
import Footer from '../../components/Footer' 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' import { createFolderStructure, listFolderStructureIssues, listLooseFiles, listGhostDirectories, moveLooseFilesToContract, FixResult, FolderStructureIssueUser, GhostDirectory, LooseFileUser } from './hooks/exoscaleMaintenance'
export default function DevManagementPage() { export default function DevManagementPage() {
const { t } = useTranslation();
const router = useRouter() const router = useRouter()
const user = useAuthStore(s => s.user) const user = useAuthStore(s => s.user)
@ -264,18 +268,16 @@ export default function DevManagementPage() {
<CommandLineIcon className="h-6 w-6 text-blue-700" /> <CommandLineIcon className="h-6 w-6 text-blue-700" />
</div> </div>
<div className="min-w-0"> <div className="min-w-0">
<h1 className="text-2xl sm:text-3xl font-extrabold text-blue-900">Dev Management</h1> <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"> <p className="text-sm sm:text-base text-blue-700">{t('autofix.k6e4a6069')}</p>
Import SQL dump files to run database migrations.
</p>
</div> </div>
</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"> <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" /> <ExclamationTriangleIcon className="h-5 w-5 mt-0.5 flex-shrink-0" />
<div> <div>
<div className="font-semibold">Use with caution</div> <div className="font-semibold">{t('autofix.k6c6e5c0f')}</div>
<div>SQL dumps run immediately and can modify production data.</div> <div>{t('autofix.k8a35cc53')}</div>
</div> </div>
</div> </div>
</header> </header>
@ -288,32 +290,28 @@ export default function DevManagementPage() {
activeTab === 'sql' ? 'bg-blue-900 text-white' : 'text-blue-800 hover:bg-blue-50' activeTab === 'sql' ? 'bg-blue-900 text-white' : 'text-blue-800 hover:bg-blue-50'
}`} }`}
> >
<CommandLineIcon className="h-4 w-4" /> SQL Import <CommandLineIcon className="h-4 w-4" />{t('autofix.k4db68c96')}</button>
</button>
<button <button
onClick={() => setActiveTab('structure')} 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 ${ 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' activeTab === 'structure' ? 'bg-blue-900 text-white' : 'text-blue-800 hover:bg-blue-50'
}`} }`}
> >
<WrenchScrewdriverIcon className="h-4 w-4" /> Folder Structure <WrenchScrewdriverIcon className="h-4 w-4" />{t('autofix.kcb491706')}</button>
</button>
<button <button
onClick={() => setActiveTab('loose')} 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 ${ 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' activeTab === 'loose' ? 'bg-blue-900 text-white' : 'text-blue-800 hover:bg-blue-50'
}`} }`}
> >
<FolderOpenIcon className="h-4 w-4" /> Loose Files <FolderOpenIcon className="h-4 w-4" />{t('autofix.k04b5cbca')}</button>
</button>
<button <button
onClick={() => setActiveTab('ghost')} 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 ${ 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' activeTab === 'ghost' ? 'bg-blue-900 text-white' : 'text-blue-800 hover:bg-blue-50'
}`} }`}
> >
<FolderOpenIcon className="h-4 w-4" /> Ghost Directories <FolderOpenIcon className="h-4 w-4" />{t('autofix.k6838438d')}</button>
</button>
</div> </div>
</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"> <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="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"> <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 */} {/* 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"> <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()} 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" 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 <ArrowUpTrayIcon className="h-4 w-4" />{t('autofix.k8a59b156')}</button>
</button>
<button <button
onClick={clearResults} 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" 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>
<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="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="text-sm text-gray-600">{t('autofix.kb6eacc9d')}</div>
<div className="mt-2 text-xs text-gray-500">Only SQL dump files are supported.</div> <div className="mt-2 text-xs text-gray-500">{t('autofix.k3ac8ca10')}</div>
{selectedFile && ( {selectedFile && (
<div className="mt-4 text-sm text-blue-900 font-semibold break-words"> <div className="mt-4 text-sm text-blue-900 font-semibold break-words">
Selected: {selectedFile.name} Selected: {selectedFile.name}
@ -374,10 +371,10 @@ export default function DevManagementPage() {
</div> </div>
<div className="rounded-2xl bg-white border border-gray-100 shadow-lg p-4 md:p-6"> <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="space-y-2 text-sm text-gray-700">
<div className="flex justify-between"> <div className="flex justify-between">
<span>Result Sets</span> <span>{t('autofix.k7938d4fd')}</span>
<span className="font-semibold text-blue-900"> <span className="font-semibold text-blue-900">
{Array.isArray(result?.result) ? (result?.result as any[]).length : result?.result ? 1 : 0} {Array.isArray(result?.result) ? (result?.result as any[]).length : result?.result ? 1 : 0}
</span> </span>
@ -391,19 +388,17 @@ export default function DevManagementPage() {
<span className="font-semibold text-blue-900">{meta?.durationMs ? `${meta.durationMs} ms` : '-'}</span> <span className="font-semibold text-blue-900">{meta?.durationMs ? `${meta.durationMs} ms` : '-'}</span>
</div> </div>
</div> </div>
<div className="mt-6 text-xs text-gray-500"> <div className="mt-6 text-xs text-gray-500">{t('autofix.k0f0395ca')}</div>
Multi-statement SQL and dump files are supported. Use with caution.
</div>
</div> </div>
</section> </section>
<section className="mt-6 md:mt-8 rounded-2xl bg-white border border-gray-100 shadow-lg p-4 md:p-6"> <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"> <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> </div>
{!result && ( {!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 && ( {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"> <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="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3 mb-4">
<div className="space-y-1"> <div className="space-y-1">
<h2 className="text-lg font-semibold text-blue-900">Exoscale Folder Structure</h2> <h2 className="text-lg font-semibold text-blue-900">{t('autofix.kd51f320c')}</h2>
<p className="text-sm text-gray-600"> <p className="text-sm text-gray-600">{t('autofix.kb1341138')}</p>
Ensures both contract and gdpr folders exist for each user.
</p>
{structureStatus && ( {structureStatus && (
<div className="text-xs text-slate-500">{structureStatus}</div> <div className="text-xs text-slate-500">{structureStatus}</div>
)} )}
@ -462,9 +455,9 @@ export default function DevManagementPage() {
)} )}
{exoscaleLoading ? ( {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 ? ( ) : 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"> <div className="divide-y divide-gray-100 rounded-xl border border-gray-100">
{structureUsers.map(user => { {structureUsers.map(user => {
@ -478,7 +471,7 @@ export default function DevManagementPage() {
<div className="text-xs text-gray-500 truncate"> <div className="text-xs text-gray-500 truncate">
#{user.userId} {user.email} {user.userType} {user.contractCategory} #{user.userId} {user.email} {user.userType} {user.contractCategory}
</div> </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 && ( {fix && (
<div className="mt-2 text-xs text-emerald-700"> <div className="mt-2 text-xs text-emerald-700">
Created {fix.created || 0}{fix.errors && fix.errors.length > 0 ? `, errors ${fix.errors.length}` : ''}. 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) && ( {(structureActionMeta || structureActionResults.length > 0) && (
<div className="mt-6 rounded-xl border border-slate-200 bg-slate-50 p-4"> <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"> <div className="text-xs text-gray-600 flex flex-wrap gap-3">
<span>Processed: {structureActionMeta?.processedUsers ?? '-'}</span> <span>Processed: {structureActionMeta?.processedUsers ?? '-'}</span>
<span>Created: {structureActionMeta?.createdTotal ?? '-'}</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"> <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="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3 mb-4">
<div className="space-y-1"> <div className="space-y-1">
<h2 className="text-lg font-semibold text-blue-900">Loose Files</h2> <h2 className="text-lg font-semibold text-blue-900">{t('autofix.k04b5cbca')}</h2>
<p className="text-sm text-gray-600"> <p className="text-sm text-gray-600">{t('autofix.kbff01823')}</p>
Shows files directly under the user folder that are not in contract or gdpr.
</p>
{looseStatus && ( {looseStatus && (
<div className="text-xs text-slate-500">{looseStatus}</div> <div className="text-xs text-slate-500">{looseStatus}</div>
)} )}
@ -576,9 +567,9 @@ export default function DevManagementPage() {
)} )}
{exoscaleLoading ? ( {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 ? ( ) : 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"> <div className="divide-y divide-gray-100 rounded-xl border border-gray-100">
{looseUsers.map(user => { {looseUsers.map(user => {
@ -591,8 +582,7 @@ export default function DevManagementPage() {
<div className="text-xs text-gray-500 truncate"> <div className="text-xs text-gray-500 truncate">
#{user.userId} {user.email} {user.userType} {user.contractCategory} #{user.userId} {user.email} {user.userType} {user.contractCategory}
</div> </div>
<div className="mt-1 text-xs text-gray-600"> <div className="mt-1 text-xs text-gray-600">{t('autofix.kf340aa10')}<span className="font-semibold text-blue-900">{user.looseObjects}</span>
Loose files: <span className="font-semibold text-blue-900">{user.looseObjects}</span>
</div> </div>
{user.sampleKeys && user.sampleKeys.length > 0 && ( {user.sampleKeys && user.sampleKeys.length > 0 && (
<div className="mt-2 text-[11px] text-gray-400 break-all"> <div className="mt-2 text-[11px] text-gray-400 break-all">
@ -622,7 +612,7 @@ export default function DevManagementPage() {
{(looseActionMeta || looseActionResults.length > 0) && ( {(looseActionMeta || looseActionResults.length > 0) && (
<div className="mt-6 rounded-xl border border-slate-200 bg-slate-50 p-4"> <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"> <div className="text-xs text-gray-600 flex flex-wrap gap-3">
<span>Processed: {looseActionMeta?.processedUsers ?? '-'}</span> <span>Processed: {looseActionMeta?.processedUsers ?? '-'}</span>
<span>Moved: {looseActionMeta?.movedTotal ?? '-'}</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"> <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="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3 mb-4">
<div className="space-y-1"> <div className="space-y-1">
<h2 className="text-lg font-semibold text-blue-900">Ghost Directories</h2> <h2 className="text-lg font-semibold text-blue-900">{t('autofix.k6838438d')}</h2>
<p className="text-sm text-gray-600"> <p className="text-sm text-gray-600">{t('autofix.k77444d5b')}</p>
Exoscale directories that do not have a matching user in the database.
</p>
{ghostStatus && ( {ghostStatus && (
<div className="text-xs text-slate-500">{ghostStatus}</div> <div className="text-xs text-slate-500">{ghostStatus}</div>
)} )}
@ -682,9 +670,9 @@ export default function DevManagementPage() {
)} )}
{exoscaleLoading ? ( {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 ? ( ) : 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"> <div className="divide-y divide-gray-100 rounded-xl border border-gray-100">
{ghostDirs.map(dir => ( {ghostDirs.map(dir => (

View File

@ -1,4 +1,7 @@
'use client' 'use client'
import { useTranslation } from '../../i18n/useTranslation';
import React, { useMemo, useState } from 'react' import React, { useMemo, useState } from 'react'
import PageLayout from '../../components/PageLayout' import PageLayout from '../../components/PageLayout'
import { useRouter } from 'next/navigation' import { useRouter } from 'next/navigation'
@ -8,6 +11,7 @@ import useAuthStore from '../../store/authStore'
import InvoiceDetailModal from './components/InvoiceDetailModal' import InvoiceDetailModal from './components/InvoiceDetailModal'
export default function FinanceManagementPage() { export default function FinanceManagementPage() {
const { t } = useTranslation();
const router = useRouter() const router = useRouter()
const accessToken = useAuthStore(s => s.accessToken) const accessToken = useAuthStore(s => s.accessToken)
const { rates, loading: vatLoading, error: vatError } = useVatRates() 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="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"> <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"> <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> <h1 className="text-3xl font-extrabold text-blue-900">{t('autofix.k777299de')}</h1>
<p className="text-sm text-blue-700">Overview of taxes, revenue, and invoices.</p> <p className="text-sm text-blue-700">{t('autofix.k01ad6d49')}</p>
</header> </header>
{/* Stats */} {/* Stats */}
@ -266,9 +270,9 @@ export default function FinanceManagementPage() {
onChange={e => setTimeframe(e.target.value as any)} 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" 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="7d">{t('autofix.k502a0057')}</option>
<option value="30d">Last 30 days</option> <option value="30d">{t('autofix.k5f74c123')}</option>
<option value="90d">Last 90 days</option> <option value="90d">{t('autofix.k915115a9')}</option>
<option value="ytd">YTD</option> <option value="ytd">YTD</option>
</select> </select>
</div> </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"> <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 className="flex items-center justify-between">
<div> <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> <p className="text-xs text-gray-600">Live data from backend; edit on a separate page.</p>
</div> </div>
<button <button
onClick={() => router.push('/admin/finance-management/vat-edit')} 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" className="rounded-lg bg-[#1C2B4A] px-4 py-2 text-sm font-semibold text-white shadow hover:bg-[#1C2B4A]/90"
> >{t('autofix.k4191cdba')}</button>
Edit VAT
</button>
</div> </div>
<div className="text-sm text-gray-700"> <div className="text-sm text-gray-700">
{vatLoading && 'Loading VAT rates...'} {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"> <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> <h2 className="text-lg font-semibold text-[#1C2B4A]">Invoices</h2>
<div className="flex flex-wrap gap-2 text-sm"> <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={() => { 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">Send Email Report</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">Export CSV</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">Export PDF</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> <button onClick={reload} className="rounded-lg border border-gray-200 px-3 py-2 hover:bg-gray-50">Reload</button>
</div> </div>
</div> </div>
@ -356,17 +358,15 @@ export default function FinanceManagementPage() {
)} )}
{(diagLoading || diagError || diagData) && ( {(diagLoading || diagError || diagData) && (
<div className="rounded-md border border-blue-100 bg-blue-50/60 px-3 py-3 text-sm mb-3"> <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 && <div className="text-red-700">{diagError}</div>}
{!diagLoading && !diagError && diagData && ( {!diagLoading && !diagError && diagData && (
<div className="space-y-2"> <div className="space-y-2">
<div className="text-blue-900 font-semibold">Pool inflow diagnostic for invoice #{diagData.invoice_id ?? '—'}</div> <div className="text-blue-900 font-semibold">Pool inflow diagnostic for invoice #{diagData.invoice_id ?? '—'}</div>
<div className="text-gray-700"> <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>
Status: <span className="font-medium">{diagData.ok ? 'OK' : 'Blocked'}</span> Reason: <span className="font-mono">{diagData.reason}</span>
</div> </div>
{diagData.ok && ( {diagData.ok && (
<div className="text-gray-700"> <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>
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> </div>
)} )}
{Array.isArray(diagData.candidates) && diagData.candidates.length > 0 && ( {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">Invoice</th>
<th className="px-3 py-2 font-semibold">Customer</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">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">Amount</th>
<th className="px-3 py-2 font-semibold">Status</th> <th className="px-3 py-2 font-semibold">Status</th>
<th className="px-3 py-2 font-semibold">Actions</th> <th className="px-3 py-2 font-semibold">Actions</th>
@ -419,9 +419,7 @@ export default function FinanceManagementPage() {
</> </>
) : filteredBills.length === 0 ? ( ) : filteredBills.length === 0 ? (
<tr> <tr>
<td colSpan={7} className="px-3 py-4 text-center text-gray-500"> <td colSpan={7} className="px-3 py-4 text-center text-gray-500">{t('autofix.kbdb02e32')}</td>
Keine Rechnungen gefunden.
</td>
</tr> </tr>
) : ( ) : (
filteredBills.map(inv => ( filteredBills.map(inv => (
@ -505,22 +503,22 @@ export default function FinanceManagementPage() {
{uploadModalOpen && ( {uploadModalOpen && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 backdrop-blur-sm"> <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]"> <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 className="grid grid-cols-1 sm:grid-cols-2 gap-3 text-sm">
<div> <div>
<label className="block text-xs font-medium text-gray-700 mb-1">Customer Name</label> <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="Max Mustermann" /> <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>
<div> <div>
<label className="block text-xs font-medium text-gray-700 mb-1">Customer Email</label> <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="kunde@example.com" /> <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>
<div> <div>
<label className="block text-xs font-medium text-gray-700 mb-1">Street</label> <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>
<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" /> <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>
<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" /> <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>
<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" /> <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>
<div> <div>
@ -575,15 +573,15 @@ export default function FinanceManagementPage() {
</select> </select>
</div> </div>
<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 }))} /> <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>
<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 }))} /> <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>
<div className="sm:col-span-2"> <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 <input
type="file" accept="application/pdf" 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" 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 && ( {emailDialogOpen && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 backdrop-blur-sm"> <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"> <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"> <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. Only <strong>paid</strong> invoices will be included in the report, regardless of the status filter.
{(billFilter.from || billFilter.to) && ( {(billFilter.from || billFilter.to) && (
<span> The current date range filter ({billFilter.from || '…'} {billFilter.to || '…'}) will be applied.</span> <span> The current date range filter ({billFilter.from || '…'} {billFilter.to || '…'}) will be applied.</span>
)} )}
</div> </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 <input
type="email" type="email"
value={reportEmail} value={reportEmail}
onChange={e => setReportEmail(e.target.value)} 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" 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 autoFocus
onKeyDown={e => { if (e.key === 'Enter' && !sendingReport) sendEmailReport() }} onKeyDown={e => { if (e.key === 'Enter' && !sendingReport) sendEmailReport() }}

View File

@ -1,4 +1,7 @@
'use client' 'use client'
import { useTranslation } from '../../../i18n/useTranslation';
import React, { useState } from 'react' import React, { useState } from 'react'
import PageLayout from '../../../components/PageLayout' import PageLayout from '../../../components/PageLayout'
import { useRouter } from 'next/navigation' import { useRouter } from 'next/navigation'
@ -7,6 +10,7 @@ import { importVatCsv } from './hooks/TaxImporter'
import { exportVatCsv, exportVatPdf } from './hooks/TaxExporter' import { exportVatCsv, exportVatPdf } from './hooks/TaxExporter'
export default function VatEditPage() { export default function VatEditPage() {
const { t } = useTranslation();
const router = useRouter() const router = useRouter()
const { rates, loading, error, reload } = useVatRates() const { rates, loading, error, reload } = useVatRates()
const [filter, setFilter] = useState('') 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="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 className="rounded-2xl bg-white border border-blue-100 shadow-lg px-8 py-8 flex items-center justify-between">
<div> <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> <p className="text-sm text-blue-700">Import, export, and review (dummy data).</p>
</div> </div>
<button <button
@ -68,15 +72,11 @@ export default function VatEditPage() {
<button <button
onClick={() => exportVatCsv(rates)} onClick={() => exportVatCsv(rates)}
className="rounded-lg border border-gray-200 px-3 py-2 hover:bg-gray-50" className="rounded-lg border border-gray-200 px-3 py-2 hover:bg-gray-50"
> >{t('autofix.k4c5e8e87')}</button>
Export CSV
</button>
<button <button
onClick={() => exportVatPdf(rates)} onClick={() => exportVatPdf(rates)}
className="rounded-lg border border-gray-200 px-3 py-2 hover:bg-gray-50" className="rounded-lg border border-gray-200 px-3 py-2 hover:bg-gray-50"
> >{t('autofix.k4c5ecd73')}</button>
Export PDF
</button>
{importResult && <span className="text-xs text-blue-900 break-all">{importResult}</span>} {importResult && <span className="text-xs text-blue-900 break-all">{importResult}</span>}
</div> </div>
@ -85,7 +85,7 @@ export default function VatEditPage() {
<input <input
value={filter} value={filter}
onChange={e => { setFilter(e.target.value); setPage(1); }} 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" 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"> <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">Code</th>
<th className="px-3 py-2 font-semibold">Standard</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">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> <th className="px-3 py-2 font-semibold">Parking</th>
</tr> </tr>
</thead> </thead>
<tbody className="divide-y divide-gray-100"> <tbody className="divide-y divide-gray-100">
{loading && ( {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 => ( {!loading && pageData.map(v => (
<tr key={v.country_code} className="border-b last:border-0"> <tr key={v.country_code} className="border-b last:border-0">
@ -115,14 +115,14 @@ export default function VatEditPage() {
</tr> </tr>
))} ))}
{!loading && !error && pageData.length === 0 && ( {!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> </tbody>
</table> </table>
</div> </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 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"> <div className="flex items-center gap-2">
<span>Rows per page:</span> <span>{t('autofix.k2f4ebc32')}</span>
<select <select
value={pageSize} value={pageSize}
onChange={e => { setPageSize(Number(e.target.value)); setPage(1); }} 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' 'use client'
import { useTranslation } from '../../../../i18n/useTranslation';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { createPortal } from 'react-dom' import { createPortal } from 'react-dom'
import { MagnifyingGlassIcon, XMarkIcon, BuildingOffice2Icon, UserIcon } from '@heroicons/react/24/outline' import { MagnifyingGlassIcon, XMarkIcon, BuildingOffice2Icon, UserIcon } from '@heroicons/react/24/outline'
@ -29,6 +32,7 @@ export default function SearchModal({
onAdd, onAdd,
policyMaxDepth // NEW policyMaxDepth // NEW
}: Props) { }: Props) {
const { t } = useTranslation();
const [query, setQuery] = useState('') const [query, setQuery] = useState('')
const [typeFilter, setTypeFilter] = useState<'all' | UserType>('all') const [typeFilter, setTypeFilter] = useState<'all' | UserType>('all')
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
@ -277,9 +281,7 @@ export default function SearchModal({
<XMarkIcon className="h-5 w-5" /> <XMarkIcon className="h-5 w-5" />
</button> </button>
</div> </div>
<p className="mt-1 text-xs text-blue-200"> <p className="mt-1 text-xs text-blue-200">{t('autofix.kd642e230')}</p>
Search by name or email. Minimum 3 characters. Existing matrix members are hidden.
</p>
</div> </div>
{/* Form */} {/* Form */}
@ -298,7 +300,7 @@ export default function SearchModal({
<input <input
value={query} value={query}
onChange={e => setQuery(e.target.value)} 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" 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> </div>
@ -310,7 +312,7 @@ export default function SearchModal({
onChange={e => setTypeFilter(e.target.value as any)} 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" 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="personal">Personal</option>
<option value="company">Company</option> <option value="company">Company</option>
</select> </select>
@ -333,8 +335,7 @@ export default function SearchModal({
</button> </button>
</div> </div>
{/* Total */} {/* Total */}
<div className="text-sm text-blue-200 self-center"> <div className="text-sm text-blue-200 self-center">{t('autofix.kc0e3b03d')}<span className="font-semibold text-white">{total}</span>
Total: <span className="font-semibold text-white">{total}</span>
</div> </div>
</form> </form>
@ -346,14 +347,10 @@ export default function SearchModal({
<div className="text-sm text-red-400 mb-4">{error}</div> <div className="text-sm text-red-400 mb-4">{error}</div>
)} )}
{!error && query.trim().length < 3 && ( {!error && query.trim().length < 3 && (
<div className="py-12 text-sm text-blue-300 text-center"> <div className="py-12 text-sm text-blue-300 text-center">{t('autofix.kb87eb38b')}</div>
Enter at least 3 characters and click Search.
</div>
)} )}
{!error && query.trim().length >= 3 && !hasSearched && !loading && ( {!error && query.trim().length >= 3 && !hasSearched && !loading && (
<div className="py-12 text-sm text-blue-300 text-center"> <div className="py-12 text-sm text-blue-300 text-center">{t('autofix.k7c740cd5')}</div>
Ready to search. Click the Search button to fetch candidates.
</div>
)} )}
{/* Skeleton only for first-time load (when no items yet) */} {/* Skeleton only for first-time load (when no items yet) */}
{!error && query.trim().length >= 3 && loading && items.length === 0 && ( {!error && query.trim().length >= 3 && loading && items.length === 0 && (
@ -367,9 +364,7 @@ export default function SearchModal({
</ul> </ul>
)} )}
{!error && hasSearched && !loading && query.trim().length >= 3 && items.length === 0 && ( {!error && hasSearched && !loading && query.trim().length >= 3 && items.length === 0 && (
<div className="py-12 text-sm text-blue-300 text-center"> <div className="py-12 text-sm text-blue-300 text-center">{t('autofix.k1e5d5139')}</div>
No users match your filters.
</div>
)} )}
{!error && hasSearched && items.length > 0 && ( {!error && hasSearched && items.length > 0 && (
<ul className="divide-y divide-blue-900/40 border border-blue-900/40 rounded-lg bg-[#132c4e]"> <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 <button
onClick={() => { setSelected(null); setParentId(undefined); }} onClick={() => { setSelected(null); setParentId(undefined); }}
className="text-xs text-blue-300 hover:text-white transition" className="text-xs text-blue-300 hover:text-white transition"
> >{t('autofix.kadd80fbc')}</button>
Clear selection
</button>
</div> </div>
<label className="flex items-center gap-2 text-xs text-blue-200"> <label className="flex items-center gap-2 text-xs text-blue-200">
@ -438,9 +431,7 @@ export default function SearchModal({
checked={advanced} checked={advanced}
onChange={e => setAdvanced(e.target.checked)} 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" className="h-3 w-3 rounded border-blue-700 bg-blue-900 text-indigo-500 focus:ring-indigo-400"
/> />{t('autofix.k11974e0f')}</label>
Advanced: choose parent manually
</label>
{advanced && ( {advanced && (
<div className="space-y-2"> <div className="space-y-2">
@ -481,9 +472,7 @@ export default function SearchModal({
checked={forceFallback} checked={forceFallback}
onChange={e => setForceFallback(e.target.checked)} 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" className="h-3 w-3 rounded border-blue-700 bg-blue-900 text-indigo-500 focus:ring-indigo-400"
/> />{t('autofix.kf823daf7')}</label>
Fallback to root if referral parent not in matrix
</label>
<p className="text-[11px] text-blue-300"> <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. If the referrer is outside the matrix, the user is placed under root; enabling fallback may mark the user as rogue.
</p> </p>

View File

@ -1,5 +1,8 @@
'use client' 'use client'
import { useTranslation } from '../../../i18n/useTranslation';
import React, { useEffect, useMemo, useState, Suspense } from 'react' // CHANGED: add Suspense import React, { useEffect, useMemo, useState, Suspense } from 'react' // CHANGED: add Suspense
import { useSearchParams, useRouter } from 'next/navigation' import { useSearchParams, useRouter } from 'next/navigation'
import PageLayout from '../../../components/PageLayout' 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, ... const LEVEL_CAP = (level: number) => Math.pow(5, level) // L1=5, L2=25, ...
function MatrixDetailPageInner() { function MatrixDetailPageInner() {
const { t } = useTranslation()
const sp = useSearchParams() const sp = useSearchParams()
const router = useRouter() 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="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"> <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="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>
</div> </div>
)} )}
@ -393,12 +397,9 @@ function MatrixDetailPageInner() {
onClick={() => router.push('/admin/matrix-management')} onClick={() => router.push('/admin/matrix-management')}
className="inline-flex items-center gap-2 text-sm text-blue-900 hover:text-blue-700" className="inline-flex items-center gap-2 text-sm text-blue-900 hover:text-blue-700"
> >
<ArrowLeftIcon className="h-4 w-4" /> <ArrowLeftIcon className="h-4 w-4" />{t('autofix.k65b67dc3')}</button>
Back to matrices
</button>
<h1 className="text-3xl font-extrabold text-blue-900">{matrixName}</h1> <h1 className="text-3xl font-extrabold text-blue-900">{matrixName}</h1>
<p className="text-base text-blue-700"> <p className="text-base text-blue-700">{t('autofix.k31d46514')}<span className="font-semibold text-blue-900">{topNodeEmail}</span>
Top node: <span className="font-semibold text-blue-900">{topNodeEmail}</span>
</p> </p>
<div className="mt-2 flex flex-wrap items-center gap-2"> <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"> <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) }} 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" 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" /> <PlusIcon className="h-5 w-5" />{t('autofix.kc7c429a6')}</button>
Add users to matrix
</button>
</div> </div>
</div> </div>
</header> </header>
@ -452,7 +451,7 @@ function MatrixDetailPageInner() {
<input <input
value={globalSearch} value={globalSearch}
onChange={e => setGlobalSearch(e.target.value)} 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" 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> </div>
@ -470,27 +469,27 @@ function MatrixDetailPageInner() {
{/* Small stats (CHANGED wording) */} {/* Small stats (CHANGED wording) */}
<div className="mb-8 grid grid-cols-1 sm:grid-cols-4 gap-6"> <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="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 className="text-xl font-semibold text-blue-900">{users.length}</div>
</div> </div>
<div className="rounded-xl border border-gray-100 bg-white p-5 shadow"> <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 className="text-xl font-semibold text-blue-900">{rogueCount}</div>
</div> </div>
<div className="rounded-xl border border-gray-100 bg-white p-5 shadow"> <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-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>
<div className="rounded-xl border border-gray-100 bg-white p-5 shadow"> <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 className="text-xl font-semibold text-blue-900">{isUnlimited ? 'Unlimited' : policyMaxDepth}</div>
</div> </div>
<div className="rounded-xl border border-gray-100 bg-white p-5 shadow"> <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 className="text-xl font-semibold text-blue-900">{fillMetrics.label}</div>
</div> </div>
<div className="rounded-xl border border-gray-100 bg-white p-5 shadow"> <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 className="text-xl font-semibold text-blue-900">{fillMetrics.highestFull}</div>
</div> </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="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"> <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> <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>
<div className="px-8 py-6"> <div className="px-8 py-6">
{!rootNode && ( {!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 && ( {rootNode && (
<ul className="flex flex-col gap-1"> <ul className="flex flex-col gap-1">
@ -516,9 +515,7 @@ function MatrixDetailPageInner() {
{/* Vacancies placeholder */} {/* Vacancies placeholder */}
<div className="rounded-2xl bg-white border border-dashed border-blue-200 shadow-sm p-6 mb-8"> <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> <h3 className="text-lg font-semibold text-blue-900 mb-2">Vacancies</h3>
<p className="text-sm text-blue-700"> <p className="text-sm text-blue-700">{t('autofix.k9b3266b5')}</p>
Pending backend wiring to MatrixController.listVacancies. This section will surface empty slots and allow reassignment.
</p>
</div> </div>
{/* Add Users Modal */} {/* Add Users Modal */}

View File

@ -1,5 +1,8 @@
'use client' 'use client'
import { useTranslation } from '../../i18n/useTranslation';
import React, { useMemo, useState, useEffect } from 'react' import React, { useMemo, useState, useEffect } from 'react'
import { import {
ChartBarIcon, ChartBarIcon,
@ -28,6 +31,7 @@ type Matrix = {
} }
export default function MatrixManagementPage() { export default function MatrixManagementPage() {
const { t } = useTranslation();
const router = useRouter() const router = useRouter()
const user = useAuthStore(s => s.user) const user = useAuthStore(s => s.user)
const token = useAuthStore(s => s.accessToken) 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"> <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 className="flex items-center justify-between">
<div> <div>
<h1 className="text-4xl font-extrabold text-blue-900 tracking-tight">Matrix Management</h1> <h1 className="text-4xl font-extrabold text-blue-900 tracking-tight">{t('autofix.kd09be3cd')}</h1>
<p className="text-lg text-blue-700 mt-2">Manage matrices, see stats, and create new ones.</p> <p className="text-lg text-blue-700 mt-2">{t('autofix.kdc22ad8a')}</p>
</div> </div>
<button <button
onClick={() => setCreateOpen(true)} 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" 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" /> <PlusIcon className="h-5 w-5" />{t('autofix.kb7849a5a')}</button>
Create Matrix
</button>
</div> </div>
</header> </header>
{/* Filters */} {/* Filters */}
<div className="flex flex-wrap items-center gap-3 mb-6"> <div className="flex flex-wrap items-center gap-3 mb-6">
<div className="flex items-center gap-2 text-xs text-blue-900"> <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('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('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>
<div className="flex items-center gap-2 text-xs text-blue-900"> <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"> <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="none">None</option>
<option value="asc">Policy </option> <option value="asc">{t('autofix.kf7a91674')}</option>
<option value="desc">Policy </option> <option value="desc">{t('autofix.kf7a91676')}</option>
</select> </select>
<select value={sortByUsers} onChange={e => setSortByUsers(e.target.value as any)} className="border rounded px-2 py-1"> <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="desc">{t('autofix.k8c3085f4')}</option>
<option value="asc">Users </option> <option value="asc">{t('autofix.k8c3085f6')}</option>
</select> </select>
</div> </div>
</div> </div>
@ -352,7 +354,7 @@ export default function MatrixManagementPage() {
</div> </div>
)) ))
) : matricesView.length === 0 ? ( ) : 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 => ( matricesView.map(m => (
<article key={m.id} className="rounded-2xl bg-white border border-gray-100 shadow-lg overflow-hidden flex flex-col"> <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> </span>
</div> </div>
<div className="mt-4 grid grid-cols-1 gap-3 text-sm text-gray-700"> <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" /> <UsersIcon className="h-5 w-5 text-gray-500" />
<span className="font-medium">{m.usersCount}</span> <span className="font-medium">{m.usersCount}</span>
<span className="text-gray-500">users</span> <span className="text-gray-500">users</span>
@ -399,9 +401,7 @@ export default function MatrixManagementPage() {
? (m.status === 'active' ? 'Deactivating…' : 'Activating…') ? (m.status === 'active' ? 'Deactivating…' : 'Activating…')
: (m.status === 'active' ? 'Deactivate' : 'Activate')} : (m.status === 'active' ? 'Deactivate' : 'Activate')}
</button> </button>
<span className="text-[11px] text-gray-500"> <span className="text-[11px] text-gray-500">{t('autofix.k27f56959')}</span>
State change will affect add/remove operations.
</span>
<button <button
className="text-sm font-medium text-blue-900 hover:text-blue-700" className="text-sm font-medium text-blue-900 hover:text-blue-700"
onClick={() => { onClick={() => {
@ -417,9 +417,7 @@ export default function MatrixManagementPage() {
}) })
router.push(`/admin/matrix-management/detail?${params.toString()}`) router.push(`/admin/matrix-management/detail?${params.toString()}`)
}} }}
> >{t('autofix.ka3c41ff8')}</button>
View details
</button>
</div> </div>
</div> </div>
</article> </article>
@ -435,7 +433,7 @@ export default function MatrixManagementPage() {
<div className="absolute inset-0 flex items-center justify-center p-4"> <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="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"> <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 <button
onClick={() => { setCreateOpen(false); resetForm() }} onClick={() => { setCreateOpen(false); resetForm() }}
className="text-sm text-gray-500 hover:text-gray-700" 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"> <form onSubmit={handleCreate} className="p-6 space-y-5">
{/* Success banner */} {/* Success banner */}
{createSuccess && ( {createSuccess && (
<div className="rounded-md border border-green-200 bg-green-50 px-3 py-2 text-sm text-green-700"> <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">
Matrix created successfully. <span className="font-semibold">{t('autofix.k0cdde8f8')}</span> {createSuccess.name}{' '}
<div className="mt-1 text-green-800"> <span className="font-semibold ml-3">{t('autofix.k31d46514')}</span> {createSuccess.email}
<span className="font-semibold">Name:</span> {createSuccess.name}{' '}
<span className="font-semibold ml-3">Top node:</span> {createSuccess.email}
</div> </div>
</div> </div>
)} )}
{/* 409 force prompt */} {/* 409 force prompt */}
{forcePrompt && ( {forcePrompt && (
<div className="rounded-md border border-amber-200 bg-amber-50 px-3 py-2 text-sm text-amber-800"> <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">
A matrix configuration already exists for this selection.
<div className="mt-2 flex items-center gap-2">
<button <button
type="button" type="button"
onClick={confirmForce} onClick={confirmForce}
@ -482,29 +476,29 @@ export default function MatrixManagementPage() {
{/* Form fields */} {/* Form fields */}
<div> <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 <input
type="text" type="text"
value={createName} value={createName}
onChange={e => setCreateName(e.target.value)} onChange={e => setCreateName(e.target.value)}
disabled={createLoading} 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" 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>
<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 <input
type="email" type="email"
value={createEmail} value={createEmail}
onChange={e => setCreateEmail(e.target.value)} onChange={e => setCreateEmail(e.target.value)}
disabled={createLoading} 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" 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>
<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 <input
type="number" type="number"
min={1} min={1}
@ -513,7 +507,7 @@ export default function MatrixManagementPage() {
onChange={e => setCreateDepth(Number(e.target.value))} onChange={e => setCreateDepth(Number(e.target.value))}
disabled={createLoading} 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" 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> </div>

View File

@ -1,5 +1,8 @@
'use client' 'use client'
import { useTranslation } from '../../i18n/useTranslation';
import React from 'react' import React from 'react'
import Header from '../../components/nav/Header' import Header from '../../components/nav/Header'
import Footer from '../../components/Footer' import Footer from '../../components/Footer'
@ -12,6 +15,7 @@ import { updateNews } from './hooks/updateNews'
import { deleteNews } from './hooks/deleteNews' import { deleteNews } from './hooks/deleteNews'
export default function NewsManagementPage() { export default function NewsManagementPage() {
const { t } = useTranslation();
const { items, loading, error, refresh } = useAdminNews() const { items, loading, error, refresh } = useAdminNews()
const [showCreate, setShowCreate] = React.useState(false) const [showCreate, setShowCreate] = React.useState(false)
const [selected, setSelected] = React.useState<any | null>(null) 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"> <main className="bg-white min-h-screen pb-20">
<div className="mx-auto max-w-7xl px-6 pt-8 pb-12"> <div className="mx-auto max-w-7xl px-6 pt-8 pb-12">
<div className="flex items-center justify-between"> <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"> <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 <PlusIcon className="h-5 w-5" />{t('autofix.k75078d0b')}</button>
</button>
</div> </div>
{error && <div className="mt-4 text-red-600">{error}</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="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="w-full max-w-md rounded-2xl bg-white shadow-xl ring-1 ring-gray-200">
<div className="px-6 pt-6"> <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> <p className="mt-2 text-sm text-gray-700">You are about to delete "{deleteTarget.title}". This action cannot be undone.</p>
</div> </div>
<div className="px-6 pb-6 pt-4 flex justify-end gap-3"> <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="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="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"> <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> <button onClick={onClose} className="text-gray-500 hover:text-gray-700"><XMarkIcon className="h-6 w-6"/></button>
</div> </div>
<form onSubmit={submit} className="p-6 space-y-4"> <form onSubmit={submit} className="p-6 space-y-4">
@ -217,7 +220,7 @@ function CreateNewsModal({
onChange={e=>{ setSlugTouched(true); setSlug(e.target.value) }} onChange={e=>{ setSlugTouched(true); setSlug(e.target.value) }}
required 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> </div>
<input className="w-full border rounded px-4 py-2 text-gray-900" placeholder="Category" value={category} onChange={e=>setCategory(e.target.value)} /> <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} /> <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 ? ( {!previewUrl ? (
<div className="text-center w-full px-6 py-10"> <div className="text-center w-full px-6 py-10">
<PhotoIcon className="mx-auto h-12 w-12 text-gray-400" /> <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> <div className="mt-4 text-sm font-medium text-gray-700">{t('autofix.kf4868273')}</div>
<p className="text-xs text-gray-500 mt-2">PNG, JPG, WebP up to 5MB</p> <p className="text-xs text-gray-500 mt-2">{t('autofix.kcd9890e5')}</p>
</div> </div>
) : ( ) : (
<div className="relative w-full h-full min-h-[200px] flex items-center justify-center bg-white p-4"> <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="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="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"> <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> <button onClick={onClose} className="text-gray-500 hover:text-gray-700"><XMarkIcon className="h-6 w-6"/></button>
</div> </div>
<form onSubmit={submit} className="p-6 space-y-4"> <form onSubmit={submit} className="p-6 space-y-4">
@ -329,8 +332,8 @@ function EditNewsModal({ item, onClose, onUpdate }: { item: any; onClose: () =>
{!displayUrl ? ( {!displayUrl ? (
<div className="text-center w-full px-6 py-10"> <div className="text-center w-full px-6 py-10">
<PhotoIcon className="mx-auto h-12 w-12 text-gray-400" /> <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> <div className="mt-4 text-sm font-medium text-gray-700">{t('autofix.kf4868273')}</div>
<p className="text-xs text-gray-500 mt-2">PNG, JPG, WebP up to 5MB</p> <p className="text-xs text-gray-500 mt-2">{t('autofix.kcd9890e5')}</p>
</div> </div>
) : ( ) : (
<div className="relative w-full h-full min-h-[200px] flex items-center justify-center bg-white p-4"> <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>
<div className="flex items-center justify-end gap-2 pt-4 border-t"> <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="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> </div>
</form> </form>
</div> </div>

View File

@ -1,4 +1,7 @@
'use client' 'use client'
import { useTranslation } from '../../../i18n/useTranslation';
import React from 'react' import React from 'react'
interface Props { interface Props {
@ -22,6 +25,7 @@ export default function CreateNewPoolModal({
success, success,
clearMessages clearMessages
}: Props) { }: Props) {
const { t } = useTranslation();
const [poolName, setPoolName] = React.useState('') const [poolName, setPoolName] = React.useState('')
const [description, setDescription] = React.useState('') const [description, setDescription] = React.useState('')
const [price, setPrice] = React.useState('0.00') const [price, setPrice] = React.useState('0.00')
@ -52,7 +56,7 @@ export default function CreateNewPoolModal({
{/* Modal */} {/* 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="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"> <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 <button
onClick={() => { clearMessages(); onClose(); }} onClick={() => { clearMessages(); onClose(); }}
className="text-gray-500 hover:text-gray-700 transition text-sm" className="text-gray-500 hover:text-gray-700 transition text-sm"
@ -88,10 +92,10 @@ export default function CreateNewPoolModal({
className="space-y-4" className="space-y-4"
> >
<div> <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 <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" 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} value={poolName}
onChange={e => setPoolName(e.target.value)} onChange={e => setPoolName(e.target.value)}
disabled={isDisabled} disabled={isDisabled}
@ -103,7 +107,7 @@ export default function CreateNewPoolModal({
<textarea <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" 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} rows={3}
placeholder="Short description of the pool" placeholder={t('autofix.kb573897d')}
value={description} value={description}
onChange={e => setDescription(e.target.value)} onChange={e => setDescription(e.target.value)}
disabled={isDisabled} disabled={isDisabled}
@ -122,10 +126,10 @@ export default function CreateNewPoolModal({
disabled={isDisabled} disabled={isDisabled}
required 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>
<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 <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" 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} value={poolType}
@ -137,7 +141,7 @@ export default function CreateNewPoolModal({
</select> </select>
</div> </div>
<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 <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" 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} value={subscriptionCoffeeId}

View File

@ -1,5 +1,8 @@
'use client' 'use client'
import { useTranslation } from '../../../i18n/useTranslation';
import React, { Suspense } from 'react' // CHANGED: add Suspense import React, { Suspense } from 'react' // CHANGED: add Suspense
import Header from '../../../components/nav/Header' import Header from '../../../components/nav/Header'
import Footer from '../../../components/Footer' import Footer from '../../../components/Footer'
@ -19,6 +22,7 @@ type PoolUser = {
} }
function PoolManagePageInner() { function PoolManagePageInner() {
const { t } = useTranslation()
const router = useRouter() const router = useRouter()
const searchParams = useSearchParams() const searchParams = useSearchParams()
const user = useAuthStore(s => s.user) const user = useAuthStore(s => s.user)
@ -281,9 +285,7 @@ function PoolManagePageInner() {
}`}> }`}>
{isCore && ( {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"> <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> <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>
Core Pool 1¢ per capsule per member
</div>
)} )}
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
@ -315,10 +317,8 @@ function PoolManagePageInner() {
<button <button
onClick={() => router.push('/admin/pool-management')} 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" 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" title={t('autofix.k6285753a')}
> >{t('autofix.k0ac84efe')}</button>
Back
</button>
</div> </div>
</header> </header>
@ -330,7 +330,7 @@ function PoolManagePageInner() {
<BanknotesIcon className="h-5 w-5 text-white" /> <BanknotesIcon className="h-5 w-5 text-white" />
</div> </div>
<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> <p className="text-2xl font-semibold text-gray-900"> {totalAmount.toLocaleString()}</p>
</div> </div>
</div> </div>
@ -341,7 +341,7 @@ function PoolManagePageInner() {
<CalendarDaysIcon className="h-5 w-5 text-white" /> <CalendarDaysIcon className="h-5 w-5 text-white" />
</div> </div>
<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> <p className="text-2xl font-semibold text-gray-900"> {amountThisYear.toLocaleString()}</p>
</div> </div>
</div> </div>
@ -352,7 +352,7 @@ function PoolManagePageInner() {
<CalendarDaysIcon className="h-5 w-5 text-white" /> <CalendarDaysIcon className="h-5 w-5 text-white" />
</div> </div>
<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> <p className="text-2xl font-semibold text-gray-900"> {amountThisMonth.toLocaleString()}</p>
</div> </div>
</div> </div>
@ -372,9 +372,7 @@ function PoolManagePageInner() {
onClick={() => { setSearchOpen(true); setQuery(''); setCandidates([]); setHasSearched(false); setError(''); }} 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" 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" /> <PlusIcon className="h-5 w-5" />{t('autofix.k750c1eb5')}</button>
Add User
</button>
</div> </div>
{removeError && ( {removeError && (
<div className="mb-4 rounded-md border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-700"> <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 && ( {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 && ( {membersError && !membersLoading && (
<div className="text-center text-red-600 py-8">{membersError}</div> <div className="text-center text-red-600 py-8">{membersError}</div>
)} )}
{users.length === 0 && !membersLoading && !membersError && ( {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 && ( {users.length > 0 && !membersLoading && (
@ -399,7 +397,7 @@ function PoolManagePageInner() {
<tr> <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">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">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">{isCore ? 'Total Earned' : 'Share'}</th>
<th className="px-4 py-3 text-right font-semibold text-gray-700" /> <th className="px-4 py-3 text-right font-semibold text-gray-700" />
</tr> </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"> <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 */} {/* Header */}
<div className="px-6 py-5 border-b border-gray-100 flex items-center justify-between"> <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 <button
onClick={() => setSearchOpen(false)} onClick={() => setSearchOpen(false)}
className="p-1.5 rounded-md text-gray-500 hover:bg-gray-100 hover:text-gray-700 transition" 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 <input
value={query} value={query}
onChange={e => setQuery(e.target.value)} 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" 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> </div>
@ -499,17 +497,13 @@ function PoolManagePageInner() {
</button> </button>
</div> </div>
</form> </form>
<div className="px-6 pt-1 pb-3 text-right text-xs text-gray-500"> <div className="px-6 pt-1 pb-3 text-right text-xs text-gray-500">{t('autofix.ke4c4a858')}</div>
Min. 3 characters
</div>
{/* Results */} {/* Results */}
<div className="px-6 py-4 overflow-y-auto min-h-0 flex-1"> <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 && <div className="text-sm text-red-600 mb-3">{error}</div>}
{!error && query.trim().length < 3 && ( {!error && query.trim().length < 3 && (
<div className="py-8 text-sm text-gray-500 text-center"> <div className="py-8 text-sm text-gray-500 text-center">{t('autofix.kb87eb38b')}</div>
Enter at least 3 characters and click Search.
</div>
)} )}
{!error && hasSearched && loading && candidates.length === 0 && ( {!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"> <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> </ul>
)} )}
{!error && hasSearched && !loading && candidates.length === 0 && ( {!error && hasSearched && !loading && candidates.length === 0 && (
<div className="py-8 text-sm text-gray-500 text-center"> <div className="py-8 text-sm text-gray-500 text-center">{t('autofix.k54f49724')}</div>
No users match your search.
</div>
)} )}
{!error && candidates.length > 0 && ( {!error && candidates.length > 0 && (
<ul className="divide-y divide-gray-200 border border-gray-200 rounded-lg bg-white"> <ul className="divide-y divide-gray-200 border border-gray-200 rounded-lg bg-white">
@ -594,7 +586,7 @@ function PoolManagePageInner() {
open={Boolean(removeConfirm)} open={Boolean(removeConfirm)}
pending={Boolean(removingMemberId)} pending={Boolean(removingMemberId)}
intent="danger" intent="danger"
title="Remove member from pool?" title={t('autofix.k959fb1a6')}
description={`This will remove ${removeConfirm?.label || 'this user'} from the pool.`} description={`This will remove ${removeConfirm?.label || 'this user'} from the pool.`}
confirmText="Remove" confirmText="Remove"
onClose={() => { if (!removingMemberId) setRemoveConfirm(null) }} onClose={() => { if (!removingMemberId) setRemoveConfirm(null) }}

View File

@ -1,5 +1,8 @@
'use client' 'use client'
import { useTranslation } from '../../i18n/useTranslation';
import React from 'react' import React from 'react'
import Header from '../../components/nav/Header' import Header from '../../components/nav/Header'
import Footer from '../../components/Footer' import Footer from '../../components/Footer'
@ -23,6 +26,7 @@ type Pool = {
} }
export default function PoolManagementPage() { export default function PoolManagementPage() {
const { t } = useTranslation();
const router = useRouter() const router = useRouter()
const [archiveError, setArchiveError] = React.useState<string>('') 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"> <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 className="flex items-center justify-between">
<div> <div>
<h1 className="text-4xl font-extrabold text-blue-900 tracking-tight">Pool Management</h1> <h1 className="text-4xl font-extrabold text-blue-900 tracking-tight">{t('autofix.k21440f8a')}</h1>
<p className="text-lg text-blue-700 mt-2">Manage system pools and members.</p> <p className="text-lg text-blue-700 mt-2">{t('autofix.k67391c88')}</p>
</div> </div>
</div> </div>
<div className="flex items-center gap-2"> <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 <button
onClick={() => setShowInactive(false)} 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'}`} 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'}`}
> >{t('autofix.k15843a06')}</button>
Active Pools
</button>
<button <button
onClick={() => setShowInactive(true)} 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'}`} 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'}`}
> >{t('autofix.kb5e0b861')}</button>
Inactive Pools
</button>
</div> </div>
</header> </header>
{/* Pools List card */} {/* Pools List card */}
<div className="rounded-2xl bg-white border border-gray-100 shadow-lg p-6 relative z-0"> <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"> <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> <span className="text-sm text-gray-600">{pools.length} total</span>
</div> </div>
@ -172,9 +172,7 @@ export default function PoolManagementPage() {
}`}> }`}>
{isCore && ( {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"> <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> <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>
Core Pool
</div>
)} )}
<div className="flex items-start justify-between"> <div className="flex items-start justify-between">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
@ -225,15 +223,13 @@ export default function PoolManagementPage() {
<button <button
className="px-4 py-2 text-xs font-medium rounded-lg bg-green-100 text-green-800 hover:bg-green-200 transition" 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)} onClick={() => handleSetActive(pool.id)}
title="Activate this pool" title={t('autofix.kd40c4f86')}
> >{t('autofix.ke697b8cb')}</button>
Set Active
</button>
) : ( ) : (
<button <button
className="px-4 py-2 text-xs font-medium rounded-lg bg-amber-100 text-amber-800 hover:bg-amber-200 transition" 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)} onClick={() => handleArchive(pool.id)}
title="Archive this pool" title={t('autofix.ke19afb3d')}
> >
Archive Archive
</button> </button>

View File

@ -1,4 +1,7 @@
'use client' 'use client'
import { useTranslation } from '../../../i18n/useTranslation';
import React, { useState, useCallback } from 'react' import React, { useState, useCallback } from 'react'
import Cropper from 'react-easy-crop' import Cropper from 'react-easy-crop'
import { Point, Area } 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) { export default function ImageCropModal({ isOpen, imageSrc, onClose, onCropComplete }: ImageCropModalProps) {
const { t } = useTranslation();
const [crop, setCrop] = useState<Point>({ x: 0, y: 0 }) const [crop, setCrop] = useState<Point>({ x: 0, y: 0 })
const [zoom, setZoom] = useState(1) const [zoom, setZoom] = useState(1)
const [croppedAreaPixels, setCroppedAreaPixels] = useState<Area | null>(null) 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"> <div className="relative w-full max-w-4xl mx-4 bg-white rounded-2xl shadow-2xl overflow-hidden">
{/* Header */} {/* 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"> <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 <button
onClick={onClose} onClick={onClose}
className="text-gray-500 hover:text-gray-700 transition" className="text-gray-500 hover:text-gray-700 transition"
@ -120,9 +124,7 @@ export default function ImageCropModal({ isOpen, imageSrc, onClose, onCropComple
<button <button
onClick={handleSave} 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" className="px-5 py-2.5 text-sm font-semibold text-white bg-blue-900 rounded-lg hover:bg-blue-800 shadow transition"
> >{t('autofix.kef1656df')}</button>
Apply Crop
</button>
</div> </div>
</div> </div>
</div> </div>

View File

@ -7,7 +7,10 @@ import Link from 'next/link';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import ImageCropModal from '../components/ImageCropModal'; import ImageCropModal from '../components/ImageCropModal';
import { useTranslation } from '../../../i18n/useTranslation';
export default function CreateSubscriptionPage() { export default function CreateSubscriptionPage() {
const { t } = useTranslation();
const { createProduct } = useCoffeeManagement(); const { createProduct } = useCoffeeManagement();
const router = useRouter(); 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"> <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 className="flex items-center justify-between">
<div> <div>
<h1 className="text-4xl font-extrabold text-blue-900 tracking-tight">Create Coffee</h1> <h1 className="text-4xl font-extrabold text-blue-900 tracking-tight">{t('autofix.kaa30f0cd')}</h1>
<p className="text-lg text-blue-700 mt-2">Add a new coffee.</p> <p className="text-lg text-blue-700 mt-2">{t('autofix.kf72d41db')}</p>
</div> </div>
<Link href="/admin/subscriptions" <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" 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> <svg className="w-5 h-5" fill="none" stroke="currentColor"><path d="M6 18L18 6M6 6l12 12"/></svg>{t('autofix.kd8a5ad17')}</Link>
Back to list
</Link>
</div> </div>
</header> </header>
@ -124,10 +125,10 @@ export default function CreateSubscriptionPage() {
<div className="text-center w-full px-6 py-10"> <div className="text-center w-full px-6 py-10">
<PhotoIcon aria-hidden="true" className="mx-auto h-16 w-16 text-blue-400" /> <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"> <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> </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>
<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-xs text-gray-500 mt-2">{t('autofix.k41ab9eb6')}</p>
</div> </div>
)} )}
{previewUrl && ( {previewUrl && (
@ -145,9 +146,7 @@ export default function CreateSubscriptionPage() {
setShowCropModal(true); 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" 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"
> >{t('autofix.k73d1d7d7')}</button>
Edit Crop
</button>
<button <button
type="button" type="button"
onClick={e => { onClick={e => {
@ -196,11 +195,11 @@ export default function CreateSubscriptionPage() {
required 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" 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} rows={3}
placeholder="Describe the product" placeholder={t('autofix.k3477c83a')}
value={description} value={description}
onChange={e => setDescription(e.target.value)} 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>
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2"> <div className="grid grid-cols-1 gap-6 sm:grid-cols-2">
@ -242,7 +241,7 @@ export default function CreateSubscriptionPage() {
{/* Subscription Billing (Locked) + Availability */} {/* Subscription Billing (Locked) + Availability */}
<div className="sm:col-span-2 grid grid-cols-1 sm:grid-cols-2 gap-6"> <div className="sm:col-span-2 grid grid-cols-1 sm:grid-cols-2 gap-6">
<div> <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> <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"> <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" /> <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"> <Link href="/admin/subscriptions" className="text-sm font-medium text-blue-900 hover:text-blue-700">
Cancel Cancel
</Link> </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"> <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>
Create Coffee
</button>
</div> </div>
{error && <p className="text-sm text-red-600">{error}</p>} {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 useCoffeeManagement, { CoffeeItem } from '../../hooks/useCoffeeManagement';
import { PhotoIcon } from '@heroicons/react/24/solid'; import { PhotoIcon } from '@heroicons/react/24/solid';
import { useTranslation } from '../../../../i18n/useTranslation';
export default function EditSubscriptionPage() { export default function EditSubscriptionPage() {
const { t } = useTranslation();
const router = useRouter(); const router = useRouter();
// next/navigation app router dynamic param // next/navigation app router dynamic param
const params = useParams(); 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"> <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 className="flex items-center justify-between">
<div> <div>
<h1 className="text-4xl font-extrabold text-blue-900 tracking-tight">Edit Coffee</h1> <h1 className="text-4xl font-extrabold text-blue-900 tracking-tight">{t('autofix.kb06fa395')}</h1>
<p className="text-lg text-blue-700 mt-2">Update details of the coffee.</p> <p className="text-lg text-blue-700 mt-2">{t('autofix.kb9e483c4')}</p>
</div> </div>
<Link href="/admin/subscriptions" <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" 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> <svg className="w-5 h-5" fill="none" stroke="currentColor"><path d="M6 18L18 6M6 6l12 12"/></svg>{t('autofix.kd8a5ad17')}</Link>
Back to list
</Link>
</div> </div>
</header> </header>
{loading && ( {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 && ( {error && !loading && (
<div className="rounded-md bg-red-50 p-4 text-red-700 text-sm mb-6">{error}</div> <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"> <div className="text-center w-full px-6 py-10">
<PhotoIcon aria-hidden="true" className="mx-auto h-16 w-16 text-blue-400" /> <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"> <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> </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> </div>
)} )}
{(previewUrl || (!removeExistingPicture && item.pictureUrl)) && ( {(previewUrl || (!removeExistingPicture && item.pictureUrl)) && (
@ -255,9 +256,9 @@ export default function EditSubscriptionPage() {
<div className="text-center w-full px-6 py-10"> <div className="text-center w-full px-6 py-10">
<PhotoIcon aria-hidden="true" className="mx-auto h-16 w-16 text-gray-400" /> <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"> <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> </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> </div>
)} )}
<input <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"> <Link href="/admin/subscriptions" className="text-sm font-medium text-blue-900 hover:text-blue-700">
Cancel Cancel
</Link> </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"> <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>
Save Changes
</button>
</div> </div>
{error && <p className="text-sm text-red-600">{error}</p>} {error && <p className="text-sm text-red-600">{error}</p>}
</form> </form>

View File

@ -4,12 +4,15 @@ import { PhotoIcon } from '@heroicons/react/24/solid';
import Link from 'next/link'; import Link from 'next/link';
import PageLayout from '../../components/PageLayout'; import PageLayout from '../../components/PageLayout';
import useCoffeeManagement, { CoffeeItem } from './hooks/useCoffeeManagement'; import useCoffeeManagement, { CoffeeItem } from './hooks/useCoffeeManagement';
import { useTranslation } from '../../i18n/useTranslation';
import useCoffeeShippingFees, { import useCoffeeShippingFees, {
CoffeeShippingFee, CoffeeShippingFee,
CoffeeShippingFeePieceCount, CoffeeShippingFeePieceCount,
} from './hooks/useCoffeeShippingFees'; } from './hooks/useCoffeeShippingFees';
export default function AdminSubscriptionsPage() { export default function AdminSubscriptionsPage() {
const { t } = useTranslation();
const { listProducts, setProductState, deleteProduct } = useCoffeeManagement(); const { listProducts, setProductState, deleteProduct } = useCoffeeManagement();
const { listShippingFees, updateShippingFee } = useCoffeeShippingFees(); 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 className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div> <div>
<h1 className="text-4xl font-extrabold text-blue-900 tracking-tight">Coffees</h1> <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> </div>
<Link <Link
href="/admin/subscriptions/createSubscription" 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" 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> <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>
Create Coffee
</Link>
</div> </div>
</header> </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 className="flex flex-col sm:flex-row sm:items-start sm:justify-between gap-4">
<div> <div>
<h2 className="text-xl font-semibold text-blue-900">Shipping Fees (ABO)</h2> <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> </div>
<button <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" 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"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{loading && ( {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 => ( {!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"> <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> </div>
))} ))}
{!loading && !items.length && ( {!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> </div>
{/* Confirm Delete Modal */} {/* 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="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="w-full max-w-md rounded-2xl bg-white shadow-xl ring-1 ring-gray-200">
<div className="px-6 pt-6"> <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> <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>
<div className="px-6 pb-6 pt-4 flex justify-end gap-3"> <div className="px-6 pb-6 pt-4 flex justify-end gap-3">

View File

@ -1,5 +1,8 @@
'use client' 'use client'
import { useTranslation } from '../../i18n/useTranslation';
import { useMemo, useState, useEffect, useCallback } from 'react' import { useMemo, useState, useEffect, useCallback } from 'react'
import PageLayout from '../../components/PageLayout' import PageLayout from '../../components/PageLayout'
import UserDetailModal from '../../components/UserDetailModal' import UserDetailModal from '../../components/UserDetailModal'
@ -35,6 +38,7 @@ const TYPES: UserType[] = ['personal','company']
const ROLES: UserRole[] = ['user','admin','guest'] const ROLES: UserRole[] = ['user','admin','guest']
export default function AdminUserManagementPage() { export default function AdminUserManagementPage() {
const { t } = useTranslation();
const { isAdmin } = useAdminUsers() const { isAdmin } = useAdminUsers()
const token = useAuthStore(state => state.accessToken) const token = useAuthStore(state => state.accessToken)
const [isClient, setIsClient] = useState(false) 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="mx-auto w-full max-w-xl rounded-2xl bg-white shadow ring-1 ring-red-500/20 p-8">
<div className="text-center"> <div className="text-center">
<ExclamationTriangleIcon className="mx-auto h-12 w-12 text-red-500 mb-4" /> <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> <h1 className="text-2xl font-bold text-red-600 mb-2">{t('autofix.k26fbc186')}</h1>
<p className="text-gray-600">You need admin privileges to access this page.</p> <p className="text-gray-600">{t('autofix.k661c032b')}</p>
</div> </div>
</div> </div>
</div> </div>
@ -248,10 +252,8 @@ export default function AdminUserManagementPage() {
{/* Header */} {/* 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"> <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> <div>
<h1 className="text-4xl font-extrabold text-blue-900 tracking-tight">User Management</h1> <h1 className="text-4xl font-extrabold text-blue-900 tracking-tight">{t('autofix.k1af97a07')}</h1>
<p className="text-lg text-blue-700 mt-2"> <p className="text-lg text-blue-700 mt-2">{t('autofix.k79e1c459')}</p>
Manage all users, view statistics, and handle verification.
</p>
</div> </div>
</header> </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="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="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="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 className="text-xl font-semibold text-blue-900">{stats.total}</div>
</div> </div>
<div className="rounded-xl bg-white border border-gray-100 p-5 text-center shadow"> <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" 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" 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'} onClick={() => window.location.href = '/admin/user-verify'}
> >{t('autofix.k2f78fabe')}</button>
Go to User Verification
</button>
</div> </div>
</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"> <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" /> <ExclamationTriangleIcon className="h-5 w-5 flex-shrink-0 text-red-500 mt-0.5" />
<div> <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> <p className="text-sm text-red-600">{error}</p>
<button <button
onClick={fetchAllUsers} onClick={fetchAllUsers}
className="mt-2 text-sm underline hover:no-underline" className="mt-2 text-sm underline hover:no-underline"
> >{t('autofix.k3b7dd87a')}</button>
Try again
</button>
</div> </div>
</div> </div>
)} )}
@ -320,9 +318,7 @@ export default function AdminUserManagementPage() {
onSubmit={applyFilter} 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" 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"> <h2 className="text-lg font-semibold text-blue-900">{t('autofix.kd1f35ccf')}</h2>
Search & Filter Users
</h2>
<div className="grid grid-cols-1 md:grid-cols-5 gap-6"> <div className="grid grid-cols-1 md:grid-cols-5 gap-6">
{/* Search */} {/* Search */}
<div className="md:col-span-2"> <div className="md:col-span-2">
@ -332,7 +328,7 @@ export default function AdminUserManagementPage() {
<input <input
value={search} value={search}
onChange={e => setSearch(e.target.value)} 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" 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>
@ -344,7 +340,7 @@ export default function AdminUserManagementPage() {
onChange={e => setFType(e.target.value as any)} 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" 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="personal">Personal</option>
<option value="company">Company</option> <option value="company">Company</option>
</select> </select>
@ -356,7 +352,7 @@ export default function AdminUserManagementPage() {
onChange={e => setFStatus(e.target.value as any)} 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" 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>)} {STATUSES.map(s => <option key={s} value={s}>{s[0].toUpperCase()+s.slice(1)}</option>)}
</select> </select>
</div> </div>
@ -367,7 +363,7 @@ export default function AdminUserManagementPage() {
onChange={e => setFRole(e.target.value as any)} 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" 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>)} {ROLES.map(r => <option key={r} value={r}>{r[0].toUpperCase()+r.slice(1)}</option>)}
</select> </select>
</div> </div>
@ -377,10 +373,8 @@ export default function AdminUserManagementPage() {
type="button" type="button"
onClick={exportCsv} 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" 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" title={t('autofix.k1387f81e')}
> >{t('autofix.k1521a376')}</button>
Export all users as CSV
</button>
<button <button
type="submit" 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" 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 */} {/* Users Table */}
<div className="bg-white rounded-2xl shadow-lg ring-1 ring-gray-100 overflow-hidden mb-8"> <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="px-8 py-6 border-b border-gray-100 flex items-center justify-between">
<div className="text-lg font-semibold text-blue-900"> <div className="text-lg font-semibold text-blue-900">{t('autofix.k10ccb626')}</div>
All Users
</div>
<div className="text-xs text-gray-500"> <div className="text-xs text-gray-500">
Showing {current.length} of {filtered.length} users Showing {current.length} of {filtered.length} users
</div> </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">Status</th>
<th className="px-4 py-3 text-left">Role</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">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> <th className="px-4 py-3 text-left">Actions</th>
</tr> </tr>
</thead> </thead>
@ -419,7 +411,7 @@ export default function AdminUserManagementPage() {
<td colSpan={7} className="px-4 py-10 text-center"> <td colSpan={7} className="px-4 py-10 text-center">
<div className="flex items-center justify-center gap-2"> <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" /> <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> </div>
</td> </td>
</tr> </tr>
@ -478,9 +470,7 @@ export default function AdminUserManagementPage() {
})} })}
{current.length === 0 && ( {current.length === 0 && (
<tr> <tr>
<td colSpan={7} className="px-4 py-10 text-center text-sm text-blue-700"> <td colSpan={7} className="px-4 py-10 text-center text-sm text-blue-700">{t('autofix.k748bf541')}</td>
No users match current filters.
</td>
</tr> </tr>
)} )}
</tbody> </tbody>
@ -496,16 +486,12 @@ export default function AdminUserManagementPage() {
disabled={page===1} disabled={page===1}
onClick={() => setPage(p => Math.max(1,p-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" 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"
> >{t('autofix.kdb27a82d')}</button>
Previous
</button>
<button <button
disabled={page===totalPages} disabled={page===totalPages}
onClick={() => setPage(p => Math.min(totalPages,p+1))} 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" 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"
> >{t('autofix.ka8ea17b8')}</button>
Next
</button>
</div> </div>
</div> </div>
</div> </div>

View File

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

View File

@ -1,5 +1,8 @@
'use client' 'use client'
import { useTranslation } from '../i18n/useTranslation';
import { useEffect, useState, useMemo } from 'react' import { useEffect, useState, useMemo } from 'react'
import PageLayout from '../components/PageLayout' import PageLayout from '../components/PageLayout'
import Waves from '../components/background/waves' 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' 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() { export default function AffiliateLinksPage() {
const { t } = useTranslation();
const [affiliates, setAffiliates] = useState<Affiliate[]>([]) const [affiliates, setAffiliates] = useState<Affiliate[]>([])
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
const [error, setError] = useState('') const [error, setError] = useState('')
@ -115,14 +119,12 @@ export default function AffiliateLinksPage() {
{/* Header (aligned with management pages) */} {/* Header (aligned with management pages) */}
<header className="flex flex-col gap-4 mb-8"> <header className="flex flex-col gap-4 mb-8">
<div> <div>
<h1 className="text-4xl font-extrabold text-blue-900 tracking-tight">Affiliate Partners</h1> <h1 className="text-4xl font-extrabold text-blue-900 tracking-tight">{t('autofix.k1b9c46e5')}</h1>
<p className="text-lg text-blue-700 mt-2"> <p className="text-lg text-blue-700 mt-2">{t('autofix.k633438a0')}</p>
Discover our trusted partners and earn commissions through affiliate links.
</p>
</div> </div>
{/* NEW: Category filter */} {/* NEW: Category filter */}
<div className="flex items-center gap-2"> <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 <select
value={selectedCategory} value={selectedCategory}
onChange={(e) => setSelectedCategory(e.target.value)} onChange={(e) => setSelectedCategory(e.target.value)}
@ -139,7 +141,7 @@ export default function AffiliateLinksPage() {
{loading && ( {loading && (
<div className="mx-auto max-w-2xl text-center"> <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" /> <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> </div>
)} )}
@ -150,9 +152,7 @@ export default function AffiliateLinksPage() {
)} )}
{!loading && !error && posts.length === 0 && ( {!loading && !error && posts.length === 0 && (
<div className="mx-auto max-w-2xl text-center text-sm text-gray-600"> <div className="mx-auto max-w-2xl text-center text-sm text-gray-600">{t('autofix.k431328cf')}</div>
No affiliate partners available at the moment.
</div>
)} )}
{/* Cards (aligned to white panels, border, shadow) */} {/* Cards (aligned to white panels, border, shadow) */}
@ -195,12 +195,8 @@ export default function AffiliateLinksPage() {
target="_blank" target="_blank"
rel="noopener noreferrer" 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" className="rounded-lg bg-indigo-600 hover:bg-indigo-500 text-white px-4 py-2 text-sm font-medium shadow transition"
> >{t('autofix.k7db4e5a9')}</a>
Visit Affiliate Link <span className="text-[11px] text-gray-500">{t('autofix.k8b89f863')}</span>
</a>
<span className="text-[11px] text-gray-500">
External partner website.
</span>
</div> </div>
</div> </div>
</article> </article>

View File

@ -39,8 +39,43 @@ interface ScanResult {
uniqueKeyCount: number; uniqueKeyCount: number;
missingKeys: Array<{ key: string; files: string[] }>; missingKeys: Array<{ key: string; files: string[] }>;
untranslatedLiterals: Array<{ text: 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 }) { async function walk(dir: string, outFiles: string[], counters: { dirs: number }) {
counters.dirs += 1; counters.dirs += 1;
const entries = await fs.readdir(dir, { withFileTypes: true }); const entries = await fs.readdir(dir, { withFileTypes: true });
@ -84,6 +119,18 @@ function extractTranslationKeys(content: string): string[] {
return keys; 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 { function isUiCodeFile(filePath: string): boolean {
const ext = path.extname(filePath).toLowerCase(); const ext = path.extname(filePath).toLowerCase();
return ext === '.tsx' || ext === '.jsx'; return ext === '.tsx' || ext === '.jsx';
@ -101,6 +148,14 @@ function extractPotentialUiLiterals(content: string): string[] {
literals.push(text); 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; return literals;
} }
@ -121,10 +176,421 @@ function shouldIgnoreLiteral(text: string): boolean {
if (/^(use client|true|false|null|undefined)$/i.test(trimmed)) return true; if (/^(use client|true|false|null|undefined)$/i.test(trimmed)) return true;
if (/(className|onClick|href|src|aria-|data-)/.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 (/^[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; 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 { function toRelativeWorkspacePath(absPath: string): string {
const rel = path.relative(process.cwd(), absPath); const rel = path.relative(process.cwd(), absPath);
return rel.split(path.sep).join('/'); return rel.split(path.sep).join('/');
@ -141,6 +607,8 @@ async function runWorkspaceScan(): Promise<ScanResult> {
const uniqueUsedKeys = new Set<string>(); const uniqueUsedKeys = new Set<string>();
const missingKeyFiles: MissingKeyMap = new Map(); const missingKeyFiles: MissingKeyMap = new Map();
const untranslatedLiteralFiles: Map<string, Set<string>> = new Map(); const untranslatedLiteralFiles: Map<string, Set<string>> = new Map();
const autoFixEligibleFilesSet = new Set<string>();
const autoFixForceConvertibleFilesSet = new Set<string>();
let translationCallCount = 0; let translationCallCount = 0;
@ -150,9 +618,10 @@ async function runWorkspaceScan(): Promise<ScanResult> {
const usedKeys = extractTranslationKeys(raw); const usedKeys = extractTranslationKeys(raw);
if (usedKeys.length > 0) { 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); uniqueUsedKeys.add(key);
if (!englishKeys.has(key)) { if (!englishKeys.has(key)) {
@ -172,6 +641,14 @@ async function runWorkspaceScan(): Promise<ScanResult> {
} }
untranslatedLiteralFiles.get(text)?.add(relativePath); 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, uniqueKeyCount: uniqueUsedKeys.size,
missingKeys, missingKeys,
untranslatedLiterals, 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 { useActiveCoffees } from './hooks/getActiveCoffees';
import { useShippingFees } from './hooks/useShippingFees'; import { useShippingFees } from './hooks/useShippingFees';
import { useTranslation } from '../i18n/useTranslation';
export default function CoffeeAbonnementPage() { export default function CoffeeAbonnementPage() {
const { t } = useTranslation();
const [selections, setSelections] = useState<Record<string, number>>({}); const [selections, setSelections] = useState<Record<string, number>>({});
const [bump, setBump] = useState<Record<string, boolean>>({}); const [bump, setBump] = useState<Record<string, boolean>>({});
const [selectedPlanCapsules, setSelectedPlanCapsules] = useState<number>(60); const [selectedPlanCapsules, setSelectedPlanCapsules] = useState<number>(60);
@ -117,7 +120,7 @@ export default function CoffeeAbonnementPage() {
<PageLayout> <PageLayout>
<div className="mx-auto max-w-7xl px-4 py-10 space-y-10 bg-gradient-to-b from-white to-[#1C2B4A0D]"> <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"> <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> </h1>
{/* Stepper */} {/* Stepper */}
@ -135,7 +138,7 @@ export default function CoffeeAbonnementPage() {
{/* Section 1: Multi coffee selection + per-coffee quantity */} {/* Section 1: Multi coffee selection + per-coffee quantity */}
<section> <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="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"> <div className="flex items-center gap-4">
<button <button
@ -155,9 +158,9 @@ export default function CoffeeAbonnementPage() {
>+</button> >+</button>
<div className="ml-4"> <div className="ml-4">
{shippingLoading ? ( {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 ? ( ) : 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> <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> </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 && ( {error && (
<div className="mb-4 rounded-md border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700"> <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} {coffee.pricePer10}
</span> </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"> <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>
per 10 pcs
</span>
</div> </div>
</div> </div>
<div className="flex items-start justify-between"> <div className="flex items-start justify-between">
@ -317,10 +318,10 @@ export default function CoffeeAbonnementPage() {
{/* Section 2: Compact preview + next steps */} {/* Section 2: Compact preview + next steps */}
<section> <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"> <div className="rounded-xl border border-[#1C2B4A]/20 p-6 bg-white/80 backdrop-blur-sm space-y-4 shadow-lg">
{selectedEntries.length === 0 && ( {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) => ( {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"> <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-[#1C2B4A] text-white hover:bg-[#1C2B4A]/90 shadow-md hover:shadow-lg'
: 'bg-gray-200 text-gray-600 cursor-not-allowed' : 'bg-gray-200 text-gray-600 cursor-not-allowed'
}`} }`}
> >{t('autofix.k02665163')}<svg
Next steps
<svg
className={`ml-2 h-5 w-5 transition-transform ${ className={`ml-2 h-5 w-5 transition-transform ${
canProceed ? 'group-hover:translate-x-0.5' : '' 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 { subscribeAbo, type SubscribeAboInput } from './hooks/subscribeAbo';
import useAuthStore from '../../store/authStore' import useAuthStore from '../../store/authStore'
import { useShippingFees } from '../hooks/useShippingFees'; import { useShippingFees } from '../hooks/useShippingFees';
import { useTranslation } from '../../i18n/useTranslation';
import { useAboContractTemplateHtml } from './hooks/useAboContractTemplateHtml' import { useAboContractTemplateHtml } from './hooks/useAboContractTemplateHtml'
import SignaturePad from './components/SignaturePad' import SignaturePad from './components/SignaturePad'
import { Dialog, DialogActions, DialogBody, DialogTitle } from '../../components/dialog' import { Dialog, DialogActions, DialogBody, DialogTitle } from '../../components/dialog'
@ -62,6 +64,7 @@ function pickFirstString(...values: unknown[]): string {
} }
export default function SummaryPage() { export default function SummaryPage() {
const { t } = useTranslation();
const router = useRouter(); const router = useRouter();
const { coffees, loading, error } = useActiveCoffees(); const { coffees, loading, error } = useActiveCoffees();
const user = useAuthStore(state => state.user) 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="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"> <div className="flex items-center justify-between">
<h1 className="text-3xl font-bold tracking-tight"> <h1 className="text-3xl font-bold tracking-tight">
<span className="text-[#1C2B4A]">Summary & Details</span> <span className="text-[#1C2B4A]">{t('autofix.k21361e0d')}</span>
</h1> </h1>
<button <button
onClick={backToSelection} onClick={backToSelection}
className="rounded-md border border-gray-300 px-3 py-2 text-sm hover:bg-gray-100" className="rounded-md border border-gray-300 px-3 py-2 text-sm hover:bg-gray-100"
> >{t('autofix.k96839795')}</button>
Back to selection
</button>
</div> </div>
{/* Stepper */} {/* Stepper */}
@ -669,9 +670,7 @@ export default function SummaryPage() {
<button <button
onClick={backToSelection} onClick={backToSelection}
className="rounded-md bg-[#1C2B4A] text-white px-4 py-2 font-semibold hover:bg-[#1C2B4A]/90" className="rounded-md bg-[#1C2B4A] text-white px-4 py-2 font-semibold hover:bg-[#1C2B4A]/90"
> >{t('autofix.k96839795')}</button>
Back to selection
</button>
</div> </div>
)} )}
@ -688,35 +687,31 @@ export default function SummaryPage() {
</div> </div>
) : selectedEntries.length === 0 ? ( ) : selectedEntries.length === 0 ? (
<div className="rounded-xl border p-6 bg-white shadow-sm"> <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 <button
onClick={backToSelection} onClick={backToSelection}
className="rounded-md bg-[#1C2B4A] text-white px-4 py-2 font-semibold hover:bg-[#1C2B4A]/90" className="rounded-md bg-[#1C2B4A] text-white px-4 py-2 font-semibold hover:bg-[#1C2B4A]/90"
> >{t('autofix.k96839795')}</button>
Back to selection
</button>
</div> </div>
) : ( ) : (
<div className="grid gap-8 lg:grid-cols-3"> <div className="grid gap-8 lg:grid-cols-3">
{/* Left: Customer data */} {/* Left: Customer data */}
<section className="lg:col-span-2"> <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"> <div className="rounded-xl border border-[#1C2B4A]/20 bg-white/80 backdrop-blur-sm p-6 shadow-lg">
<button <button
type="button" type="button"
onClick={fillFromLoggedInData} 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" className="mb-4 w-full rounded-md border border-[#1C2B4A] px-3 py-2 text-sm font-medium text-[#1C2B4A] hover:bg-[#1C2B4A]/5"
> >{t('autofix.k9c1a5ecc')}</button>
Fill fields with logged in data
</button>
<div className="grid gap-4 sm:grid-cols-2"> <div className="grid gap-4 sm:grid-cols-2">
{/* inputs translated */} {/* inputs translated */}
<div> <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]" /> <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>
<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]" /> <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>
<div className="sm:col-span-2"> <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]" /> <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>
<div className="sm:col-span-2"> <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]" /> <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>
<div> <div>
@ -751,7 +746,7 @@ export default function SummaryPage() {
{/* Payment method */} {/* Payment method */}
<div className="mt-6 border-t border-gray-200 pt-4"> <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"> <div className="flex flex-wrap gap-3">
{(['sepa', 'card', 'sofort'] as const).map(method => ( {(['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'}`}> <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> </div>
<label className="mt-3 flex items-center gap-2 text-sm"> <label className="mt-3 flex items-center gap-2 text-sm">
<input type="checkbox" name="invoiceByEmail" checked={form.invoiceByEmail} onChange={handleCheckbox} className="accent-[#1C2B4A]" /> <input type="checkbox" name="invoiceByEmail" checked={form.invoiceByEmail} onChange={handleCheckbox} className="accent-[#1C2B4A]" />{t('autofix.ke33e6fbf')}</label>
Send invoice by email
</label>
</div> </div>
{/* Invoice address */} {/* Invoice address */}
<div className="mt-6 border-t border-gray-200 pt-4"> <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 && ( {isCompanyCustomer && (
<div className="mb-3 rounded-lg border border-blue-200 bg-blue-50 px-3 py-2 text-xs text-blue-900"> <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. Unternehmer mit gueltiger UID und Rechnungsland ausserhalb von {HOME_COUNTRY_CODE} werden per Reverse Charge ohne ausgewiesene MwSt verrechnet.
</div> </div>
)} )}
<label className="flex items-center gap-2 text-sm mb-3"> <label className="flex items-center gap-2 text-sm mb-3">
<input type="checkbox" name="invoiceSameAsShipping" checked={form.invoiceSameAsShipping} onChange={handleCheckbox} className="accent-[#1C2B4A]" /> <input type="checkbox" name="invoiceSameAsShipping" checked={form.invoiceSameAsShipping} onChange={handleCheckbox} className="accent-[#1C2B4A]" />{t('autofix.k528eede9')}</label>
Same as shipping address
</label>
{isCompanyCustomer && ( {isCompanyCustomer && (
<div className="mb-4"> <div className="mb-4">
<label className="block text-sm font-medium mb-1">UID Number (optional)</label> <label className="block text-sm font-medium mb-1">UID Number (optional)</label>
@ -785,22 +776,20 @@ export default function SummaryPage() {
name="uidNumber" name="uidNumber"
value={form.uidNumber} value={form.uidNumber}
onChange={handleInput} 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]" 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"> <p className="mt-1 text-xs text-gray-600">{t('autofix.kefe5f0dd')}</p>
Ohne gueltige UID wird die Rechnung mit normaler MwSt erstellt.
</p>
</div> </div>
)} )}
{!form.invoiceSameAsShipping && ( {!form.invoiceSameAsShipping && (
<div className="grid gap-4 sm:grid-cols-2"> <div className="grid gap-4 sm:grid-cols-2">
<div className="sm:col-span-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]" /> <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>
<div className="sm:col-span-2"> <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]" /> <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>
<div> <div>
@ -826,14 +815,10 @@ export default function SummaryPage() {
{/* Contract preview + signature */} {/* Contract preview + signature */}
<div className="mt-6 border-t border-gray-200 pt-6"> <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> <h3 className="text-base font-semibold text-gray-900 mb-2">Contract preview (ABO)</h3>
<p className="text-xs text-gray-600 mb-3"> <p className="text-xs text-gray-600 mb-3">{t('autofix.k155166db')}</p>
Contract variables are auto-populated from your form data.
</p>
{contractLoading ? ( {contractLoading ? (
<div className="rounded-lg border border-gray-200 bg-gray-50 px-4 py-3 text-sm text-gray-700"> <div className="rounded-lg border border-gray-200 bg-gray-50 px-4 py-3 text-sm text-gray-700">{t('autofix.k0bbc633d')}</div>
Loading contract preview
</div>
) : contractError ? ( ) : contractError ? (
<div className="rounded-lg border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-900"> <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} Contract preview could not be loaded: {contractError}
@ -843,21 +828,17 @@ export default function SummaryPage() {
type="button" type="button"
onClick={openContractPreview} 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" 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"
> >{t('autofix.kd379df9b')}</button>
Open preview
</button>
) : ( ) : (
<div className="rounded-lg border border-gray-200 bg-gray-50 px-4 py-3 text-sm text-gray-700"> <div className="rounded-lg border border-gray-200 bg-gray-50 px-4 py-3 text-sm text-gray-700">{t('autofix.ke74b1adf')}</div>
Contract template is not available.
</div>
)} )}
<div className="mt-4 space-y-3"> <div className="mt-4 space-y-3">
<div> <div>
<label className="block text-sm font-medium mb-1">Ort (Signing City) *</label> <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 && ( {!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> </div>
<SignaturePad value={signatureDataUrl} onChange={setSignatureDataUrl} required error={!hasSignature && submitError ? 'Signature is required.' : null} /> <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} PDF preview could not be generated: {contractPdfError}
</div> </div>
) : contractPdfLoading ? ( ) : contractPdfLoading ? (
<div className="rounded-lg border border-gray-200 bg-gray-50 px-4 py-3 text-sm text-gray-700"> <div className="rounded-lg border border-gray-200 bg-gray-50 px-4 py-3 text-sm text-gray-700">{t('autofix.k0b2445d5')}</div>
Generating PDF preview
</div>
) : contractPdfUrl ? ( ) : contractPdfUrl ? (
<div className="rounded-lg border border-gray-300 bg-white overflow-hidden"> <div className="rounded-lg border border-gray-300 bg-white overflow-hidden">
<iframe <iframe
title="ABO Contract PDF Preview" title={t('autofix.kaa5e5363')}
className="w-full h-[75vh]" className="w-full h-[75vh]"
src={contractPdfUrl} src={contractPdfUrl}
/> />
</div> </div>
) : ( ) : (
<div className="rounded-lg border border-gray-200 bg-gray-50 px-4 py-3 text-sm text-gray-700"> <div className="rounded-lg border border-gray-200 bg-gray-50 px-4 py-3 text-sm text-gray-700">{t('autofix.ka56b7b2b')}</div>
No PDF preview available.
</div>
)} )}
</DialogBody> </DialogBody>
<DialogActions> <DialogActions>
@ -913,16 +890,14 @@ export default function SummaryPage() {
</svg> </svg>
</button> </button>
{!canSubmit && ( {!canSubmit && (
<p className="text-xs text-gray-500 mt-2"> <p className="text-xs text-gray-500 mt-2">{t('autofix.k1824f78d')}</p>
Please select coffees and fill all required buyer fields, signing city, and signature.
</p>
)} )}
</div> </div>
</section> </section>
{/* Right: Order summary */} {/* Right: Order summary */}
<section className="lg:col-span-1"> <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"> <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 => ( {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"> <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> <span className="text-sm font-medium">{taxAmountWithShipping.toFixed(2)}</span>
</div> </div>
<div className="flex items-center justify-between"> <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> <span className="text-xl font-extrabold text-[#1C2B4A]">{totalWithTax.toFixed(2)}</span>
</div> </div>
{isReverseCharge && ( {isReverseCharge && (
<div className="mt-2 rounded-md border border-blue-200 bg-blue-50 px-3 py-2 text-xs text-blue-900"> <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>
Reverse Charge aktiv: gueltige UID und auslaendisches Rechnungsland erkannt.
</div>
)} )}
{/* Validation summary (refined design) */} {/* Validation summary (refined design) */}
<div className="mt-2 text-xs text-gray-700"> <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" /> <path d="M20 6L9 17l-5-5" strokeLinecap="round" strokeLinejoin="round" />
</svg> </svg>
</div> </div>
<h3 className="text-2xl font-bold">Thanks for your subscription!</h3> <h3 className="text-2xl font-bold">{t('autofix.k0853cfa6')}</h3>
<p className="mt-1 text-sm text-gray-600"> <p className="mt-1 text-sm text-gray-600">{t('autofix.kd3092148')}</p>
Subscription created.
</p>
<div className="mt-6 grid gap-3 sm:grid-cols-2"> <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"> <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>
Back to selection
</button>
<button onClick={() => setShowThanks(false)} className="rounded-lg border border-gray-300 px-4 py-2 font-semibold hover:bg-gray-50"> <button onClick={() => setShowThanks(false)} className="rounded-lg border border-gray-300 px-4 py-2 font-semibold hover:bg-gray-50">
Close Close
</button> </button>

View File

@ -1,5 +1,8 @@
'use client' 'use client'
import { useTranslation } from '../i18n/useTranslation';
import { useEffect } from 'react' import { useEffect } from 'react'
import { useRouter } from 'next/navigation' import { useRouter } from 'next/navigation'
import useAuthStore from '../store/authStore' import useAuthStore from '../store/authStore'
@ -17,6 +20,7 @@ import {
} from '@heroicons/react/24/outline' } from '@heroicons/react/24/outline'
export default function CommunityPage() { export default function CommunityPage() {
const { t } = useTranslation();
const router = useRouter() const router = useRouter()
const user = useAuthStore(state => state.user) const user = useAuthStore(state => state.user)
@ -113,12 +117,8 @@ export default function CommunityPage() {
<div className="max-w-7xl mx-auto"> <div className="max-w-7xl mx-auto">
{/* Header Section */} {/* Header Section */}
<div className="text-center mb-12"> <div className="text-center mb-12">
<h1 className="text-4xl font-bold text-gray-900 mb-4"> <h1 className="text-4xl font-bold text-gray-900 mb-4">{t('autofix.k08c92a12')}</h1>
Welcome to Profit Planet Community 🌍 <p className="text-xl text-gray-600 max-w-3xl mx-auto">{t('autofix.k3c32c87f')}</p>
</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>
</div> </div>
{/* Community Stats */} {/* 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="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<div className="flex items-center justify-between mb-6"> <div className="flex items-center justify-between mb-6">
<h2 className="text-xl font-semibold text-gray-900 flex items-center"> <h2 className="text-xl font-semibold text-gray-900 flex items-center">
<TrophyIcon className="h-6 w-6 text-[#8D6B1D] mr-2" /> <TrophyIcon className="h-6 w-6 text-[#8D6B1D] mr-2" />{t('autofix.k5c598bc0')}</h2>
Trending Groups <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" />
</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" />
</button> </button>
</div> </div>
@ -160,9 +156,7 @@ export default function CommunityPage() {
<p className="text-xs text-gray-500">{group.members} members</p> <p className="text-xs text-gray-500">{group.members} members</p>
</div> </div>
</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"> <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>
Join Group
</button>
</div> </div>
))} ))}
</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="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<div className="flex items-center justify-between mb-6"> <div className="flex items-center justify-between mb-6">
<h2 className="text-xl font-semibold text-gray-900 flex items-center"> <h2 className="text-xl font-semibold text-gray-900 flex items-center">
<ChatBubbleLeftRightIcon className="h-6 w-6 text-[#8D6B1D] mr-2" /> <ChatBubbleLeftRightIcon className="h-6 w-6 text-[#8D6B1D] mr-2" />{t('autofix.k70bcafbd')}</h2>
Recent Discussions <button className="text-[#8D6B1D] hover:text-[#7A5E1A] text-sm font-medium">{t('autofix.k9c3db145')}</button>
</h2>
<button className="text-[#8D6B1D] hover:text-[#7A5E1A] text-sm font-medium">
Start Discussion
</button>
</div> </div>
<div className="space-y-6"> <div className="space-y-6">
@ -220,41 +210,35 @@ export default function CommunityPage() {
<div className="space-y-6"> <div className="space-y-6">
{/* Quick Actions */} {/* Quick Actions */}
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6"> <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"> <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"> <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" /> <PlusIcon className="h-4 w-4 mr-2" />{t('autofix.k6a486e3e')}</button>
Create Group
</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"> <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" /> <ChatBubbleLeftRightIcon className="h-4 w-4 mr-2" />{t('autofix.k9c3db145')}</button>
Start Discussion
</button>
<button <button
onClick={() => router.push('/dashboard')} 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" 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"
> >{t('autofix.kd00443f2')}</button>
Go to Dashboard
</button>
</div> </div>
</div> </div>
{/* My Groups */} {/* My Groups */}
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6"> <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="space-y-3">
<div className="flex items-center space-x-3 p-2 rounded-lg hover:bg-gray-50 cursor-pointer"> <div className="flex items-center space-x-3 p-2 rounded-lg hover:bg-gray-50 cursor-pointer">
<div className="text-lg">🌱</div> <div className="text-lg">🌱</div>
<div> <div>
<p className="text-sm font-medium text-gray-900">Eco Warriors</p> <p className="text-sm font-medium text-gray-900">{t('autofix.k58424b1d')}</p>
<p className="text-xs text-gray-500">1,284 members</p> <p className="text-xs text-gray-500">{t('autofix.kaf787fe5')}</p>
</div> </div>
</div> </div>
<div className="flex items-center space-x-3 p-2 rounded-lg hover:bg-gray-50 cursor-pointer"> <div className="flex items-center space-x-3 p-2 rounded-lg hover:bg-gray-50 cursor-pointer">
<div className="text-lg"></div> <div className="text-lg"></div>
<div> <div>
<p className="text-sm font-medium text-gray-900">Zero Waste Living</p> <p className="text-sm font-medium text-gray-900">{t('autofix.k6de13000')}</p>
<p className="text-xs text-gray-500">892 members</p> <p className="text-xs text-gray-500">{t('autofix.k258c3515')}</p>
</div> </div>
</div> </div>
</div> </div>
@ -262,16 +246,14 @@ export default function CommunityPage() {
{/* Community Guidelines */} {/* Community Guidelines */}
<div className="bg-gradient-to-br from-[#8D6B1D]/10 to-[#C49225]/10 rounded-lg p-6 border border-[#8D6B1D]/20"> <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"> <ul className="text-sm text-gray-700 space-y-1">
<li> Be respectful and kind</li> <li>{t('autofix.kccf7593a')}</li>
<li> Stay on topic</li> <li>{t('autofix.kf69154f8')}</li>
<li> Share authentic experiences</li> <li>{t('autofix.k483aa95a')}</li>
<li> Help others learn and grow</li> <li>{t('autofix.k75d83433')}</li>
</ul> </ul>
<button className="text-xs text-[#8D6B1D] hover:underline mt-3"> <button className="text-xs text-[#8D6B1D] hover:underline mt-3">{t('autofix.k6aa2d843')}</button>
Read full guidelines
</button>
</div> </div>
</div> </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 { ChevronDownIcon } from '@heroicons/react/20/solid';
import { useTranslation } from '../i18n/useTranslation'; import { useTranslation } from '../i18n/useTranslation';
// Built-in language info (code → name + flag emoji) interface LangEntry { code: string; name: string; flag: string }
const BUILTIN_LANG_INFO: Record<string, { name: string; flag: string }> = {
const FALLBACK_LANG_INFO: Record<string, { name: string; flag: string }> = {
en: { name: 'English', flag: '🇬🇧' }, en: { name: 'English', flag: '🇬🇧' },
de: { name: 'Deutsch', flag: '🇩🇪' }, de: { name: 'Deutsch', flag: '🇩🇪' },
}; };
interface LangEntry { code: string; name: string; flag: string }
interface LanguageSwitcherProps { interface LanguageSwitcherProps {
variant?: 'light' | 'dark'; variant?: 'light' | 'dark';
} }
export default function LanguageSwitcher({ variant = 'light' }: LanguageSwitcherProps) { 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[] = languages.map((lang) => ({
const allLangs: LangEntry[] = [ code: lang.code,
...Object.entries(BUILTIN_LANG_INFO).map(([code, info]) => ({ code, ...info })), name: lang.name,
...customI18n.languages flag: FALLBACK_LANG_INFO[lang.code]?.flag ?? '🏳️',
.filter((l) => !BUILTIN_LANG_INFO[l.code]) }));
.map((l) => ({ code: l.code, name: l.name, flag: l.flag ?? '🏳️' })),
];
const activeLang: LangEntry = const activeLang: LangEntry =
allLangs.find((l) => l.code === language) ?? { code: language, name: language, flag: '🏳️' }; 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 Footer from './Footer';
import PageTransitionEffect from './animation/pageTransitionEffect'; import PageTransitionEffect from './animation/pageTransitionEffect';
import { useTranslation } from '../i18n/useTranslation';
// Utility to detect mobile devices // Utility to detect mobile devices
function isMobileDevice() { function isMobileDevice() {
if (typeof navigator === 'undefined') return false; if (typeof navigator === 'undefined') return false;
@ -27,6 +29,7 @@ export default function PageLayout({
className = 'bg-white text-gray-900', className = 'bg-white text-gray-900',
contentClassName = 'flex-1 relative z-10 w-full', contentClassName = 'flex-1 relative z-10 w-full',
}: PageLayoutProps) { }: PageLayoutProps) {
const { t } = useTranslation();
const isMobile = isMobileDevice(); const isMobile = isMobileDevice();
const [isLoggingOut, setIsLoggingOut] = useState(false); // NEW const [isLoggingOut, setIsLoggingOut] = useState(false); // NEW
const pathname = usePathname(); 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="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="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" /> <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>
</div> </div>
)} )}

View File

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

View File

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

View File

@ -1,5 +1,8 @@
'use client' 'use client'
import { useTranslation } from '../i18n/useTranslation';
import { Fragment, useState, useEffect } from 'react' import { Fragment, useState, useEffect } from 'react'
import { Dialog, Transition } from '@headlessui/react' import { Dialog, Transition } from '@headlessui/react'
import { import {
@ -30,6 +33,7 @@ interface UserDetailModalProps {
} }
export default function UserDetailModal({ isOpen, onClose, userId, onUserUpdated }: UserDetailModalProps) { export default function UserDetailModal({ isOpen, onClose, userId, onUserUpdated }: UserDetailModalProps) {
const { t } = useTranslation();
const [userDetails, setUserDetails] = useState<DetailedUserInfo | null>(null) const [userDetails, setUserDetails] = useState<DetailedUserInfo | null>(null)
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null) const [error, setError] = useState<string | null>(null)
@ -259,16 +263,14 @@ export default function UserDetailModal({ isOpen, onClose, userId, onUserUpdated
User Details User Details
{isEditing && ( {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"> <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" /> <PencilSquareIcon className="h-3 w-3" />{t('autofix.k73d110fa')}</span>
Edit Mode
</span>
)} )}
</Dialog.Title> </Dialog.Title>
{loading && ( {loading && (
<div className="flex items-center justify-center py-12"> <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" /> <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> </div>
)} )}
@ -284,28 +286,28 @@ export default function UserDetailModal({ isOpen, onClose, userId, onUserUpdated
<div className="bg-gray-50 rounded-lg p-4"> <div className="bg-gray-50 rounded-lg p-4">
<div className="flex items-center gap-3 mb-4"> <div className="flex items-center gap-3 mb-4">
<UserIcon className="h-5 w-5 text-gray-600" /> <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>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 text-sm"> <div className="grid grid-cols-1 sm:grid-cols-2 gap-4 text-sm">
<div> <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> <span className="ml-2 text-gray-600">{userDetails.user.email}</span>
</div> </div>
<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> <span className="ml-2 text-gray-600 capitalize">{userDetails.user.user_type}</span>
</div> </div>
<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> <span className="ml-2 text-gray-600 capitalize">{userDetails.user.role}</span>
</div> </div>
<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> <span className="ml-2 text-gray-600">{formatDate(userDetails.user.created_at)}</span>
</div> </div>
{userDetails.user.last_login_at && ( {userDetails.user.last_login_at && (
<div> <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> <span className="ml-2 text-gray-600">{formatDate(userDetails.user.last_login_at)}</span>
</div> </div>
)} )}
@ -317,7 +319,7 @@ export default function UserDetailModal({ isOpen, onClose, userId, onUserUpdated
<div className="bg-blue-50 rounded-lg p-4"> <div className="bg-blue-50 rounded-lg p-4">
<div className="flex items-center gap-3 mb-4"> <div className="flex items-center gap-3 mb-4">
<ShieldCheckIcon className="h-5 w-5 text-blue-600" /> <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>
<div className="grid grid-cols-2 sm:grid-cols-3 gap-4"> <div className="grid grid-cols-2 sm:grid-cols-3 gap-4">
<div className="flex items-center justify-between"> <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} /> <StatusBadge status={userDetails.userStatus.contract_signed === 1} />
</div> </div>
<div className="flex items-center justify-between"> <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 <StatusBadge
status={userDetails.userStatus.is_admin_verified === 1} status={userDetails.userStatus.is_admin_verified === 1}
verified={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" /> <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> </div>
{isEditing && editedProfile ? ( {isEditing && editedProfile ? (
@ -365,7 +367,7 @@ export default function UserDetailModal({ isOpen, onClose, userId, onUserUpdated
{userDetails.personalProfile && ( {userDetails.personalProfile && (
<> <>
<div> <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 <input
type="text" type="text"
value={editedProfile.first_name || ''} value={editedProfile.first_name || ''}
@ -374,7 +376,7 @@ export default function UserDetailModal({ isOpen, onClose, userId, onUserUpdated
/> />
</div> </div>
<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 <input
type="text" type="text"
value={editedProfile.last_name || ''} value={editedProfile.last_name || ''}
@ -392,7 +394,7 @@ export default function UserDetailModal({ isOpen, onClose, userId, onUserUpdated
/> />
</div> </div>
<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 <input
type="date" type="date"
value={editedProfile.date_of_birth || ''} value={editedProfile.date_of_birth || ''}
@ -419,7 +421,7 @@ export default function UserDetailModal({ isOpen, onClose, userId, onUserUpdated
/> />
</div> </div>
<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 <input
type="text" type="text"
value={editedProfile.zip_code || ''} value={editedProfile.zip_code || ''}
@ -442,7 +444,7 @@ export default function UserDetailModal({ isOpen, onClose, userId, onUserUpdated
{userDetails.companyProfile && ( {userDetails.companyProfile && (
<> <>
<div> <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 <input
type="text" type="text"
value={editedProfile.company_name || ''} value={editedProfile.company_name || ''}
@ -451,7 +453,7 @@ export default function UserDetailModal({ isOpen, onClose, userId, onUserUpdated
/> />
</div> </div>
<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 <input
type="text" type="text"
value={editedProfile.tax_id || ''} value={editedProfile.tax_id || ''}
@ -460,7 +462,7 @@ export default function UserDetailModal({ isOpen, onClose, userId, onUserUpdated
/> />
</div> </div>
<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 <input
type="text" type="text"
value={editedProfile.registration_number || ''} value={editedProfile.registration_number || ''}
@ -496,7 +498,7 @@ export default function UserDetailModal({ isOpen, onClose, userId, onUserUpdated
/> />
</div> </div>
<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 <input
type="text" type="text"
value={editedProfile.zip_code || ''} value={editedProfile.zip_code || ''}
@ -522,26 +524,26 @@ export default function UserDetailModal({ isOpen, onClose, userId, onUserUpdated
{userDetails.personalProfile && ( {userDetails.personalProfile && (
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 text-sm"> <div className="grid grid-cols-1 sm:grid-cols-2 gap-4 text-sm">
<div> <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"> <span className="ml-2 text-gray-600">
{userDetails.personalProfile.first_name} {userDetails.personalProfile.last_name} {userDetails.personalProfile.first_name} {userDetails.personalProfile.last_name}
</span> </span>
</div> </div>
{userDetails.personalProfile.phone && ( {userDetails.personalProfile.phone && (
<div> <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> <span className="ml-2 text-gray-600">{userDetails.personalProfile.phone}</span>
</div> </div>
)} )}
{userDetails.personalProfile.date_of_birth && ( {userDetails.personalProfile.date_of_birth && (
<div> <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> <span className="ml-2 text-gray-600">{formatDate(userDetails.personalProfile.date_of_birth)}</span>
</div> </div>
)} )}
{userDetails.personalProfile.address && ( {userDetails.personalProfile.address && (
<div className="sm:col-span-2"> <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"> <span className="ml-2 text-gray-600">
{userDetails.personalProfile.address}, {userDetails.personalProfile.zip_code} {userDetails.personalProfile.city}, {userDetails.personalProfile.country} {userDetails.personalProfile.address}, {userDetails.personalProfile.zip_code} {userDetails.personalProfile.city}, {userDetails.personalProfile.country}
</span> </span>
@ -553,30 +555,30 @@ export default function UserDetailModal({ isOpen, onClose, userId, onUserUpdated
{userDetails.companyProfile && ( {userDetails.companyProfile && (
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 text-sm"> <div className="grid grid-cols-1 sm:grid-cols-2 gap-4 text-sm">
<div> <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> <span className="ml-2 text-gray-600">{userDetails.companyProfile.company_name}</span>
</div> </div>
{userDetails.companyProfile.tax_id && ( {userDetails.companyProfile.tax_id && (
<div> <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> <span className="ml-2 text-gray-600">{userDetails.companyProfile.tax_id}</span>
</div> </div>
)} )}
{userDetails.companyProfile.registration_number && ( {userDetails.companyProfile.registration_number && (
<div> <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> <span className="ml-2 text-gray-600">{userDetails.companyProfile.registration_number}</span>
</div> </div>
)} )}
{userDetails.companyProfile.phone && ( {userDetails.companyProfile.phone && (
<div> <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> <span className="ml-2 text-gray-600">{userDetails.companyProfile.phone}</span>
</div> </div>
)} )}
{userDetails.companyProfile.address && ( {userDetails.companyProfile.address && (
<div className="sm:col-span-2"> <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"> <span className="ml-2 text-gray-600">
{userDetails.companyProfile.address}, {userDetails.companyProfile.zip_code} {userDetails.companyProfile.city}, {userDetails.companyProfile.country} {userDetails.companyProfile.address}, {userDetails.companyProfile.zip_code} {userDetails.companyProfile.city}, {userDetails.companyProfile.country}
</span> </span>
@ -600,7 +602,7 @@ export default function UserDetailModal({ isOpen, onClose, userId, onUserUpdated
{/* Regular Documents */} {/* Regular Documents */}
{userDetails.documents.length > 0 && ( {userDetails.documents.length > 0 && (
<div className="mb-4"> <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"> <div className="space-y-2">
{userDetails.documents.map((doc) => ( {userDetails.documents.map((doc) => (
<div key={doc.id} className="flex items-center justify-between bg-white p-2 rounded border"> <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 */} {/* ID Documents */}
{userDetails.idDocuments.length > 0 && ( {userDetails.idDocuments.length > 0 && (
<div> <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"> <div className="space-y-4">
{userDetails.idDocuments.map((idDoc) => ( {userDetails.idDocuments.map((idDoc) => (
<div key={idDoc.id} className="bg-white p-3 rounded border"> <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"> <div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
{idDoc.frontUrl && ( {idDoc.frontUrl && (
<div> <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 <img
src={idDoc.frontUrl} src={idDoc.frontUrl}
alt="ID Front" alt={t('autofix.k4a055849')}
className="max-w-full h-32 object-contain border rounded" className="max-w-full h-32 object-contain border rounded"
/> />
</div> </div>
)} )}
{idDoc.backUrl && ( {idDoc.backUrl && (
<div> <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 <img
src={idDoc.backUrl} src={idDoc.backUrl}
alt="ID Back" alt={t('autofix.kbc6a6543')}
className="max-w-full h-32 object-contain border rounded" className="max-w-full h-32 object-contain border rounded"
/> />
</div> </div>
@ -765,9 +767,7 @@ export default function UserDetailModal({ isOpen, onClose, userId, onUserUpdated
</> </>
) : ( ) : (
<> <>
<CheckCircleIcon className="h-4 w-4" /> <CheckCircleIcon className="h-4 w-4" />{t('autofix.k5a489751')}</>
Save Changes
</>
)} )}
</button> </button>
<button <button
@ -796,9 +796,7 @@ export default function UserDetailModal({ isOpen, onClose, userId, onUserUpdated
disabled={saving} 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" 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" /> <PencilSquareIcon className="h-4 w-4" />{t('autofix.k70972912')}</button>
Edit Profile
</button>
{userDetails?.userStatus && ( {userDetails?.userStatus && (
<button <button

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,10 +1,14 @@
'use client' 'use client'
import { useTranslation } from '../i18n/useTranslation';
import * as Headless from '@headlessui/react' import * as Headless from '@headlessui/react'
import React, { useState } from 'react' import React, { useState } from 'react'
import { NavbarItem } from './navbar' import { NavbarItem } from './navbar'
function OpenMenuIcon() { function OpenMenuIcon() {
const { t } = useTranslation();
return ( return (
<svg data-slot="icon" viewBox="0 0 20 20" aria-hidden="true"> <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" /> <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="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"> <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 /> <CloseMenuIcon />
</Headless.CloseButton> </Headless.CloseButton>
</div> </div>
@ -64,7 +68,7 @@ export function SidebarLayout({
{/* Navbar on mobile */} {/* Navbar on mobile */}
<header className="flex items-center px-4 lg:hidden"> <header className="flex items-center px-4 lg:hidden">
<div className="py-2.5"> <div className="py-2.5">
<NavbarItem onClick={() => setShowSidebar(true)} aria-label="Open navigation"> <NavbarItem onClick={() => setShowSidebar(true)} aria-label={t('autofix.k6af9037b')}>
<OpenMenuIcon /> <OpenMenuIcon />
</NavbarItem> </NavbarItem>
</div> </div>

View File

@ -1,10 +1,14 @@
'use client' 'use client'
import { useTranslation } from '../i18n/useTranslation';
import * as Headless from '@headlessui/react' import * as Headless from '@headlessui/react'
import React, { useState } from 'react' import React, { useState } from 'react'
import { NavbarItem } from './navbar' import { NavbarItem } from './navbar'
function OpenMenuIcon() { function OpenMenuIcon() {
const { t } = useTranslation();
return ( return (
<svg data-slot="icon" viewBox="0 0 20 20" aria-hidden="true"> <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" /> <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="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"> <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 /> <CloseMenuIcon />
</Headless.CloseButton> </Headless.CloseButton>
</div> </div>
@ -61,7 +65,7 @@ export function StackedLayout({
{/* Navbar */} {/* Navbar */}
<header className="flex items-center px-4"> <header className="flex items-center px-4">
<div className="py-2.5 lg:hidden"> <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 /> <OpenMenuIcon />
</NavbarItem> </NavbarItem>
</div> </div>

View File

@ -1,5 +1,8 @@
'use client' 'use client'
import { useTranslation } from '../../i18n/useTranslation';
import React, { import React, {
createContext, createContext,
useCallback, useCallback,
@ -45,6 +48,7 @@ let toastPortalRoot: ReturnType<typeof createRoot> | null = null
let toastPortalMounted = false let toastPortalMounted = false
function notifyToastListeners() { function notifyToastListeners() {
const { t } = useTranslation();
for (const listener of toastListeners) { for (const listener of toastListeners) {
listener(globalToasts) listener(globalToasts)
} }
@ -271,7 +275,7 @@ function ToastItem({ toast, onClose }: ToastItemProps) {
type="button" type="button"
onClick={onClose} 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" 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> </button>

View File

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

View File

@ -32,52 +32,3 @@ export function unflattenObject(flat: Record<string, string>): Record<string, an
return result; 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?', sessionDetectedMessage: 'Du bist bereits angemeldet. Möchtest du dich abmelden und ein neues Konto registrieren?',
sessionContinue: 'Zum Dashboard', sessionContinue: 'Zum Dashboard',
sessionLogout: 'Abmelden und registrieren', 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: { passwordReset: {
@ -398,16 +428,111 @@ export const de: Translations = {
referralManagement: { referralManagement: {
title: 'Empfehlungsverwaltung', title: 'Empfehlungsverwaltung',
subtitle: 'Verwalte deine Empfehlungslinks.', subtitle: 'Verwalte deine Empfehlungslinks.',
description: 'Erstelle und verwalte deine Empfehlungslinks. Verfolge die Performance auf einen Blick.',
createLink: 'Empfehlungslink erstellen', createLink: 'Empfehlungslink erstellen',
copyLink: 'Link kopieren', copyLink: 'Link kopieren',
copy: 'Kopieren',
copyMobile: 'Link kopieren',
copied: 'Kopiert',
copiedToClipboard: 'In die Zwischenablage kopiert!', copiedToClipboard: 'In die Zwischenablage kopiert!',
linkExpiry: 'Läuft ab', linkExpiry: 'Läuft ab',
noLinks: 'Noch keine Empfehlungslinks.', noLinks: 'Keine Empfehlungslinks gefunden.',
generating: 'Wird erstellt…', generating: 'Wird erstellt…',
generateLink: 'Link generieren',
usesRemaining: 'verbleibende Nutzungen', usesRemaining: 'verbleibende Nutzungen',
unlimited: 'Unbegrenzt', unlimited: 'Unbegrenzt',
never: 'Nie',
createSuccess: 'Empfehlungslink erfolgreich erstellt.', createSuccess: 'Empfehlungslink erfolgreich erstellt.',
createError: 'Empfehlungslink konnte nicht erstellt werden.', 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: { quickactionDashboard: {
@ -793,6 +918,7 @@ export const de: Translations = {
codeDuplicate: 'Sprache existiert bereits.', codeDuplicate: 'Sprache existiert bereits.',
codeRequired: 'Sprachcode ist erforderlich.', codeRequired: 'Sprachcode ist erforderlich.',
nameRequired: 'Sprachname ist erforderlich.', nameRequired: 'Sprachname ist erforderlich.',
wizardInputPlaceholder: 'Übersetzung eingeben',
}, },
contractManagement: { contractManagement: {
@ -926,6 +1052,646 @@ export const de: Translations = {
reasonNoActivePools: 'Keine aktiven System-Pools gefunden', 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 ──────────────────────────── // ─── Notifications / Toasts ────────────────────────────
toasts: { toasts: {
loginSuccess: 'Anmeldung erfolgreich', 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?', sessionDetectedMessage: 'You are already logged in. Do you want to log out and register a new account?',
sessionContinue: 'Continue to dashboard', sessionContinue: 'Continue to dashboard',
sessionLogout: 'Log out and register', 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: { passwordReset: {
@ -398,16 +428,111 @@ export const en: Translations = {
referralManagement: { referralManagement: {
title: 'Referral Management', title: 'Referral Management',
subtitle: 'Manage your referral links.', subtitle: 'Manage your referral links.',
description: 'Create and manage your referral links. Track performance at a glance.',
createLink: 'Create referral link', createLink: 'Create referral link',
copyLink: 'Copy link', copyLink: 'Copy link',
copy: 'Copy',
copyMobile: 'Copy link',
copied: 'Copied',
copiedToClipboard: 'Copied to clipboard!', copiedToClipboard: 'Copied to clipboard!',
linkExpiry: 'Expires', linkExpiry: 'Expires',
noLinks: 'No referral links yet.', noLinks: 'No referral links found.',
generating: 'Generating…', generating: 'Generating…',
generateLink: 'Generate Link',
usesRemaining: 'uses remaining', usesRemaining: 'uses remaining',
unlimited: 'Unlimited', unlimited: 'Unlimited',
never: 'Never',
createSuccess: 'Referral link created successfully.', createSuccess: 'Referral link created successfully.',
createError: 'Could not create referral link.', 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: { quickactionDashboard: {
@ -793,6 +918,7 @@ export const en: Translations = {
codeDuplicate: 'Language already exists.', codeDuplicate: 'Language already exists.',
codeRequired: 'Language code is required.', codeRequired: 'Language code is required.',
nameRequired: 'Language name is required.', nameRequired: 'Language name is required.',
wizardInputPlaceholder: 'Enter translated text',
}, },
contractManagement: { contractManagement: {
@ -926,6 +1052,646 @@ export const en: Translations = {
reasonNoActivePools: 'No active system pools found', 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 ──────────────────────────── // ─── Notifications / Toasts ────────────────────────────
toasts: { toasts: {
loginSuccess: 'Login successful', loginSuccess: 'Login successful',

View File

@ -145,6 +145,36 @@ export interface Translations {
sessionDetectedMessage: string; sessionDetectedMessage: string;
sessionContinue: string; sessionContinue: string;
sessionLogout: 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: { passwordReset: {
@ -368,16 +398,111 @@ export interface Translations {
referralManagement: { referralManagement: {
title: string; title: string;
subtitle: string; subtitle: string;
description: string;
createLink: string; createLink: string;
copyLink: string; copyLink: string;
copy: string;
copyMobile: string;
copied: string;
copiedToClipboard: string; copiedToClipboard: string;
linkExpiry: string; linkExpiry: string;
noLinks: string; noLinks: string;
generating: string; generating: string;
generateLink: string;
usesRemaining: string; usesRemaining: string;
unlimited: string; unlimited: string;
never: string;
createSuccess: string; createSuccess: string;
createError: 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: { quickactionDashboard: {
@ -759,6 +884,7 @@ export interface Translations {
codeDuplicate: string; codeDuplicate: string;
codeRequired: string; codeRequired: string;
nameRequired: string; nameRequired: string;
wizardInputPlaceholder: string;
}; };
contractManagement: { contractManagement: {
@ -892,6 +1018,8 @@ export interface Translations {
reasonNoActivePools: string; reasonNoActivePools: string;
}; };
autofix: Record<string, string>;
// ─── Notifications / Toasts ──────────────────────────── // ─── Notifications / Toasts ────────────────────────────
toasts: { toasts: {
loginSuccess: string; loginSuccess: string;

View File

@ -1,22 +1,47 @@
'use client'; 'use client';
import { createContext, useContext, useState, useEffect, useCallback, ReactNode } from 'react'; 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 { en } from './translations/en';
import { de } from './translations/de'; 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 // 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 { interface I18nContextType {
language: string; language: string;
setLanguage: (lang: string) => void; setLanguage: (lang: string) => void;
t: (key: string) => string; t: (key: string) => string;
customI18n: CustomI18nData; languages: LanguageEntry[];
reloadCustomI18n: () => void; reloadTranslations: () => Promise<void>;
} }
const I18nContext = createContext<I18nContextType | undefined>(undefined); const I18nContext = createContext<I18nContextType | undefined>(undefined);
@ -27,38 +52,84 @@ interface I18nProviderProps {
export function I18nProvider({ children }: I18nProviderProps) { export function I18nProvider({ children }: I18nProviderProps) {
const [language, setLanguage] = useState<string>(DEFAULT_LANGUAGE); 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(() => { const reloadTranslations = useCallback(async () => {
setCustomI18n(loadCustomI18n()); 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(() => { useEffect(() => {
reloadCustomI18n(); void reloadTranslations();
}, [reloadCustomI18n]); }, [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 => { const t = useCallback((key: string): string => {
// 1. Check custom translation overrides for this language // 1. Check translation loaded from translation files API.
const customOverride = customI18n.translations[language]?.[key]; const fileValue = translationFiles.translations[language]?.[key];
if (customOverride !== undefined && customOverride !== '') return customOverride; 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]; const builtIn = builtInTranslations[language];
if (builtIn) { if (builtIn) {
const keys = key.split('.'); const value = getNestedValue(builtIn, key);
let value: any = builtIn; if (value !== null) return value;
for (const k of keys) {
value = value?.[k];
}
if (typeof value === 'string') return value;
} }
// 3. Fallback to English (flat map) // 3. Fallback to English.
return enFlat[key] ?? key; return enFlat[key] ?? key;
}, [language, customI18n]); }, [language, translationFiles.translations]);
return ( return (
<I18nContext.Provider value={{ language, setLanguage, t, customI18n, reloadCustomI18n }}> <I18nContext.Provider
value={{
language,
setLanguage,
t,
languages: translationFiles.languages,
reloadTranslations,
}}
>
{children} {children}
</I18nContext.Provider> </I18nContext.Provider>
); );
@ -81,10 +152,3 @@ export function getAllTranslationKeys(): string[] {
export function getEnglishValue(key: string): string { export function getEnglishValue(key: string): string {
return enFlat[key] ?? key; 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' 'use client'
import { useTranslation } from '../../i18n/useTranslation';
import { useState, useEffect } from 'react' import { useState, useEffect } from 'react'
import { useRouter } from 'next/navigation' import { useRouter } from 'next/navigation'
import { EyeIcon, EyeSlashIcon } from '@heroicons/react/24/outline' 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)' const GLASS_BG = 'rgba(255,255,255,0.55)'
export default function LoginForm() { export default function LoginForm() {
const { t } = useTranslation();
const [showPassword, setShowPassword] = useState(false) const [showPassword, setShowPassword] = useState(false)
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
email: '', email: '',
@ -158,12 +162,8 @@ export default function LoginForm() {
> >
{/* Title + Subtitle (restored) */} {/* Title + Subtitle (restored) */}
<div className="mb-6 text-center"> <div className="mb-6 text-center">
<h1 className="text-2xl md:text-3xl font-extrabold tracking-tight text-[#0F172A] drop-shadow-sm"> <h1 className="text-2xl md:text-3xl font-extrabold tracking-tight text-[#0F172A] drop-shadow-sm">{t('autofix.k3d01de91')}</h1>
PROFIT PLANET <p className="mt-1 text-sm md:text-base text-slate-700/90">{t('autofix.k00394342')}</p>
</h1>
<p className="mt-1 text-sm md:text-base text-slate-700/90">
Welcome back! Log in to continue.
</p>
</div> </div>
<form <form
@ -182,9 +182,7 @@ export default function LoginForm() {
fontSize: isMobile ? '0.875rem' : isTablet ? '0.9rem' : undefined, fontSize: isMobile ? '0.875rem' : isTablet ? '0.9rem' : undefined,
marginBottom: isMobile ? '0.25rem' : undefined, marginBottom: isMobile ? '0.25rem' : undefined,
}} }}
> >{t('autofix.keccee79f')}</label>
Email address
</label>
<input <input
id="email" id="email"
name="email" name="email"
@ -197,7 +195,7 @@ export default function LoginForm() {
fontSize: isMobile ? '0.95rem' : isTablet ? '1rem' : '1rem', fontSize: isMobile ? '0.95rem' : isTablet ? '1rem' : '1rem',
padding: isMobile ? '0.5rem 0.75rem' : isTablet ? '0.6rem 0.875rem' : '0.7rem 1rem', padding: isMobile ? '0.5rem 0.75rem' : isTablet ? '0.6rem 0.875rem' : '0.7rem 1rem',
}} }}
placeholder="you@example.com" placeholder={t('autofix.k8dda5201')}
required required
/> />
</div> </div>
@ -231,7 +229,7 @@ export default function LoginForm() {
? '0.6rem 2.75rem 0.6rem 0.875rem' ? '0.6rem 2.75rem 0.6rem 0.875rem'
: '0.7rem 3rem 0.7rem 1rem', : '0.7rem 3rem 0.7rem 1rem',
}} }}
placeholder="Your password" placeholder={t('autofix.k6d85810b')}
required required
/> />
<button <button
@ -272,9 +270,7 @@ export default function LoginForm() {
> >
{loading ? ( {loading ? (
<div className="flex items-center justify-center gap-2"> <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> <div className="animate-spin rounded-full h-4 w-4 border-b-2 border-current"></div>{t('autofix.k2786bc5f')}</div>
Signing in...
</div>
) : ( ) : (
'Sign in' 'Sign in'
)} )}
@ -287,9 +283,7 @@ export default function LoginForm() {
type="button" type="button"
className="text-[#8D6B1D] hover:text-[#7A5E1A] hover:underline text-sm font-medium transition-colors" className="text-[#8D6B1D] hover:text-[#7A5E1A] hover:underline text-sm font-medium transition-colors"
onClick={() => router.push("/password-reset")} onClick={() => router.push("/password-reset")}
> >{t('autofix.kc3d181e2')}</button>
Forgot password?
</button>
</div> </div>
</form> </form>
</div> </div>

View File

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

View File

@ -1,5 +1,8 @@
'use client' 'use client'
import { useTranslation } from '../i18n/useTranslation';
import { useState, useEffect } from 'react' import { useState, useEffect } from 'react'
import { useRouter } from 'next/navigation' import { useRouter } from 'next/navigation'
import LoginForm from './components/LoginForm' import LoginForm from './components/LoginForm'
@ -12,6 +15,7 @@ import BlueBlurryBackground from '../components/background/blueblurry' // NEW
import CurvedLoop from '../components/curvedLoop' import CurvedLoop from '../components/curvedLoop'
export default function LoginPage() { export default function LoginPage() {
const { t } = useTranslation();
const [hasHydrated, setHasHydrated] = useState(false) const [hasHydrated, setHasHydrated] = useState(false)
const [isMobile, setIsMobile] = useState(false) const [isMobile, setIsMobile] = useState(false)
const router = useRouter() const router = useRouter()
@ -50,7 +54,7 @@ export default function LoginPage() {
<div className="min-h-screen flex items-center justify-center"> <div className="min-h-screen flex items-center justify-center">
<div className="text-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> <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>
</div> </div>
</PageLayout> </PageLayout>

View File

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

View File

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

View File

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

View File

@ -15,7 +15,10 @@ import {
import type { ComponentType, SVGProps } from 'react'; import type { ComponentType, SVGProps } from 'react';
import type { DashboardPlatformIconName } from './utils/dashboardPlatforms'; import type { DashboardPlatformIconName } from './utils/dashboardPlatforms';
import { useTranslation } from './i18n/useTranslation';
export default function HomePage() { export default function HomePage() {
const { t } = useTranslation();
const containerRef = useRef<HTMLDivElement | null>(null); const containerRef = useRef<HTMLDivElement | null>(null);
const [isMobile, setIsMobile] = useState(() => { const [isMobile, setIsMobile] = useState(() => {
if (typeof window === 'undefined') return false; 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"> <div className="flex flex-col sm:flex-row sm:items-center gap-5 sm:gap-6">
<img <img
src="/images/logos/PP_Logo_BW_round.png" 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" className="h-16 w-16 sm:h-20 sm:w-20 object-contain"
/> />
<div className="min-w-0 flex-1"> <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"> <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 Welcome
</div> </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"> <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>
Profit Planet <p className="mt-3 text-sm sm:text-base text-gray-700">{t('autofix.kde5c689e')}</p>
</h1>
<p className="mt-3 text-sm sm:text-base text-gray-700">
Pick a platform to continue.
</p>
</div> </div>
</div> </div>
</div> </div>
@ -106,15 +105,13 @@ export default function HomePage() {
<div className="flex items-start justify-between gap-4"> <div className="flex items-start justify-between gap-4">
<div> <div>
<h2 className="text-xl sm:text-2xl font-bold text-gray-900">Platforms</h2> <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> </div>
<div className="mt-6"> <div className="mt-6">
{loading && ( {loading && (
<div className="rounded-2xl border border-gray-200 bg-white p-6 text-center text-sm text-gray-600"> <div className="rounded-2xl border border-gray-200 bg-white p-6 text-center text-sm text-gray-600">{t('autofix.k832387c5')}</div>
Loading
</div>
)} )}
{error && ( {error && (
@ -185,9 +182,7 @@ export default function HomePage() {
})} })}
{platforms.length === 0 && ( {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"> <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>
No platforms available.
</div>
)} )}
</div> </div>
)} )}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,10 +1,16 @@
'use client';
import { useTranslation } from '../../i18n/useTranslation';
import React from 'react'; import React from 'react';
export default function ProfileCompletion({ profileComplete }: { profileComplete: number }) { export default function ProfileCompletion({ profileComplete }: { profileComplete: number }) {
const { t } = useTranslation();
return ( 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="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"> <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]"> <span className="text-sm font-medium text-[#8D6B1D]">
{profileComplete}% {profileComplete}%
</span> </span>
@ -15,9 +21,7 @@ export default function ProfileCompletion({ profileComplete }: { profileComplete
style={{ width: `${profileComplete}%` }} style={{ width: `${profileComplete}%` }}
></div> ></div>
</div> </div>
<p className="text-sm text-gray-600 mt-2"> <p className="text-sm text-gray-600 mt-2">{t('autofix.k772cc77b')}</p>
Complete your profile to unlock all features
</p>
</div> </div>
); );
} }

View File

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

View File

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

View File

@ -1,5 +1,8 @@
'use client' 'use client'
import { useTranslation } from '../../i18n/useTranslation';
import React, { useEffect } from 'react' import React, { useEffect } from 'react'
import { useRouter } from 'next/navigation' import { useRouter } from 'next/navigation'
import useAuthStore from '../../store/authStore' import useAuthStore from '../../store/authStore'
@ -55,6 +58,7 @@ const formatMoney = (value?: string | number | null, currency?: string | null) =
} }
export default function ProfileSubscriptionsPage() { export default function ProfileSubscriptionsPage() {
const { t } = useTranslation();
const router = useRouter() const router = useRouter()
const user = useAuthStore(state => state.user) const user = useAuthStore(state => state.user)
const isAuthReady = useAuthStore(state => state.isAuthReady) 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="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 className="flex items-center justify-between gap-3 flex-wrap">
<div> <div>
<h1 className="text-3xl font-bold text-gray-900">My Subscriptions</h1> <h1 className="text-3xl font-bold text-gray-900">{t('autofix.k744fda01')}</h1>
<p className="text-gray-600 mt-2">Select any subscription to view details and included items.</p> <p className="text-gray-600 mt-2">{t('autofix.kbf7bde57')}</p>
</div> </div>
<button <button
onClick={() => router.push('/profile')} onClick={() => router.push('/profile')}
className="rounded-md border border-gray-300 px-4 py-2 text-sm text-gray-700 hover:bg-gray-50" className="rounded-md border border-gray-300 px-4 py-2 text-sm text-gray-700 hover:bg-gray-50"
> >{t('autofix.kf2a1257e')}</button>
Back to profile
</button>
</div> </div>
{loading ? ( {loading ? (
<div className="rounded-md border border-white/60 bg-white/70 backdrop-blur-md p-4 text-sm text-gray-600 shadow-lg"> <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>
Loading subscriptions
</div>
) : error ? ( ) : error ? (
<div className="rounded-md border border-red-200 bg-red-50/80 backdrop-blur-md p-4 text-sm text-red-700"> <div className="rounded-md border border-red-200 bg-red-50/80 backdrop-blur-md p-4 text-sm text-red-700">
{error} {error}
</div> </div>
) : subscriptions.length === 0 ? ( ) : 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"> <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>
You dont have any subscriptions yet.
</div>
) : ( ) : (
<div className="space-y-6"> <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"> <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"> <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> <p className="text-xs text-gray-600">{subscriptions.length} total</p>
</div> </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"> <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 className="flex items-start justify-between gap-3 flex-wrap">
<div> <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> <p className="text-sm text-gray-600 mt-1">{selectedAbo.name || 'Coffee Subscription'}</p>
</div> </div>
<span <span
@ -345,7 +343,7 @@ export default function ProfileSubscriptionsPage() {
</button> </button>
)} )}
{(status === 'finished' || status === 'cancelled') && ( {(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> </div>
{statusError && ( {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="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"> <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> <p className="font-medium text-gray-900">{selectedAbo.id}</p>
</div> </div>
<div className="rounded-md bg-white/80 border border-gray-200 px-3 py-2"> <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> <p className="font-medium text-gray-900">{formatDate(selectedAbo.startedAt || selectedAbo.createdAt)}</p>
</div> </div>
<div className="rounded-md bg-white/80 border border-gray-200 px-3 py-2"> <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> <p className="font-medium text-gray-900">{formatDate(selectedAbo.nextBillingAt)}</p>
</div> </div>
</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"> <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 className="flex items-center justify-between gap-3 flex-wrap">
<div> <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-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> </div>
{!editingContent && canChangeContent && ( {!editingContent && canChangeContent && (
<button <button
onClick={startEditingContent} onClick={startEditingContent}
className="rounded-md border border-gray-300 px-3 py-1.5 text-xs text-gray-700 hover:bg-gray-50" className="rounded-md border border-gray-300 px-3 py-1.5 text-xs text-gray-700 hover:bg-gray-50"
> >{t('autofix.k9bc83f50')}</button>
Change coffees for next month
</button>
)} )}
</div> </div>
{!canChangeContent && ( {!canChangeContent && (
<p className="mt-3 text-xs text-gray-600"> <p className="mt-3 text-xs text-gray-600">{t('autofix.kcc15636b')}</p>
Coffee content can only be changed while a subscription is issued, ongoing, or paused.
</p>
)} )}
{includedItems.length === 0 ? ( {includedItems.length === 0 ? (
<div className="mt-4 rounded-md bg-white/80 border border-gray-200 p-3 text-sm text-gray-600"> <div className="mt-4 rounded-md bg-white/80 border border-gray-200 p-3 text-sm text-gray-600">{t('autofix.k9772afa4')}</div>
No included items were returned for this subscription.
</div>
) : ( ) : (
<div className="mt-4 grid grid-cols-1 md:grid-cols-2 gap-3"> <div className="mt-4 grid grid-cols-1 md:grid-cols-2 gap-3">
{includedItems.map((item, index) => { {includedItems.map((item, index) => {
@ -438,12 +430,12 @@ export default function ProfileSubscriptionsPage() {
{editingContent && canChangeContent && ( {editingContent && canChangeContent && (
<div className="mt-4 rounded-md border border-gray-200 bg-white/90 p-4"> <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"> <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> <p className="text-xs text-gray-600">Selected packs: {draftTotalPacks} (minimum 6)</p>
</div> </div>
{coffeesLoading ? ( {coffeesLoading ? (
<p className="text-sm text-gray-600">Loading available coffees</p> <p className="text-sm text-gray-600">{t('autofix.kc813a103')}</p>
) : coffeesError ? ( ) : coffeesError ? (
<p className="text-sm text-red-600">{coffeesError}</p> <p className="text-sm text-red-600">{coffeesError}</p>
) : ( ) : (

View File

@ -62,7 +62,7 @@ const init: CompanyProfileData = {
function ModernSelect({ function ModernSelect({
label, label,
placeholder = 'Select…', placeholder={t('autofix.ka5bf342b')},
value, value,
onChange, onChange,
options, options,
@ -145,7 +145,7 @@ function ModernSelect({
<input <input
value={query} value={query}
onChange={e => setQuery(e.target.value)} 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 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" focus:outline-none focus:ring-2 focus:ring-[#8D6B1D] focus:border-transparent"
autoFocus autoFocus
@ -153,7 +153,7 @@ function ModernSelect({
</div> </div>
<div className="max-h-[42vh] overflow-auto p-1"> <div className="max-h-[42vh] overflow-auto p-1">
{filtered.length === 0 ? ( {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 => { filtered.map(o => {
const active = o.value === value const active = o.value === value
@ -554,7 +554,7 @@ export default function CompanyAdditionalInformationPage() {
value={form.companyPhone} value={form.companyPhone}
onChange={handleChange} onChange={handleChange}
onInput={handlePhoneInput} onInput={handlePhoneInput}
placeholder="e.g. +43 1 234567" placeholder={t('autofix.k8eaa7b3b')}
ref={companyPhoneRef} ref={companyPhoneRef}
required required
/> />
@ -580,7 +580,7 @@ export default function CompanyAdditionalInformationPage() {
value={form.contactPersonPhone} value={form.contactPersonPhone}
onChange={handleChange} onChange={handleChange}
onInput={handlePhoneInput} onInput={handlePhoneInput}
placeholder="e.g. +43 676 1234567" placeholder={t('autofix.k9f56d4ac')}
ref={contactPhoneRef} ref={contactPhoneRef}
required required
/> />
@ -593,7 +593,7 @@ export default function CompanyAdditionalInformationPage() {
name="registrationNumber" name="registrationNumber"
value={form.registrationNumber} value={form.registrationNumber}
onChange={handleChange} 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" 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> </div>
@ -605,7 +605,7 @@ export default function CompanyAdditionalInformationPage() {
name="uidNumber" name="uidNumber"
value={form.uidNumber} value={form.uidNumber}
onChange={handleChange} 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" 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> </div>
@ -673,7 +673,7 @@ export default function CompanyAdditionalInformationPage() {
name="accountHolder" name="accountHolder"
value={form.accountHolder} value={form.accountHolder}
onChange={handleChange} 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" 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 required
/> />
@ -686,7 +686,7 @@ export default function CompanyAdditionalInformationPage() {
name="iban" name="iban"
value={form.iban} value={form.iban}
onChange={handleChange} 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" 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 required
/> />
@ -735,7 +735,7 @@ export default function CompanyAdditionalInformationPage() {
name="emergencyName" name="emergencyName"
value={form.emergencyName} value={form.emergencyName}
onChange={handleChange} 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" 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> </div>

View File

@ -80,7 +80,7 @@ type SelectOption = { value: string; label: string }
function ModernSelect({ function ModernSelect({
label, label,
placeholder = 'Select…', placeholder={t('autofix.ka5bf342b')},
searchPlaceholder = 'Search…', searchPlaceholder = 'Search…',
noResults = 'No results', noResults = 'No results',
value, value,
@ -651,7 +651,7 @@ export default function PersonalAdditionalInformationPage() {
value={form.phone} value={form.phone}
onChange={handleChange} onChange={handleChange}
onInput={handlePhoneInput} onInput={handlePhoneInput}
placeholder="e.g. +43 676 1234567" placeholder={t('autofix.k9f56d4ac')}
ref={phoneRef} ref={phoneRef}
required required
/> />
@ -690,7 +690,7 @@ export default function PersonalAdditionalInformationPage() {
name="street" name="street"
value={form.street} value={form.street}
onChange={handleChange} 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" 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 required
/> />
@ -703,7 +703,7 @@ export default function PersonalAdditionalInformationPage() {
name="postalCode" name="postalCode"
value={form.postalCode} value={form.postalCode}
onChange={handleChange} 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" 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 required
/> />
@ -716,7 +716,7 @@ export default function PersonalAdditionalInformationPage() {
name="city" name="city"
value={form.city} value={form.city}
onChange={handleChange} 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" 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 required
/> />
@ -751,7 +751,7 @@ export default function PersonalAdditionalInformationPage() {
name="accountHolder" name="accountHolder"
value={form.accountHolder} value={form.accountHolder}
onChange={handleChange} 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" 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 required
/> />
@ -764,7 +764,7 @@ export default function PersonalAdditionalInformationPage() {
name="iban" name="iban"
value={form.iban} value={form.iban}
onChange={handleChange} 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" 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 required
/> />
@ -801,7 +801,7 @@ export default function PersonalAdditionalInformationPage() {
name="emergencyName" name="emergencyName"
value={form.emergencyName} value={form.emergencyName}
onChange={handleChange} 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" 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> </div>

View File

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

View File

@ -1,6 +1,7 @@
'use client' 'use client'
import ConfirmActionModal from '../../components/modals/ConfirmActionModal' import ConfirmActionModal from '../../components/modals/ConfirmActionModal'
import { useTranslation } from '../../i18n/useTranslation'
interface DeactivateReferralLinkModalProps { interface DeactivateReferralLinkModalProps {
open: boolean open: boolean
@ -19,20 +20,21 @@ export default function DeactivateReferralLinkModal({
onClose, onClose,
onConfirm, onConfirm,
}: DeactivateReferralLinkModalProps) { }: DeactivateReferralLinkModalProps) {
const { t } = useTranslation()
return ( return (
<ConfirmActionModal <ConfirmActionModal
open={open} open={open}
pending={pending} pending={pending}
intent="danger" intent="danger"
title="Deactivate referral link?" title={t('referralManagement.deactivateModalTitle')}
description="This will immediately deactivate the selected referral link so it can no longer be used." description={t('referralManagement.deactivateModalDescription')}
confirmText="Deactivate" confirmText={t('referralManagement.deactivate')}
onClose={onClose} onClose={onClose}
onConfirm={onConfirm} onConfirm={onConfirm}
extraContent={ extraContent={
linkPreview ? ( linkPreview ? (
<div> <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"> <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} {linkPreview}
</div> </div>

View File

@ -3,12 +3,14 @@
import React, { useMemo, useState } from 'react' import React, { useMemo, useState } from 'react'
import { ClipboardDocumentIcon } from '@heroicons/react/24/outline' import { ClipboardDocumentIcon } from '@heroicons/react/24/outline'
import { createReferralLink } from '../hooks/generateReferralLink' import { createReferralLink } from '../hooks/generateReferralLink'
import { useTranslation } from '../../i18n/useTranslation'
interface Props { interface Props {
onCreated?: () => void | Promise<void> onCreated?: () => void | Promise<void>
} }
export default function GenerateReferralLinkWidget({ onCreated }: Props) { export default function GenerateReferralLinkWidget({ onCreated }: Props) {
const { t } = useTranslation()
// Defaults: Unlimited + Never expires // Defaults: Unlimited + Never expires
const [maxUses, setMaxUses] = useState<string>('-1') const [maxUses, setMaxUses] = useState<string>('-1')
const [expiresInDays, setExpiresInDays] = useState<string>('-1') const [expiresInDays, setExpiresInDays] = useState<string>('-1')
@ -21,27 +23,27 @@ export default function GenerateReferralLinkWidget({ onCreated }: Props) {
const expiryOptions = useMemo( const expiryOptions = useMemo(
() => [ () => [
{ value: '1', label: '1 day' }, { value: '1', label: t('referralManagement.expiry1Day') },
{ value: '2', label: '2 days' }, { value: '2', label: t('referralManagement.expiry2Days') },
{ value: '3', label: '3 days' }, { value: '3', label: t('referralManagement.expiry3Days') },
{ value: '4', label: '4 days' }, { value: '4', label: t('referralManagement.expiry4Days') },
{ value: '5', label: '5 days' }, { value: '5', label: t('referralManagement.expiry5Days') },
{ value: '6', label: '6 days' }, { value: '6', label: t('referralManagement.expiry6Days') },
{ value: '7', label: '7 days' }, { value: '7', label: t('referralManagement.expiry7Days') },
{ value: '-1', label: 'Never expires' }, { value: '-1', label: t('referralManagement.expiryNever') },
], ],
[] [t]
) )
const maxUsesOptions = useMemo( const maxUsesOptions = useMemo(
() => [ () => [
{ value: '1', label: '1 use' }, { value: '1', label: t('referralManagement.maxUses1') },
{ value: '5', label: '5 uses' }, { value: '5', label: t('referralManagement.maxUses5') },
{ value: '10', label: '10 uses' }, { value: '10', label: t('referralManagement.maxUses10') },
{ value: '50', label: '50 uses' }, { value: '50', label: t('referralManagement.maxUses50') },
{ value: '-1', label: 'Unlimited' }, { value: '-1', label: t('referralManagement.maxUsesUnlimited') },
], ],
[] [t]
) )
// Handlers that enforce coupling // Handlers that enforce coupling
@ -117,10 +119,10 @@ export default function GenerateReferralLinkWidget({ onCreated }: Props) {
return ( return (
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6"> <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 className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div> <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 <select
value={maxUses} value={maxUses}
onChange={(e) => onChangeMaxUses(e.target.value)} 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> <option key={opt.value} value={opt.value}>{opt.label}</option>
))} ))}
</select> </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>
<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 <select
value={expiresInDays} value={expiresInDays}
onChange={(e) => onChangeExpires(e.target.value)} 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> <option key={opt.value} value={opt.value}>{opt.label}</option>
))} ))}
</select> </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>
</div> </div>
@ -156,7 +158,7 @@ export default function GenerateReferralLinkWidget({ onCreated }: Props) {
disabled={isGenerating} 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" 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> </button>
{generatedLink && ( {generatedLink && (
<div className="flex items-center gap-2"> <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" 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" /> <ClipboardDocumentIcon className="h-4 w-4" />
{isCopying ? 'Copied' : 'Copy'} {isCopying ? t('referralManagement.copied') : t('referralManagement.copy')}
</button> </button>
</div> </div>
)} )}

View File

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

View File

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

View File

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

View File

@ -2,6 +2,7 @@
import React, { useMemo, useState } from 'react' import React, { useMemo, useState } from 'react'
import { UsersIcon } from '@heroicons/react/24/outline' import { UsersIcon } from '@heroicons/react/24/outline'
import { useTranslation } from '../../i18n/useTranslation'
type UserType = 'personal' | 'company' type UserType = 'personal' | 'company'
type UserStatus = 'active' | 'inactive' | 'pending' | 'blocked' type UserStatus = 'active' | 'inactive' | 'pending' | 'blocked'
@ -59,6 +60,7 @@ function exportCsv(rows: RegisteredUser[]) {
} }
export default function RegisteredUserList({ users, loading }: Props) { export default function RegisteredUserList({ users, loading }: Props) {
const { t } = useTranslation()
// Normalize backend shape to local RegisteredUser shape // Normalize backend shape to local RegisteredUser shape
const normalizedUsers = useMemo<RegisteredUser[]>(() => { const normalizedUsers = useMemo<RegisteredUser[]>(() => {
if (!users || users.length === 0) return [] 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="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 className="p-6 border-b border-gray-100 flex items-start justify-between gap-4">
<div> <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"> <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"> <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" /> <UsersIcon className="h-4 w-4" />
TOTAL REGISTERED USER WITH YOUR REF LINK {t('referralManagement.totalRefBadge')}
</span> </span>
<span className="inline-flex items-center rounded-full bg-violet-600 text-white px-2 py-1 text-[11px] font-bold"> <span className="inline-flex items-center rounded-full bg-violet-600 text-white px-2 py-1 text-[11px] font-bold">
{totalRegistered} {totalRegistered}
</span> </span>
</div> </div>
<p className="text-sm text-gray-600 mt-2"> <p className="text-sm text-gray-600 mt-2">
Users who signed up using one of your referral links. {t('referralManagement.registeredUsersSubtitle')}
</p> </p>
<p className="text-xs text-gray-500 mt-2"> <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> </p>
</div> </div>
<button <button
onClick={resetAndOpen} 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" 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> </button>
</div> </div>
@ -158,11 +160,11 @@ export default function RegisteredUserList({ users, loading }: Props) {
<table className="min-w-full divide-y divide-gray-200"> <table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50"> <thead className="bg-gray-50">
<tr> <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">{t('referralManagement.colUser')}</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">{t('referralManagement.colEmail')}</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">{t('referralManagement.colType')}</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">{t('referralManagement.colRegistered')}</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.colStatus')}</th>
</tr> </tr>
</thead> </thead>
<tbody className="divide-y divide-gray-100 bg-white"> <tbody className="divide-y divide-gray-100 bg-white">
@ -175,7 +177,7 @@ export default function RegisteredUserList({ users, loading }: Props) {
) : rows.length === 0 ? ( ) : rows.length === 0 ? (
<tr> <tr>
<td className="px-6 py-6 text-sm text-gray-500" colSpan={5}> <td className="px-6 py-6 text-sm text-gray-500" colSpan={5}>
No registered users found. {t('referralManagement.noRegisteredUsers')}
</td> </td>
</tr> </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 text-gray-700">{u.email}</td>
<td className="px-6 py-4 text-sm"> <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)}`}> <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> </span>
</td> </td>
<td className="px-6 py-4 text-sm text-gray-700">{date}</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="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 className="px-6 py-4 border-b border-gray-200 flex items-center justify-between gap-3">
<div> <div>
<h3 className="text-lg font-semibold text-gray-900">All Registered Users via Your Referral</h3> <h3 className="text-lg font-semibold text-gray-900">{t('referralManagement.allRegisteredUsersTitle')}</h3>
<p className="text-xs text-gray-600">Search, filter, paginate, or export the full list.</p> <p className="text-xs text-gray-600">{t('referralManagement.allRegisteredUsersSubtitle')}</p>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<button <button
onClick={() => exportCsv(filtered)} 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" 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>
<button <button
onClick={() => setOpen(false)} 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" 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> </button>
</div> </div>
</div> </div>
@ -236,7 +238,7 @@ export default function RegisteredUserList({ users, loading }: Props) {
<input <input
value={query} value={query}
onChange={e => { setQuery(e.target.value); setPage(1) }} 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" 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 <select
@ -244,20 +246,20 @@ export default function RegisteredUserList({ users, loading }: Props) {
onChange={e => { setTypeFilter(e.target.value as any); setPage(1) }} 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" 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="all">{t('referralManagement.filterAllTypes')}</option>
<option value="personal">Personal</option> <option value="personal">{t('referralManagement.typePersonal')}</option>
<option value="company">Company</option> <option value="company">{t('referralManagement.typeCompany')}</option>
</select> </select>
<select <select
value={statusFilter} value={statusFilter}
onChange={e => { setStatusFilter(e.target.value as any); setPage(1) }} 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" 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="all">{t('referralManagement.filterAllStatus')}</option>
<option value="active">Active</option> <option value="active">{t('referralManagement.filterActive')}</option>
<option value="inactive">Inactive</option> <option value="inactive">{t('referralManagement.filterInactive')}</option>
<option value="pending">Pending</option> <option value="pending">{t('referralManagement.filterPending')}</option>
<option value="blocked">Blocked</option> <option value="blocked">{t('referralManagement.filterBlocked')}</option>
</select> </select>
</div> </div>
@ -265,18 +267,18 @@ export default function RegisteredUserList({ users, loading }: Props) {
<table className="min-w-full divide-y divide-gray-200"> <table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50"> <thead className="bg-gray-50">
<tr> <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">{t('referralManagement.colUser')}</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">{t('referralManagement.colEmail')}</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">{t('referralManagement.colType')}</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">{t('referralManagement.colRegistered')}</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.colStatus')}</th>
</tr> </tr>
</thead> </thead>
<tbody className="divide-y divide-gray-100 bg-white"> <tbody className="divide-y divide-gray-100 bg-white">
{pageRows.length === 0 ? ( {pageRows.length === 0 ? (
<tr> <tr>
<td className="px-6 py-6 text-sm text-gray-500" colSpan={5}> <td className="px-6 py-6 text-sm text-gray-500" colSpan={5}>
No users match your filters. {t('referralManagement.noUsersMatchFilters')}
</td> </td>
</tr> </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 text-gray-700">{u.email}</td>
<td className="px-6 py-3 text-sm"> <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)}`}> <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> </span>
</td> </td>
<td className="px-6 py-3 text-sm text-gray-700">{date}</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"> <div className="px-6 py-4 flex items-center justify-between gap-3">
<span className="text-xs text-gray-600"> <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> </span>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<button <button
@ -315,17 +317,17 @@ export default function RegisteredUserList({ users, loading }: Props) {
onClick={() => setPage(p => Math.max(1, p - 1))} 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" 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> </button>
<span className="text-sm text-gray-700"> <span className="text-sm text-gray-700">
Page {page} of {totalPages} {t('referralManagement.pageOf').replace('{page}', String(page)).replace('{total}', String(totalPages))}
</span> </span>
<button <button
disabled={page >= totalPages} disabled={page >= totalPages}
onClick={() => setPage(p => Math.min(totalPages, p + 1))} 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" 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> </button>
</div> </div>
</div> </div>

View File

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

View File

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

View File

@ -3,6 +3,7 @@
import { Fragment } from 'react' import { Fragment } from 'react'
import { Dialog, Transition } from '@headlessui/react' import { Dialog, Transition } from '@headlessui/react'
import { ExclamationTriangleIcon } from '@heroicons/react/24/outline' import { ExclamationTriangleIcon } from '@heroicons/react/24/outline'
import { useTranslation } from '../../i18n/useTranslation'
interface SessionDetectedModalProps { interface SessionDetectedModalProps {
open: boolean open: boolean
@ -16,8 +17,7 @@ export default function SessionDetectedModal({
onLogout, onLogout,
onCancel, onCancel,
inline = false inline = false
}: SessionDetectedModalProps) { }: SessionDetectedModalProps) { const { t } = useTranslation() // Make inline + non-inline consistent
// Make inline + non-inline consistent
if (!open) return null if (!open) return null
if (inline) { if (inline) {
@ -30,10 +30,10 @@ export default function SessionDetectedModal({
</div> </div>
<div> <div>
<h3 className="text-base font-semibold leading-6 text-[#0F172A]"> <h3 className="text-base font-semibold leading-6 text-[#0F172A]">
Active session detected {t('register.sessionDetectedTitle')}
</h3> </h3>
<p className="mt-2 text-sm text-[#4A4A4A]"> <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> </p>
<div className="mt-5 flex flex-col sm:flex-row gap-3 sm:justify-end"> <div className="mt-5 flex flex-col sm:flex-row gap-3 sm:justify-end">
<button <button
@ -41,14 +41,14 @@ export default function SessionDetectedModal({
onClick={onCancel} 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" 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>
<button <button
type="button" type="button"
onClick={onLogout} 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" 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> </button>
</div> </div>
</div> </div>
@ -98,11 +98,11 @@ export default function SessionDetectedModal({
as="h3" as="h3"
className="text-base font-semibold leading-6 text-[#0F172A]" className="text-base font-semibold leading-6 text-[#0F172A]"
> >
Active session detected {t('register.sessionDetectedTitle')}
</Dialog.Title> </Dialog.Title>
<div className="mt-2"> <div className="mt-2">
<p className="text-sm text-[#4A4A4A]"> <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> </p>
</div> </div>
</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" 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} onClick={onLogout}
> >
Log out and register {t('register.sessionLogout')}
</button> </button>
<button <button
type="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" 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} onClick={onCancel}
> >
Go to dashboard {t('register.goToDashboard')}
</button> </button>
</div> </div>
</Dialog.Panel> </Dialog.Panel>

View File

@ -2,6 +2,7 @@
import React from 'react' import React from 'react'
import { ExclamationTriangleIcon } from '@heroicons/react/24/outline' import { ExclamationTriangleIcon } from '@heroicons/react/24/outline'
import { useTranslation } from '../../i18n/useTranslation'
interface InvalidRefLinkModalProps { interface InvalidRefLinkModalProps {
open: boolean open: boolean
@ -18,6 +19,7 @@ export default function InvalidRefLinkModal({
onClose, onClose,
onGoHome onGoHome
}: InvalidRefLinkModalProps) { }: InvalidRefLinkModalProps) {
const { t } = useTranslation()
if (!open) return null if (!open) return null
const Content = ( const Content = (
@ -27,13 +29,13 @@ export default function InvalidRefLinkModal({
<ExclamationTriangleIcon className="h-6 w-6 text-red-600" /> <ExclamationTriangleIcon className="h-6 w-6 text-red-600" />
</div> </div>
<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"> <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> </p>
{token && ( {token && (
<p className="mt-2 text-xs text-slate-500"> <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> </p>
)} )}
<div className="mt-4 flex flex-wrap items-center gap-2"> <div className="mt-4 flex flex-wrap items-center gap-2">
@ -41,14 +43,14 @@ export default function InvalidRefLinkModal({
onClick={onGoHome} 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" 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> </button>
{onClose && ( {onClose && (
<button <button
onClick={onClose} 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" 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> </button>
)} )}
</div> </div>

View File

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

View File

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

View File

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

View File

@ -1,5 +1,8 @@
"use client" "use client"
import { useTranslation } from '../../i18n/useTranslation';
import { useState, useEffect } from 'react' import { useState, useEffect } from 'react'
import { notFound } from 'next/navigation' import { notFound } from 'next/navigation'
import { StarIcon } from '@heroicons/react/20/solid' import { StarIcon } from '@heroicons/react/20/solid'
@ -30,6 +33,7 @@ function classNames(...classes: (string | undefined | null | boolean)[]): string
} }
export default function VipShopPage() { export default function VipShopPage() {
const { t } = useTranslation();
const SHOW_SHOP = process.env.NEXT_PUBLIC_SHOW_SHOP === 'true' const SHOW_SHOP = process.env.NEXT_PUBLIC_SHOW_SHOP === 'true'
if (!SHOW_SHOP) notFound() if (!SHOW_SHOP) notFound()
@ -67,7 +71,7 @@ export default function VipShopPage() {
<div className="min-h-screen flex items-center justify-center"> <div className="min-h-screen flex items-center justify-center">
<div className="text-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> <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>
</div> </div>
</PageLayout> </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="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="relative mx-auto max-w-7xl px-4 sm:static sm:px-6 lg:px-8">
<div className="sm:max-w-lg"> <div className="sm:max-w-lg">
<h1 className="text-4xl font-bold tracking-tight text-gray-900 sm:text-6xl"> <h1 className="text-4xl font-bold tracking-tight text-gray-900 sm:text-6xl">{t('autofix.kf663ef67')}</h1>
Shop with an infinite variety of products <p className="mt-4 text-xl text-gray-500">{t('autofix.kdca959c3')}</p>
</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>
</div> </div>
<div> <div>
<div className="mt-10"> <div className="mt-10">
@ -124,9 +124,7 @@ export default function VipShopPage() {
</div> </div>
</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"> <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>
Shop Collection
</a>
</div> </div>
</div> </div>
</div> </div>
@ -136,10 +134,8 @@ export default function VipShopPage() {
<div className="bg-white"> <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="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"> <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> <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"> <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>
Browse all trending
<span aria-hidden="true"> &rarr;</span>
</a> </a>
</div> </div>
@ -159,9 +155,7 @@ export default function VipShopPage() {
</div> </div>
<div className="mt-6 sm:hidden"> <div className="mt-6 sm:hidden">
<a href="#" className="block text-sm font-semibold text-indigo-600 hover:text-indigo-500"> <a href="#" className="block text-sm font-semibold text-indigo-600 hover:text-indigo-500">{t('autofix.k2cd79a3d')}<span aria-hidden="true"> &rarr;</span>
Browse all favorites
<span aria-hidden="true"> &rarr;</span>
</a> </a>
</div> </div>
</div> </div>

View File

@ -3,25 +3,27 @@
import Link from 'next/link' import Link from 'next/link'
import PageLayout from '../components/PageLayout' import PageLayout from '../components/PageLayout'
import PageTransitionEffect from '../components/animation/pageTransitionEffect' import PageTransitionEffect from '../components/animation/pageTransitionEffect'
import { useTranslation } from '../i18n/useTranslation'
export default function SuspendedPage() { export default function SuspendedPage() {
const { t } = useTranslation()
return ( return (
<PageTransitionEffect> <PageTransitionEffect>
<PageLayout showFooter={true} className="bg-[#F5F5F0] text-slate-900"> <PageLayout showFooter={true} className="bg-[#F5F5F0] text-slate-900">
<div className="min-h-[70vh] flex items-center justify-center px-6"> <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="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> <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"> <p className="mt-3 text-slate-700">
Your account has been suspended. For more information, contact {t('suspended.message')}
{' '}<a className="text-[#8D6B1D] font-semibold" href="mailto:office@profit-planet.com">office@profit-planet.com</a>.
</p> </p>
<div className="mt-6"> <div className="mt-6">
<Link <Link
href="/login" 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" 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> </Link>
</div> </div>
</div> </div>

View File

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