11
22import { Link } from "react-router" ;
33import { motion } from "framer-motion" ;
4- import { Briefcase , MapPin , Building2 , ArrowUpRight , Clock , Search , ExternalLink , X } from "lucide-react" ;
5- import { useQuery , useQueryClient } from "@tanstack/react-query" ;
4+ import { Briefcase , MapPin , Building2 , ArrowUpRight , Clock , Search , ExternalLink , X , Trash2 } from "lucide-react" ;
5+ import { useMutation , useQuery , useQueryClient } from "@tanstack/react-query" ;
66import React , { useState , useMemo , useEffect , useCallback } from "react" ;
77import api from "../../../lib/axios" ;
88import { queryKeys } from "../../../lib/query-keys" ;
99import type { Application } from "../../../lib/types" ;
1010import { LoadingScreen } from "../../../components/LoadingScreen" ;
1111import { SEO } from "../../../components/SEO" ;
12+ import { ConfirmDialog } from "../../../components/ui/ConfirmDialog" ;
1213import toast from "@/components/ui/toast" ;
1314interface ExternalApplication {
1415 id : number ;
@@ -154,8 +155,10 @@ const ApplicationCard = React.memo(function ApplicationCard({
154155
155156const ExternalApplicationCard = React . memo ( function ExternalApplicationCard ( {
156157 app,
158+ onRemove,
157159} : {
158160 app : ExternalApplication ;
161+ onRemove : ( id : number ) => void ;
159162} ) {
160163 return (
161164 < div className = "group relative flex flex-col bg-white dark:bg-stone-900 p-5 rounded-md border border-stone-200 dark:border-white/10 hover:border-stone-400 dark:hover:border-white/30 transition-colors" >
@@ -199,69 +202,42 @@ const ExternalApplicationCard = React.memo(function ExternalApplicationCard({
199202 year : "numeric" ,
200203 } ) }
201204 </ span >
202- { app . adminJob . applyLink && (
203- < a
204- href = { app . adminJob . applyLink }
205- target = "_blank"
206- rel = "noopener noreferrer"
207- className = "inline-flex items-center gap-1 text-[10px] font-mono uppercase tracking-widest text-stone-900 dark:text-stone-50 hover:text-lime-600 dark:hover:text-lime-400 no-underline transition-colors"
208- >
209- View posting < ExternalLink className = "w-3 h-3" />
210- </ a >
211- ) }
212- </ div >
213- </ div >
214- ) ;
215- } ) ;
216-
217- const PAGE_SIZE = 10 ;
218- function WithdrawModal ( {
219- open,
220- onCancel,
221- onConfirm,
222- } : {
223- open : boolean ;
224- onCancel : ( ) => void ;
225- onConfirm : ( ) => void ;
226- } ) {
227- if ( ! open ) return null ;
228- return (
229- < div className = "fixed inset-0 z-50 flex items-center justify-center bg-black/50" >
230- < div className = "bg-white dark:bg-stone-900 border border-stone-200 dark:border-white/10 rounded-md p-6 max-w-sm w-full mx-4 space-y-4" >
231- < h3 className = "text-base font-bold text-stone-900 dark:text-stone-50" >
232- Withdraw Application?
233- </ h3 >
234- < p className = "text-sm text-stone-500" >
235- Are you sure you want to withdraw this application? This action cannot be undone.
236- </ p >
237- < div className = "flex gap-3" >
238- < button
239- onClick = { onCancel }
240- className = "flex-1 px-4 py-2 rounded-md text-xs font-mono uppercase tracking-widest text-stone-600 dark:text-stone-400 border border-stone-200 dark:border-white/10 hover:border-stone-400 transition-colors bg-transparent cursor-pointer"
241- >
242- Cancel
243- </ button >
205+ < div className = "flex items-center gap-2" >
244206 < button
245- onClick = { onConfirm }
246- className = "flex-1 px-4 py-2 rounded-md text-xs font-mono uppercase tracking-widest text-white bg-red- 500 hover:bg -red-600 transition-colors cursor-pointer border-0"
207+ onClick = { ( ) => onRemove ( app . id ) }
208+ className = "inline-flex items-center gap-1 text-[10px] font-mono uppercase tracking-widest text-stone- 500 hover:text -red-500 transition-colors bg-transparent border-0 cursor-pointer px-2 py-1 "
247209 >
248- Withdraw
210+ < Trash2 className = "w-3 h-3" /> Remove
249211 </ button >
212+ { app . adminJob . applyLink && (
213+ < a
214+ href = { app . adminJob . applyLink }
215+ target = "_blank"
216+ rel = "noopener noreferrer"
217+ className = "inline-flex items-center gap-1 text-[10px] font-mono uppercase tracking-widest text-stone-900 dark:text-stone-50 hover:text-lime-600 dark:hover:text-lime-400 no-underline transition-colors"
218+ >
219+ View posting < ExternalLink className = "w-3 h-3" />
220+ </ a >
221+ ) }
250222 </ div >
251223 </ div >
252224 </ div >
253225 ) ;
254- }
226+ } ) ;
255227
228+ const PAGE_SIZE = 10 ;
256229
230+ type PendingDelete =
231+ | { kind : "internal" ; id : number }
232+ | { kind : "external" ; id : number } ;
257233
258234export default function MyApplicationsPage ( ) {
259235 const queryClient = useQueryClient ( ) ;
260236 const [ search , setSearch ] = useState ( "" ) ;
261237 const [ debouncedSearch , setDebouncedSearch ] = useState ( "" ) ;
262- const [ page , setPage ] = useState ( 1 ) ;
263- const [ withdrawId , setWithdrawId ] = useState < number | null > ( null ) ;
264- const [ showWithdrawModal , setShowWithdrawModal ] = useState ( false ) ;
238+ const [ page , setPage ] = useState ( 1 ) ;
239+ const [ pendingDelete , setPendingDelete ] = useState < PendingDelete | null > ( null ) ;
240+
265241 useEffect ( ( ) => {
266242 const t = setTimeout ( ( ) => setDebouncedSearch ( search ) , 200 ) ;
267243 return ( ) => clearTimeout ( t ) ;
@@ -302,38 +278,80 @@ export default function MyApplicationsPage() {
302278
303279 const totalAll = applications . length + externalApplications . length ;
304280
305- const handleWithdraw = useCallback (
306- async ( id : number ) => {
307- setWithdrawId ( id ) ;
308- setShowWithdrawModal ( true ) ;
281+ const deleteMutation = useMutation ( {
282+ mutationFn : async ( item : PendingDelete ) => {
283+ if ( item . kind === "internal" ) {
284+ await api . delete ( `/student/applications/${ item . id } ` ) ;
285+ } else {
286+ await api . delete ( `/student/external-applications/${ item . id } ` ) ;
287+ }
288+ return item ;
309289 } ,
310- [ ]
311- ) ;
290+ onSuccess : ( item ) => {
291+ if ( item . kind === "internal" ) {
292+ queryClient . setQueryData < {
293+ applications : Application [ ] ;
294+ externalApplications : ExternalApplication [ ] ;
295+ } > ( queryKeys . applications . mine ( ) , ( old ) => {
296+ if ( ! old ) return old ;
297+ return {
298+ ...old ,
299+ applications : old . applications . map ( ( a ) =>
300+ a . id === item . id ? { ...a , status : "WITHDRAWN" as const } : a
301+ ) ,
302+ } ;
303+ } ) ;
304+ toast . success ( "Application withdrawn successfully" ) ;
305+ } else {
306+ queryClient . setQueryData < {
307+ applications : Application [ ] ;
308+ externalApplications : ExternalApplication [ ] ;
309+ } > ( queryKeys . applications . mine ( ) , ( old ) => {
310+ if ( ! old ) return old ;
311+ return {
312+ ...old ,
313+ externalApplications : old . externalApplications . filter ( ( a ) => a . id !== item . id ) ,
314+ } ;
315+ } ) ;
316+ toast . success ( "Application removed" ) ;
317+ }
318+ queryClient . invalidateQueries ( { queryKey : queryKeys . applications . mine ( ) } ) ;
319+ } ,
320+ onError : ( _err , item ) => {
321+ toast . error (
322+ item . kind === "internal"
323+ ? "Failed to withdraw application"
324+ : "Failed to remove application"
325+ ) ;
326+ } ,
327+ } ) ;
328+
329+ const handleWithdraw = useCallback ( ( id : number ) => {
330+ setPendingDelete ( { kind : "internal" , id } ) ;
331+ } , [ ] ) ;
332+
333+ const handleRemoveExternal = useCallback ( ( id : number ) => {
334+ setPendingDelete ( { kind : "external" , id } ) ;
335+ } , [ ] ) ;
336+
337+ const confirmDelete = useCallback ( ( ) => {
338+ if ( ! pendingDelete ) return ;
339+ const item = pendingDelete ;
340+ setPendingDelete ( null ) ;
341+ deleteMutation . mutate ( item ) ;
342+ } , [ pendingDelete , deleteMutation ] ) ;
343+
344+ const cancelDelete = useCallback ( ( ) => setPendingDelete ( null ) , [ ] ) ;
345+
346+ const isInternalDelete = pendingDelete ?. kind === "internal" ;
347+ const confirmTitle = isInternalDelete
348+ ? "Withdraw application?"
349+ : "Remove tracked application?" ;
350+ const confirmDescription = isInternalDelete
351+ ? "The recruiter will see this change. This action cannot be undone."
352+ : "This only removes it from your list. The job posting won't be affected." ;
353+ const confirmLabel = isInternalDelete ? "Withdraw" : "Remove" ;
312354
313- const confirmWithdraw = useCallback ( async ( ) => {
314- if ( ! withdrawId ) return ;
315- const idToWithdraw = withdrawId ;
316- setShowWithdrawModal ( false ) ;
317- setWithdrawId ( null ) ;
318- try {
319- await api . delete ( `/student/applications/${ idToWithdraw } ` ) ;
320- queryClient . setQueryData < {
321- applications : Application [ ] ;
322- externalApplications : ExternalApplication [ ] ;
323- } > ( queryKeys . applications . mine ( ) , ( old ) => {
324- if ( ! old ) return old ;
325- return {
326- ...old ,
327- applications : old . applications . map ( ( a ) =>
328- a . id === idToWithdraw ? { ...a , status : "WITHDRAWN" as const } : a
329- ) ,
330- } ;
331- } ) ;
332- toast . success ( "Application withdrawn successfully" ) ;
333- } catch {
334- toast . error ( "Failed to withdraw application" ) ;
335- }
336- } , [ withdrawId , queryClient ] ) ;
337355 if ( isLoading ) return < LoadingScreen /> ;
338356
339357
@@ -343,10 +361,14 @@ export default function MyApplicationsPage() {
343361 return (
344362 < div className = "relative pb-16" >
345363 < SEO title = "My Applications" noIndex />
346- < WithdrawModal
347- open = { showWithdrawModal }
348- onCancel = { ( ) => setShowWithdrawModal ( false ) }
349- onConfirm = { confirmWithdraw }
364+ < ConfirmDialog
365+ open = { pendingDelete !== null }
366+ title = { confirmTitle }
367+ description = { confirmDescription }
368+ confirmLabel = { confirmLabel }
369+ cancelLabel = "Cancel"
370+ onConfirm = { confirmDelete }
371+ onCancel = { cancelDelete }
350372 />
351373
352374 { /* Header */ }
@@ -465,7 +487,7 @@ export default function MyApplicationsPage() {
465487 { item . kind === "internal" ? (
466488 < ApplicationCard app = { item . app } onWithdraw = { handleWithdraw } />
467489 ) : (
468- < ExternalApplicationCard app = { item . app } />
490+ < ExternalApplicationCard app = { item . app } onRemove = { handleRemoveExternal } />
469491 ) }
470492 </ motion . div >
471493 ) ) }
0 commit comments