feat: setup auth schema
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
export const AndroidIcon = (props: any) => (
|
||||
<svg viewBox="0 0 24 24" fill="#3DDC84" xmlns="http://www.w3.org/2000/svg" {...props}>
|
||||
<path d="M17.5 12C18.33 12 19 11.33 19 10.5C19 9.67 18.33 9 17.5 9C16.67 9 16 9.67 16 10.5C16 11.33 16.67 12 17.5 12ZM6.5 12C7.33 12 8 11.33 8 10.5C8 9.67 7.33 9 6.5 9C5.67 9 5 9.67 5 10.5C5 11.33 5.67 12 6.5 12Z" />
|
||||
<path d="M19.46 8.52C19.31 8.52 19.17 8.56 19.04 8.65L17.12 9.99C15.56 9.29 13.83 8.9 12 8.9C10.17 8.9 8.44 9.29 6.88 9.99L4.96 8.65C4.83 8.56 4.69 8.52 4.54 8.52C4.1 8.52 3.74 8.88 3.74 9.32C3.74 9.58 3.87 9.82 4.08 9.97L5.7 11.11C3.47 12.8 2 15.39 2 18.29H22C22 15.39 20.53 12.8 18.3 11.11L19.92 9.97C20.13 9.82 20.26 9.58 20.26 9.32C20.26 8.88 19.9 8.52 19.46 8.52Z" />
|
||||
</svg>
|
||||
);
|
||||
@@ -0,0 +1,16 @@
|
||||
export const ChromeIcon = (props: any) => (
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2ZM12 7C9.23858 7 7 9.23858 7 12C7 14.7614 9.23858 17 12 17C14.7614 17 17 14.7614 17 12C17 9.23858 14.7614 7 12 7Z"
|
||||
fill="#FACC15"
|
||||
/>
|
||||
<path d="M12 2C16.8 2 20.8 5.4 21.8 10H12L7 18.6C5.1 16.9 4 14.6 4 12C4 7.6 7.6 4 12 4V2Z" fill="#F87171" />
|
||||
<path
|
||||
d="M22 12C22 17.5 17.5 22 12 22C10 22 8.1 21.4 6.5 20.3L11.5 11.6L21.8 10C21.9 10.6 22 11.3 22 12Z"
|
||||
fill="#4ADE80"
|
||||
/>
|
||||
<circle cx="12" cy="12" r="4" fill="#3B82F6" />
|
||||
</svg>
|
||||
);
|
||||
@@ -0,0 +1,20 @@
|
||||
export const GoogleIcon = (props: any) => (
|
||||
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" {...props}>
|
||||
<path
|
||||
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
|
||||
fill="#4285F4"
|
||||
/>
|
||||
<path
|
||||
d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
|
||||
fill="#34A853"
|
||||
/>
|
||||
<path
|
||||
d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"
|
||||
fill="#FBBC05"
|
||||
/>
|
||||
<path
|
||||
d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
|
||||
fill="#EA4335"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
@@ -0,0 +1,12 @@
|
||||
export const IconCircle = ({ x, y, children }: { x: number; y: number; children: React.ReactNode }) => (
|
||||
<div
|
||||
className="absolute flex items-center justify-center w-10 h-10 bg-white rounded-full shadow-lg"
|
||||
style={{
|
||||
left: `calc(50% + ${x}px)`,
|
||||
top: `calc(50% + ${y}px)`,
|
||||
transform: 'translate(-50%, -50%)',
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
@@ -0,0 +1,8 @@
|
||||
export const MicrosoftIcon = (props: any) => (
|
||||
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" {...props}>
|
||||
<path d="M1 1h10.5v10.5H1z" fill="#f35325" />
|
||||
<path d="M12.5 1H23v10.5H12.5z" fill="#81bc06" />
|
||||
<path d="M1 12.5h10.5V23H1z" fill="#05a6f0" />
|
||||
<path d="M12.5 12.5H23V23H12.5z" fill="#ffba08" />
|
||||
</svg>
|
||||
);
|
||||
@@ -0,0 +1,121 @@
|
||||
import { Apple, Globe, Mail, MessageCircle } from 'lucide-react';
|
||||
import { IconCircle } from './icon-circle';
|
||||
import { Ring } from './ring';
|
||||
import { AndroidIcon } from './android-icon';
|
||||
import { ChromeIcon } from './chrome-icon';
|
||||
|
||||
export function RightSection() {
|
||||
return (
|
||||
// <div className="relative flex flex-col items-center justify-center w-full mt-6 mr-6 mb-6 p-12 overflow-hidden shadow-inner bg-linear-to-b from-brand-50 via-brand-300 to-brand-50 border-l border-brand-100 rounded-2xl">
|
||||
<div className="relative flex flex-col items-center justify-center w-full p-12 overflow-hidden shadow-inner bg-linear-to-b from-brand-50 via-brand-300 to-brand-50 border-brand-100 ">
|
||||
{/* 1. Glossy & Glassmorphic Orbs */}
|
||||
<div className="absolute -top-10 -right-10 w-72 h-72 bg-white/40 rounded-full blur-3xl pointer-events-none mix-blend-overlay" />
|
||||
<div className="absolute top-1/3 left-1/4 w-96 h-96 bg-brand-400/20 rounded-full blur-3xl pointer-events-none" />
|
||||
<div className="absolute -bottom-10 -left-10 w-80 h-80 bg-white/60 rounded-full blur-3xl pointer-events-none" />
|
||||
|
||||
<h2 className="relative z-10 mt-6 mb-3 text-3xl xl:text-4xl font-bold tracking-tight text-center text-brand-950 leading-tight">
|
||||
Write Better {/* Wrapper luar KHUSUS untuk Drop Shadow */}
|
||||
<span className="block sm:inline filter drop-shadow-[0_2px_8px_rgba(var(--color-brand-500),0.15)]">
|
||||
{/* Elemen dalam KHUSUS untuk Gradien Teks */}
|
||||
<span className="text-transparent bg-clip-text bg-linear-to-r from-brand-700 via-brand-600 to-brand-700">
|
||||
Everywhere
|
||||
</span>
|
||||
</span>
|
||||
</h2>
|
||||
|
||||
{/* Graphics Area */}
|
||||
<div className="flex-1 w-full relative flex items-center justify-center min-h-100">
|
||||
{/* Concentric Rings */}
|
||||
<Ring size={200} />
|
||||
<Ring size={300} />
|
||||
<Ring size={420} />
|
||||
<Ring size={540} />
|
||||
|
||||
{/* Center Logo */}
|
||||
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 z-20">
|
||||
<div className="w-18 h-18 bg-brand-600 rounded-full flex items-center justify-center shadow-xl shadow-brand-600/40 relative">
|
||||
{/* Glowing backdrop */}
|
||||
<div className="absolute inset-0 bg-brand-600 rounded-full blur-md opacity-40"></div>
|
||||
{/* Custom P/Pen Logo from reference */}
|
||||
<svg
|
||||
viewBox="0 0 32 32"
|
||||
className="w-10.5 h-10.5 text-white relative z-10"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M12 7C12 6.44772 12.4477 6 13 6H18C21.3137 6 24 8.68629 24 12C24 15.3137 21.3137 18 18 18H15V25C15 25.5523 14.5523 26 14 26H13C12.4477 26 12 25.5523 12 25V7ZM15 9V15H18C19.6569 15 21 13.6569 21 12C21 10.3431 19.6569 9 18 9H15Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path d="M16.5 10L16.5 14L15 14L15 10L16.5 10Z" className="fill-brand-600" />
|
||||
<path d="M15.75 16L14 20L17.5 20L15.75 16Z" className="fill-brand-600" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Orbiting Icons */}
|
||||
|
||||
{/* Ring 1 (r=100) */}
|
||||
<div className="animate-floating" style={{ animationDelay: '0s' }}>
|
||||
<IconCircle x={-70} y={-70}>
|
||||
<Mail className="w-5 h-5 text-[#0078D4]" fill="#0078D4" stroke="#fff" />
|
||||
</IconCircle>
|
||||
</div>
|
||||
|
||||
<div className="animate-floating" style={{ animationDelay: '0.5s' }}>
|
||||
<IconCircle x={85} y={35}>
|
||||
<MessageCircle className="w-5.5 h-5.5 text-[#0084FF]" fill="#0084FF" stroke="none" />
|
||||
</IconCircle>
|
||||
</div>
|
||||
|
||||
{/* Ring 2 (r=150) */}
|
||||
<div className="animate-floating" style={{ animationDelay: '1s' }}>
|
||||
<IconCircle x={0} y={-150}>
|
||||
<Globe className="w-6 h-6 text-[#0078D7]" />
|
||||
</IconCircle>
|
||||
</div>
|
||||
|
||||
<div className="animate-floating" style={{ animationDelay: '0.2s' }}>
|
||||
<IconCircle x={-60} y={138}>
|
||||
<span className="text-[#6001D2] font-bold italic text-xl -ml-1">y!</span>
|
||||
</IconCircle>
|
||||
</div>
|
||||
|
||||
{/* Ring 3 (r=210) */}
|
||||
<div className="animate-floating" style={{ animationDelay: '0.8s' }}>
|
||||
<IconCircle x={-185} y={-70}>
|
||||
<AndroidIcon className="w-6 h-6" />
|
||||
</IconCircle>
|
||||
</div>
|
||||
|
||||
<div className="animate-floating" style={{ animationDelay: '1.5s' }}>
|
||||
<IconCircle x={200} y={50}>
|
||||
<Apple className="w-5.5 h-5.5 text-black" fill="black" />
|
||||
</IconCircle>
|
||||
</div>
|
||||
|
||||
{/* Ring 4 (r=270) */}
|
||||
<div className="animate-floating" style={{ animationDelay: '0.4s' }}>
|
||||
<IconCircle x={-190} y={180}>
|
||||
<ChromeIcon className="w-6.5 h-6.5" />
|
||||
</IconCircle>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="text-[13px] text-center text-gray-600 mt-6 max-w-[85%] z-10 leading-[1.6]">
|
||||
Compatible with{' '}
|
||||
<span className="font-bold italic text-gray-900">Gmail, Outlook Web, LinkedIn and most web editors</span> for a
|
||||
smooth writing experience anywhere online.
|
||||
</p>
|
||||
|
||||
{/* Pagination Indicators */}
|
||||
<div className="flex gap-1.5 mt-8 z-10 mb-2">
|
||||
<div className="w-6 h-1 rounded-full bg-brand-500"></div>
|
||||
<div className="w-6 h-1 rounded-full bg-brand-200"></div>
|
||||
<div className="w-6 h-1 rounded-full bg-brand-200"></div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export const Ring = ({ size }: { size: number }) => (
|
||||
<div
|
||||
className="absolute rounded-full border border-white/60"
|
||||
style={{ width: size, height: size, top: '50%', left: '50%', transform: 'translate(-50%, -50%)' }}
|
||||
/>
|
||||
);
|
||||
@@ -1,104 +1,115 @@
|
||||
import { TextInput, PasswordInput, Checkbox, Button, Divider } from '@repo/ui/components';
|
||||
import { Mail, Lock, LogIn, Apple, Globe, MessageCircle } from 'lucide-react';
|
||||
import {
|
||||
FieldTextInput,
|
||||
FieldPasswordInput,
|
||||
Checkbox,
|
||||
Button,
|
||||
Divider,
|
||||
Select,
|
||||
Image,
|
||||
notifications,
|
||||
} from '@repo/ui/components';
|
||||
import { Lock, Apple, LogInIcon, UserRound, Globe } from 'lucide-react';
|
||||
import { GoogleIcon } from './components/google-icon';
|
||||
import { MicrosoftIcon } from './components/microsoft-icon';
|
||||
import { RightSection } from './components/right-section';
|
||||
import { useTranslation, registerModuleNamespace } from '@repo/core-i18n';
|
||||
import { AppStorageKey, appStorage } from '../../../core/storage/local';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { createLoginSchema } from './validators/login.validator';
|
||||
import { useMemo, useState } from 'react';
|
||||
|
||||
const GoogleIcon = (props: any) => (
|
||||
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" {...props}>
|
||||
<path
|
||||
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
|
||||
fill="#4285F4"
|
||||
/>
|
||||
<path
|
||||
d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
|
||||
fill="#34A853"
|
||||
/>
|
||||
<path
|
||||
d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"
|
||||
fill="#FBBC05"
|
||||
/>
|
||||
<path
|
||||
d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
|
||||
fill="#EA4335"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
import loginEn from './languages/en/login.json';
|
||||
import loginId from './languages/id/login.json';
|
||||
import { CommonRemoteDataServices } from '@repo/core-api/data-services';
|
||||
import { apiClient } from '../../../core/lib/api-client';
|
||||
import { initiateAuthSession } from '../../../core/lib/auth.helper';
|
||||
|
||||
const MicrosoftIcon = (props: any) => (
|
||||
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" {...props}>
|
||||
<path d="M1 1h10.5v10.5H1z" fill="#f35325" />
|
||||
<path d="M12.5 1H23v10.5H12.5z" fill="#81bc06" />
|
||||
<path d="M1 12.5h10.5V23H1z" fill="#05a6f0" />
|
||||
<path d="M12.5 12.5H23V23H12.5z" fill="#ffba08" />
|
||||
</svg>
|
||||
);
|
||||
registerModuleNamespace('auth-login', {
|
||||
en: loginEn,
|
||||
id: loginId,
|
||||
});
|
||||
|
||||
const AndroidIcon = (props: any) => (
|
||||
<svg viewBox="0 0 24 24" fill="#3DDC84" xmlns="http://www.w3.org/2000/svg" {...props}>
|
||||
<path d="M17.5 12C18.33 12 19 11.33 19 10.5C19 9.67 18.33 9 17.5 9C16.67 9 16 9.67 16 10.5C16 11.33 16.67 12 17.5 12ZM6.5 12C7.33 12 8 11.33 8 10.5C8 9.67 7.33 9 6.5 9C5.67 9 5 9.67 5 10.5C5 11.33 5.67 12 6.5 12Z" />
|
||||
<path d="M19.46 8.52C19.31 8.52 19.17 8.56 19.04 8.65L17.12 9.99C15.56 9.29 13.83 8.9 12 8.9C10.17 8.9 8.44 9.29 6.88 9.99L4.96 8.65C4.83 8.56 4.69 8.52 4.54 8.52C4.1 8.52 3.74 8.88 3.74 9.32C3.74 9.58 3.87 9.82 4.08 9.97L5.7 11.11C3.47 12.8 2 15.39 2 18.29H22C22 15.39 20.53 12.8 18.3 11.11L19.92 9.97C20.13 9.82 20.26 9.58 20.26 9.32C20.26 8.88 19.9 8.52 19.46 8.52Z" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
const ChromeIcon = (props: any) => (
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2ZM12 7C9.23858 7 7 9.23858 7 12C7 14.7614 9.23858 17 12 17C14.7614 17 17 14.7614 17 12C17 9.23858 14.7614 7 12 7Z"
|
||||
fill="#FACC15"
|
||||
/>
|
||||
<path d="M12 2C16.8 2 20.8 5.4 21.8 10H12L7 18.6C5.1 16.9 4 14.6 4 12C4 7.6 7.6 4 12 4V2Z" fill="#F87171" />
|
||||
<path
|
||||
d="M22 12C22 17.5 17.5 22 12 22C10 22 8.1 21.4 6.5 20.3L11.5 11.6L21.8 10C21.9 10.6 22 11.3 22 12Z"
|
||||
fill="#4ADE80"
|
||||
/>
|
||||
<circle cx="12" cy="12" r="4" fill="#3B82F6" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
const Ring = ({ size }: { size: number }) => (
|
||||
<div
|
||||
className="absolute rounded-full border border-white/60"
|
||||
style={{ width: size, height: size, top: '50%', left: '50%', transform: 'translate(-50%, -50%)' }}
|
||||
/>
|
||||
);
|
||||
|
||||
const IconCircle = ({ x, y, children }: { x: number; y: number; children: React.ReactNode }) => (
|
||||
<div
|
||||
className="absolute flex items-center justify-center w-10 h-10 bg-white rounded-full shadow-lg"
|
||||
style={{
|
||||
left: `calc(50% + ${x}px)`,
|
||||
top: `calc(50% + ${y}px)`,
|
||||
transform: 'translate(-50%, -50%)',
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
export const authDataService = new CommonRemoteDataServices(apiClient, {});
|
||||
|
||||
export default function LoginPage() {
|
||||
const { t, i18n } = useTranslation('auth-login');
|
||||
const [loadingLogin, setLoadingLogin] = useState<boolean>(false);
|
||||
|
||||
const validator = useMemo(() => {
|
||||
return createLoginSchema(t);
|
||||
}, [t, i18n.language]);
|
||||
|
||||
const formControl = useForm({
|
||||
resolver: zodResolver(validator),
|
||||
defaultValues: { username: '', password: '' },
|
||||
});
|
||||
const { control } = formControl;
|
||||
|
||||
const onSubmit = async (data: any) => {
|
||||
setLoadingLogin(true);
|
||||
try {
|
||||
const response = await authDataService.customRequest({ url: '/api/v1/auth', method: 'POST', data });
|
||||
await initiateAuthSession(response.data);
|
||||
} catch (error: any) {
|
||||
const message = error?.response?.data?.message;
|
||||
notifications.show({
|
||||
title: t('common:notifications.errorTitle'),
|
||||
message: message ?? error?.message ?? 'Login failed',
|
||||
color: 'red',
|
||||
});
|
||||
} finally {
|
||||
setLoadingLogin(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-white w-full min-h-screen flex overflow-hidden">
|
||||
{/* Left Column - Login Form */}
|
||||
<div className="w-full lg:w-1/2 p-6 sm:p-10 lg:p-16 flex flex-col justify-center relative">
|
||||
{/* Logo (Top Left) */}
|
||||
<div className="absolute top-8 left-8 sm:top-12 sm:left-12 flex items-center gap-2">
|
||||
{/* Image Logo (Uncomment and set src to your logo path) */}
|
||||
{/* <img src="/logo.png" alt="Logo" className="w-8 h-8 object-contain" /> */}
|
||||
<div className="absolute top-8 left-8 right-8 sm:top-12 sm:left-12 sm:right-12 z-20 flex items-center justify-between">
|
||||
{/* Logo (Top Left) */}
|
||||
<div className="flex items-center gap-2">
|
||||
<Image
|
||||
h="auto"
|
||||
w={{ base: 40, sm: 50 }}
|
||||
alt="Logo"
|
||||
fit="contain"
|
||||
src="/brand/logo-primary.svg"
|
||||
style={{ flexShrink: 0 }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Text Logo (Remove if using image) */}
|
||||
<div className="w-8 h-8 bg-brand-600 rounded-lg flex items-center justify-center text-white font-bold text-xl">
|
||||
E
|
||||
{/* Language Switcher (Top Right) */}
|
||||
<div className="flex items-center gap-2 px-2 py-1.5 border border-gray-200 rounded-lg transition-colors hover:bg-gray-50">
|
||||
<Globe size={12} className="text-gray-500" />
|
||||
<Select
|
||||
size="xs"
|
||||
variant="unstyled"
|
||||
w={90}
|
||||
data={[
|
||||
{ value: 'en', label: t('common:english') },
|
||||
{ value: 'id', label: t('common:indonesian') },
|
||||
]}
|
||||
value={i18n.resolvedLanguage || i18n.language}
|
||||
onChange={async (val) => {
|
||||
if (!val) return;
|
||||
await i18n.changeLanguage(val);
|
||||
await appStorage.setItem(AppStorageKey.LANGUAGE, val);
|
||||
}}
|
||||
styles={{
|
||||
input: { textAlign: 'left', cursor: 'pointer', height: '20px', minHeight: '20px' },
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="font-bold text-gray-900 text-xl tracking-tight select-none">Eigen</div>
|
||||
</div>
|
||||
|
||||
<div className="w-full max-w-[400px] mx-auto mt-12 lg:mt-0">
|
||||
<div className="w-full max-w-100 mx-auto mt-12 lg:mt-0">
|
||||
{/* Header */}
|
||||
<div className="relative mb-6">
|
||||
<div className="absolute inset-0 flex items-center justify-center opacity-40 z-0">
|
||||
<div
|
||||
// className="w-[180px] h-[100px]"
|
||||
className="w-[480px] h-[120px]"
|
||||
className="w-120 h-30"
|
||||
style={{
|
||||
backgroundImage:
|
||||
'linear-gradient(to right, #cbd5e1 1px, transparent 1px), linear-gradient(to bottom, #cbd5e1 1px, transparent 1px)',
|
||||
@@ -108,66 +119,69 @@ export default function LoginPage() {
|
||||
}}
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<div className="w-14 h-14 bg-brand-600 rounded-2xl flex items-center justify-center text-white shadow-lg shadow-brand-500/30 mx-auto relative z-10">
|
||||
<LogIn size={26} strokeWidth={2.5} />
|
||||
<LogInIcon size={26} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h1 className="text-2xl sm:text-[26px] font-bold text-gray-900 text-center mb-2 tracking-tight">
|
||||
Login to your account!
|
||||
<h1 className="text-2xl sm:text-[26px] font-normal text-gray-900 text-center mb-2 tracking-tight">
|
||||
{t('title')}
|
||||
</h1>
|
||||
<p className="text-gray-500 text-sm text-center mb-10">
|
||||
Enter your registered email address and password to login!
|
||||
</p>
|
||||
<p className="text-gray-500 text-sm text-center mb-10">{t('description')}</p>
|
||||
|
||||
{/* Form */}
|
||||
<div className="space-y-5">
|
||||
<TextInput
|
||||
label="Email"
|
||||
placeholder="eg. pixelcot@gmail.com"
|
||||
leftSection={<Mail size={18} className="text-gray-400" />}
|
||||
<form className="space-y-5" onSubmit={formControl.handleSubmit(onSubmit)}>
|
||||
<FieldTextInput
|
||||
control={control}
|
||||
name="username"
|
||||
label={t('username_label')}
|
||||
placeholder={t('username_placeholder')}
|
||||
leftSection={<UserRound size={16} />}
|
||||
radius={'lg'}
|
||||
size="md"
|
||||
styles={{
|
||||
label: { color: '#64748b', fontSize: '13px', fontWeight: 500, marginBottom: '6px' },
|
||||
input: { borderRadius: '10px', border: '1px solid #e2e8f0', color: '#1e293b' },
|
||||
}}
|
||||
/>
|
||||
|
||||
<PasswordInput
|
||||
label="Password"
|
||||
placeholder="***************"
|
||||
leftSection={<Lock size={18} className="text-gray-400" />}
|
||||
<FieldPasswordInput
|
||||
control={control}
|
||||
name="password"
|
||||
label={t('password_label')}
|
||||
placeholder={t('password_placeholder')}
|
||||
leftSection={<Lock size={16} />}
|
||||
radius={'lg'}
|
||||
size="md"
|
||||
styles={{
|
||||
label: { color: '#64748b', fontSize: '13px', fontWeight: 500, marginBottom: '6px' },
|
||||
input: { borderRadius: '10px', border: '1px solid #e2e8f0', color: '#1e293b' },
|
||||
}}
|
||||
styles={{ label: { color: '#64748b', fontSize: '13px', fontWeight: 500, marginBottom: '6px' } }}
|
||||
/>
|
||||
|
||||
<div className="flex items-center justify-between pt-1">
|
||||
<Checkbox
|
||||
label="Remember me"
|
||||
label={t('remember_me')}
|
||||
size="sm"
|
||||
styles={{
|
||||
label: { color: '#64748b', fontSize: '13px', fontWeight: 500 },
|
||||
}}
|
||||
/>
|
||||
<a href="#" className="text-[13px] text-brand-600 font-semibold hover:underline">
|
||||
Forgot Password ?
|
||||
{t('forgot_password')}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
fullWidth
|
||||
size="md"
|
||||
loading={loadingLogin}
|
||||
className="bg-brand-600 hover:bg-brand-700 mt-2 transition-colors duration-200"
|
||||
styles={{ root: { borderRadius: '10px', height: '44px', fontWeight: 600 } }}
|
||||
>
|
||||
Login
|
||||
{t('login_button')}
|
||||
</Button>
|
||||
|
||||
<Divider
|
||||
label="Or login with"
|
||||
label={t('or_login_with')}
|
||||
labelPosition="center"
|
||||
my="lg"
|
||||
styles={{ label: { color: '#94a3b8', fontSize: '12px' } }}
|
||||
@@ -175,6 +189,7 @@ export default function LoginPage() {
|
||||
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<Button
|
||||
type="button"
|
||||
variant="default"
|
||||
size="md"
|
||||
className="border-gray-200 hover:bg-gray-50 transition-colors"
|
||||
@@ -183,14 +198,16 @@ export default function LoginPage() {
|
||||
<GoogleIcon className="w-5 h-5" />
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="default"
|
||||
size="md"
|
||||
className="border-gray-200 hover:bg-gray-50 transition-colors"
|
||||
styles={{ root: { borderRadius: '10px', height: '44px' } }}
|
||||
>
|
||||
<Apple className="w-[22px] h-[22px] text-black" fill="black" />
|
||||
<Apple className="w-5.5 h-5.5 text-black" fill="black" />
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="default"
|
||||
size="md"
|
||||
className="border-gray-200 hover:bg-gray-50 transition-colors"
|
||||
@@ -199,116 +216,13 @@ export default function LoginPage() {
|
||||
<MicrosoftIcon className="w-5 h-5" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Column - Graphic */}
|
||||
<div className="hidden lg:flex w-1/2 bg-gradient-to-br from-brand-300 via-brand-100 to-brand-50 relative flex-col items-center justify-center p-12 overflow-hidden shadow-inner border-l border-brand-100">
|
||||
{/* 1. Glossy & Glassmorphic Orbs */}
|
||||
<div className="absolute -top-10 -right-10 w-72 h-72 bg-white/40 rounded-full blur-3xl pointer-events-none mix-blend-overlay" />
|
||||
<div className="absolute top-1/3 left-1/4 w-96 h-96 bg-brand-400/20 rounded-full blur-3xl pointer-events-none" />
|
||||
<div className="absolute -bottom-10 -left-10 w-80 h-80 bg-white/60 rounded-full blur-3xl pointer-events-none" />
|
||||
<h2 className="z-10 mt-6 mb-3 text-3xl xl:text-4xl font-bold tracking-tight text-center text-brand-950 leading-tight">
|
||||
Write Better{' '}
|
||||
<span className="block sm:inline text-transparent bg-clip-text bg-gradient-to-r from-brand-700 via-brand-600 to-brand-500 filter drop-shadow-[0_2px_8px_rgba(var(--color-brand-500),0.15)]">
|
||||
Everywhere
|
||||
</span>
|
||||
</h2>
|
||||
|
||||
{/* Graphics Area */}
|
||||
<div className="flex-1 w-full relative flex items-center justify-center min-h-[400px]">
|
||||
{/* Concentric Rings */}
|
||||
<Ring size={200} />
|
||||
<Ring size={300} />
|
||||
<Ring size={420} />
|
||||
<Ring size={540} />
|
||||
|
||||
{/* Center Logo */}
|
||||
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 z-20">
|
||||
<div className="w-[72px] h-[72px] bg-brand-600 rounded-full flex items-center justify-center shadow-xl shadow-brand-600/40 relative">
|
||||
{/* Glowing backdrop */}
|
||||
<div className="absolute inset-0 bg-brand-600 rounded-full blur-md opacity-40"></div>
|
||||
{/* Custom P/Pen Logo from reference */}
|
||||
<svg
|
||||
viewBox="0 0 32 32"
|
||||
className="w-[42px] h-[42px] text-white relative z-10"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M12 7C12 6.44772 12.4477 6 13 6H18C21.3137 6 24 8.68629 24 12C24 15.3137 21.3137 18 18 18H15V25C15 25.5523 14.5523 26 14 26H13C12.4477 26 12 25.5523 12 25V7ZM15 9V15H18C19.6569 15 21 13.6569 21 12C21 10.3431 19.6569 9 18 9H15Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path d="M16.5 10L16.5 14L15 14L15 10L16.5 10Z" className="fill-brand-600" />
|
||||
<path d="M15.75 16L14 20L17.5 20L15.75 16Z" className="fill-brand-600" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Orbiting Icons */}
|
||||
|
||||
{/* Ring 1 (r=100) */}
|
||||
<div className="animate-floating" style={{ animationDelay: '0s' }}>
|
||||
<IconCircle x={-70} y={-70}>
|
||||
<Mail className="w-5 h-5 text-[#0078D4]" fill="#0078D4" stroke="#fff" />
|
||||
</IconCircle>
|
||||
</div>
|
||||
|
||||
<div className="animate-floating" style={{ animationDelay: '0.5s' }}>
|
||||
<IconCircle x={85} y={35}>
|
||||
<MessageCircle className="w-[22px] h-[22px] text-[#0084FF]" fill="#0084FF" stroke="none" />
|
||||
</IconCircle>
|
||||
</div>
|
||||
|
||||
{/* Ring 2 (r=150) */}
|
||||
<div className="animate-floating" style={{ animationDelay: '1s' }}>
|
||||
<IconCircle x={0} y={-150}>
|
||||
<Globe className="w-6 h-6 text-[#0078D7]" />
|
||||
</IconCircle>
|
||||
</div>
|
||||
|
||||
<div className="animate-floating" style={{ animationDelay: '0.2s' }}>
|
||||
<IconCircle x={-60} y={138}>
|
||||
<span className="text-[#6001D2] font-bold italic text-xl -ml-1">y!</span>
|
||||
</IconCircle>
|
||||
</div>
|
||||
|
||||
{/* Ring 3 (r=210) */}
|
||||
<div className="animate-floating" style={{ animationDelay: '0.8s' }}>
|
||||
<IconCircle x={-185} y={-70}>
|
||||
<AndroidIcon className="w-6 h-6" />
|
||||
</IconCircle>
|
||||
</div>
|
||||
|
||||
<div className="animate-floating" style={{ animationDelay: '1.5s' }}>
|
||||
<IconCircle x={200} y={50}>
|
||||
<Apple className="w-[22px] h-[22px] text-black" fill="black" />
|
||||
</IconCircle>
|
||||
</div>
|
||||
|
||||
{/* Ring 4 (r=270) */}
|
||||
<div className="animate-floating" style={{ animationDelay: '0.4s' }}>
|
||||
<IconCircle x={-190} y={180}>
|
||||
<ChromeIcon className="w-[26px] h-[26px]" />
|
||||
</IconCircle>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="text-[13px] text-center text-gray-600 mt-6 max-w-[85%] z-10 leading-[1.6]">
|
||||
Compatible with{' '}
|
||||
<span className="font-bold italic text-gray-900">Gmail, Outlook Web, LinkedIn and most web editors</span> for
|
||||
a smooth writing experience anywhere online.
|
||||
</p>
|
||||
|
||||
{/* Pagination Indicators */}
|
||||
<div className="flex gap-[6px] mt-8 z-10 mb-2">
|
||||
<div className="w-6 h-[4px] rounded-full bg-brand-500"></div>
|
||||
<div className="w-6 h-[4px] rounded-full bg-brand-200"></div>
|
||||
<div className="w-6 h-[4px] rounded-full bg-brand-200"></div>
|
||||
</div>
|
||||
<div className="hidden lg:flex w-1/2">
|
||||
<RightSection />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"title": "Login to your account!",
|
||||
"description": "Enter your registered username and password to login!",
|
||||
"username_label": "Username",
|
||||
"username_placeholder": "eg. administrator",
|
||||
"password_label": "Password",
|
||||
"password_placeholder": "********",
|
||||
"remember_me": "Remember me",
|
||||
"forgot_password": "Forgot Password ?",
|
||||
"login_button": "Login",
|
||||
"or_login_with": "Or login with"
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"title": "Login ke akun Anda!",
|
||||
"description": "Masukkan username dan password yang terdaftar untuk login!",
|
||||
"username_label": "Username",
|
||||
"username_placeholder": "cth. administrator",
|
||||
"password_label": "Password",
|
||||
"password_placeholder": "********",
|
||||
"remember_me": "Ingat saya",
|
||||
"forgot_password": "Lupa Kata Sandi ?",
|
||||
"login_button": "Masuk",
|
||||
"or_login_with": "Atau login dengan"
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { z } from 'zod';
|
||||
import { compose, required } from '@repo/ui/validators';
|
||||
|
||||
export const createLoginSchema = (t: any) => {
|
||||
return z.object({
|
||||
username: compose(z.string(), required(t('username_label'))),
|
||||
password: compose(z.string(), required(t('password_label'))),
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,6 @@
|
||||
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<!-- Blok Utama: Ungu lebih terang agar kontras di latar gelap -->
|
||||
<rect x="6" y="6" width="16" height="16" rx="4" fill="#8a57f3"/>
|
||||
<!-- Blok Sekunder: Sedikit lebih terang -->
|
||||
<rect x="18" y="18" width="16" height="16" rx="4" fill="#8a57f3" fill-opacity="0.8"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 374 B |
Reference in New Issue
Block a user