updated frontend and auth backend service

This commit is contained in:
2025-07-31 17:20:49 +03:00
parent 0ce522d04a
commit 924b538559
55 changed files with 1711 additions and 286 deletions

View File

@@ -3,109 +3,60 @@ import Link from 'next/link';
import { useState } from 'react';
import { useTranslations } from 'next-intl';
import { z } from 'zod';
import { Eye, EyeOff, Lock, Mail, User } from "lucide-react";
const loginSchema = z.object({
email: z.string().email('Invalid email'),
password: z.string().min(6, 'Password too short')
});
type LoginFormData = z.infer<typeof loginSchema>;
const getRandomColor = () => {
const colors = ['bg-indigo-500', 'bg-purple-500', 'bg-pink-500', 'bg-blue-500', 'bg-teal-500'];
return colors[Math.floor(Math.random() * colors.length)];
};
import { Eye, EyeOff, Lock, Mail } from "lucide-react";
import { apiPostFetcher } from '@/lib/fetcher';
import { useRouter } from '@/i18n/routing';
export default function LoginPage() {
const t = useTranslations('Login');
const [formData, setFormData] = useState<LoginFormData>({
email: '',
password: ''
const loginSchema = z.object({
accessKey: z.string().email(t('emailWrong')),
password: z.string().min(6, t('passwordWrong')),
rememberMe: z.boolean().default(false),
});
const [errors, setErrors] = useState<Partial<LoginFormData>>({});
type LoginInterface = z.infer<typeof loginSchema>;
interface LoginFormErrors {
accessKey?: boolean;
password?: boolean;
rememberMe?: boolean;
}
const [errors, setErrors] = useState<LoginFormErrors>({});
const [showPassword, setShowPassword] = useState(false);
const [isAccordionOpen, setIsAccordionOpen] = useState(false);
const recentUser = {
name: 'Mika Lee',
initial: 'M',
color: getRandomColor()
};
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setFormData({ ...formData, [e.target.name]: e.target.value });
};
const handleSubmit = async (e: React.FormEvent) => {
const router = useRouter();
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
try {
loginSchema.parse(formData);
setErrors({});
console.log('Form submitted:', formData);
// Here you would typically call your authentication API
} catch (err) {
if (err instanceof z.ZodError) {
const fieldErrors: Partial<LoginFormData> = {};
Object.entries(err.flatten().fieldErrors).forEach(([key, value]) => {
if (Array.isArray(value) && value.length > 0) {
const fieldKey = key as keyof LoginFormData;
fieldErrors[fieldKey] = value[0];
}
});
setErrors(fieldErrors);
const form = e.currentTarget;
const formData = new FormData(form);
const loginData: LoginInterface = {
accessKey: formData.get('email') as string,
password: formData.get('password') as string,
rememberMe: formData.get('rememberMe') === 'on'
};
const result = loginSchema.safeParse(loginData);
if (!result.success) {
const fieldErrors: LoginFormErrors = {};
if (result.error.issues.some(issue => issue.path.includes('email'))) {
fieldErrors.accessKey = true;
}
if (result.error.issues.some(issue => issue.path.includes('password'))) {
fieldErrors.password = true;
}
setErrors(fieldErrors);
} else {
setErrors({})
console.log('Form submitted successfully:', loginData);
apiPostFetcher({ url: '/api/auth/login', body: loginData, isNoCache: true }).then((res) => {
if (res.success) {
console.log('Login successful, redirecting to select page');
router.push('/select');
}
}).catch((error) => { console.error('Login failed:', error) });
}
};
return (
<div className="min-h-screen bg-gradient-to-br from-indigo-50 via-white to-cyan-50 flex items-center justify-center p-2 sm:p-4">
<div className="w-full max-w-7xl mx-auto">
{/* Recent logins accordion */}
{/* <div className="card bg-white/90 backdrop-blur-sm shadow-xl border border-indigo-100 rounded-2xl w-full mb-4">
<div className="card-body p-4 sm:p-6">
<div
className="flex items-center justify-between cursor-pointer"
onClick={() => setIsAccordionOpen(!isAccordionOpen)}
>
<div className="flex items-center gap-2 sm:gap-3">
<div className="avatar placeholder">
<div className="w-8 h-8 sm:w-10 sm:h-10 rounded-xl bg-gradient-to-r from-indigo-500 to-purple-600 text-white flex items-center justify-center">
<User className="w-4 h-4 sm:w-5 sm:h-5" />
</div>
</div>
<div>
<h2 className="text-lg sm:text-xl font-bold text-gray-800">{t('recentLogins')}</h2>
</div>
</div>
<svg
className={`w-5 h-5 text-gray-500 transition-transform duration-300 ${isAccordionOpen ? 'rotate-180' : ''}`}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</div>
{isAccordionOpen && (
<div className="mt-4 pt-4 border-t border-gray-200">
<p className="text-xs sm:text-sm text-gray-500 mb-4">{t('clickPictureOrAdd')}</p>
<div className="grid grid-cols-2 sm:grid-cols-3 gap-2 sm:gap-3 w-full">
<div className="flex flex-col items-center cursor-pointer group">
<div className="w-full aspect-square mb-1 sm:mb-2 bg-gradient-to-br from-gray-100 to-gray-200 rounded-2xl flex items-center justify-center transition-all duration-300 group-hover:scale-105 group-hover:shadow-lg border-2 border-dashed border-indigo-200 group-hover:border-indigo-400">
<span className="text-xl sm:text-2xl text-indigo-400 group-hover:text-indigo-600 transition-colors">+</span>
</div>
<span className="text-xs sm:text-sm font-medium text-gray-600 group-hover:text-indigo-600 transition-colors">
{t('addAccount')}
</span>
</div>
</div>
</div>
)}
</div>
</div> */}
<div className="flex flex-col lg:flex-row gap-4 sm:gap-6 md:gap-8 items-center justify-center w-full min-h-[60vh] md:min-h-[70vh] lg:min-h-[80vh]">
{/* Left side - Login form (now takes full width) */}
<div className="card bg-white/90 backdrop-blur-sm shadow-xl border border-indigo-100 rounded-2xl w-full overflow-auto">
@@ -122,27 +73,25 @@ export default function LoginPage() {
</label>
<div className="relative">
<input
type="email"
type="text"
name="email"
value={formData.email}
onChange={handleChange}
className={`input input-bordered rounded-2xl text-black h-14 bg-white w-full pl-8 sm:pl-10 md:pl-12 py-3 sm:py-4 transition-all
duration-300 border-gray-200 focus:border-indigo-500 focus:ring-2 focus:ring-indigo-200 ${errors.email ? 'border-red-500 focus:border-red-500 focus:ring-red-200' : ''}`}
duration-300 border-gray-200 focus:border-indigo-500 focus:ring-2 focus:ring-indigo-200 ${errors.accessKey ? 'border-red-500 focus:border-red-500 focus:ring-red-200' : ''}`}
placeholder={t('email')}
style={{
WebkitBackgroundClip: 'text',
WebkitTextFillColor: 'black',
}}
// style={{
// WebkitBackgroundClip: 'text',
// WebkitTextFillColor: 'black',
// }}
/>
<Mail className="absolute left-2 sm:left-3 md:left-4 top-1/2 transform -translate-y-1/2 w-3 h-3 sm:w-4 sm:h-4 md:w-5 md:h-5 text-gray-400 pointer-events-none z-10" />
</div>
{errors.email && (
{errors.accessKey && (
<div className="label p-0 pt-1">
<span className="label-text-alt text-red-500 flex items-center gap-1 text-xs sm:text-sm">
<svg className="w-2.5 h-2.5 sm:w-3 sm:h-3 md:w-4 md:h-4" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clipRule="evenodd" />
</svg>
{errors.email}
{t('emailWrong')}
</span>
</div>
)}
@@ -156,14 +105,12 @@ export default function LoginPage() {
<input
type={showPassword ? 'text' : 'password'}
name="password"
value={formData.password}
onChange={handleChange}
className={`input input-bordered rounded-2xl text-black h-14 bg-white w-full pl-8 sm:pl-10 md:pl-12 pr-8 sm:pr-10 md:pr-12 py-3 sm:py-4 transition-all duration-300 border-gray-200 focus:border-indigo-500 focus:ring-2 focus:ring-indigo-200 ${errors.password ? 'border-red-500 focus:border-red-500 focus:ring-red-200' : ''}`}
placeholder="••••••••"
style={{
WebkitBackgroundClip: 'text',
WebkitTextFillColor: 'black',
}}
// style={{
// WebkitBackgroundClip: 'text',
// WebkitTextFillColor: 'black',
// }}
/>
<Lock className="absolute left-2 sm:left-3 md:left-4 top-1/2 transform -translate-y-1/2 w-3 h-3 sm:w-4 sm:h-4 md:w-5 md:h-5 text-gray-400 pointer-events-none z-10" />
<button
@@ -181,7 +128,7 @@ export default function LoginPage() {
<svg className="w-2.5 h-2.5 sm:w-3 sm:h-3 md:w-4 md:h-4" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clipRule="evenodd" />
</svg>
{errors.password}
{t('passwordWrong')}
</span>
</div>
)}
@@ -189,7 +136,7 @@ export default function LoginPage() {
<div className="flex items-center justify-between flex-wrap gap-2">
<label className="label cursor-pointer flex items-center gap-1.5 sm:gap-2 p-0">
<input type="checkbox" className="checkbox checkbox-primary checkbox-sm [--chkbg:theme(colors.indigo.500)] [--chkfg:theme(colors.white)] border-indigo-300" />
<input name="rememberMe" type="checkbox" className="checkbox checkbox-primary checkbox-sm [--chkbg:theme(colors.indigo.500)] [--chkfg:theme(colors.white)] border-indigo-300" />
<span className="label-text text-gray-700 text-xs sm:text-sm">{t('rememberMe')}</span>
</label>
<Link href="/forgot-password" className="text-indigo-600 hover:text-indigo-800 text-xs sm:text-sm font-medium transition-colors">

View File

@@ -1,21 +1,18 @@
'use server';
import LocaleSwitcherServer from '@/components/LocaleSwitcherServer';
import LoginPage from './LoginPage';
import FromFigma from './fromFigma';
import { Locale } from 'next-intl';
import { checkAccessOnLoginPage } from '@/app/api/guards';
type Props = {
params: Promise<{ locale: string }>;
};
export default async function PageLogin({ params }: Props) {
export default async function PageLogin({ params }: { params: Promise<{ locale: string }> }) {
const { locale } = await params;
await checkAccessOnLoginPage(locale as Locale);
return (
<div>
<div className='absolute top-2 right-2'>
<LocaleSwitcherServer locale={locale} pathname="/login" />
</div>
<LoginPage />
{/* <FromFigma /> */}
</div>
);
}

View File

@@ -0,0 +1,282 @@
'use client';
import { useEffect, useState } from 'react';
import { useTranslations } from 'next-intl';
import { useRouter } from 'next/navigation';
import { apiGetFetcher } from '@/lib/fetcher';
export default function PageSelect() {
const t = useTranslations('Select');
const router = useRouter();
const [selectionList, setSelectionList] = useState<{ type: string, list: any[] } | null>(null);
const [selectedOption, setSelectedOption] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
const fetchSelectionList = async () => {
setIsLoading(true);
try {
apiGetFetcher({ url: '/api/auth/selections', isNoCache: true }).then((res) => {
if (res.success) {
if (res.data && typeof res.data === 'object' && 'type' in res.data && 'list' in res.data) {
setSelectionList(res.data as { type: string, list: any[] });
}
}
})
} catch (error) {
console.error('Error fetching selection list:', error);
} finally {
setIsLoading(false);
}
};
fetchSelectionList();
}, []);
const handleSelection = (id: string) => { setSelectedOption(id) };
const handleContinue = async () => {
if (!selectedOption) return;
setIsLoading(true);
try {
console.log('Selected option:', selectedOption);
const payload = { uuid: selectedOption };
const response = await fetch('/api/auth/select', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), });
const result = await response.json();
if (response.ok && result.status === 200) {
console.log('Selection successful, redirecting to venue page');
router.push('/venue');
} else {
console.error('Selection failed:', result.message);
alert(`Selection failed: ${result.message || 'Unknown error'}`);
}
} catch (error) {
console.error('Error submitting selection:', error);
alert('An error occurred while submitting your selection. Please try again.');
} finally { setIsLoading(false) }
};
return (
<div className="min-h-screen bg-gradient-to-br from-indigo-50 via-white to-cyan-50 p-4 sm:p-6 md:p-8">
<div className="max-w-6xl mx-auto w-full h-full flex flex-col">
<div className="text-center mb-8 sm:mb-10 mt-4 sm:mt-6">
<h1 className="text-2xl sm:text-3xl md:text-4xl font-bold text-gray-800 mb-2">{t('title')}</h1>
<p className="text-base sm:text-lg text-gray-600">{t('description')}</p>
</div>
<div className="flex-grow flex flex-col">
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 sm:gap-6 flex-grow">
{selectionList?.list?.map((item: any) => {
if (selectionList.type === 'employee') {
const staff = item.staff;
const department = staff?.duties?.departments;
const company = department?.companies;
return (
<div
key={item.uu_id}
className={`rounded-2xl p-6 cursor-pointer transition-all duration-300 flex flex-col justify-between h-full shadow-lg border-2 ${selectedOption === item.uu_id
? 'bg-indigo-100 border-indigo-500 shadow-indigo-200 transform scale-[1.02]'
: 'bg-white/90 border-indigo-100 hover:bg-indigo-50 hover:border-indigo-300 hover:shadow-indigo-100'}`}
onClick={() => handleSelection(item.uu_id)}
>
<div>
<div className="flex items-center mb-4">
<div className="w-12 h-12 rounded-xl bg-gradient-to-r from-indigo-500 to-purple-600 flex items-center justify-center text-white font-bold text-lg mr-4">
{staff?.staff_code?.charAt(0) || 'E'}
</div>
<h3 className="text-lg font-bold text-gray-800">{t('staff')}: {staff?.staff_code || t('employee')}</h3>
</div>
<div className="space-y-2">
<div className="flex items-center">
<svg className="w-4 h-4 text-indigo-500 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
<span className="font-medium text-xs text-gray-700">{t('uuid')}:</span>
<span className="ml-2 font-mono text-xs text-gray-600">{item?.uu_id}</span>
</div>
<div className="pt-2 border-t border-gray-100 mt-2">
<div className="flex items-center mb-1">
<svg className="w-4 h-4 text-indigo-500 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<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"></path>
</svg>
<span className="font-medium text-gray-700">{t('department')}</span>
</div>
<div className="ml-6 mt-1 space-y-1">
<div className="flex items-center">
<span className="text-xs text-gray-500 w-16">{t('name')}:</span>
<span className="text-sm text-gray-600">{department?.department_name || 'N/A'}</span>
</div>
<div className="flex items-center">
<span className="text-xs text-gray-500 w-16">{t('code')}:</span>
<span className="text-sm text-gray-600">{department?.department_code || 'N/A'}</span>
</div>
</div>
</div>
<div className="flex items-center">
<svg className="w-4 h-4 text-indigo-500 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M8 14v3m4-3v3m4-3v3M3 21h18M3 10h18M3 7l9-4 9 4M4 10h16v11H4V10z"></path>
</svg>
<span className="font-medium text-gray-700">{t('company')}:</span>
<span className="ml-2 text-sm text-gray-600">{company?.public_name || company?.formal_name || 'N/A'}</span>
</div>
</div>
</div>
{selectedOption === item.uu_id && (
<div className="mt-4 flex justify-end">
<div className="w-6 h-6 rounded-full bg-indigo-500 flex items-center justify-center">
<svg className="w-4 h-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M5 13l4 4L19 7"></path>
</svg>
</div>
</div>
)}
</div>
);
}
if (selectionList.type === 'occupant') {
const occupantType = item.occupant_types;
const buildPart = item.build_parts;
const build = buildPart?.build;
const enums = buildPart?.api_enum_dropdown_build_parts_part_type_idToapi_enum_dropdown;
return (
<div
key={item.uu_id}
className={`rounded-2xl p-6 cursor-pointer transition-all duration-300 flex flex-col justify-between h-full shadow-lg border-2 ${selectedOption === item.uu_id
? 'bg-indigo-100 border-indigo-500 shadow-indigo-200 transform scale-[1.02]'
: 'bg-white/90 border-indigo-100 hover:bg-indigo-50 hover:border-indigo-300 hover:shadow-indigo-100'}`}
onClick={() => handleSelection(item.uu_id)}
>
<div>
<div className="flex items-center mb-4">
<div className="w-12 h-12 rounded-xl bg-gradient-to-r from-indigo-500 to-purple-600 flex items-center justify-center text-white font-bold text-lg mr-4">
{occupantType?.occupant_code?.charAt(0) || 'O'}
</div>
<h3 className="text-lg font-bold text-gray-800">{t('occupant_type')}: {occupantType?.occupant_type}</h3>
</div>
<div className="space-y-2">
<div className="flex items-center">
<svg className="w-4 h-4 text-indigo-500 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
<span className="font-medium text-xs text-gray-700">{t('uuid')}:</span>
<span className="ml-2 font-mono text-xs text-gray-600">{item?.uu_id}</span>
</div>
<div className="flex items-center">
<svg className="w-4 h-4 text-indigo-500 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
<span className="font-medium text-gray-700">{t('occupant_code')}:</span>
<span className="ml-2 font-semibold text-indigo-600">{occupantType?.occupant_code}</span>
</div>
<div className="flex items-center">
<svg className="w-4 h-4 text-indigo-500 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<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"></path>
</svg>
<span className="font-medium text-gray-700">{t('building')}:</span>
<span className="ml-2 text-gray-600">{build?.build_name || 'Building'}</span>
</div>
<div className="flex items-center">
<svg className="w-4 h-4 text-indigo-500 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M21 13.255A23.931 23.931 0 0112 15c-3.183 0-6.22-.62-9-1.745M16 6V4a2 2 0 00-2-2h-4a2 2 0 00-2 2v2m4 6h.01M5 20h14a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"></path>
</svg>
<span className="font-medium text-gray-700">{t('type')}:</span>
<span className="ml-2 text-gray-600">{enums?.value}</span>
</div>
<div className="pt-2 border-t border-gray-100 mt-2">
<div className="flex items-center mb-1">
<svg className="w-4 h-4 text-indigo-500 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<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"></path>
</svg>
<span className="font-medium text-gray-700">{t('part_details')}</span>
</div>
<div className="grid grid-cols-2 gap-2 ml-6 mt-1">
<div className="flex items-center">
<span className="text-xs text-gray-500 w-12">{t('code')}:</span>
<span className="text-sm text-gray-600">{buildPart?.part_code}</span>
</div>
<div className="flex items-center">
<span className="text-xs text-gray-500 w-12">{t('no')}:</span>
<span className="text-sm text-gray-600">{buildPart?.part_no}</span>
</div>
<div className="flex items-center">
<span className="text-xs text-gray-500 w-12">{t('level')}:</span>
<span className="text-sm text-gray-600">{buildPart?.part_level}</span>
</div>
<div className="flex items-center">
<span className="text-xs text-gray-500 w-12">{t('status')}:</span>
<span className={`text-sm font-medium ${buildPart?.human_livable ? 'text-green-600' : 'text-red-600'}`}>
{buildPart?.human_livable ? t('livable') : t('not_livable')}
</span>
</div>
</div>
</div>
</div>
</div>
{selectedOption === item.uu_id && (
<div className="mt-4 flex justify-end">
<div className="w-6 h-6 rounded-full bg-indigo-500 flex items-center justify-center">
<svg className="w-4 h-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M5 13l4 4L19 7"></path>
</svg>
</div>
</div>
)}
</div>
);
}
return (
<div
key={item.uu_id}
className={`rounded-2xl p-6 cursor-pointer transition-all duration-300 flex flex-col justify-between h-full shadow-lg border-2 ${selectedOption === item.uu_id
? 'bg-indigo-100 border-indigo-500 shadow-indigo-200 transform scale-[1.02]'
: 'bg-white/90 border-indigo-100 hover:bg-indigo-50 hover:border-indigo-300 hover:shadow-indigo-100'}`}
onClick={() => handleSelection(item.uu_id)}
>
<div>
<div className="flex items-center mb-4">
<div className="w-12 h-12 rounded-xl bg-gradient-to-r from-indigo-500 to-purple-600 flex items-center justify-center text-white font-bold text-lg mr-4">
{item.uu_id?.charAt(0) || 'S'}
</div>
<h3 className="text-lg font-bold text-gray-800">{selectionList.type || t('selection')}</h3>
</div>
<p className="text-gray-600 text-sm">{item.uu_id || t('id')}</p>
</div>
{selectedOption === item.uu_id && (
<div className="mt-4 flex justify-end">
<div className="w-6 h-6 rounded-full bg-indigo-500 flex items-center justify-center">
<svg className="w-4 h-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M5 13l4 4L19 7"></path>
</svg>
</div>
</div>
)}
</div>
);
})}
</div>
<div className="mt-8 sm:mt-10 flex justify-center">
<button
className={`px-8 py-4 rounded-xl font-bold text-white transition-all duration-300 text-lg ${selectedOption
? 'bg-gradient-to-r from-indigo-600 to-purple-600 hover:from-indigo-700 hover:to-purple-700 shadow-lg shadow-indigo-200 hover:shadow-indigo-300 transform hover:scale-105'
: 'bg-gray-400 cursor-not-allowed'}`}
disabled={!selectedOption || isLoading}
onClick={handleContinue}
>
{isLoading ? t('processing') : selectedOption ? t('continue') : t('select_option')}
</button>
</div>
</div>
</div>
</div>
);
}

View File

@@ -1,3 +1,11 @@
export default function SelectPage() {
return <div></div>;
'use server';
import { Locale } from 'next-intl';
import { checkAccess, checkSelectionOnSelectPage } from '@/app/api/guards';
import SelectPageClient from './SelectPage';
export default async function PageSelect({ params }: { params: Promise<{ locale: string }> }) {
const { locale } = await params;
await checkAccess(locale as Locale);
await checkSelectionOnSelectPage(locale as Locale);
return <SelectPageClient />;
}

View File

@@ -20,13 +20,18 @@ function removeSubStringFromPath(headersList: Headers) {
return removeLocaleFromPath(currentRoute);
}
function getLocaleFromPath(path: string) {
const locale = path.split('/')[0];
return locale;
}
export default async function ProtectedLayout({
children,
}: {
children: ReactNode,
}) {
// Server component approach to get URL
const headersList = await headers();
// const locale = getLocaleFromPath(removeSubStringFromPath(headersList));
const removedLocaleRoute = removeSubStringFromPath(headersList);
console.log('Removed locale route:', removedLocaleRoute);
return <>{children}</>;

View File

@@ -1,3 +1,3 @@
export default function OfficePage() {
return <div></div>;
return <div>Office Page</div>;
}

View File

@@ -1,3 +1,3 @@
export default function VenuePage() {
return <div></div>;
return <div>Venue Page</div>;
}

View File

@@ -35,7 +35,7 @@ type SearchFormData = z.infer<typeof searchFormSchema>;
export default function TrialPage() {
const pathname = usePathname();
const cleanPathname = removeLocaleFromPath(pathname);
const cleanPathname = removeLocaleFromPath(pathname || '');
const cacheKeyCreateForm = buildCacheKey({ url: cleanPathname, form: 'trialCreateForm', field: 'trialCreateField' });
const cacheKeySelectForm = buildCacheKey({ url: cleanPathname, form: 'trialSelectForm', field: 'trialSelectField' });
@@ -67,7 +67,7 @@ export default function TrialPage() {
} catch (error) {
console.error('Error saving form data:', error);
setCreateFormErrors({
error: "Error saving form data"
error: "Error saving form data"
});
}
} else {

View File

@@ -0,0 +1,9 @@
import { NextResponse } from "next/server";
import { isAccessTokenValid } from "@/fetchers/token/access";
export async function GET() {
const isValid = await isAccessTokenValid();
return !isValid
? NextResponse.json({ ok: false }, { status: 401 })
: NextResponse.json({ ok: true }, { status: 200 });
}

View File

@@ -0,0 +1,9 @@
import { NextResponse } from "next/server";
import { isSelectTokenValid } from "@/fetchers/token/select";
export async function GET() {
const isSlcTokenValid = await isSelectTokenValid();
return !isSlcTokenValid
? NextResponse.json({ ok: false }, { status: 401 })
: NextResponse.json({ ok: true }, { status: 200 });
}

View File

@@ -0,0 +1,28 @@
import { NextRequest, NextResponse } from "next/server";
import { doLogin } from "@/fetchers/auth/login/fetch";
import { setCookieAccessToken } from "@/fetchers/token/cookies";
export async function POST(req: NextRequest) {
try {
const { accessKey, password, rememberMe } = await req.json();
console.log("Login attempt for:", accessKey);
const response = await doLogin({ accessKey, password, rememberMe });
if (response.status !== 200) {
console.log("Login failed with status:", response.status);
return NextResponse.json({ status: 401 });
}
const data = response.data as any;
const token = data.token;
console.log("Token received:", token ? "[PRESENT]" : "[MISSING]");
if (!token) {
console.error("No token received from login response");
return NextResponse.json({ status: 500, message: "No token received" });
}
await setCookieAccessToken(token);
console.log("Cookie set via setCookieAccessToken");
return NextResponse.json({ status: 200 });
} catch (error) {
console.error("Error in login route:", error);
return NextResponse.json({ status: 500, message: "Internal server error" });
}
}

View File

@@ -0,0 +1,36 @@
import { NextRequest, NextResponse } from "next/server";
import { doSelect } from "@/fetchers/auth/login/fetch";
import { setCookieSelectToken } from "@/fetchers/token/cookies";
export async function POST(req: NextRequest) {
try {
const { uuid } = await req.json();
console.log("Select attempt for UUID:", uuid);
const response = await doSelect({ uuid });
if (response.status !== 200) {
console.log("Select failed with status:", response.status);
return NextResponse.json({ status: 401, message: "Select failed" });
}
const data = response.data as any;
const token = data.token;
console.log("Select token received:", token ? "[PRESENT]" : "[MISSING]");
if (!token) {
console.error("No token received from select response");
return NextResponse.json({ status: 500, message: "No token received" });
}
// Set the cookie using the server-side utility function
await setCookieSelectToken(token);
console.log("Select cookie set via setCookieSelectToken");
// Return the response
return NextResponse.json({ status: 200 });
} catch (error) {
console.error("Error in select route:", error);
return NextResponse.json({ status: 500, message: "Internal server error" });
}
}

View File

@@ -0,0 +1,6 @@
import { NextRequest, NextResponse } from "next/server";
import { getSelectionList } from "@/fetchers/auth/selection/list/fetch";
export async function GET(req: NextRequest) {
return await getSelectionList();
}

View File

@@ -1,5 +1,5 @@
import { NextRequest, NextResponse } from "next/server";
import { deleteKey } from "@/libss/redisService";
import { deleteKey } from "@/fetchers/redis/redisService";
export async function POST(req: NextRequest) {
const { key } = await req.json();

View File

@@ -1,5 +1,5 @@
import { NextRequest, NextResponse } from "next/server";
import { exists } from "@/libss/redisService";
import { exists } from "@/fetchers/redis/redisService";
export async function POST(req: NextRequest) {
const { key } = await req.json();

View File

@@ -1,5 +1,5 @@
import { NextRequest, NextResponse } from "next/server";
import { getJSON } from "@/libss/redisService";
import { getJSON } from "@/fetchers/redis/redisService";
export async function POST(req: NextRequest) {
const { key } = await req.json();

View File

@@ -1,5 +1,5 @@
import { NextRequest, NextResponse } from "next/server";
import { updateField } from "@/libss/redisService";
import { updateField } from "@/fetchers/redis/redisService";
export async function POST(req: NextRequest) {
try {

View File

@@ -1,45 +0,0 @@
import { NextRequest, NextResponse } from "next/server";
import {
setJSON,
getJSON,
updateJSON,
deleteKey,
exists,
} from "@/libss/redisService";
export async function POST(req: NextRequest) {
try {
const { action, key, value, ttlSeconds } = await req.json();
switch (action) {
case "set":
await setJSON({ key, value, ttlSeconds });
return NextResponse.json({ status: "ok", message: `Set ${key}` });
case "get": {
const result = await getJSON({ key });
return result
? NextResponse.json({ status: "ok", data: result })
: NextResponse.json({ status: "not_found" }, { status: 404 });
}
case "update":
await updateJSON({ key, value });
return NextResponse.json({ status: "ok", message: `Updated ${key}` });
case "delete":
await deleteKey({ key });
return NextResponse.json({ status: "ok", message: `Deleted ${key}` });
case "exists":
const doesExist = await exists({ key });
return NextResponse.json({ status: "ok", exists: doesExist });
default:
return NextResponse.json({ error: "Invalid action" }, { status: 400 });
}
} catch (e: any) {
console.error("[redis-api] error:", e.message);
return NextResponse.json({ error: e.message }, { status: 500 });
}
}

View File

@@ -1,5 +1,5 @@
import { NextRequest, NextResponse } from "next/server";
import { setJSON } from "@/libss/redisService";
import { setJSON } from "@/fetchers/redis/redisService";
export async function POST(req: NextRequest) {
const { key, value, ttlSeconds } = await req.json();

View File

@@ -1,5 +1,5 @@
import { NextRequest, NextResponse } from "next/server";
import { updateJSON } from "@/libss/redisService";
import { updateJSON } from "@/fetchers/redis/redisService";
export async function POST(req: NextRequest) {
try {

View File

@@ -0,0 +1,35 @@
'use server';
import { redirect } from '@/i18n/navigation';
import { Locale } from 'next-intl';
import { isAccessTokenValid } from '@/fetchers/token/access';
import { isSelectTokenValid } from '@/fetchers/token/select';
async function checkAccessOnLoginPage(locale: Locale) {
const access = await isAccessTokenValid();
if (access) {
return redirect({ href: '/select', locale: locale });
}
}
async function checkAccess(locale: Locale) {
const access = await isAccessTokenValid();
if (!access) {
return redirect({ href: '/login', locale: locale });
}
}
async function checkSelectionOnSelectPage(locale: Locale) {
const select = await isSelectTokenValid();
if (select) {
return redirect({ href: '/venue', locale: locale });
}
}
async function checkSelection(locale: Locale) {
const select = await isSelectTokenValid();
if (!select) {
return redirect({ href: '/select', locale: locale });
} 1
}
export { checkAccess, checkSelection, checkAccessOnLoginPage, checkSelectionOnSelectPage };

View File

@@ -6,6 +6,8 @@ import LocaleSwitcherClient from '@/components/LocaleSwitcherClient';
export default function HomePage() {
const t = useTranslations('Index');
const n = useTranslations('Index.navigation');
const router = useRouter();
const params = useParams();
@@ -18,11 +20,13 @@ export default function HomePage() {
<main>
<h1>{t('title')}</h1>
<p>{t('description')}</p>
<p>{t('navigation.title')} : {params.locale}</p>
<p>{n('title')} : {params?.locale || 'tr'}</p>
<div className='flex flex-col gap-2'>
<LocaleSwitcherClient />
<button onClick={() => handleNavigation('/about')}>{t('navigation.about')}</button>
<button onClick={() => handleNavigation('/home')}>{t('navigation.home')}</button>
<button onClick={() => handleNavigation('/about')}>{n('about')}</button>
<button onClick={() => handleNavigation('/home')}>{n('home')}</button>
<button onClick={() => handleNavigation('/login')}>{n('login')}</button>
<button onClick={() => handleNavigation('/select')}>{n('select')}</button>
</div>
</main>
);