22
33import { useCallback , useEffect , useId , useMemo , useRef , useState } from 'react' ;
44import { useTranslations } from 'next-intl' ;
5- import { KeyRound , Plus , Loader , Copy , Trash2 , Check , X , Info , CalendarDays , Pencil } from 'lucide-react' ;
5+ import { KeyRound , Plus , Loader , Copy , Trash2 , Check , X , Info , CalendarDays , Pencil , Maximize2 } from 'lucide-react' ;
66import { motion , AnimatePresence } from 'motion/react' ;
77import { Input } from '@/components/ui/input' ;
88import { Calendar } from '@/components/ui/calendar' ;
99import { Popover , PopoverContent , PopoverTrigger } from '@/components/ui/popover' ;
1010import { Switch } from '@/components/ui/switch' ;
1111import { Badge } from '@/components/ui/badge' ;
12+ import {
13+ MorphingDialog ,
14+ MorphingDialogContainer ,
15+ MorphingDialogContent ,
16+ MorphingDialogTrigger ,
17+ useMorphingDialog ,
18+ } from '@/components/ui/morphing-dialog' ;
1219import {
1320 useAPIKeyList ,
1421 useCreateAPIKey ,
@@ -255,7 +262,7 @@ function APIKeyForm({ apiKey, isPending, submitLabel, onSubmit, onClose }: APIKe
255262 disabled = { isPending }
256263 aria-pressed = { neverExpire }
257264 className = { cn (
258- 'h-9 px-3 rounded-xl border text-sm transition-colors' ,
265+ 'h-9 px-3 rounded-xl border text-sm transition-colors whitespace-nowrap shrink-0 ' ,
259266 neverExpire
260267 ? 'bg-primary text-primary-foreground border-primary/30'
261268 : 'border-border bg-muted/20 text-foreground hover:bg-muted/30' ,
@@ -336,88 +343,39 @@ function APIKeyForm({ apiKey, isPending, submitLabel, onSubmit, onClose }: APIKe
336343 ) ;
337344}
338345
339- function APIKeyAddOverlay ( {
340- layoutId,
341- onClose,
342- } : {
343- layoutId : string ;
344- onClose : ( ) => void ;
345- } ) {
346- const t = useTranslations ( 'setting' ) ;
347- const createAPIKey = useCreateAPIKey ( ) ;
348-
349- const handleSubmit = useCallback ( ( data : Omit < APIKey , 'id' | 'api_key' > ) => {
350- createAPIKey . mutate ( data , {
351- onSuccess : ( ) => {
352- toast . success ( t ( 'apiKey.toast.createSuccess' ) ) ;
353- onClose ( ) ;
354- } ,
355- onError : ( error ) => {
356- const msg = ( error as unknown as ApiError ) ?. message ;
357- toast . error ( t ( 'apiKey.toast.createError' ) , { description : msg } ) ;
358- } ,
359- } ) ;
360- } , [ createAPIKey , onClose , t ] ) ;
361-
362- return (
363- < motion . div
364- layoutId = { layoutId }
365- className = "absolute inset-x-0 top-0 z-20 bg-card p-5 rounded-3xl border border-border custom-shadow"
366- transition = { { type : 'spring' , stiffness : 400 , damping : 30 } }
367- >
368- < APIKeyForm
369- isPending = { createAPIKey . isPending }
370- submitLabel = { t ( 'apiKey.form.create' ) }
371- onSubmit = { handleSubmit }
372- onClose = { onClose }
373- />
374- </ motion . div >
375- ) ;
376- }
377-
378- function APIKeyEditOverlay ( {
346+ function APIKeyFormOverlay ( {
379347 layoutId,
380348 apiKey,
349+ isPending,
350+ submitLabel,
351+ onSubmit,
381352 onClose,
382353} : {
383354 layoutId : string ;
384- apiKey : APIKey ;
355+ apiKey ?: APIKey ;
356+ isPending : boolean ;
357+ submitLabel : string ;
358+ onSubmit : ( data : Omit < APIKey , 'id' | 'api_key' > ) => void ;
385359 onClose : ( ) => void ;
386360} ) {
387- const t = useTranslations ( 'setting' ) ;
388- const updateAPIKey = useUpdateAPIKey ( ) ;
389-
390- const handleSubmit = useCallback ( ( data : Omit < APIKey , 'id' | 'api_key' > ) => {
391- updateAPIKey . mutate ( { id : apiKey . id , ...data } , {
392- onSuccess : ( ) => {
393- toast . success ( t ( 'apiKey.toast.updateSuccess' ) ) ;
394- onClose ( ) ;
395- } ,
396- onError : ( error ) => {
397- const msg = ( error as unknown as ApiError ) ?. message ;
398- toast . error ( t ( 'apiKey.toast.updateError' ) , { description : msg } ) ;
399- } ,
400- } ) ;
401- } , [ updateAPIKey , apiKey . id , onClose , t ] ) ;
402-
403361 return (
404362 < motion . div
405363 layoutId = { layoutId }
406- className = "absolute inset-x-0 top-0 z-20 bg-card p-5 rounded-3xl border border-border custom-shadow"
364+ className = "absolute left-1/2 top-1/2 z-20 w-[min(420px,calc(100vw-2rem))] -translate-x-1/2 -translate-y-1/2 bg-card p-5 rounded-3xl border border-border custom-shadow max-h-[80vh] overflow-auto "
407365 transition = { { type : 'spring' , stiffness : 400 , damping : 30 } }
408366 >
409367 < APIKeyForm
410368 apiKey = { apiKey }
411- isPending = { updateAPIKey . isPending }
412- submitLabel = { t ( 'apiKey.form.save' ) }
413- onSubmit = { handleSubmit }
369+ isPending = { isPending }
370+ submitLabel = { submitLabel }
371+ onSubmit = { onSubmit }
414372 onClose = { onClose }
415373 />
416374 </ motion . div >
417375 ) ;
418376}
419377
420- function APIKeyStatsOverlay ( {
378+ function APIKeyStatsCard ( {
421379 layoutId,
422380 apiKey,
423381 onClose,
@@ -433,7 +391,7 @@ function APIKeyStatsOverlay({
433391 return (
434392 < motion . div
435393 layoutId = { layoutId }
436- className = "absolute inset-x-0 top-0 z-30 flex flex-col bg-card p-5 rounded-3xl border border-border custom-shadow"
394+ className = "absolute left-1/2 top-1/2 z-30 w-[min(320px,calc(100vw-2rem))] -translate-x-1/2 -translate-y-1/2 flex flex-col bg-card p-5 rounded-3xl border border-border custom-shadow max-h-[80vh] overflow-auto "
437395 transition = { { type : 'spring' , stiffness : 400 , damping : 30 } }
438396 >
439397 < div className = "flex items-center justify-between gap-2 mb-3" >
@@ -501,10 +459,11 @@ function APIKeyStatsOverlay({
501459 ) ;
502460}
503461
504- function KeyItem ( {
462+ function APIKeyKeyItem ( {
505463 apiKey,
506464 statsLayoutId,
507465 editLayoutId,
466+ deleteLayoutId,
508467 onViewStats,
509468 onEdit,
510469 onDelete,
@@ -513,6 +472,7 @@ function KeyItem({
513472 apiKey : APIKey ;
514473 statsLayoutId : string ;
515474 editLayoutId : string ;
475+ deleteLayoutId : string ;
516476 onViewStats : ( ) => void ;
517477 onEdit : ( ) => void ;
518478 onDelete : ( ) => void ;
@@ -613,7 +573,7 @@ function KeyItem({
613573
614574 { ! confirmDelete && (
615575 < motion . button
616- layoutId = { `delete-btn- ${ apiKey . id } ` }
576+ layoutId = { deleteLayoutId }
617577 onClick = { ( ) => setConfirmDelete ( true ) }
618578 className = "flex h-8 w-8 items-center justify-center rounded-lg bg-destructive/10 text-destructive transition-colors hover:bg-destructive hover:text-destructive-foreground"
619579 >
@@ -625,7 +585,7 @@ function KeyItem({
625585 < AnimatePresence >
626586 { confirmDelete && (
627587 < motion . div
628- layoutId = { `delete-btn- ${ apiKey . id } ` }
588+ layoutId = { deleteLayoutId }
629589 className = "absolute inset-0 flex items-center justify-center gap-2 bg-destructive p-3 rounded-xl"
630590 transition = { { type : 'spring' , stiffness : 400 , damping : 30 } }
631591 >
@@ -650,16 +610,31 @@ function KeyItem({
650610 ) ;
651611}
652612
653- export function SettingAPIKey ( ) {
613+ function APIKeyPanelBase ( {
614+ idPrefix,
615+ containerClassName,
616+ listClassName,
617+ renderHeaderExtra,
618+ } : {
619+ idPrefix : string ;
620+ containerClassName : string ;
621+ listClassName : string ;
622+ renderHeaderExtra ?: ( ctx : {
623+ disabled : boolean ;
624+ onCloseAllOverlays : ( ) => void ;
625+ } ) => React . ReactNode ;
626+ } ) {
654627 const t = useTranslations ( 'setting' ) ;
655628 const { data : apiKeys , isLoading : apiKeysLoading , error : apiKeysError } = useAPIKeyList ( ) ;
656629 const createAPIKey = useCreateAPIKey ( ) ;
630+ const updateAPIKey = useUpdateAPIKey ( ) ;
657631 const deleteAPIKey = useDeleteAPIKey ( ) ;
658632
659633 const instanceId = useId ( ) ;
660- const addLayoutId = `add-btn-apikey-${ instanceId } ` ;
661- const statsPrefix = `apikey-stats-${ instanceId } ` ;
662- const editPrefix = `apikey-edit-${ instanceId } ` ;
634+ const addLayoutId = `add-btn-${ idPrefix } -${ instanceId } ` ;
635+ const statsPrefix = `${ idPrefix } -stats-${ instanceId } ` ;
636+ const editPrefix = `${ idPrefix } -edit-${ instanceId } ` ;
637+ const deletePrefix = `${ idPrefix } -delete-` ;
663638
664639 const [ isAdding , setIsAdding ] = useState ( false ) ;
665640 const [ viewingStats , setViewingStats ] = useState < { apiKey : APIKey ; layoutId : string } | null > ( null ) ;
@@ -691,37 +666,71 @@ export function SettingAPIKey() {
691666 setEditingKey ( null ) ;
692667 } , [ ] ) ;
693668
669+ const disabledHeaderActions = createAPIKey . isPending || isAdding || ! ! viewingStats || ! ! editingKey ;
670+
671+ const handleCreate = useCallback ( ( data : Omit < APIKey , 'id' | 'api_key' > ) => {
672+ createAPIKey . mutate ( data , {
673+ onSuccess : ( ) => {
674+ toast . success ( t ( 'apiKey.toast.createSuccess' ) ) ;
675+ setIsAdding ( false ) ;
676+ } ,
677+ onError : ( error ) => {
678+ const msg = ( error as unknown as ApiError ) ?. message ;
679+ toast . error ( t ( 'apiKey.toast.createError' ) , { description : msg } ) ;
680+ } ,
681+ } ) ;
682+ } , [ createAPIKey , t ] ) ;
683+
684+ const handleUpdate = useCallback ( ( apiKey : APIKey , data : Omit < APIKey , 'id' | 'api_key' > ) => {
685+ updateAPIKey . mutate ( { id : apiKey . id , ...data } , {
686+ onSuccess : ( ) => {
687+ toast . success ( t ( 'apiKey.toast.updateSuccess' ) ) ;
688+ setEditingKey ( null ) ;
689+ } ,
690+ onError : ( error ) => {
691+ const msg = ( error as unknown as ApiError ) ?. message ;
692+ toast . error ( t ( 'apiKey.toast.updateError' ) , { description : msg } ) ;
693+ } ,
694+ } ) ;
695+ } , [ t , updateAPIKey ] ) ;
696+
694697 return (
695- < div className = "rounded-3xl border border-border bg-card p-6 custom-shadow space-y-5 relative" >
698+ < div className = { containerClassName } >
696699 < div className = "flex items-center justify-between gap-3" >
697700 < h2 className = "text-lg font-bold text-card-foreground flex items-center gap-2" >
698701 < KeyRound className = "h-5 w-5" />
699702 { t ( 'apiKey.title' ) }
700703 </ h2 >
701- < motion . button
702- layoutId = { addLayoutId }
703- type = "button"
704- onClick = { ( ) => setIsAdding ( true ) }
705- disabled = { createAPIKey . isPending || isAdding || ! ! viewingStats || ! ! editingKey }
706- className = "h-9 w-9 flex items-center justify-center rounded-lg bg-muted/60 text-muted-foreground transition-colors hover:bg-muted disabled:opacity-50"
707- title = { t ( 'apiKey.add' ) ?? '添加' }
708- >
709- < Plus className = "h-4 w-4" />
710- </ motion . button >
704+ < div className = "flex items-center gap-2" >
705+ < motion . button
706+ layoutId = { addLayoutId }
707+ type = "button"
708+ onClick = { ( ) => setIsAdding ( true ) }
709+ disabled = { disabledHeaderActions }
710+ className = "h-9 w-9 flex items-center justify-center rounded-lg bg-muted/60 text-muted-foreground transition-colors hover:bg-muted disabled:opacity-50"
711+ title = { t ( 'apiKey.add' ) }
712+ >
713+ < Plus className = "h-4 w-4" />
714+ </ motion . button >
715+ { renderHeaderExtra ?.( { disabled : disabledHeaderActions , onCloseAllOverlays : closeAllOverlays } ) }
716+ </ div >
711717 </ div >
712718
713719 < AnimatePresence >
714720 { isAdding && (
715- < APIKeyAddOverlay
721+ < APIKeyFormOverlay
716722 layoutId = { addLayoutId }
723+ isPending = { createAPIKey . isPending }
724+ submitLabel = { t ( 'apiKey.form.create' ) }
725+ onSubmit = { handleCreate }
717726 onClose = { ( ) => setIsAdding ( false ) }
718727 />
719728 ) }
720729 </ AnimatePresence >
721730
722731 < AnimatePresence >
723732 { viewingStats && (
724- < APIKeyStatsOverlay
733+ < APIKeyStatsCard
725734 layoutId = { viewingStats . layoutId }
726735 apiKey = { viewingStats . apiKey }
727736 onClose = { ( ) => setViewingStats ( null ) }
@@ -731,15 +740,18 @@ export function SettingAPIKey() {
731740
732741 < AnimatePresence >
733742 { editingKey && (
734- < APIKeyEditOverlay
743+ < APIKeyFormOverlay
735744 layoutId = { editingKey . layoutId }
736745 apiKey = { editingKey . apiKey }
746+ isPending = { updateAPIKey . isPending }
747+ submitLabel = { t ( 'apiKey.form.save' ) }
748+ onSubmit = { ( data ) => handleUpdate ( editingKey . apiKey , data ) }
737749 onClose = { ( ) => setEditingKey ( null ) }
738750 />
739751 ) }
740752 </ AnimatePresence >
741753
742- < div className = "space-y-2 h-32 overflow-y-auto" >
754+ < div className = { listClassName } >
743755 { apiKeysLoading ? (
744756 < div className = "h-full flex items-center justify-center text-sm text-muted-foreground" >
745757 < Loader className = "h-4 w-4 animate-spin" />
@@ -757,12 +769,14 @@ export function SettingAPIKey() {
757769 { sortedApiKeys . map ( ( apiKey ) => {
758770 const statsLayoutId = `${ statsPrefix } -${ apiKey . id } ` ;
759771 const editLayoutId = `${ editPrefix } -${ apiKey . id } ` ;
772+ const deleteLayoutId = `${ deletePrefix } ${ apiKey . id } ` ;
760773 return (
761- < KeyItem
774+ < APIKeyKeyItem
762775 key = { apiKey . id }
763776 apiKey = { apiKey }
764777 statsLayoutId = { statsLayoutId }
765778 editLayoutId = { editLayoutId }
779+ deleteLayoutId = { deleteLayoutId }
766780 onViewStats = { ( ) => {
767781 closeAllOverlays ( ) ;
768782 setViewingStats ( { apiKey, layoutId : statsLayoutId } ) ;
@@ -782,3 +796,46 @@ export function SettingAPIKey() {
782796 </ div >
783797 ) ;
784798}
799+
800+ function APIKeyDialogPanel ( ) {
801+ const { setIsOpen } = useMorphingDialog ( ) ;
802+ return (
803+ < APIKeyPanelBase
804+ idPrefix = "apikey-dialog"
805+ containerClassName = "rounded-3xl border border-border bg-card p-6 space-y-5 relative w-screen max-w-full md:max-w-xl"
806+ listClassName = "space-y-2 max-h-[70vh] overflow-y-auto"
807+ renderHeaderExtra = { ( ) => (
808+ < button
809+ type = "button"
810+ onClick = { ( ) => setIsOpen ( false ) }
811+ className = "h-9 w-9 flex items-center justify-center rounded-lg bg-muted/60 text-muted-foreground transition-colors hover:bg-muted"
812+ title = "Close"
813+ >
814+ < X className = "h-4 w-4" />
815+ </ button >
816+ ) }
817+ />
818+ ) ;
819+ }
820+
821+ export function SettingAPIKey ( ) {
822+ return (
823+ < APIKeyPanelBase
824+ idPrefix = "apikey"
825+ containerClassName = "rounded-3xl border border-border bg-card p-6 custom-shadow space-y-5 relative"
826+ listClassName = "space-y-2 h-32 overflow-y-auto"
827+ renderHeaderExtra = { ( ) => (
828+ < MorphingDialog >
829+ < MorphingDialogTrigger className = "h-9 w-9 flex items-center justify-center rounded-lg bg-muted/60 text-muted-foreground transition-colors hover:bg-muted" >
830+ < Maximize2 className = "h-4 w-4" />
831+ </ MorphingDialogTrigger >
832+ < MorphingDialogContainer >
833+ < MorphingDialogContent className = "relative" >
834+ < APIKeyDialogPanel />
835+ </ MorphingDialogContent >
836+ </ MorphingDialogContainer >
837+ </ MorphingDialog >
838+ ) }
839+ />
840+ ) ;
841+ }
0 commit comments