|
|
@@ -17,7 +17,10 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
For commercial licensing, please contact support@quantumnous.com
|
|
|
*/
|
|
|
import { zodResolver } from '@hookform/resolvers/zod'
|
|
|
+import { ImageUpload01Icon } from '@hugeicons/core-free-icons'
|
|
|
+import { HugeiconsIcon } from '@hugeicons/react'
|
|
|
import type { Resolver } from 'react-hook-form'
|
|
|
+import { useState } from 'react'
|
|
|
import { useTranslation } from 'react-i18next'
|
|
|
import * as z from 'zod'
|
|
|
|
|
|
@@ -31,6 +34,9 @@ import {
|
|
|
FormMessage,
|
|
|
} from '@/components/ui/form'
|
|
|
import { Input } from '@/components/ui/input'
|
|
|
+import { Spinner } from '@/components/ui/spinner'
|
|
|
+import { DEFAULT_LOGO } from '@/lib/constants'
|
|
|
+import { cn } from '@/lib/utils'
|
|
|
|
|
|
import { FormDirtyIndicator } from '../components/form-dirty-indicator'
|
|
|
import { FormNavigationGuard } from '../components/form-navigation-guard'
|
|
|
@@ -165,7 +171,9 @@ export function SystemInfoSection({ defaultValues }: SystemInfoSectionProps) {
|
|
|
},
|
|
|
})
|
|
|
|
|
|
- const { handleAppIconUpload } = useIconUpload()
|
|
|
+ const { handleAppIconUpload, isUploading } = useIconUpload()
|
|
|
+ const [isDragActive, setIsDragActive] = useState(false)
|
|
|
+ const logoUrl = normalizedDefaults.Logo || DEFAULT_LOGO
|
|
|
|
|
|
return (
|
|
|
<>
|
|
|
@@ -267,57 +275,72 @@ export function SystemInfoSection({ defaultValues }: SystemInfoSectionProps) {
|
|
|
)}
|
|
|
/>
|
|
|
|
|
|
- <div className='grid grid-cols-1 gap-6 md:grid-cols-2'>
|
|
|
- <div className='space-y-2'>
|
|
|
- <label className='text-sm leading-none font-medium peer-disabled:cursor-not-allowed peer-disabled:opacity-70'>
|
|
|
- {t('App Icon')}
|
|
|
- </label>
|
|
|
+ <div className='space-y-2'>
|
|
|
+ <label className='text-sm leading-none font-medium peer-disabled:cursor-not-allowed peer-disabled:opacity-70'>
|
|
|
+ {t('App Icon')}
|
|
|
+ </label>
|
|
|
+ <div className='flex items-stretch gap-4'>
|
|
|
+ <div className='relative size-20 shrink-0 overflow-hidden rounded-2xl border border-border/80 bg-muted/30 shadow-sm sm:size-24'>
|
|
|
+ <img
|
|
|
+ src={logoUrl}
|
|
|
+ alt={t('App Icon')}
|
|
|
+ className='size-full object-cover'
|
|
|
+ />
|
|
|
+ {isUploading && (
|
|
|
+ <div className='absolute inset-0 flex items-center justify-center bg-background/70 backdrop-blur-sm'>
|
|
|
+ <Spinner className='size-5 text-primary' />
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
<div
|
|
|
- className='cursor-pointer rounded-lg border-2 border-dashed border-gray-300 p-6 text-center transition-colors hover:border-gray-400'
|
|
|
+ role='button'
|
|
|
+ tabIndex={0}
|
|
|
+ className={cn(
|
|
|
+ 'group relative flex flex-1 cursor-pointer flex-col items-center justify-center gap-2 rounded-2xl border-2 border-dashed border-border/80 bg-muted/20 px-4 py-5 text-center transition-all hover:border-primary/40 hover:bg-primary/5 focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/45 focus-visible:outline-none',
|
|
|
+ isDragActive && 'border-primary bg-primary/5',
|
|
|
+ isUploading && 'pointer-events-none opacity-60'
|
|
|
+ )}
|
|
|
onClick={() =>
|
|
|
+ !isUploading &&
|
|
|
document.getElementById('app-icon-input')?.click()
|
|
|
}
|
|
|
+ onKeyDown={(e) => {
|
|
|
+ if (
|
|
|
+ !isUploading &&
|
|
|
+ (e.key === 'Enter' || e.key === ' ')
|
|
|
+ ) {
|
|
|
+ e.preventDefault()
|
|
|
+ document.getElementById('app-icon-input')?.click()
|
|
|
+ }
|
|
|
+ }}
|
|
|
onDragOver={(e) => {
|
|
|
e.preventDefault()
|
|
|
- e.currentTarget.classList.add(
|
|
|
- 'border-blue-400',
|
|
|
- 'bg-blue-50'
|
|
|
- )
|
|
|
+ setIsDragActive(true)
|
|
|
}}
|
|
|
onDragLeave={(e) => {
|
|
|
- e.currentTarget.classList.remove(
|
|
|
- 'border-blue-400',
|
|
|
- 'bg-blue-50'
|
|
|
- )
|
|
|
+ e.preventDefault()
|
|
|
+ setIsDragActive(false)
|
|
|
}}
|
|
|
onDrop={(e) => {
|
|
|
e.preventDefault()
|
|
|
- e.currentTarget.classList.remove(
|
|
|
- 'border-blue-400',
|
|
|
- 'bg-blue-50'
|
|
|
- )
|
|
|
+ setIsDragActive(false)
|
|
|
const file = e.dataTransfer.files?.[0]
|
|
|
if (file && file.type === 'image/png') {
|
|
|
handleAppIconUpload(file)
|
|
|
}
|
|
|
}}
|
|
|
>
|
|
|
- <div className='space-y-2'>
|
|
|
- <svg
|
|
|
- className='mx-auto h-10 w-10 text-gray-400'
|
|
|
- fill='none'
|
|
|
- stroke='currentColor'
|
|
|
- viewBox='0 0 24 24'
|
|
|
- >
|
|
|
- <path
|
|
|
- strokeLinecap='round'
|
|
|
- strokeLinejoin='round'
|
|
|
- strokeWidth={2}
|
|
|
- d='M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z'
|
|
|
- />
|
|
|
- </svg>
|
|
|
- <p className='text-sm text-gray-600'>{t('Click or drag to upload')}</p>
|
|
|
- <p className='text-xs text-gray-400'>{t('PNG only')}</p>
|
|
|
+ <HugeiconsIcon
|
|
|
+ icon={ImageUpload01Icon}
|
|
|
+ className='size-7 text-muted-foreground/70 transition-colors group-hover:text-primary'
|
|
|
+ />
|
|
|
+ <div className='space-y-0.5'>
|
|
|
+ <p className='text-sm font-medium text-foreground'>
|
|
|
+ {t('Click or drag to upload')}
|
|
|
+ </p>
|
|
|
+ <p className='text-xs text-muted-foreground'>
|
|
|
+ {t('PNG only')}
|
|
|
+ </p>
|
|
|
</div>
|
|
|
<input
|
|
|
id='app-icon-input'
|