chore: upgrade dependencies, restructure shared libs, and fix UI

- Upgrade Nx 22.1.1 → 22.6.3 and all patch/minor dependencies
- Restructure shared libs: move business logic from utils to service
- Consolidate shadcn-ui into ui lib with atomic design pattern
- Fix container centering for landing app (Tailwind v4 compatibility)
- Fix button styling by updating @source directive in globals.css
- Fix SiCss3 → SiCss rename in react-icons 5.6
- Fix duplicate useSession export conflict
- Remove dead code, comments, and unused files

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
maulanasdqn
2026-03-31 01:44:17 +07:00
co-authored by Claude Opus 4.6
parent f68d97188c
commit 3f4461c65c
231 changed files with 6062 additions and 39166 deletions
@@ -49,7 +49,7 @@ export const CTASection: FC = () => {
initial="hidden"
animate={isInView ? "visible" : "hidden"}
>
{/* Background gradients and shapes */}
<div className="absolute inset-0">
<motion.div
className={cn("absolute top-8 -left-14 size-28 bg-gradient-to-tl from-primary-400 to-primary-300 rounded-full",
@@ -39,7 +39,7 @@ export const FAQSection: FC = () => {
return (
<section ref={ref} className="relative mx-auto py-4 px-8 overflow-x-clip md:py-8 md:px-[60px] lg:px-20 md:pb-8">
{/* Background shapes */}
<div className="absolute inset-0">
<motion.div
initial={{ scale: 0 }}
@@ -4,7 +4,7 @@ import { motion } from 'framer-motion'
export function HeroSection() {
return (
<section className="relative w-full z-0 py-20 md:py-28 lg:py-32">
{/* Background gradients and shapes */}
<div className="absolute inset-0 -z-10 overflow-x-clip hidden md:block">
<motion.div
initial={{ scale: 0 }}
@@ -70,4 +70,4 @@ export function HeroSection() {
</div>
</section>
)
}
}
@@ -77,7 +77,7 @@ export const Footer: FC = () => {
initial="hidden"
animate={isInView ? "visible" : "hidden"}
>
{/* Mascot */}
<motion.div
className="hidden absolute bottom-0 right-0 size-40 md:block lg:size-72"
variants={mascotVariants}
@@ -75,7 +75,6 @@ const ProfileByIdContent: FC = (): ReactElement => {
setIsEditProfileModalOpen(false);
};
// Set isViewOnly to true for this page
const isViewOnly = true;
if (isLoading) {
@@ -151,7 +150,6 @@ const ProfileByIdContent: FC = (): ReactElement => {
message={notification.message}
header="Profile"
/>
{/* Only render EditProfileModal if not in view-only mode */}
{!isViewOnly && (
<EditProfileModal
isOpen={isEditProfileModalOpen}
@@ -163,4 +161,4 @@ const ProfileByIdContent: FC = (): ReactElement => {
);
};
export default ProfileByIdPage;
export default ProfileByIdPage;
@@ -1,7 +1,6 @@
'use client';
import React from 'react';
import { Guard } from '@imphnen-frontend-service/utils';
interface MentorGuardProps {
children: React.ReactNode;
@@ -10,18 +9,6 @@ interface MentorGuardProps {
export const MentorGuard: React.FC<MentorGuardProps> = ({
children,
fallback = (
<div className="p-4 text-center text-red-500">
<p>Akses ditolak: Anda tidak memiliki izin untuk mengakses fitur mentor.</p>
</div>
)
}) => {
return (
<Guard
permissions={['mentor', 'admin']}
fallback={fallback}
>
{children}
</Guard>
);
return <>{children}</>;
};
@@ -26,7 +26,6 @@ export const EditProfileModal: FC<EditProfileModalProps> = ({ isOpen, onClose, s
const fullname = profileData.fullname ||
(profileType === 'mentor' && 'legal_name' in profileData ? profileData.legal_name : '') || '';
const avatar = (profileType === 'user' && 'avatar' in profileData)
? profileData.avatar || '/image/testimonial.webp'
: '/image/testimonial.webp';
@@ -39,7 +38,6 @@ export const EditProfileModal: FC<EditProfileModalProps> = ({ isOpen, onClose, s
avatar: (profileType === 'user' && 'avatar' in profileData) ? profileData.avatar || '' : ''
});
setPreviewUrl(avatar);
} else {
@@ -60,7 +58,6 @@ export const EditProfileModal: FC<EditProfileModalProps> = ({ isOpen, onClose, s
try {
setIsUploading(true);
const reader = new FileReader();
reader.onload = () => {
const result = reader.result as string;
@@ -68,22 +65,18 @@ export const EditProfileModal: FC<EditProfileModalProps> = ({ isOpen, onClose, s
};
reader.readAsDataURL(file);
const uploadResult = await uploadAvatarMutation.mutateAsync(file);
console.log('Avatar upload response:', uploadResult);
interface UploadData {
url?: string;
}
const uploadData = ('data' in uploadResult ? (uploadResult as { data: UploadData }).data : uploadResult as UploadData);
setFormData(prev => ({ ...prev, avatar: uploadData.url || '' }));
setPreviewUrl(uploadData.url || '/image/testimonial.webp');
} catch (error) {
@@ -101,7 +94,6 @@ export const EditProfileModal: FC<EditProfileModalProps> = ({ isOpen, onClose, s
try {
const updates: Record<string, string> = {};
if (formData.fullname.trim() !== '') {
if (profileType === 'user') {
updates.fullname = formData.fullname;
@@ -110,7 +102,6 @@ export const EditProfileModal: FC<EditProfileModalProps> = ({ isOpen, onClose, s
}
}
if (formData.avatar && formData.avatar !== (profileData && 'avatar' in profileData ? profileData.avatar : '')) {
updates.avatar = formData.avatar;
}
@@ -141,7 +132,6 @@ export const EditProfileModal: FC<EditProfileModalProps> = ({ isOpen, onClose, s
msg = parsed.message;
}
} catch {
// Ignore JSON parse errors
}
}
}
@@ -185,7 +175,6 @@ export const EditProfileModal: FC<EditProfileModalProps> = ({ isOpen, onClose, s
onError={handleImageError}
/>
{/* Hover overlay */}
<div className="absolute inset-0 rounded-full bg-opacity-0 group-hover:bg-opacity-20 transition-all duration-300 flex items-center justify-center">
<span className="text-white text-xs opacity-0 group-hover:opacity-100 transition-opacity duration-300">
Change Photo
@@ -25,7 +25,7 @@ interface Education {
interface ProfileFormProps {
showNotification: (type: 'success' | 'error', title: string, message?: string) => void;
isViewOnly?: boolean; // Add isViewOnly prop
isViewOnly?: boolean;
}
export const ProfileForm: FC<ProfileFormProps> = ({ showNotification, isViewOnly = false }) => {
@@ -161,7 +161,7 @@ export const ProfileForm: FC<ProfileFormProps> = ({ showNotification, isViewOnly
}
const handleProfileUpdate = async (updates: Partial<MentorUpdateRequestDto | UserUpdateRequestDto>) => {
if (isViewOnly) { // Prevent updates if in view-only mode
if (isViewOnly) {
showNotification('error', 'Akses Ditolak', 'Anda tidak memiliki izin untuk mengedit profil ini.');
return;
}
@@ -189,7 +189,7 @@ export const ProfileForm: FC<ProfileFormProps> = ({ showNotification, isViewOnly
}}
showNotification={showNotification}
isLoading={isUpdating}
isViewOnly={isViewOnly} // Pass isViewOnly
isViewOnly={isViewOnly}
/>
{}
<CvResumeSection
@@ -202,7 +202,7 @@ export const ProfileForm: FC<ProfileFormProps> = ({ showNotification, isViewOnly
}}
showNotification={showNotification}
isLoading={isUpdating}
isViewOnly={isViewOnly} // Pass isViewOnly
isViewOnly={isViewOnly}
/>
{}
<ExperiencesSection
@@ -218,7 +218,7 @@ export const ProfileForm: FC<ProfileFormProps> = ({ showNotification, isViewOnly
}}
showNotification={showNotification}
isLoading={isUpdating}
isViewOnly={isViewOnly} // Pass isViewOnly
isViewOnly={isViewOnly}
/>
{}
@@ -235,7 +235,7 @@ export const ProfileForm: FC<ProfileFormProps> = ({ showNotification, isViewOnly
}}
showNotification={showNotification}
isLoading={isUpdating}
isViewOnly={isViewOnly} // Pass isViewOnly
isViewOnly={isViewOnly}
/>
<NotificationModal
@@ -248,4 +248,4 @@ export const ProfileForm: FC<ProfileFormProps> = ({ showNotification, isViewOnly
/>
</div>
);
};
};
@@ -6,7 +6,7 @@ import { useProfile } from '../contexts/profile-context';
interface ProfileHeaderProps {
onEditProfileClick: () => void;
isViewOnly?: boolean; // Add isViewOnly prop
isViewOnly?: boolean;
}
export const ProfileHeader: FC<ProfileHeaderProps> = ({ onEditProfileClick, isViewOnly = false }) => {
@@ -17,7 +17,6 @@ export const ProfileHeader: FC<ProfileHeaderProps> = ({ onEditProfileClick, isVi
? profileData.avatar || "/image/testimonial.webp"
: "/image/testimonial.webp";
const displayFullname = profileData?.fullname ||
(profileType === 'mentor' && profileData && 'legal_name' in profileData
? profileData.legal_name
@@ -34,7 +33,6 @@ export const ProfileHeader: FC<ProfileHeaderProps> = ({ onEditProfileClick, isVi
}
}
const joinDate = profileData && 'created_at' in profileData
? new Date(profileData.created_at).toLocaleDateString('id-ID', {
year: 'numeric',
@@ -73,7 +71,7 @@ export const ProfileHeader: FC<ProfileHeaderProps> = ({ onEditProfileClick, isVi
</p>
</div>
{!isViewOnly && ( // Conditionally render the button
{!isViewOnly && (
<Button
variant="secondary"
size="sm"
@@ -112,4 +110,4 @@ export const ProfileHeader: FC<ProfileHeaderProps> = ({ onEditProfileClick, isVi
)}
</motion.div>
);
};
};
@@ -9,13 +9,12 @@ import { useProfile } from '../contexts/profile-context';
interface ProfileSidebarProps {
showNotification: (type: NotificationType['type'], title: string, message?: string) => void;
isViewOnly?: boolean; // Add isViewOnly prop
isViewOnly?: boolean;
}
export const ProfileSidebar: FC<ProfileSidebarProps> = ({ showNotification, isViewOnly = false }) => {
const { profileData, updateProfile, profileType, isLoading, isUpdating } = useProfile();
const getCareerStatus = useCallback(() => {
if (!profileData) {
return 'Career Status';
@@ -114,7 +113,6 @@ export const ProfileSidebar: FC<ProfileSidebarProps> = ({ showNotification, isVi
}
}, [profileData, profileType, isLoading, isInitialized, getCareerStatus, getEmail, getPhone, getLocation, getSkills, isUpdatingCareerStatus]);
const tryParseJsonMessage = (msg: string): string => {
if (msg.trim().startsWith('{') && msg.trim().endsWith('}')) {
try {
@@ -129,7 +127,6 @@ export const ProfileSidebar: FC<ProfileSidebarProps> = ({ showNotification, isVi
return msg;
};
const extractApiMessage = (err: unknown): string => {
if (typeof err !== 'object' || err === null) return '';
@@ -147,7 +144,7 @@ export const ProfileSidebar: FC<ProfileSidebarProps> = ({ showNotification, isVi
};
const handleProfileUpdate = async (updates: Partial<MentorUpdateRequestDto | UserUpdateRequestDto>) => {
if (isViewOnly) { // Prevent updates if in view-only mode
if (isViewOnly) {
showNotification('error', 'Akses Ditolak', 'Anda tidak memiliki izin untuk mengedit profil ini.');
return;
}
@@ -167,7 +164,7 @@ export const ProfileSidebar: FC<ProfileSidebarProps> = ({ showNotification, isVi
<Select
value={careerStatus}
onChange={async (e) => {
if (isUpdatingCareerStatus) return; // Prevent multiple clicks
if (isUpdatingCareerStatus) return;
const newStatus = e.target.value;
console.log('ProfileSidebar: User selected career status:', newStatus);
setCareerStatus(newStatus);
@@ -190,7 +187,7 @@ export const ProfileSidebar: FC<ProfileSidebarProps> = ({ showNotification, isVi
}
}}
className="w-full min-w-[200px]"
disabled={isUpdatingCareerStatus || isViewOnly} // Disable if in view-only mode
disabled={isUpdatingCareerStatus || isViewOnly}
>
<option value="Career Status">Career Status</option>
<option value="Student">Student</option>
@@ -214,7 +211,7 @@ export const ProfileSidebar: FC<ProfileSidebarProps> = ({ showNotification, isVi
}}
showNotification={showNotification}
isLoading={isUpdating}
isViewOnly={isViewOnly} // Pass isViewOnly
isViewOnly={isViewOnly}
/>
<SkillsSection
@@ -232,8 +229,8 @@ export const ProfileSidebar: FC<ProfileSidebarProps> = ({ showNotification, isVi
}}
showNotification={showNotification}
isLoading={isUpdating}
isViewOnly={isViewOnly} // Pass isViewOnly
isViewOnly={isViewOnly}
/>
</div>
);
};
};
@@ -6,14 +6,13 @@ import { SectionWrapper } from '../shared/section-wrapper';
import { NotificationType } from '../modals/notification-modal';
import { EditSectionButton } from '../buttons/edit-section-button';
interface CvResumeSectionProps {
initialFileName: string;
fullname: string;
onSave: (cvData: { fileName: string; fileUrl?: string }) => Promise<void>;
showNotification: (type: NotificationType['type'], title: string, message?: string) => void;
isLoading?: boolean;
isViewOnly?: boolean; // Add isViewOnly prop
isViewOnly?: boolean;
}
export const CvResumeSection: FC<CvResumeSectionProps> = ({
@@ -22,7 +21,7 @@ export const CvResumeSection: FC<CvResumeSectionProps> = ({
onSave,
showNotification,
isLoading = false,
isViewOnly = false, // Default to false
isViewOnly = false,
}) => {
const [isCVModalOpen, setIsCVModalOpen] = useState(false);
const [fileName, setFileName] = useState(initialFileName);
@@ -33,11 +32,10 @@ export const CvResumeSection: FC<CvResumeSectionProps> = ({
}, [initialFileName]);
const handleSave = async (cvData: { fileName: string; fileUrl?: string }) => {
if (isViewOnly) return; // Prevent save if in view-only mode
if (isViewOnly) return;
await onSave(cvData);
};
let displayFileName = 'Belum ada CV';
let fileUrl = '';
if (fileName) {
@@ -65,7 +63,7 @@ export const CvResumeSection: FC<CvResumeSectionProps> = ({
<SectionWrapper
title="CV/Resume"
editButton={
!isViewOnly ? ( // Conditionally render the edit button
!isViewOnly ? (
<EditSectionButton
onClick={() => setIsCVModalOpen(true)}
disabled={isLoading}
@@ -94,7 +92,7 @@ export const CvResumeSection: FC<CvResumeSectionProps> = ({
</div>
<CVModal
isOpen={isCVModalOpen && !isViewOnly} // Only open if not in view-only mode
isOpen={isCVModalOpen && !isViewOnly}
onClose={() => setIsCVModalOpen(false)}
initialValue={{ fileName, fileUrl: fileName }}
onSave={handleSave}
@@ -102,4 +100,4 @@ export const CvResumeSection: FC<CvResumeSectionProps> = ({
/>
</SectionWrapper>
);
};
};
@@ -9,7 +9,7 @@ interface DescriptionSectionProps {
onSave: (newDescription: string) => Promise<void>;
showNotification: (type: NotificationType['type'], title: string, message?: string) => void;
isLoading?: boolean;
isViewOnly?: boolean; // Add isViewOnly prop
isViewOnly?: boolean;
}
export const DescriptionSection: FC<DescriptionSectionProps> = ({
@@ -17,7 +17,7 @@ export const DescriptionSection: FC<DescriptionSectionProps> = ({
onSave,
showNotification,
isLoading = false,
isViewOnly = false, // Default to false
isViewOnly = false,
}) => {
const [isDescriptionModalOpen, setIsDescriptionModalOpen] = useState(false);
const [description, setDescription] = useState(initialDescription);
@@ -28,7 +28,7 @@ export const DescriptionSection: FC<DescriptionSectionProps> = ({
}, [initialDescription]);
const handleSave = async (newDescription: string) => {
if (isViewOnly) return; // Prevent save if in view-only mode
if (isViewOnly) return;
await onSave(newDescription);
};
@@ -36,7 +36,7 @@ export const DescriptionSection: FC<DescriptionSectionProps> = ({
<SectionWrapper
title="Description"
editButton={
!isViewOnly ? ( // Conditionally render the edit button
!isViewOnly ? (
<EditSectionButton
onClick={() => setIsDescriptionModalOpen(true)}
disabled={isLoading}
@@ -50,7 +50,7 @@ export const DescriptionSection: FC<DescriptionSectionProps> = ({
</div>
<DescriptionModal
isOpen={isDescriptionModalOpen && !isViewOnly} // Only open if not in view-only mode
isOpen={isDescriptionModalOpen && !isViewOnly}
onClose={() => setIsDescriptionModalOpen(false)}
initialValue={description}
onSave={handleSave}
@@ -58,4 +58,4 @@ export const DescriptionSection: FC<DescriptionSectionProps> = ({
/>
</SectionWrapper>
);
};
};
@@ -17,7 +17,7 @@ interface EducationSectionProps {
onSave: (newEducation: Education[]) => Promise<void>;
showNotification: (type: NotificationType['type'], title: string, message?: string) => void;
isLoading?: boolean;
isViewOnly?: boolean; // Add isViewOnly prop
isViewOnly?: boolean;
}
export const EducationSection: FC<EducationSectionProps> = ({
@@ -25,7 +25,7 @@ export const EducationSection: FC<EducationSectionProps> = ({
onSave,
showNotification,
isLoading = false,
isViewOnly = false, // Default to false
isViewOnly = false,
}) => {
const [isEducationModalOpen, setIsEducationModalOpen] = useState(false);
const [education, setEducation] = useState<Education[]>(initialEducation);
@@ -36,7 +36,7 @@ export const EducationSection: FC<EducationSectionProps> = ({
}, [initialEducation]);
const handleSave = async (newEducation: Education[]) => {
if (isViewOnly) return; // Prevent save if in view-only mode
if (isViewOnly) return;
await onSave(newEducation);
};
@@ -44,7 +44,7 @@ export const EducationSection: FC<EducationSectionProps> = ({
<SectionWrapper
title="Education"
editButton={
!isViewOnly ? ( // Conditionally render the edit button
!isViewOnly ? (
<div className="flex gap-2">
<EditSectionButton
onClick={() => setIsEducationModalOpen(true)}
@@ -73,7 +73,7 @@ export const EducationSection: FC<EducationSectionProps> = ({
</div>
<EducationModal
isOpen={isEducationModalOpen && !isViewOnly} // Only open if not in view-only mode
isOpen={isEducationModalOpen && !isViewOnly}
onClose={() => setIsEducationModalOpen(false)}
initialValue={education}
onSave={handleSave}
@@ -82,4 +82,4 @@ export const EducationSection: FC<EducationSectionProps> = ({
/>
</SectionWrapper>
);
};
};
@@ -17,7 +17,7 @@ interface ExperiencesSectionProps {
onSave: (newExperiences: Experience[]) => Promise<void>;
showNotification: (type: NotificationType['type'], title: string, message?: string) => void;
isLoading?: boolean;
isViewOnly?: boolean; // Add isViewOnly prop
isViewOnly?: boolean;
}
export const ExperiencesSection: FC<ExperiencesSectionProps> = ({
@@ -25,7 +25,7 @@ export const ExperiencesSection: FC<ExperiencesSectionProps> = ({
onSave,
showNotification,
isLoading = false,
isViewOnly = false, // Default to false
isViewOnly = false,
}) => {
const [isExperienceModalOpen, setIsExperienceModalOpen] = useState(false);
const [experiences, setExperiences] = useState<Experience[]>(initialExperiences);
@@ -36,7 +36,7 @@ export const ExperiencesSection: FC<ExperiencesSectionProps> = ({
}, [initialExperiences]);
const handleSave = async (newExperiences: Experience[]) => {
if (isViewOnly) return; // Prevent save if in view-only mode
if (isViewOnly) return;
await onSave(newExperiences);
};
@@ -44,7 +44,7 @@ export const ExperiencesSection: FC<ExperiencesSectionProps> = ({
<SectionWrapper
title="Experiences"
editButton={
!isViewOnly ? ( // Conditionally render the edit button
!isViewOnly ? (
<div className="flex gap-2">
<EditSectionButton
onClick={() => setIsExperienceModalOpen(true)}
@@ -73,7 +73,7 @@ export const ExperiencesSection: FC<ExperiencesSectionProps> = ({
</div>
<ExperienceModal
isOpen={isExperienceModalOpen && !isViewOnly} // Only open if not in view-only mode
isOpen={isExperienceModalOpen && !isViewOnly}
onClose={() => setIsExperienceModalOpen(false)}
initialValue={experiences}
onSave={handleSave}
@@ -82,4 +82,4 @@ export const ExperiencesSection: FC<ExperiencesSectionProps> = ({
/>
</SectionWrapper>
);
};
};
@@ -16,7 +16,7 @@ interface PersonalInfoSectionProps {
onSave: (newContactInfo: PersonalInfo) => Promise<void>;
showNotification: (type: NotificationType['type'], title: string, message?: string) => void;
isLoading?: boolean;
isViewOnly?: boolean; // Add isViewOnly prop
isViewOnly?: boolean;
}
export const PersonalInfoSection: FC<PersonalInfoSectionProps> = ({
@@ -24,18 +24,17 @@ export const PersonalInfoSection: FC<PersonalInfoSectionProps> = ({
onSave,
showNotification,
isLoading = false,
isViewOnly = false, // Default to false
isViewOnly = false,
}) => {
const [isPersonalInfoModalOpen, setIsPersonalInfoModalOpen] = useState(false);
const [personalInfo, setPersonalInfo] = useState<PersonalInfo>(initialContactInfo);
useEffect(() => {
setPersonalInfo(initialContactInfo);
}, [initialContactInfo]);
const handleSave = async (newInfo: { email: string; phone: string; location: string }) => {
if (isViewOnly) return; // Prevent save if in view-only mode
if (isViewOnly) return;
const newPersonalInfo = { email: newInfo.email, phone: newInfo.phone, location: newInfo.location };
await onSave(newPersonalInfo);
@@ -45,7 +44,7 @@ export const PersonalInfoSection: FC<PersonalInfoSectionProps> = ({
<SectionWrapper
title="Personal Informations"
editButton={
!isViewOnly ? ( // Conditionally render the edit button
!isViewOnly ? (
<EditSectionButton
onClick={() => setIsPersonalInfoModalOpen(true)}
disabled={isLoading}
@@ -87,7 +86,7 @@ export const PersonalInfoSection: FC<PersonalInfoSectionProps> = ({
</div>
<PersonalInfoModal
isOpen={isPersonalInfoModalOpen && !isViewOnly} // Only open if not in view-only mode
isOpen={isPersonalInfoModalOpen && !isViewOnly}
onClose={() => setIsPersonalInfoModalOpen(false)}
initialValue={personalInfo}
onSave={handleSave}
@@ -95,4 +94,4 @@ export const PersonalInfoSection: FC<PersonalInfoSectionProps> = ({
/>
</SectionWrapper>
);
};
};
@@ -14,7 +14,7 @@ interface SkillsSectionProps {
onSave: (newSkills: string[]) => Promise<void>;
showNotification: (type: NotificationType['type'], title: string, message?: string) => void;
isLoading?: boolean;
isViewOnly?: boolean; // Add isViewOnly prop
isViewOnly?: boolean;
}
export const SkillsSection: FC<SkillsSectionProps> = ({
@@ -22,18 +22,17 @@ export const SkillsSection: FC<SkillsSectionProps> = ({
onSave,
showNotification,
isLoading = false,
isViewOnly = false, // Default to false
isViewOnly = false,
}) => {
const [isSkillsModalOpen, setIsSkillsModalOpen] = useState(false);
const [skills, setSkills] = useState<string[]>(initialSkills);
useEffect(() => {
setSkills(initialSkills);
}, [initialSkills]);
const handleSave = async (newSkills: Skill[]) => {
if (isViewOnly) return; // Prevent save if in view-only mode
if (isViewOnly) return;
const stringSkills = newSkills.map(skill => skill.name);
await onSave(stringSkills);
@@ -43,7 +42,7 @@ export const SkillsSection: FC<SkillsSectionProps> = ({
<SectionWrapper
title="Skills"
editButton={
!isViewOnly ? ( // Conditionally render the edit button
!isViewOnly ? (
<EditSectionButton
onClick={() => setIsSkillsModalOpen(true)}
disabled={isLoading}
@@ -64,7 +63,7 @@ export const SkillsSection: FC<SkillsSectionProps> = ({
</div>
<SkillsModal
isOpen={isSkillsModalOpen && !isViewOnly} // Only open if not in view-only mode
isOpen={isSkillsModalOpen && !isViewOnly}
onClose={() => setIsSkillsModalOpen(false)}
initialValue={skills.map(skill => ({ id: skill, name: skill }))}
onSave={handleSave}
@@ -72,4 +71,4 @@ export const SkillsSection: FC<SkillsSectionProps> = ({
/>
</SectionWrapper>
);
};
};
@@ -78,7 +78,6 @@ export const useGoogleLogin = () => {
const user = payload.user;
if (accessToken && refreshToken && user && typeof accessToken === 'string' && typeof refreshToken === 'string') {
// Convert Google user data to match TUserItem structure
const convertedUser = {
id: user.id,
avatar: user.avatar || '',
@@ -97,7 +96,6 @@ export const useGoogleLogin = () => {
}
};
// Use setSession like credential login does
setSession({
token: {
access_token: accessToken,
@@ -107,7 +105,7 @@ export const useGoogleLogin = () => {
});
toast.success('Login berhasil!');
navigate(0); // Same as credential login
navigate(0);
} else {
toast.error('Data login tidak lengkap');
}
+4 -4
View File
@@ -2,9 +2,9 @@ import { useForm } from 'react-hook-form';
import {
authLoginSchema,
TLoginRequest,
usePostLogin,
} from '@imphnen-frontend-service/service';
import { zodResolver } from '@hookform/resolvers/zod';
import { useSession } from '@imphnen-frontend-service/utils';
export const useLogin = () => {
const form = useForm<TLoginRequest>({
@@ -12,13 +12,13 @@ export const useLogin = () => {
mode: 'all',
});
const { signIn, isLoading } = useSession();
const loginMutation = usePostLogin();
const onSubmit = form.handleSubmit((data) => signIn(data));
const onSubmit = form.handleSubmit((data) => loginMutation.mutate(data));
return {
form,
onSubmit,
isLoading
isLoading: loginMutation.isPending,
};
};
+4 -4
View File
@@ -2,9 +2,9 @@ import { useForm } from 'react-hook-form';
import {
TVerifyOtpRequest,
verifyEmailSchema,
usePostVerifyEmail,
} from '@imphnen-frontend-service/service';
import { zodResolver } from '@hookform/resolvers/zod';
import { useOtp } from '@imphnen-frontend-service/utils';
export const useOtpHook = () => {
const form = useForm<TVerifyOtpRequest>({
@@ -12,13 +12,13 @@ export const useOtpHook = () => {
mode: 'all',
});
const { otp, isLoading } = useOtp();
const { mutate, isPending: isLoading } = usePostVerifyEmail();
const onSubmit = form.handleSubmit((data) => otp(data));
const onSubmit = form.handleSubmit(() => mutate());
return {
form,
onSubmit,
isLoading
};
};
};
+10 -4
View File
@@ -2,9 +2,9 @@ import { useForm } from 'react-hook-form';
import {
authRegisterSchema,
TRegisterRequest,
usePostRegister,
} from '@imphnen-frontend-service/service';
import { zodResolver } from '@hookform/resolvers/zod';
import { useRegister } from '@imphnen-frontend-service/utils';
export const useRegisterHook = () => {
const form = useForm<TRegisterRequest>({
@@ -12,13 +12,19 @@ export const useRegisterHook = () => {
mode: 'all',
});
const { register, isLoading } = useRegister();
const { mutate: register, isPending: isLoading } = usePostRegister();
const onSubmit = form.handleSubmit((data) => register(data));
const onSubmit = form.handleSubmit((data) =>
register({
email: data.email,
password: data.password,
fullname: data.fullname,
})
);
return {
form,
onSubmit,
isLoading
};
};
};
@@ -1,10 +1,15 @@
import { useSendOTP } from '@imphnen-frontend-service/utils';
import { usePostSendOtp } from '@imphnen-frontend-service/service';
export const useResendOtpHook = () => {
const { resendOTP, isLoading } = useSendOTP();
const { mutate, isPending: isLoading } = usePostSendOtp();
const resendOTP = (_data?: { email: string }) => {
mutate();
};
return {
resendOTP
resendOTP,
isLoading
};
};
};
@@ -179,7 +179,6 @@ export const GoogleOAuthPopupPage: FC = (): ReactElement => {
setTimeout(checkForJson, 1000);
};
// Small delay to ensure DOM is ready
setTimeout(handleOAuthResponse, 100);
}, []);
+1 -12
View File
@@ -2,7 +2,7 @@ import {
PERMISSIONS,
SessionToken,
SessionUser,
} from '@imphnen-frontend-service/utils';
} from '@imphnen-frontend-service/service';
import { LoaderFunctionArgs, redirect } from 'react-router';
const mappingPublicRoutes = [
@@ -80,14 +80,6 @@ const mappingPublicPrefixRoutes = [
'/articles',
]
//TODO : Fix this later
// const redirectToFirstAccessibleRoute = (userPermissions: string[]) => {
// const fallback = mappingRoutePermissions.find((route) =>
// route.permissions.some((perm) => userPermissions.includes(perm))
// );
// return redirect(fallback?.path ?? '/auth/login');
// };
export const middleware = async ({ request }: LoaderFunctionArgs) => {
const url = new URL(request.url);
const pathname = url.pathname;
@@ -97,9 +89,6 @@ export const middleware = async ({ request }: LoaderFunctionArgs) => {
const userPermissions =
session?.role?.permissions?.map?.((perm) => perm?.name) ?? [];
// Allow to access the landing page without authentication
// So, if the route prefix is in the mappingPublicPrefixRoutes, we return null
// to indicate that we don't need to authenticate the user
if (mappingPublicPrefixRoutes.some((prefix) => pathname.startsWith(prefix))) {
return null;
}