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:
co-authored by
Claude Opus 4.6
parent
f68d97188c
commit
3f4461c65c
@@ -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>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
+6
-6
@@ -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>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
+6
-6
@@ -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>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
+6
-7
@@ -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');
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user