Merge branch 'develop' of github.com:IMPHNEN/imphnen-frontend-service into backoffice/for-hackathon

This commit is contained in:
Hafid Nur
2025-12-02 18:13:03 +07:00
12 changed files with 359 additions and 133 deletions
+50 -1
View File
@@ -1,7 +1,7 @@
import { useState } from 'react';
import { useGitHubAuth, useSignup } from '@imphnen-frontend-service/service';
import { GithubOutlined } from '@ant-design/icons';
import { useNavigate, Link, Links } from 'react-router';
import { useNavigate, Link } from 'react-router';
import { toast } from 'sonner';
import { Icon } from '@iconify/react';
import { ThemeToggle } from '../../../components/theme-toggle';
@@ -32,8 +32,14 @@ const signupSchema = z
type SignupFormData = z.infer<typeof signupSchema>;
// Registration deadline: 2025-11-30 23:29:00 WIB (UTC+7)
const REGISTRATION_DEADLINE = new Date('2025-11-30T16:29:00Z');
export default function SignupPage() {
const navigate = useNavigate();
// Check if registration is closed
const isRegistrationClosed = new Date() >= REGISTRATION_DEADLINE;
const { signInWithGitHub } = useGitHubAuth();
const signupMutation = useSignup();
const [isGithubLoading, setIsGithubLoading] = useState(false);
@@ -70,6 +76,49 @@ export default function SignupPage() {
}
};
// Show closed registration screen
if (isRegistrationClosed) {
return (
<div className="flex justify-center items-center min-h-screen bg-gray-50 dark:bg-gray-950 p-4">
<div className="bg-white dark:bg-gray-900 w-full max-w-md p-8 rounded-2xl shadow-lg border border-gray-200 dark:border-gray-700 text-center">
<div className="mb-6">
<div className="mx-auto w-16 h-16 bg-red-100 dark:bg-red-900/30 rounded-full flex items-center justify-center mb-4">
<Icon
icon="mdi:clock-alert"
className="text-3xl text-red-600 dark:text-red-400"
/>
</div>
<h2 className="text-3xl font-bold text-gray-900 dark:text-white mb-2">
Registration Closed
</h2>
<p className="text-gray-600 dark:text-gray-400">
The registration period for this hackathon has ended.
</p>
</div>
<div className="space-y-4">
<p className="text-sm text-gray-600 dark:text-gray-400">
Thank you for your interest! Registration closed on November 30, 2025 at 23:29 WIB.
</p>
<Link to="/auth/login">
<button className="w-full py-3 bg-primary-600 text-white rounded-lg font-semibold hover:bg-primary-700 transition-colors cursor-pointer">
Go to Login
</button>
</Link>
<button
onClick={() => navigate('/')}
className="w-full py-3 text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white transition-colors cursor-pointer"
>
Back to Homepage
</button>
</div>
</div>
</div>
);
}
// Show success screen after registration
if (registrationSuccess) {
return (
+31 -15
View File
@@ -11,6 +11,9 @@ import { Button } from '@imphnen-frontend-service/ui/atoms';
import { Icon } from '@iconify/react';
import ProfilePage from '../profile/page';
// Team features deadline: 2025-11-30 23:59:00 WIB (UTC+7)
const TEAM_FEATURES_DEADLINE = new Date('2025-11-30T16:59:00Z');
type Invitation = {
id: string;
team: {
@@ -34,6 +37,9 @@ type Invitation = {
const DashboardPage: FC = (): ReactElement => {
const { session } = useAuthStore();
const [showProfileModal, setShowProfileModal] = useState(false);
// Check if team features are closed
const isTeamFeaturesClosed = new Date() >= TEAM_FEATURES_DEADLINE;
// Lock background scroll when profile modal is open
useEffect(() => {
if (showProfileModal) {
@@ -93,6 +99,14 @@ const DashboardPage: FC = (): ReactElement => {
<h2 className="text-xl font-bold text-gray-900 dark:text-white mb-4">
Team Invitations ({invitations.length})
</h2>
{isTeamFeaturesClosed && (
<div className="mb-4 bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800 rounded-lg p-3">
<p className="text-sm text-amber-700 dark:text-amber-300 flex items-center gap-2">
<Icon icon="mdi:clock-alert" className="text-lg shrink-0" />
Team features are closed. You can no longer accept invitations.
</p>
</div>
)}
<div className="space-y-3 font-sans">
{invitations.map((invitation) => (
<div
@@ -108,21 +122,23 @@ const DashboardPage: FC = (): ReactElement => {
{invitation.inviter?.fullname ?? 'Unknown User'}
</p>
</div>
<div className="flex space-x-2">
<Button
size="sm"
onClick={() => handleAcceptInvitation(invitation.id)}
>
Accept
</Button>
<Button
size="sm"
variant="secondary"
onClick={() => handleRejectInvitation(invitation.id)}
>
Decline
</Button>
</div>
{!isTeamFeaturesClosed && (
<div className="flex space-x-2">
<Button
size="sm"
onClick={() => handleAcceptInvitation(invitation.id)}
>
Accept
</Button>
<Button
size="sm"
variant="secondary"
onClick={() => handleRejectInvitation(invitation.id)}
>
Decline
</Button>
</div>
)}
</div>
))}
</div>
+12 -6
View File
@@ -17,6 +17,9 @@ import { Icon } from '@iconify/react';
const MAX_TEAM_MEMBERS = 5;
// Team features deadline: 2025-11-30 23:59:00 WIB (UTC+7)
const TEAM_FEATURES_DEADLINE = new Date('2025-11-30T16:59:00Z');
// Image component with loading state
const ImageWithLoader: FC<{
src: string;
@@ -52,6 +55,9 @@ const TeamDashboardPage: FC = (): ReactElement => {
const { teamId } = useParams<{ teamId: string }>();
const navigate = useNavigate();
const { session } = useAuthStore();
// Check if team features are closed
const isTeamFeaturesClosed = new Date() >= TEAM_FEATURES_DEADLINE;
const [showInviteModal, setShowInviteModal] = useState(false);
const [showJoinRequestsModal, setShowJoinRequestsModal] = useState(false);
const [showLeaveModal, setShowLeaveModal] = useState(false);
@@ -369,8 +375,8 @@ const TeamDashboardPage: FC = (): ReactElement => {
Team Management
</h2>
<div className="grid gap-3 sm:grid-cols-2">
{/* Hide invite/join management after submission */}
{!team.has_submission && (
{/* Hide invite/join management after submission or deadline */}
{!team.has_submission && !isTeamFeaturesClosed && (
<>
<Button
className="w-full"
@@ -397,7 +403,7 @@ const TeamDashboardPage: FC = (): ReactElement => {
</Button>
</>
)}
{!team.has_submission && (
{!team.has_submission && !isTeamFeaturesClosed && (
<Link to={`/teams/${teamId}/edit`}>
<Button className="w-full" variant="secondary">
<Icon icon="mdi:pencil" className="inline-block mr-2" />
@@ -441,8 +447,8 @@ const TeamDashboardPage: FC = (): ReactElement => {
Maximum team size reached ({MAX_TEAM_MEMBERS} members)
</p>
)}
{/* Danger Zone - Only show when leader is alone and no submission */}
{members.length === 1 && !team.has_submission && (
{/* Danger Zone - Only show when leader is alone, no submission, and features not closed */}
{members.length === 1 && !team.has_submission && !isTeamFeaturesClosed && (
<div className="mt-6 pt-4 border-t border-gray-200 dark:border-gray-700">
<h3 className="text-sm font-medium text-red-600 dark:text-red-400 mb-3">
Danger Zone
@@ -480,7 +486,7 @@ const TeamDashboardPage: FC = (): ReactElement => {
</Button>
</Link>
)}
{!team.has_submission && (
{!team.has_submission && !isTeamFeaturesClosed && (
<Button
className="w-full bg-red-600 hover:bg-red-700 text-white"
onClick={() => setShowLeaveModal(true)}
@@ -60,20 +60,46 @@ const SubmissionViewPage: FC = (): ReactElement => {
<div className="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
{/* Status Banner */}
<div className="bg-green-50 dark:bg-green-900/20 border border-green-500 rounded-lg p-6 mb-6">
<div className="flex items-center space-x-3">
<span className="text-4xl">✅</span>
<div>
<h3 className="font-bold text-green-900 dark:text-green-100 text-lg">Project Submitted Successfully</h3>
<p className="text-green-700 dark:text-green-300 text-sm">
Submitted on {submittedDate}
</p>
<p className="text-green-600 dark:text-green-400 text-xs mt-1">
This submission is now read-only and cannot be edited
</p>
{submission.status === 'submitted' ? (
<div className="bg-green-50 dark:bg-green-900/20 border border-green-500 rounded-lg p-6 mb-6">
<div className="flex items-center space-x-3">
<span className="text-4xl">✅</span>
<div>
<h3 className="font-bold text-green-900 dark:text-green-100 text-lg">Project Submitted Successfully</h3>
<p className="text-green-700 dark:text-green-300 text-sm">
Submitted on {submittedDate}
</p>
<p className="text-green-600 dark:text-green-400 text-xs mt-1">
This submission is now read-only and cannot be edited
</p>
</div>
</div>
</div>
</div>
) : submission.status === 'pending_verification' ? (
<div className="bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-500 rounded-lg p-6 mb-6">
<div className="flex items-center space-x-3">
<span className="text-4xl">⏳</span>
<div>
<h3 className="font-bold text-yellow-900 dark:text-yellow-100 text-lg">Submission Pending Verification</h3>
<p className="text-yellow-700 dark:text-yellow-300 text-sm">
Your submission is being processed
</p>
</div>
</div>
</div>
) : (
<div className="bg-orange-50 dark:bg-orange-900/20 border border-orange-500 rounded-lg p-6 mb-6">
<div className="flex items-center space-x-3">
<span className="text-4xl">📝</span>
<div>
<h3 className="font-bold text-orange-900 dark:text-orange-100 text-lg">Draft Submission</h3>
<p className="text-orange-700 dark:text-orange-300 text-sm">
This submission is still in draft and has not been finalized
</p>
</div>
</div>
</div>
)}
<div className="bg-white dark:bg-neutral-900 rounded-lg shadow-md dark:shadow-neutral-950/50 overflow-hidden">
{/* Project Header */}
@@ -124,21 +150,6 @@ const SubmissionViewPage: FC = (): ReactElement => {
</div>
)}
{/* Presentation URL */}
{submission.presentation_url && (
<div>
<h3 className="text-lg font-bold text-gray-900 dark:text-white mb-3">Presentation</h3>
<a
href={submission.presentation_url}
target="_blank"
rel="noopener noreferrer"
className="flex items-center space-x-2 text-blue-600 dark:text-primary-400 hover:text-blue-800 dark:hover:text-primary-300"
>
<span>📊</span>
<span className="break-all">{submission.presentation_url}</span>
</a>
</div>
)}
</div>
{/* Screenshots */}
@@ -173,8 +184,18 @@ const SubmissionViewPage: FC = (): ReactElement => {
<div className="grid gap-2 text-sm">
<div className="flex justify-between">
<span className="text-gray-600 dark:text-neutral-400">Status:</span>
<span className="font-medium text-green-600 dark:text-green-400">
{submission.status === 'submitted' ? ' Submitted' : 'Draft'}
<span className={`font-medium ${
submission.status === 'submitted'
? 'text-green-600 dark:text-green-400'
: submission.status === 'pending_verification'
? 'text-yellow-600 dark:text-yellow-400'
: 'text-orange-600 dark:text-orange-400'
}`}>
{submission.status === 'submitted'
? ' Submitted'
: submission.status === 'pending_verification'
? ' Pending Verification'
: '📝 Draft'}
</span>
</div>
<div className="flex justify-between">
@@ -243,16 +243,6 @@ const SubmitProjectPage: FC = (): ReactElement => {
size="lg"
/>
{/* Presentation URL */}
<ControlledInputField
control={form.control}
label="Presentation URL (Optional)"
placeholder="https://slides.com/your-presentation or Google Drive link"
name="presentation_url"
type="url"
size="lg"
/>
{/* Screenshots */}
<div>
<label className="block text-label1 font-medium text-neutral-800 dark:text-gray-300">
+51 -2
View File
@@ -17,6 +17,9 @@ import { Icon } from '@iconify/react';
const DEFAULT_PER_PAGE = 12;
const PER_PAGE_OPTIONS = [6, 12, 24, 48];
// Team features deadline: 2025-11-30 23:59:00 WIB (UTC+7)
const TEAM_FEATURES_DEADLINE = new Date('2025-11-30T16:59:00Z');
// Member filter options
const MEMBER_FILTER_OPTIONS = [
{ label: 'All Teams', value: '', minMembers: undefined, maxMembers: undefined },
@@ -28,6 +31,13 @@ const MEMBER_FILTER_OPTIONS = [
{ label: '5 Members (Full)', value: '5', minMembers: 5, maxMembers: 5 },
];
// Submission status filter options
const SUBMISSION_FILTER_OPTIONS = [
{ label: 'All Teams', value: '' },
{ label: 'Submitted', value: 'true' },
{ label: 'Not Submitted', value: 'false' },
];
// Skeleton card component for loading state
const TeamCardSkeleton: FC = () => (
<div className="bg-white dark:bg-gray-900 rounded-lg shadow-md overflow-hidden flex flex-col border dark:border-gray-800 animate-pulse">
@@ -57,6 +67,9 @@ const BrowseTeamsPage: FC = (): ReactElement => {
const navigate = useNavigate();
const [searchParams, setSearchParams] = useSearchParams();
// Check if team features are closed
const isTeamFeaturesClosed = new Date() >= TEAM_FEATURES_DEADLINE;
// Initialize state from URL params
const initialPage = parseInt(searchParams.get('page') || '1', 10);
const initialPerPage = parseInt(
@@ -66,11 +79,13 @@ const BrowseTeamsPage: FC = (): ReactElement => {
const initialSearch = searchParams.get('search') || '';
const initialCity = searchParams.get('city') || '';
const initialMembers = searchParams.get('members') || '';
const initialSubmission = searchParams.get('submission') || '';
const [searchTerm, setSearchTerm] = useState(initialSearch);
const [debouncedSearch, setDebouncedSearch] = useState(initialSearch);
const [selectedCity, setSelectedCity] = useState(initialCity);
const [selectedMembers, setSelectedMembers] = useState(initialMembers);
const [selectedSubmission, setSelectedSubmission] = useState(initialSubmission);
const [selectedTeamId, setSelectedTeamId] = useState<string | null>(null);
const [showJoinModal, setShowJoinModal] = useState(false);
const [currentPage, setCurrentPage] = useState(
@@ -88,6 +103,7 @@ const BrowseTeamsPage: FC = (): ReactElement => {
search?: string;
city?: string;
members?: string;
submission?: string;
}) => {
const newParams = new URLSearchParams(searchParams);
@@ -131,6 +147,14 @@ const BrowseTeamsPage: FC = (): ReactElement => {
}
}
if (params.submission !== undefined) {
if (params.submission === '') {
newParams.delete('submission');
} else {
newParams.set('submission', params.submission);
}
}
setSearchParams(newParams, { replace: true });
},
[searchParams, setSearchParams]
@@ -161,6 +185,9 @@ const BrowseTeamsPage: FC = (): ReactElement => {
(opt) => opt.value === selectedMembers
);
// Convert submission filter value to boolean
const hasSubmissionFilter = selectedSubmission === 'true' ? true : selectedSubmission === 'false' ? false : undefined;
const {
data: teamsData,
isLoading,
@@ -173,6 +200,7 @@ const BrowseTeamsPage: FC = (): ReactElement => {
visibility: ETeamVisibility.PUBLIC,
minMembers: memberFilter?.minMembers,
maxMembers: memberFilter?.maxMembers,
hasSubmission: hasSubmissionFilter,
});
const { data: myTeamsData } = useMyTeams();
@@ -272,7 +300,7 @@ const BrowseTeamsPage: FC = (): ReactElement => {
{/* Filters */}
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
<div className="bg-white dark:bg-gray-900 p-6 rounded-lg shadow-sm mb-6 border dark:border-gray-800">
<div className="grid gap-4 md:grid-cols-3">
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Search Teams
@@ -315,6 +343,26 @@ const BrowseTeamsPage: FC = (): ReactElement => {
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Submission Status
</label>
<select
value={selectedSubmission}
onChange={(e) => {
setSelectedSubmission(e.target.value);
setCurrentPage(1);
updateUrlParams({ submission: e.target.value, page: 1 });
}}
className="w-full h-[42px] px-3 border border-gray-300 dark:border-gray-700 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white dark:bg-gray-800 text-gray-900 dark:text-white"
>
{SUBMISSION_FILTER_OPTIONS.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
</div>
</div>
</div>
@@ -416,7 +464,8 @@ const BrowseTeamsPage: FC = (): ReactElement => {
<>
{myTeams.length === 0 &&
(team.member_count || 0) < 5 &&
!team.has_submission && (
!team.has_submission &&
!isTeamFeaturesClosed && (
<Button
className="w-full"
onClick={() => handleJoinRequest(team.id)}
@@ -6,13 +6,21 @@ import { useForm, Controller } from 'react-hook-form';
import { teamCreateSchema, TTeamCreateForm, useCreateTeam, ETeamVisibility, useUploadFile } from '@imphnen-frontend-service/service';
import { zodResolver } from '@hookform/resolvers/zod';
import { toast } from 'sonner';
import { Icon } from '@iconify/react';
import { CitySelect } from '../../../components/city-select';
const MAX_FILE_SIZE = 2 * 1024 * 1024; // 2MB
// Team features deadline: 2025-11-30 23:59:00 WIB (UTC+7)
const TEAM_FEATURES_DEADLINE = new Date('2025-11-30T16:59:00Z');
const CreateTeamPage: FC = (): ReactElement => {
const navigate = useNavigate();
// Check if team features are closed
const isTeamFeaturesClosed = new Date() >= TEAM_FEATURES_DEADLINE;
const [logoFile, setLogoFile] = useState<File | null>(null);
const [logoPreview, setLogoPreview] = useState<string>('');
const [bannerFile, setBannerFile] = useState<File | null>(null);
@@ -103,6 +111,50 @@ const CreateTeamPage: FC = (): ReactElement => {
}
});
// Show closed screen if team features are closed
if (isTeamFeaturesClosed) {
return (
<div className="flex justify-center items-center min-h-screen bg-gray-50 dark:bg-gray-950 p-4">
<div className="bg-white dark:bg-gray-900 w-full max-w-md p-8 rounded-2xl shadow-lg border border-gray-200 dark:border-gray-700 text-center">
<div className="mb-6">
<div className="mx-auto w-16 h-16 bg-red-100 dark:bg-red-900/30 rounded-full flex items-center justify-center mb-4">
<Icon
icon="mdi:clock-alert"
className="text-3xl text-red-600 dark:text-red-400"
/>
</div>
<h2 className="text-3xl font-bold text-gray-900 dark:text-white mb-2">
Team Features Closed
</h2>
<p className="text-gray-600 dark:text-gray-400">
Team creation is no longer available.
</p>
</div>
<div className="space-y-4">
<p className="text-sm text-gray-600 dark:text-gray-400">
The deadline for team features was November 30, 2025 at 23:59 WIB.
</p>
<button
onClick={() => navigate('/dashboard')}
className="w-full py-3 bg-primary-600 text-white rounded-lg font-semibold hover:bg-primary-700 transition-colors cursor-pointer"
>
Back to Dashboard
</button>
<button
onClick={() => navigate('/teams/browse')}
className="w-full py-3 text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white transition-colors cursor-pointer"
>
Browse Teams
</button>
</div>
</div>
</div>
);
}
return (
<div className="min-h-screen bg-gray-50 dark:bg-neutral-950">
{/* Header */}
+49 -52
View File
@@ -131,61 +131,58 @@ const UserProfilePage: FC = (): ReactElement => {
Team
</h2>
<div className="space-y-4">
{userTeams.map((item: any) => {
const team = item.team || item;
return (
<Link
key={team.id}
to={'/teams/' + team.id}
className="block bg-white dark:bg-gray-800 rounded-lg shadow-md dark:shadow-gray-950/50 overflow-hidden hover:shadow-lg transition-shadow border dark:border-gray-700"
>
<img
src={team.banner || '/images/banner-imphnen.webp'}
alt={team.name}
className="w-full aspect-3/1 object-cover"
/>
<div className="p-4">
<div className="flex items-center space-x-3 mb-3">
{team.logo ? (
<img
src={team.logo}
alt={team.name}
className="w-12 h-12 rounded-full object-cover"
/>
) : (
<div className="w-12 h-12 rounded-full bg-gray-200 dark:bg-gray-700 flex items-center justify-center">
<Icon icon="mdi:account-group" className="text-gray-500 dark:text-gray-400 text-xl" />
</div>
)}
<div className="flex-1 min-w-0">
<h3 className="font-bold text-gray-900 dark:text-white line-clamp-1 text-lg">
{team.name}
</h3>
<div className="text-sm text-gray-600 dark:text-gray-400 flex items-center gap-3 font-sans mt-1">
{team.has_submission && (
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded text-xs font-medium bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-400 shrink-0">
<Icon icon="mdi:check-circle" className="text-sm" />
Submitted
</span>
)}
{team.city && (
<span className="flex items-center gap-1 truncate">
<Icon icon="mdi:map-marker" className="shrink-0" />
<span className="truncate">{team.city}</span>
</span>
)}
</div>
{userTeams.map((team: any) => (
<Link
key={team.id}
to={'/teams/' + team.id}
className="block bg-white dark:bg-gray-800 rounded-lg shadow-md dark:shadow-gray-950/50 overflow-hidden hover:shadow-lg transition-shadow border dark:border-gray-700"
>
<img
src={team.banner || '/images/banner-imphnen.webp'}
alt={team.name}
className="w-full aspect-3/1 object-cover"
/>
<div className="p-4">
<div className="flex items-center space-x-3 mb-3">
{team.logo ? (
<img
src={team.logo}
alt={team.name}
className="w-12 h-12 rounded-full object-cover"
/>
) : (
<div className="w-12 h-12 rounded-full bg-gray-200 dark:bg-gray-700 flex items-center justify-center">
<Icon icon="mdi:account-group" className="text-gray-500 dark:text-gray-400 text-xl" />
</div>
)}
<div className="flex-1 min-w-0">
<h3 className="font-bold text-gray-900 dark:text-white line-clamp-1 text-lg">
{team.name}
</h3>
<div className="text-sm text-gray-600 dark:text-gray-400 flex items-center gap-3 font-sans mt-1">
{team.has_submission && (
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded text-xs font-medium bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-400 shrink-0">
<Icon icon="mdi:check-circle" className="text-sm" />
Submitted
</span>
)}
{team.city && (
<span className="flex items-center gap-1 truncate">
<Icon icon="mdi:map-marker" className="shrink-0" />
<span className="truncate">{team.city}</span>
</span>
)}
</div>
</div>
{team.description && (
<p className="text-gray-600 dark:text-gray-400 text-sm line-clamp-2 font-sans">
{team.description}
</p>
)}
</div>
</Link>
);
})}
{team.description && (
<p className="text-gray-600 dark:text-gray-400 text-sm line-clamp-2 font-sans">
{team.description}
</p>
)}
</div>
</Link>
))}
</div>
</div>
) : (