Initial React project
This commit is contained in:
11
dist/assets/index-B0Iioc8H.js
vendored
Normal file
11
dist/assets/index-B0Iioc8H.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
dist/assets/index-BQiLnvLH.css
vendored
1
dist/assets/index-BQiLnvLH.css
vendored
File diff suppressed because one or more lines are too long
1
dist/assets/index-Copv87DE.css
vendored
Normal file
1
dist/assets/index-Copv87DE.css
vendored
Normal file
File diff suppressed because one or more lines are too long
11
dist/assets/index-Drgr1jYo.js
vendored
11
dist/assets/index-Drgr1jYo.js
vendored
File diff suppressed because one or more lines are too long
4
dist/index.html
vendored
4
dist/index.html
vendored
@@ -7,8 +7,8 @@
|
||||
<title>arbejd-react</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script src="https://code.iconify.design/iconify-icon/1.0.7/iconify-icon.min.js"></script>
|
||||
<script type="module" crossorigin src="/assets/index-Drgr1jYo.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-BQiLnvLH.css">
|
||||
<script type="module" crossorigin src="/assets/index-B0Iioc8H.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-Copv87DE.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useEffect, useMemo, useState, type ChangeEvent } from 'react';
|
||||
import {
|
||||
ArrowLeft,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
BadgeCheck,
|
||||
Briefcase,
|
||||
Car,
|
||||
CalendarDays,
|
||||
CheckCircle2,
|
||||
FileText,
|
||||
Globe,
|
||||
@@ -10,8 +14,10 @@ import {
|
||||
GraduationCap,
|
||||
LayoutPanelTop,
|
||||
PenLine,
|
||||
Search,
|
||||
Star,
|
||||
UserRound,
|
||||
X,
|
||||
} from 'lucide-react';
|
||||
import type {
|
||||
CertificationInterface,
|
||||
@@ -85,6 +91,26 @@ const FALLBACK_EDUCATIONS = [
|
||||
},
|
||||
];
|
||||
|
||||
type CvWizardView = 'menu' | 'experience' | 'education' | 'personal' | 'skills' | 'language' | 'driverLicense' | 'certification';
|
||||
|
||||
const ESCO_SKILLS = [
|
||||
'HTML',
|
||||
'CSS',
|
||||
'JavaScript',
|
||||
'TypeScript',
|
||||
'React',
|
||||
'Vue.js',
|
||||
'Frontend Udvikling',
|
||||
'Backend Udvikling',
|
||||
'Node.js',
|
||||
'Agile/Scrum',
|
||||
'UI/UX Design',
|
||||
'Salg',
|
||||
'Projektledelse',
|
||||
'Kundeservice',
|
||||
'SEO',
|
||||
];
|
||||
|
||||
function safeDate(value: Date | string | null | undefined): Date | null {
|
||||
if (!value) {
|
||||
return null;
|
||||
@@ -115,6 +141,17 @@ function formatBirthday(value: Date | string | null | undefined): string {
|
||||
return new Intl.DateTimeFormat('da-DK', { day: '2-digit', month: 'long', year: 'numeric' }).format(date);
|
||||
}
|
||||
|
||||
function formatDateInput(value: Date | string | null | undefined): string {
|
||||
const date = safeDate(value);
|
||||
if (!date) {
|
||||
return '';
|
||||
}
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
return `${year}-${month}-${day}`;
|
||||
}
|
||||
|
||||
function sortByFromDateDesc<T extends { fromDate: Date | string }>(list: T[]): T[] {
|
||||
return [...list].sort((a, b) => {
|
||||
const left = safeDate(a.fromDate)?.getTime() ?? 0;
|
||||
@@ -144,6 +181,35 @@ export function CvPage({ onLogout, onNavigate, onToggleTheme, theme }: CvPagePro
|
||||
const [designMode, setDesignMode] = useState<'standard' | 'reference'>('standard');
|
||||
const [snapshot, setSnapshot] = useState<CvPageSnapshot>(EMPTY_SNAPSHOT);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isWizardOpen, setIsWizardOpen] = useState(false);
|
||||
const [wizardView, setWizardView] = useState<CvWizardView>('menu');
|
||||
const [jobTitle, setJobTitle] = useState('');
|
||||
const [companyName, setCompanyName] = useState('');
|
||||
const [startMonth, setStartMonth] = useState('');
|
||||
const [endMonth, setEndMonth] = useState('');
|
||||
const [isCurrentRole, setIsCurrentRole] = useState(false);
|
||||
const [description, setDescription] = useState('');
|
||||
const [skillsQuery, setSkillsQuery] = useState('');
|
||||
const [selectedSkills, setSelectedSkills] = useState<string[]>([]);
|
||||
const [personalFirstName, setPersonalFirstName] = useState('');
|
||||
const [personalLastName, setPersonalLastName] = useState('');
|
||||
const [personalEmail, setPersonalEmail] = useState('');
|
||||
const [personalPhone, setPersonalPhone] = useState('');
|
||||
const [personalBirthday, setPersonalBirthday] = useState('');
|
||||
const [personalGender, setPersonalGender] = useState('');
|
||||
const [personalZip, setPersonalZip] = useState('');
|
||||
const [personalCity, setPersonalCity] = useState('');
|
||||
const [isBirthdayOpen, setIsBirthdayOpen] = useState(false);
|
||||
const [birthdayViewYear, setBirthdayViewYear] = useState(() => new Date().getFullYear());
|
||||
const [birthdayViewMonth, setBirthdayViewMonth] = useState(() => new Date().getMonth());
|
||||
const [personalDescriptionText, setPersonalDescriptionText] = useState('');
|
||||
const [personalImagePreview, setPersonalImagePreview] = useState('');
|
||||
const [personalImageFileName, setPersonalImageFileName] = useState('');
|
||||
const [personalImageObjectUrl, setPersonalImageObjectUrl] = useState<string | null>(null);
|
||||
const [languageName, setLanguageName] = useState('');
|
||||
const [languageLevelName, setLanguageLevelName] = useState('');
|
||||
const [driverLicenseNameInput, setDriverLicenseNameInput] = useState('');
|
||||
const [certificationInput, setCertificationInput] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
let active = true;
|
||||
@@ -166,7 +232,48 @@ export function CvPage({ onLogout, onNavigate, onToggleTheme, theme }: CvPagePro
|
||||
};
|
||||
}, [viewModel]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isWizardOpen) {
|
||||
return undefined;
|
||||
}
|
||||
const previousOverflow = document.body.style.overflow;
|
||||
document.body.style.overflow = 'hidden';
|
||||
const handleEscape = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Escape') {
|
||||
setIsWizardOpen(false);
|
||||
}
|
||||
};
|
||||
window.addEventListener('keydown', handleEscape);
|
||||
return () => {
|
||||
document.body.style.overflow = previousOverflow;
|
||||
window.removeEventListener('keydown', handleEscape);
|
||||
};
|
||||
}, [isWizardOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (personalImageObjectUrl) {
|
||||
URL.revokeObjectURL(personalImageObjectUrl);
|
||||
}
|
||||
};
|
||||
}, [personalImageObjectUrl]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isBirthdayOpen) {
|
||||
return undefined;
|
||||
}
|
||||
const handleOutside = (event: MouseEvent) => {
|
||||
const target = event.target as HTMLElement | null;
|
||||
if (!target?.closest('.cv-birthday-picker')) {
|
||||
setIsBirthdayOpen(false);
|
||||
}
|
||||
};
|
||||
document.addEventListener('click', handleOutside);
|
||||
return () => document.removeEventListener('click', handleOutside);
|
||||
}, [isBirthdayOpen]);
|
||||
|
||||
const candidate = snapshot.candidate;
|
||||
const candidateAddress = candidate?.address;
|
||||
const name = candidate?.firstName?.trim() || candidate?.name?.trim() || 'Lasse';
|
||||
const firstName = candidate?.firstName || 'Lasse';
|
||||
const lastName = candidate?.lastName || 'Hansen';
|
||||
@@ -187,6 +294,97 @@ export function CvPage({ onLogout, onNavigate, onToggleTheme, theme }: CvPagePro
|
||||
{ id: 'da', name: 'Dansk', level: 'Modersmal' },
|
||||
{ id: 'en', name: 'Engelsk', level: 'Flydende' },
|
||||
];
|
||||
const canSaveWizard = wizardView === 'experience'
|
||||
? Boolean(jobTitle.trim() && companyName.trim() && startMonth)
|
||||
: wizardView === 'personal'
|
||||
? Boolean(personalFirstName.trim() && personalLastName.trim() && personalEmail.trim())
|
||||
: wizardView === 'language'
|
||||
? Boolean(languageName.trim() && languageLevelName.trim())
|
||||
: wizardView === 'driverLicense'
|
||||
? Boolean(driverLicenseNameInput.trim())
|
||||
: wizardView === 'certification'
|
||||
? Boolean(certificationInput.trim())
|
||||
: true;
|
||||
const filteredSkills = ESCO_SKILLS.filter((skill) => !selectedSkills.includes(skill) && skill.toLowerCase().includes(skillsQuery.toLowerCase()));
|
||||
|
||||
const openWizard = () => {
|
||||
setIsWizardOpen(true);
|
||||
setWizardView('menu');
|
||||
};
|
||||
|
||||
const closeWizard = () => {
|
||||
setIsWizardOpen(false);
|
||||
setWizardView('menu');
|
||||
};
|
||||
|
||||
const openWizardView = (view: Exclude<CvWizardView, 'menu'>) => {
|
||||
setWizardView(view);
|
||||
};
|
||||
|
||||
const removeSkill = (value: string) => {
|
||||
setSelectedSkills((current) => current.filter((skill) => skill !== value));
|
||||
};
|
||||
|
||||
const selectSkill = (value: string) => {
|
||||
setSelectedSkills((current) => (current.includes(value) ? current : [...current, value]));
|
||||
setSkillsQuery('');
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!candidate || isWizardOpen) {
|
||||
return;
|
||||
}
|
||||
setPersonalFirstName(candidate.firstName || '');
|
||||
setPersonalLastName(candidate.lastName || '');
|
||||
setPersonalEmail(candidate.email || '');
|
||||
setPersonalPhone(candidate.phoneNumber || '');
|
||||
setPersonalBirthday(formatDateInput(candidate.birthday));
|
||||
setPersonalGender(candidate.gender || '');
|
||||
setPersonalZip(candidateAddress?.zip || '');
|
||||
setPersonalCity(candidateAddress?.zipName || candidateAddress?.additionalCityName || '');
|
||||
setPersonalDescriptionText(candidate.personalDescription || '');
|
||||
setPersonalImagePreview(profileImage || '');
|
||||
setPersonalImageFileName('');
|
||||
}, [candidate, candidateAddress, isWizardOpen, profileImage]);
|
||||
|
||||
const handlePersonalImageChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
const file = event.target.files?.[0];
|
||||
if (!file) {
|
||||
return;
|
||||
}
|
||||
if (personalImageObjectUrl) {
|
||||
URL.revokeObjectURL(personalImageObjectUrl);
|
||||
}
|
||||
const objectUrl = URL.createObjectURL(file);
|
||||
setPersonalImageObjectUrl(objectUrl);
|
||||
setPersonalImagePreview(objectUrl);
|
||||
setPersonalImageFileName(file.name);
|
||||
};
|
||||
|
||||
const birthdayDate = personalBirthday ? new Date(`${personalBirthday}T00:00:00`) : null;
|
||||
const birthdayLabel = birthdayDate
|
||||
? new Intl.DateTimeFormat('da-DK', { day: '2-digit', month: 'short', year: 'numeric' }).format(birthdayDate)
|
||||
: 'Vælg dato';
|
||||
const monthLabel = new Intl.DateTimeFormat('da-DK', { month: 'long' }).format(new Date(birthdayViewYear, birthdayViewMonth, 1));
|
||||
const monthStartDay = new Date(birthdayViewYear, birthdayViewMonth, 1).getDay();
|
||||
const normalizedOffset = (monthStartDay + 6) % 7;
|
||||
const monthDays = new Date(birthdayViewYear, birthdayViewMonth + 1, 0).getDate();
|
||||
const dayCells = Array.from({ length: normalizedOffset + monthDays }, (_, index) => (index < normalizedOffset ? null : index - normalizedOffset + 1));
|
||||
|
||||
const openBirthdayPicker = () => {
|
||||
if (birthdayDate) {
|
||||
setBirthdayViewYear(birthdayDate.getFullYear());
|
||||
setBirthdayViewMonth(birthdayDate.getMonth());
|
||||
}
|
||||
setIsBirthdayOpen(true);
|
||||
};
|
||||
|
||||
const selectBirthdayDay = (day: number) => {
|
||||
const month = String(birthdayViewMonth + 1).padStart(2, '0');
|
||||
const date = String(day).padStart(2, '0');
|
||||
setPersonalBirthday(`${birthdayViewYear}-${month}-${date}`);
|
||||
setIsBirthdayOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<section className={`dash-root ${theme === 'dark' ? 'theme-dark' : ''}`}>
|
||||
@@ -215,7 +413,7 @@ export function CvPage({ onLogout, onNavigate, onToggleTheme, theme }: CvPagePro
|
||||
<div>
|
||||
<h1>Dit CV</h1>
|
||||
</div>
|
||||
<button type="button" className="cv-edit-btn"><PenLine size={16} strokeWidth={1.8} /> Rediger CV</button>
|
||||
<button type="button" className="cv-edit-btn" onClick={openWizard}><PenLine size={16} strokeWidth={1.8} /> Rediger CV</button>
|
||||
</div>
|
||||
|
||||
{isLoading ? <p className="dash-loading">Indlaeser CV...</p> : null}
|
||||
@@ -375,6 +573,327 @@ export function CvPage({ onLogout, onNavigate, onToggleTheme, theme }: CvPagePro
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
{isWizardOpen ? (
|
||||
<div className="cv-modal-overlay" onClick={closeWizard} role="presentation">
|
||||
<div className="cv-modal" onClick={(event) => event.stopPropagation()} role="dialog" aria-modal="true" aria-label="Rediger CV">
|
||||
<div className="cv-modal-header">
|
||||
<div className="cv-modal-title-wrap">
|
||||
{wizardView !== 'menu' ? (
|
||||
<button type="button" className="cv-modal-icon-btn" onClick={() => setWizardView('menu')} aria-label="Tilbage">
|
||||
<ArrowLeft size={18} strokeWidth={1.8} />
|
||||
</button>
|
||||
) : null}
|
||||
<h2>
|
||||
{wizardView === 'menu' && 'Tilføj til CV'}
|
||||
{wizardView === 'experience' && 'Tilføj Erhvervserfaring'}
|
||||
{wizardView === 'education' && 'Tilføj Uddannelse'}
|
||||
{wizardView === 'personal' && 'Opdater Personlige Oplysninger'}
|
||||
{wizardView === 'skills' && 'Tilføj Kvalifikationer'}
|
||||
{wizardView === 'language' && 'Tilføj Sprog'}
|
||||
{wizardView === 'driverLicense' && 'Tilføj Kørekort'}
|
||||
{wizardView === 'certification' && 'Tilføj Certifikat'}
|
||||
</h2>
|
||||
</div>
|
||||
<button type="button" className="cv-modal-icon-btn" onClick={closeWizard} aria-label="Luk">
|
||||
<X size={18} strokeWidth={1.8} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="cv-modal-body custom-scrollbar">
|
||||
{wizardView === 'menu' ? (
|
||||
<div className="cv-wizard-grid">
|
||||
<button type="button" className="cv-wizard-menu-card is-cyan" onClick={() => openWizardView('personal')}>
|
||||
<span className="cv-wizard-menu-icon"><UserRound size={20} strokeWidth={1.8} /></span>
|
||||
<strong>Personlige oplysninger</strong>
|
||||
<p>Opdater kontaktinfo, billede og grundlæggende detaljer.</p>
|
||||
</button>
|
||||
<button type="button" className="cv-wizard-menu-card is-teal" onClick={() => openWizardView('experience')}>
|
||||
<span className="cv-wizard-menu-icon"><Briefcase size={20} strokeWidth={1.8} /></span>
|
||||
<strong>Erhvervserfaring</strong>
|
||||
<p>Tilføj tidligere eller nuværende jobs og ansvarsområder.</p>
|
||||
</button>
|
||||
<button type="button" className="cv-wizard-menu-card is-indigo" onClick={() => openWizardView('education')}>
|
||||
<span className="cv-wizard-menu-icon"><GraduationCap size={20} strokeWidth={1.8} /></span>
|
||||
<strong>Uddannelse</strong>
|
||||
<p>Tilføj skoler, universiteter og studieretninger.</p>
|
||||
</button>
|
||||
<button type="button" className="cv-wizard-menu-card is-amber" onClick={() => openWizardView('skills')}>
|
||||
<span className="cv-wizard-menu-icon"><Star size={20} strokeWidth={1.8} /></span>
|
||||
<strong>Kvalifikationer</strong>
|
||||
<p>Fremhæv dine faglige færdigheder og kompetencer.</p>
|
||||
</button>
|
||||
<button type="button" className="cv-wizard-menu-card is-cyan" onClick={() => openWizardView('language')}>
|
||||
<span className="cv-wizard-menu-icon"><Globe size={20} strokeWidth={1.8} /></span>
|
||||
<strong>Sprog</strong>
|
||||
<p>Tilføj sprog og dit niveau.</p>
|
||||
</button>
|
||||
<button type="button" className="cv-wizard-menu-card is-indigo" onClick={() => openWizardView('driverLicense')}>
|
||||
<span className="cv-wizard-menu-icon"><Car size={20} strokeWidth={1.8} /></span>
|
||||
<strong>Kørekort</strong>
|
||||
<p>Tilføj de kørekortkategorier du har.</p>
|
||||
</button>
|
||||
<button type="button" className="cv-wizard-menu-card is-teal" onClick={() => openWizardView('certification')}>
|
||||
<span className="cv-wizard-menu-icon"><BadgeCheck size={20} strokeWidth={1.8} /></span>
|
||||
<strong>Certifikater</strong>
|
||||
<p>Tilføj relevante certificeringer.</p>
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{wizardView === 'personal' ? (
|
||||
<form className="cv-wizard-form" onSubmit={(event) => event.preventDefault()}>
|
||||
<div className="cv-upload-wrap">
|
||||
<div className="cv-upload-preview">
|
||||
{personalImagePreview ? <img src={personalImagePreview} alt="Profil" /> : <UserRound size={30} strokeWidth={1.8} />}
|
||||
</div>
|
||||
<div className="cv-upload-meta">
|
||||
<label className="cv-upload-btn">
|
||||
<input type="file" accept="image/*" onChange={handlePersonalImageChange} />
|
||||
Upload billede
|
||||
</label>
|
||||
<small>{personalImageFileName || 'PNG/JPG op til 5MB'}</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="cv-wizard-grid-2">
|
||||
<label className="cv-field">
|
||||
<span>Fornavn *</span>
|
||||
<input value={personalFirstName} onChange={(event) => setPersonalFirstName(event.target.value)} placeholder="Fornavn" />
|
||||
</label>
|
||||
<label className="cv-field">
|
||||
<span>Efternavn *</span>
|
||||
<input value={personalLastName} onChange={(event) => setPersonalLastName(event.target.value)} placeholder="Efternavn" />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="cv-wizard-grid-2">
|
||||
<label className="cv-field">
|
||||
<span>E-mail *</span>
|
||||
<input type="email" value={personalEmail} onChange={(event) => setPersonalEmail(event.target.value)} placeholder="mail@eksempel.dk" />
|
||||
</label>
|
||||
<label className="cv-field">
|
||||
<span>Telefon</span>
|
||||
<input value={personalPhone} onChange={(event) => setPersonalPhone(event.target.value)} placeholder="+45 12 34 56 78" />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="cv-wizard-grid-2">
|
||||
<label className="cv-field">
|
||||
<span>Fødselsdato</span>
|
||||
<div className="cv-birthday-picker">
|
||||
<button type="button" className="cv-birthday-trigger" onClick={openBirthdayPicker}>
|
||||
<CalendarDays size={16} strokeWidth={1.8} />
|
||||
<span>{birthdayLabel}</span>
|
||||
</button>
|
||||
{isBirthdayOpen ? (
|
||||
<div className="cv-birthday-popover">
|
||||
<div className="cv-birthday-header">
|
||||
<button type="button" onClick={() => {
|
||||
if (birthdayViewMonth === 0) {
|
||||
setBirthdayViewMonth(11);
|
||||
setBirthdayViewYear((year) => year - 1);
|
||||
return;
|
||||
}
|
||||
setBirthdayViewMonth((month) => month - 1);
|
||||
}}
|
||||
>
|
||||
<ChevronLeft size={16} strokeWidth={1.8} />
|
||||
</button>
|
||||
<strong>{monthLabel} {birthdayViewYear}</strong>
|
||||
<button type="button" onClick={() => {
|
||||
if (birthdayViewMonth === 11) {
|
||||
setBirthdayViewMonth(0);
|
||||
setBirthdayViewYear((year) => year + 1);
|
||||
return;
|
||||
}
|
||||
setBirthdayViewMonth((month) => month + 1);
|
||||
}}
|
||||
>
|
||||
<ChevronRight size={16} strokeWidth={1.8} />
|
||||
</button>
|
||||
</div>
|
||||
<div className="cv-birthday-weekdays">
|
||||
<span>Ma</span><span>Ti</span><span>On</span><span>To</span><span>Fr</span><span>Lø</span><span>Sø</span>
|
||||
</div>
|
||||
<div className="cv-birthday-days">
|
||||
{dayCells.map((day, index) => {
|
||||
if (!day) {
|
||||
return <span key={`empty-${index}`} className="cv-birthday-empty" />;
|
||||
}
|
||||
const isSelected = birthdayDate
|
||||
? birthdayDate.getFullYear() === birthdayViewYear
|
||||
&& birthdayDate.getMonth() === birthdayViewMonth
|
||||
&& birthdayDate.getDate() === day
|
||||
: false;
|
||||
return (
|
||||
<button
|
||||
key={`${birthdayViewYear}-${birthdayViewMonth}-${day}`}
|
||||
type="button"
|
||||
className={isSelected ? 'is-selected' : ''}
|
||||
onClick={() => selectBirthdayDay(day)}
|
||||
>
|
||||
{day}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</label>
|
||||
<label className="cv-field">
|
||||
<span>Køn</span>
|
||||
<input value={personalGender} onChange={(event) => setPersonalGender(event.target.value)} placeholder="F.eks. Mand/Kvinde/Andet" />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="cv-wizard-grid-2">
|
||||
<label className="cv-field">
|
||||
<span>Postnummer</span>
|
||||
<input value={personalZip} onChange={(event) => setPersonalZip(event.target.value)} placeholder="F.eks. 2100" />
|
||||
</label>
|
||||
<label className="cv-field">
|
||||
<span>By</span>
|
||||
<input value={personalCity} onChange={(event) => setPersonalCity(event.target.value)} placeholder="F.eks. København Ø" />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<label className="cv-field">
|
||||
<span>Personlig beskrivelse</span>
|
||||
<textarea rows={4} value={personalDescriptionText} onChange={(event) => setPersonalDescriptionText(event.target.value)} placeholder="Kort beskrivelse af dig selv..." />
|
||||
</label>
|
||||
</form>
|
||||
) : null}
|
||||
|
||||
{wizardView === 'experience' ? (
|
||||
<form className="cv-wizard-form" onSubmit={(event) => event.preventDefault()}>
|
||||
<div className="cv-wizard-grid-2">
|
||||
<label className="cv-field">
|
||||
<span>Stillingstitel *</span>
|
||||
<input value={jobTitle} onChange={(event) => setJobTitle(event.target.value)} placeholder="F.eks. Frontend Udvikler" />
|
||||
</label>
|
||||
<label className="cv-field">
|
||||
<span>Virksomhed *</span>
|
||||
<input value={companyName} onChange={(event) => setCompanyName(event.target.value)} placeholder="F.eks. Arbejd.com" />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="cv-wizard-grid-2">
|
||||
<label className="cv-field">
|
||||
<span>Startdato *</span>
|
||||
<div className="cv-field-icon-wrap">
|
||||
<CalendarDays size={16} strokeWidth={1.8} />
|
||||
<input type="month" value={startMonth} onChange={(event) => setStartMonth(event.target.value)} />
|
||||
</div>
|
||||
</label>
|
||||
<label className="cv-field">
|
||||
<span>Slutdato</span>
|
||||
<div className="cv-field-icon-wrap">
|
||||
<CalendarDays size={16} strokeWidth={1.8} />
|
||||
<input type="month" value={endMonth} onChange={(event) => setEndMonth(event.target.value)} disabled={isCurrentRole} />
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<label className="cv-wizard-checkbox">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={isCurrentRole}
|
||||
onChange={(event) => {
|
||||
setIsCurrentRole(event.target.checked);
|
||||
if (event.target.checked) {
|
||||
setEndMonth('');
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<span>Jeg arbejder her stadig</span>
|
||||
</label>
|
||||
|
||||
<label className="cv-field">
|
||||
<span>Beskrivelse</span>
|
||||
<textarea
|
||||
rows={4}
|
||||
value={description}
|
||||
onChange={(event) => setDescription(event.target.value)}
|
||||
placeholder="Beskriv dine primære opgaver og resultater..."
|
||||
/>
|
||||
</label>
|
||||
|
||||
<div className="cv-field">
|
||||
<span>Faerdigheder (ESCO)</span>
|
||||
<div className="cv-skill-pills">
|
||||
{selectedSkills.map((skill) => (
|
||||
<span key={skill} className="cv-skill-pill">
|
||||
{skill}
|
||||
<button type="button" onClick={() => removeSkill(skill)} aria-label={`Fjern ${skill}`}>
|
||||
<X size={12} strokeWidth={2} />
|
||||
</button>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<div className="cv-skill-search">
|
||||
<Search size={16} strokeWidth={1.8} />
|
||||
<input
|
||||
value={skillsQuery}
|
||||
onChange={(event) => setSkillsQuery(event.target.value)}
|
||||
placeholder="Søg færdigheder (f.eks. JavaScript, Salg...)"
|
||||
/>
|
||||
</div>
|
||||
{skillsQuery.trim().length > 0 ? (
|
||||
<div className="cv-skill-dropdown custom-scrollbar">
|
||||
{filteredSkills.length > 0 ? filteredSkills.map((skill) => (
|
||||
<button key={skill} type="button" onClick={() => selectSkill(skill)}>{skill}</button>
|
||||
)) : <span className="cv-skill-empty">Ingen resultater fundet.</span>}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</form>
|
||||
) : null}
|
||||
|
||||
{wizardView === 'education' ? <p className="cv-wizard-placeholder">Uddannelsesformularen bliver tilfoejet i naeste iteration.</p> : null}
|
||||
{wizardView === 'skills' ? <p className="cv-wizard-placeholder">Kvalifikationsformularen bliver tilfoejet i naeste iteration.</p> : null}
|
||||
{wizardView === 'language' ? (
|
||||
<form className="cv-wizard-form" onSubmit={(event) => event.preventDefault()}>
|
||||
<label className="cv-field">
|
||||
<span>Sprog *</span>
|
||||
<input value={languageName} onChange={(event) => setLanguageName(event.target.value)} placeholder="F.eks. Engelsk" />
|
||||
</label>
|
||||
<label className="cv-field">
|
||||
<span>Niveau *</span>
|
||||
<input value={languageLevelName} onChange={(event) => setLanguageLevelName(event.target.value)} placeholder="F.eks. Flydende" />
|
||||
</label>
|
||||
</form>
|
||||
) : null}
|
||||
{wizardView === 'driverLicense' ? (
|
||||
<form className="cv-wizard-form" onSubmit={(event) => event.preventDefault()}>
|
||||
<label className="cv-field">
|
||||
<span>Kørekortkategori *</span>
|
||||
<input value={driverLicenseNameInput} onChange={(event) => setDriverLicenseNameInput(event.target.value)} placeholder="F.eks. B (Almindelig bil)" />
|
||||
</label>
|
||||
</form>
|
||||
) : null}
|
||||
{wizardView === 'certification' ? (
|
||||
<form className="cv-wizard-form" onSubmit={(event) => event.preventDefault()}>
|
||||
<label className="cv-field">
|
||||
<span>Certifikat *</span>
|
||||
<input value={certificationInput} onChange={(event) => setCertificationInput(event.target.value)} placeholder="F.eks. AWS Certified Developer" />
|
||||
</label>
|
||||
</form>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="cv-modal-footer">
|
||||
<button type="button" className="cv-modal-cancel" onClick={closeWizard}>Annuller</button>
|
||||
{wizardView !== 'menu' ? (
|
||||
<button type="button" className="cv-modal-save" disabled={!canSaveWizard} onClick={closeWizard}>
|
||||
Gem aendringer
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -352,6 +352,519 @@
|
||||
background: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
|
||||
.cv-modal-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 120;
|
||||
background: rgba(17, 24, 39, 0.3);
|
||||
backdrop-filter: blur(8px);
|
||||
-webkit-backdrop-filter: blur(8px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.cv-modal {
|
||||
width: min(760px, 100%);
|
||||
max-height: min(90vh, 920px);
|
||||
background: rgba(255, 255, 255, 0.42);
|
||||
border: 1px solid rgba(255, 255, 255, 0.7);
|
||||
border-radius: 2rem;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 28px 60px rgba(15, 23, 42, 0.2);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.cv-modal-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 1.1rem 1.25rem;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.45);
|
||||
background: rgba(255, 255, 255, 0.18);
|
||||
}
|
||||
|
||||
.cv-modal-title-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.6rem;
|
||||
}
|
||||
|
||||
.cv-modal-title-wrap h2 {
|
||||
margin: 0;
|
||||
font-size: 1.2rem;
|
||||
letter-spacing: -0.01em;
|
||||
font-weight: 500;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.cv-modal-icon-btn {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
border: 0;
|
||||
border-radius: 999px;
|
||||
background: transparent;
|
||||
color: #4b5563;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.cv-modal-icon-btn:hover {
|
||||
background: rgba(255, 255, 255, 0.45);
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.cv-modal-body {
|
||||
padding: 1.25rem;
|
||||
overflow: auto;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.cv-wizard-grid {
|
||||
display: grid;
|
||||
gap: 0.8rem;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.cv-wizard-menu-card {
|
||||
border: 1px solid rgba(255, 255, 255, 0.65);
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
border-radius: 1rem;
|
||||
padding: 1rem;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.cv-wizard-menu-card:hover {
|
||||
background: rgba(255, 255, 255, 0.58);
|
||||
}
|
||||
|
||||
.cv-wizard-menu-icon {
|
||||
width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
border-radius: 0.75rem;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 0.8rem;
|
||||
}
|
||||
|
||||
.cv-wizard-menu-card.is-teal .cv-wizard-menu-icon {
|
||||
background: #f0fdfa;
|
||||
color: #0f766e;
|
||||
border: 1px solid #ccfbf1;
|
||||
}
|
||||
|
||||
.cv-wizard-menu-card.is-indigo .cv-wizard-menu-icon {
|
||||
background: #eef2ff;
|
||||
color: #4338ca;
|
||||
border: 1px solid #c7d2fe;
|
||||
}
|
||||
|
||||
.cv-wizard-menu-card.is-cyan .cv-wizard-menu-icon {
|
||||
background: #ecfeff;
|
||||
color: #0891b2;
|
||||
border: 1px solid #bae6fd;
|
||||
}
|
||||
|
||||
.cv-wizard-menu-card.is-amber .cv-wizard-menu-icon {
|
||||
background: #fffbeb;
|
||||
color: #d97706;
|
||||
border: 1px solid #fde68a;
|
||||
}
|
||||
|
||||
.cv-wizard-menu-card strong {
|
||||
display: block;
|
||||
color: #111827;
|
||||
font-size: 0.96rem;
|
||||
margin-bottom: 0.3rem;
|
||||
}
|
||||
|
||||
.cv-wizard-menu-card p {
|
||||
margin: 0;
|
||||
color: #4b5563;
|
||||
font-size: 0.76rem;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.cv-wizard-form {
|
||||
display: grid;
|
||||
gap: 0.9rem;
|
||||
}
|
||||
|
||||
.cv-wizard-grid-2 {
|
||||
display: grid;
|
||||
gap: 0.9rem;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.cv-field {
|
||||
display: grid;
|
||||
gap: 0.35rem;
|
||||
}
|
||||
|
||||
.cv-field span {
|
||||
font-size: 0.82rem;
|
||||
color: #1f2937;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.cv-field input,
|
||||
.cv-field textarea {
|
||||
width: 100%;
|
||||
border: 1px solid rgba(255, 255, 255, 0.65);
|
||||
background: rgba(255, 255, 255, 0.38);
|
||||
border-radius: 0.8rem;
|
||||
padding: 0.65rem 0.8rem;
|
||||
color: #111827;
|
||||
font-size: 0.86rem;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.cv-field input:focus,
|
||||
.cv-field textarea:focus {
|
||||
border-color: #14b8a6;
|
||||
box-shadow: 0 0 0 1px rgba(20, 184, 166, 0.5);
|
||||
background: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
|
||||
.cv-field textarea {
|
||||
resize: vertical;
|
||||
min-height: 96px;
|
||||
}
|
||||
|
||||
.cv-field-icon-wrap {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.cv-field-icon-wrap svg {
|
||||
position: absolute;
|
||||
left: 0.7rem;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.cv-field-icon-wrap input {
|
||||
padding-left: 2rem;
|
||||
}
|
||||
|
||||
.cv-birthday-picker {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.cv-birthday-trigger {
|
||||
width: 100%;
|
||||
border: 1px solid rgba(255, 255, 255, 0.65);
|
||||
background: rgba(255, 255, 255, 0.38);
|
||||
border-radius: 0.8rem;
|
||||
padding: 0.65rem 0.8rem;
|
||||
color: #111827;
|
||||
font-size: 0.86rem;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.55rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.cv-birthday-trigger:hover {
|
||||
background: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
|
||||
.cv-birthday-popover {
|
||||
position: absolute;
|
||||
top: calc(100% + 0.45rem);
|
||||
left: 0;
|
||||
z-index: 30;
|
||||
width: 268px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.75);
|
||||
background: rgba(255, 255, 255, 0.75);
|
||||
backdrop-filter: blur(18px);
|
||||
-webkit-backdrop-filter: blur(18px);
|
||||
border-radius: 0.9rem;
|
||||
box-shadow: 0 14px 35px rgba(15, 23, 42, 0.14);
|
||||
padding: 0.7rem;
|
||||
}
|
||||
|
||||
.cv-birthday-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.cv-birthday-header button {
|
||||
width: 1.8rem;
|
||||
height: 1.8rem;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
border-radius: 0.55rem;
|
||||
color: #4b5563;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.cv-birthday-header button:hover {
|
||||
background: rgba(255, 255, 255, 0.75);
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.cv-birthday-header strong {
|
||||
font-size: 0.84rem;
|
||||
color: #111827;
|
||||
font-weight: 600;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.cv-birthday-weekdays,
|
||||
.cv-birthday-days {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, minmax(0, 1fr));
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.cv-birthday-weekdays span {
|
||||
text-align: center;
|
||||
font-size: 0.7rem;
|
||||
color: #6b7280;
|
||||
padding: 0.15rem 0;
|
||||
}
|
||||
|
||||
.cv-birthday-days button,
|
||||
.cv-birthday-empty {
|
||||
width: 100%;
|
||||
height: 1.9rem;
|
||||
border-radius: 0.55rem;
|
||||
}
|
||||
|
||||
.cv-birthday-days button {
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: #111827;
|
||||
font-size: 0.78rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.cv-birthday-days button:hover {
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
.cv-birthday-days button.is-selected {
|
||||
background: #0f766e;
|
||||
color: #ffffff;
|
||||
box-shadow: 0 4px 10px rgba(15, 118, 110, 0.28);
|
||||
}
|
||||
|
||||
.cv-upload-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.9rem;
|
||||
padding: 0.85rem;
|
||||
border: 1px solid rgba(255, 255, 255, 0.65);
|
||||
border-radius: 0.95rem;
|
||||
background: rgba(255, 255, 255, 0.35);
|
||||
}
|
||||
|
||||
.cv-upload-preview {
|
||||
width: 72px;
|
||||
height: 72px;
|
||||
border-radius: 1rem;
|
||||
border: 2px solid rgba(255, 255, 255, 0.85);
|
||||
background: rgba(255, 255, 255, 0.45);
|
||||
display: grid;
|
||||
place-items: center;
|
||||
overflow: hidden;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.cv-upload-preview img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.cv-upload-meta {
|
||||
display: grid;
|
||||
gap: 0.35rem;
|
||||
}
|
||||
|
||||
.cv-upload-meta small {
|
||||
color: #6b7280;
|
||||
font-size: 0.72rem;
|
||||
}
|
||||
|
||||
.cv-upload-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 1px solid rgba(255, 255, 255, 0.7);
|
||||
border-radius: 0.7rem;
|
||||
background: rgba(255, 255, 255, 0.5);
|
||||
color: #111827;
|
||||
padding: 0.48rem 0.72rem;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.cv-upload-btn:hover {
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
|
||||
.cv-upload-btn input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.cv-wizard-checkbox {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
color: #374151;
|
||||
font-size: 0.8rem;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.cv-skill-pills {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.45rem;
|
||||
}
|
||||
|
||||
.cv-skill-pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
padding: 0.35rem 0.58rem;
|
||||
border-radius: 0.6rem;
|
||||
border: 1px solid #99f6e4;
|
||||
background: rgba(240, 253, 250, 0.95);
|
||||
color: #115e59;
|
||||
font-size: 0.73rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.cv-skill-pill button {
|
||||
border: 0;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
border-radius: 999px;
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.cv-skill-search {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.cv-skill-search svg {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 0.72rem;
|
||||
transform: translateY(-50%);
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.cv-skill-search input {
|
||||
width: 100%;
|
||||
border: 1px solid rgba(255, 255, 255, 0.65);
|
||||
background: rgba(255, 255, 255, 0.38);
|
||||
border-radius: 0.8rem;
|
||||
padding: 0.65rem 0.8rem 0.65rem 2rem;
|
||||
color: #111827;
|
||||
font-size: 0.86rem;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.cv-skill-dropdown {
|
||||
display: grid;
|
||||
gap: 0.2rem;
|
||||
max-height: 170px;
|
||||
overflow: auto;
|
||||
border: 1px solid rgba(255, 255, 255, 0.7);
|
||||
background: rgba(255, 255, 255, 0.55);
|
||||
border-radius: 0.75rem;
|
||||
padding: 0.3rem;
|
||||
}
|
||||
|
||||
.cv-skill-dropdown button {
|
||||
border: 0;
|
||||
text-align: left;
|
||||
background: transparent;
|
||||
border-radius: 0.55rem;
|
||||
padding: 0.45rem 0.55rem;
|
||||
color: #1f2937;
|
||||
font-size: 0.83rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.cv-skill-dropdown button:hover {
|
||||
background: rgba(255, 255, 255, 0.65);
|
||||
}
|
||||
|
||||
.cv-skill-empty {
|
||||
color: #6b7280;
|
||||
font-size: 0.82rem;
|
||||
padding: 0.45rem 0.55rem;
|
||||
}
|
||||
|
||||
.cv-wizard-placeholder {
|
||||
margin: 0;
|
||||
font-size: 0.9rem;
|
||||
color: #4b5563;
|
||||
padding: 0.5rem 0.1rem;
|
||||
}
|
||||
|
||||
.cv-modal-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
gap: 0.6rem;
|
||||
padding: 1rem 1.25rem;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.45);
|
||||
background: rgba(255, 255, 255, 0.18);
|
||||
}
|
||||
|
||||
.cv-modal-cancel,
|
||||
.cv-modal-save {
|
||||
border: 0;
|
||||
border-radius: 0.8rem;
|
||||
font-size: 0.82rem;
|
||||
font-weight: 500;
|
||||
padding: 0.62rem 1rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.cv-modal-cancel {
|
||||
border: 1px solid rgba(255, 255, 255, 0.65);
|
||||
background: rgba(255, 255, 255, 0.4);
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.cv-modal-cancel:hover {
|
||||
background: rgba(255, 255, 255, 0.62);
|
||||
}
|
||||
|
||||
.cv-modal-save {
|
||||
background: #111827;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.cv-modal-save:disabled {
|
||||
opacity: 0.45;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.cv-design-reference .cv-card {
|
||||
border-radius: 28px;
|
||||
box-shadow: 0 10px 28px rgba(15, 23, 42, 0.05);
|
||||
@@ -468,4 +981,12 @@
|
||||
.cv-design-toggle span {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.cv-wizard-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.cv-wizard-grid-2 {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,8 @@ export function HomePage() {
|
||||
const [isNavOpen, setIsNavOpen] = useState(false);
|
||||
const [isTipsOpen, setIsTipsOpen] = useState(false);
|
||||
const [isHowOpen, setIsHowOpen] = useState(false);
|
||||
const [isMobileTipsOpen, setIsMobileTipsOpen] = useState(false);
|
||||
const [isMobileHowOpen, setIsMobileHowOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isNavOpen) {
|
||||
@@ -56,6 +58,8 @@ export function HomePage() {
|
||||
function handleResize() {
|
||||
if (window.innerWidth > 990) {
|
||||
setIsNavOpen(false);
|
||||
setIsMobileHowOpen(false);
|
||||
setIsMobileTipsOpen(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -156,19 +160,60 @@ export function HomePage() {
|
||||
className="homepage-nav-hamburger"
|
||||
aria-expanded={isNavOpen}
|
||||
aria-label={isNavOpen ? 'Luk menu' : 'Åbn menu'}
|
||||
onClick={() => setIsNavOpen((current) => !current)}
|
||||
onClick={() => {
|
||||
setIsNavOpen((current) => {
|
||||
const next = !current;
|
||||
if (!next) {
|
||||
setIsMobileHowOpen(false);
|
||||
setIsMobileTipsOpen(false);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
}}
|
||||
>
|
||||
<IconifyIcon icon={isNavOpen ? 'solar:close-circle-linear' : 'solar:hamburger-menu-linear'} className="text-xl text-gray-800" style={{ strokeWidth: 1.8 }} />
|
||||
</button>
|
||||
|
||||
<div className={isNavOpen ? 'homepage-nav-popup open' : 'homepage-nav-popup'}>
|
||||
<a href="/pricing" onClick={() => setIsNavOpen(false)}>Priser</a>
|
||||
<a href="#" onClick={() => setIsNavOpen(false)}>Sådan virker det: For virksomheder</a>
|
||||
<a href="#" onClick={() => setIsNavOpen(false)}>Sådan virker det: For jobsøgere</a>
|
||||
<a href="#" onClick={() => setIsNavOpen(false)}>Sådan virker det: FAQ</a>
|
||||
<a href="#" onClick={() => setIsNavOpen(false)}>Sådan virker det: Nyhedsbrev</a>
|
||||
<a href="/stories" onClick={() => setIsNavOpen(false)}>Tips og tricks: Stories</a>
|
||||
<a href="/jobordbogen" onClick={() => setIsNavOpen(false)}>Tips og tricks: Jobordbogen</a>
|
||||
<div className="homepage-nav-popup-group">
|
||||
<button
|
||||
type="button"
|
||||
className="homepage-nav-popup-group-trigger"
|
||||
onClick={() => {
|
||||
setIsMobileHowOpen((current) => !current);
|
||||
setIsMobileTipsOpen(false);
|
||||
}}
|
||||
aria-expanded={isMobileHowOpen}
|
||||
>
|
||||
Sådan virker det
|
||||
<IconifyIcon icon="solar:alt-arrow-down-linear" className={`text-base transition-transform ${isMobileHowOpen ? 'rotate-180' : ''}`} style={{ strokeWidth: 1.5 }} />
|
||||
</button>
|
||||
<div className={isMobileHowOpen ? 'homepage-nav-popup-submenu open' : 'homepage-nav-popup-submenu'}>
|
||||
<a href="#" onClick={() => setIsNavOpen(false)}>For virksomheder</a>
|
||||
<a href="#" onClick={() => setIsNavOpen(false)}>For jobsøgere</a>
|
||||
<a href="#" onClick={() => setIsNavOpen(false)}>FAQ</a>
|
||||
<a href="#" onClick={() => setIsNavOpen(false)}>Nyhedsbrev</a>
|
||||
</div>
|
||||
</div>
|
||||
<div className="homepage-nav-popup-group">
|
||||
<button
|
||||
type="button"
|
||||
className="homepage-nav-popup-group-trigger"
|
||||
onClick={() => {
|
||||
setIsMobileTipsOpen((current) => !current);
|
||||
setIsMobileHowOpen(false);
|
||||
}}
|
||||
aria-expanded={isMobileTipsOpen}
|
||||
>
|
||||
Tips og tricks
|
||||
<IconifyIcon icon="solar:alt-arrow-down-linear" className={`text-base transition-transform ${isMobileTipsOpen ? 'rotate-180' : ''}`} style={{ strokeWidth: 1.5 }} />
|
||||
</button>
|
||||
<div className={isMobileTipsOpen ? 'homepage-nav-popup-submenu open' : 'homepage-nav-popup-submenu'}>
|
||||
<a href="/stories" onClick={() => setIsNavOpen(false)}>Stories</a>
|
||||
<a href="/jobordbogen" onClick={() => setIsNavOpen(false)}>Jobordbogen</a>
|
||||
</div>
|
||||
</div>
|
||||
<a href="#" onClick={() => setIsNavOpen(false)}>Log ind</a>
|
||||
<a href="#" className="homepage-nav-popup-cta" onClick={() => setIsNavOpen(false)}>Opret dig</a>
|
||||
</div>
|
||||
@@ -224,14 +269,17 @@ export function HomePage() {
|
||||
<div className="absolute top-1/4 left-[10%] w-24 h-24 rounded-full bg-gradient-to-tr from-white/40 via-white/10 to-teal-50/30 backdrop-blur-xl border border-white/60 shadow-[0_8px_40px_rgba(0,0,0,0.06)] z-0 animate-float-ambient-home" />
|
||||
<div className="absolute bottom-1/5 right-[10%] w-32 h-32 rounded-[2rem] rotate-12 bg-gradient-to-tr from-white/40 via-white/10 to-indigo-50/30 backdrop-blur-xl border border-white/60 shadow-[0_8px_40px_rgba(0,0,0,0.06)] z-0 animate-float-ambient-home [animation-delay:2s]" />
|
||||
|
||||
<div className="relative flex items-center justify-center w-full max-w-5xl mx-auto transform scale-[0.65] sm:scale-75 md:scale-90 lg:scale-100 -space-x-12 sm:-space-x-16 md:-space-x-20">
|
||||
<div className="homepage-glass-glare relative w-[320px] h-[680px] sm:w-[360px] sm:h-[740px] rounded-[3.5rem] p-3 bg-gradient-to-br from-white/40 to-white/10 backdrop-blur-3xl border border-white/50 shadow-[0_30px_60px_rgba(0,0,0,0.15),inset_0_0_20px_rgba(255,255,255,0.6)] animate-float-1-home z-20">
|
||||
<div className="relative flex items-center justify-center w-full max-w-5xl mx-auto transform scale-[0.55] sm:scale-75 md:scale-90 lg:scale-100 -space-x-12 sm:-space-x-16 md:-space-x-20">
|
||||
<div className="homepage-glass-glare relative shrink-0 w-[320px] h-[680px] sm:w-[360px] sm:h-[740px] rounded-[3.5rem] p-3 bg-gradient-to-br from-white/40 to-white/10 backdrop-blur-3xl border border-white/50 shadow-[0_30px_60px_rgba(0,0,0,0.15),inset_0_0_20px_rgba(255,255,255,0.6)] animate-float-1-home z-20">
|
||||
<div className="absolute -left-[2px] top-28 w-1 h-8 bg-white/40 border border-white/50 rounded-l-md shadow-sm" />
|
||||
<div className="absolute -left-[2px] top-40 w-1 h-14 bg-white/40 border border-white/50 rounded-l-md shadow-sm" />
|
||||
<div className="absolute -left-[2px] top-56 w-1 h-14 bg-white/40 border border-white/50 rounded-l-md shadow-sm" />
|
||||
<div className="absolute -right-[2px] top-44 w-1 h-20 bg-white/40 border border-white/50 rounded-r-md shadow-sm" />
|
||||
|
||||
<div className="relative w-full h-full rounded-[2.8rem] bg-white overflow-hidden shadow-[inset_0_0_10px_rgba(255,255,255,0.1)] border border-white/60 isolate">
|
||||
<div
|
||||
className="relative w-full h-full rounded-[2.8rem] bg-white overflow-hidden isolate"
|
||||
style={{ WebkitMaskImage: '-webkit-radial-gradient(white, black)' }}
|
||||
>
|
||||
<img src={screen1Image} alt="App UI Design 1" className="absolute inset-0 w-full h-full object-cover z-0" />
|
||||
|
||||
<div className="absolute top-2.5 left-1/2 -translate-x-1/2 w-[100px] h-[30px] bg-black rounded-full z-50 shadow-[0_4px_10px_rgba(0,0,0,0.3)] flex items-center justify-between px-3">
|
||||
@@ -254,13 +302,16 @@ export function HomePage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="homepage-glass-glare relative w-[320px] h-[680px] sm:w-[360px] sm:h-[740px] rounded-[3.5rem] p-3 bg-gradient-to-br from-white/30 to-white/5 backdrop-blur-2xl border border-white/40 shadow-[0_20px_50px_rgba(0,0,0,0.1),inset_0_0_20px_rgba(255,255,255,0.4)] animate-float-2-home z-10">
|
||||
<div className="homepage-glass-glare relative shrink-0 w-[320px] h-[680px] sm:w-[360px] sm:h-[740px] rounded-[3.5rem] p-3 bg-gradient-to-br from-white/30 to-white/5 backdrop-blur-2xl border border-white/40 shadow-[0_20px_50px_rgba(0,0,0,0.1),inset_0_0_20px_rgba(255,255,255,0.4)] animate-float-2-home z-10">
|
||||
<div className="absolute -left-[2px] top-28 w-1 h-8 bg-white/30 border border-white/40 rounded-l-md shadow-sm" />
|
||||
<div className="absolute -left-[2px] top-40 w-1 h-14 bg-white/30 border border-white/40 rounded-l-md shadow-sm" />
|
||||
<div className="absolute -left-[2px] top-56 w-1 h-14 bg-white/30 border border-white/40 rounded-l-md shadow-sm" />
|
||||
<div className="absolute -right-[2px] top-44 w-1 h-20 bg-white/30 border border-white/40 rounded-r-md shadow-sm" />
|
||||
|
||||
<div className="relative w-full h-full rounded-[2.8rem] bg-white overflow-hidden shadow-[inset_0_0_10px_rgba(255,255,255,0.1)] border border-white/60 isolate">
|
||||
<div
|
||||
className="relative w-full h-full rounded-[2.8rem] bg-white overflow-hidden isolate"
|
||||
style={{ WebkitMaskImage: '-webkit-radial-gradient(white, black)' }}
|
||||
>
|
||||
<img src={screen2Image} alt="App UI Design 2" className="absolute inset-0 w-full h-full object-cover z-0" />
|
||||
|
||||
<div className="absolute top-2.5 left-1/2 -translate-x-1/2 w-[100px] h-[30px] bg-black rounded-full z-50 shadow-[0_4px_10px_rgba(0,0,0,0.3)] flex items-center justify-between px-3">
|
||||
@@ -301,7 +352,7 @@ export function HomePage() {
|
||||
<IconifyIcon icon="solar:document-text-linear" className="text-2xl text-teal-600" style={{ strokeWidth: 1.5 }} />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-medium text-gray-900 tracking-tight mb-1">AI-understøttet CV-optimering</h3>
|
||||
<h3 className="text-lg font-medium text-gray-900 tracking-tight mb-1">Smart CV-optimering</h3>
|
||||
<p className="text-base text-gray-600 font-normal">Få skræddersyet dit CV til præcis den stilling du søger, så du altid står skarpest muligt.</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -311,7 +362,7 @@ export function HomePage() {
|
||||
<IconifyIcon icon="solar:pen-new-square-linear" className="text-2xl text-indigo-600" style={{ strokeWidth: 1.5 }} />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-medium text-gray-900 tracking-tight mb-1">AI Ansøgninger</h3>
|
||||
<h3 className="text-lg font-medium text-gray-900 tracking-tight mb-1">Målrettede ansøgninger</h3>
|
||||
<p className="text-base text-gray-600 font-normal">Generer målrettede og personlige ansøgninger, der fanger arbejdsgiverens opmærksomhed.</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -321,7 +372,7 @@ export function HomePage() {
|
||||
<IconifyIcon icon="solar:gamepad-linear" className="text-2xl text-cyan-600" style={{ strokeWidth: 1.5 }} />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-medium text-gray-900 tracking-tight mb-1">AI-interview Simulator</h3>
|
||||
<h3 className="text-lg font-medium text-gray-900 tracking-tight mb-1">Interview-simulator</h3>
|
||||
<p className="text-base text-gray-600 font-normal">Øv dig til samtalen med vores AI. Få øjeblikkelig feedback og personlige anbefalinger.</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -331,7 +382,7 @@ export function HomePage() {
|
||||
<IconifyIcon icon="solar:radar-linear" className="text-2xl text-amber-600" style={{ strokeWidth: 1.5 }} />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-medium text-gray-900 tracking-tight mb-1">AI-agenter søger for dig</h3>
|
||||
<h3 className="text-lg font-medium text-gray-900 tracking-tight mb-1">Agenter søger for dig</h3>
|
||||
<p className="text-base text-gray-600 font-normal">Lad vores intelligente agenter overvåge markedet og finde det perfekte match til din profil.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -231,6 +231,49 @@
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.homepage-nav-popup-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.3rem;
|
||||
}
|
||||
|
||||
.homepage-nav-popup-group-trigger {
|
||||
width: 100%;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: #374151;
|
||||
font-size: 1rem;
|
||||
border-radius: 0.85rem;
|
||||
padding: 0.7rem 0.9rem;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease, color 0.2s ease;
|
||||
}
|
||||
|
||||
.homepage-nav-popup-group-trigger:hover {
|
||||
background: rgba(15, 23, 42, 0.06);
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.homepage-nav-popup-submenu {
|
||||
display: none;
|
||||
padding-left: 0.4rem;
|
||||
}
|
||||
|
||||
.homepage-nav-popup-submenu.open {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.2rem;
|
||||
}
|
||||
|
||||
.homepage-nav-popup-submenu a {
|
||||
font-size: 0.95rem;
|
||||
padding: 0.55rem 0.85rem;
|
||||
color: #4b5563;
|
||||
}
|
||||
|
||||
.homepage-nav-popup-cta {
|
||||
color: #ffffff !important;
|
||||
background: linear-gradient(to right, #111827, #1f2937);
|
||||
|
||||
Reference in New Issue
Block a user