Compare commits
	
		
			10 Commits
		
	
	
		
			07ce8dea90
			...
			6fd8f0f85f
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 6fd8f0f85f | |||
| 9001356f53 | |||
| 52619a0330 | |||
| 3c29d1d99c | |||
| 655b3a9216 | |||
| 57c7f35c09 | |||
| ae9ab3668d | |||
| 193bd380c3 | |||
| 3b50bb5877 | |||
| d5f18b143b | 
							
								
								
									
										1
									
								
								TODO.md
									
									
									
									
									
								
							
							
						
						
									
										1
									
								
								TODO.md
									
									
									
									
									
								
							| @ -4,6 +4,7 @@ A list of all tasks that need to be completed in the app<br> | ||||
| A more complex version of this list is available [here](https://trello.com/b/kJw6Aapn/svchat). | ||||
| 
 | ||||
| - [x] Account / Profile management | ||||
| - [x] Avatar cropping | ||||
| - [ ] Channel context menus | ||||
| - [x] Containerization with docker and docker-compose | ||||
| - [ ] Editing messages | ||||
|  | ||||
							
								
								
									
										10
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								package.json
									
									
									
									
									
								
							| @ -46,25 +46,27 @@ | ||||
|     "@types/better-sqlite3": "^7.6.12", | ||||
|     "@types/express": "^5.0.0", | ||||
|     "@types/markdown-it-link-attributes": "^3.0.5", | ||||
|     "@types/minio": "^7.1.1", | ||||
|     "autoprefixer": "^10.4.20", | ||||
|     "better-auth": "^1.1.16", | ||||
|     "better-sqlite3": "^11.8.1", | ||||
|     "bits-ui": "^1.3.5", | ||||
|     "bits-ui": "1.3.2", | ||||
|     "cassandra-driver": "^4.7.2", | ||||
|     "dotenv": "^16.4.7", | ||||
|     "express": "^4.21.2", | ||||
|     "lucide-svelte": "^0.477.0", | ||||
|     "lucide-svelte": "^0.475.0", | ||||
|     "markdown-it": "^14.1.0", | ||||
|     "markdown-it-highlightjs": "^4.2.0", | ||||
|     "markdown-it-link-attributes": "^4.0.1", | ||||
|     "minio": "^8.0.4", | ||||
|     "mode-watcher": "^0.5.1", | ||||
|     "runed": "^0.23.4", | ||||
|     "socket.io": "^4.8.1", | ||||
|     "socket.io-client": "^4.8.1", | ||||
|     "svelte-easy-crop": "^4.0.0", | ||||
|     "svelte-radix": "^2.0.1", | ||||
|     "svelte-toolbelt": "^0.7.1", | ||||
|     "sveltekit-superforms": "^2.23.1", | ||||
|     "tailwind-merge": "^3.0.2", | ||||
|     "tailwind-merge": "^2.6.0", | ||||
|     "tailwind-variants": "^0.3.1", | ||||
|     "tailwindcss": "^3.4.17", | ||||
|     "tsm": "^2.3.0", | ||||
|  | ||||
| @ -23,7 +23,7 @@ | ||||
|     --input: 20 5.9% 90%; | ||||
|     --primary: 24 9.8% 10%; | ||||
|     --primary-foreground: 60 9.1% 97.8%; | ||||
|     --secondary: 60 4.8% 95.9%; | ||||
|     --secondary: 240 6% 90%; | ||||
|     --secondary-foreground: 24 9.8% 10%; | ||||
|     --accent: 60 4.8% 95.9%; | ||||
|     --accent-foreground: 24 9.8% 10%; | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| <script lang="ts"> | ||||
|   import { page } from '$app/state'; | ||||
|   import MessageSquare from 'lucide-svelte/icons/message-square'; | ||||
|   import { MessageSquare } from 'lucide-svelte'; | ||||
| 
 | ||||
|   interface Props { | ||||
|     channelName: string; | ||||
|  | ||||
| @ -1,29 +1,49 @@ | ||||
| <script lang="ts"> | ||||
|   import { buttonVariants } from '$lib/components/ui/button/index'; | ||||
|   import * as ImageCropper from '$lib/components/ui/image-cropper'; | ||||
|   import { getFileFromUrl } from '$lib/components/ui/image-cropper'; | ||||
|   import { generateStream } from '$lib/functions/generateReadableStream'; | ||||
|   import { Button } from '$lib/components/ui/button/index'; | ||||
|   import { Edit } from 'lucide-svelte'; | ||||
|   import type { PageData } from '../../../routes/(main)/account/$types'; | ||||
| 
 | ||||
|   let files: FileList; | ||||
|   const { data }: { data: PageData } = $props(); | ||||
|   let src = $state(data.user.image ?? `https://api.dicebear.com/9.x/identicon/svg?seed=${data.session?.user.id}`); | ||||
|   let error: string | null = $state(null); | ||||
| 
 | ||||
|   async function submit(e: SubmitEvent) { | ||||
|     e.preventDefault(); | ||||
|     if (files.length === 0) return; | ||||
|     await generateStream(files[0]).then((res) => { | ||||
|   async function submit(file: File) { | ||||
|     await generateStream(file).then((res) => { | ||||
|       if (res.ok) window.location.reload(); | ||||
|     }); | ||||
|   } | ||||
| </script> | ||||
| 
 | ||||
| <form class="grid w-full items-start gap-3" onsubmit={submit}> | ||||
|   <fieldset class="flex size-full flex-col justify-center gap-3 rounded-lg border p-4"> | ||||
| <form class="grid w-full items-start gap-3"> | ||||
|   <fieldset class="flex size-full flex-col items-center justify-center gap-3 rounded-lg border p-4"> | ||||
|     <legend class="-ml-1 px-1 text-sm font-medium"> Upload Profile Image </legend> | ||||
|     <input | ||||
|       class="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 | ||||
|       file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 | ||||
|       focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50" | ||||
|       type="file" | ||||
|       accept="image/jpeg, image/png" | ||||
|       bind:files | ||||
|     /> | ||||
|     <Button type="submit">Update Profile Photo</Button> | ||||
|     <ImageCropper.Root | ||||
|       bind:error | ||||
|       bind:src | ||||
|       onCropped={async (url) => { | ||||
|         const file = await getFileFromUrl(url); | ||||
|         submit(file); | ||||
|       }} | ||||
|     > | ||||
|       <div class="relative"> | ||||
|         <ImageCropper.UploadTrigger> | ||||
|           <ImageCropper.Preview class="rounded-md border bg-white" /> | ||||
|           <div class="absolute -bottom-3 -left-3 size-9 rounded-lg {buttonVariants({ variant: 'outline' })}"> | ||||
|             <Edit class="size-4" /> | ||||
|           </div> | ||||
|         </ImageCropper.UploadTrigger> | ||||
|       </div> | ||||
|       <ImageCropper.Dialog> | ||||
|         <ImageCropper.Cropper cropShape="rect" /> | ||||
|         <ImageCropper.Controls> | ||||
|           <ImageCropper.Cancel /> | ||||
|           <ImageCropper.Crop /> | ||||
|         </ImageCropper.Controls> | ||||
|       </ImageCropper.Dialog> | ||||
|     </ImageCropper.Root> | ||||
|     {#if error}<span class="text-sm text-red-500">{error}</span>{/if} | ||||
|   </fieldset> | ||||
| </form> | ||||
|  | ||||
| @ -1,5 +1,5 @@ | ||||
| <script lang="ts"> | ||||
|   import MessagesSquare from 'lucide-svelte/icons/messages-square'; | ||||
|   import { MessageSquare } from 'lucide-svelte'; | ||||
|   import type { Snippet } from 'svelte'; | ||||
|   import type { PageData } from '../../routes/(main)/$types'; | ||||
|   import Channel from './channel.svelte'; | ||||
| @ -23,7 +23,7 @@ | ||||
|       <div class="flex h-full max-h-screen flex-col gap-2"> | ||||
|         <div class="flex h-14 items-center border-b px-4 lg:h-[60px] lg:px-6"> | ||||
|           <a href="/" class="flex items-center gap-2 font-semibold"> | ||||
|             <MessagesSquare class="h-6 w-6" /> | ||||
|             <MessageSquare class="h-6 w-6" /> | ||||
|             <span class="">SVChat</span> | ||||
|           </a> | ||||
|           <ModeSwitcher /> | ||||
|  | ||||
| @ -4,10 +4,7 @@ | ||||
|   import renderMarkdown from '$lib/functions/renderMarkdown'; | ||||
|   import { type TypeMessage } from '$lib/types'; | ||||
| 
 | ||||
|   import Clipboard from 'lucide-svelte/icons/clipboard'; | ||||
|   import SquareUserRound from 'lucide-svelte/icons/square-user-round'; | ||||
|   import IDCard from 'lucide-svelte/icons/id-card'; | ||||
|   import CalendarClock from 'lucide-svelte/icons/calendar-clock'; | ||||
|   import { Clipboard, SquareUserRound, IdCard, CalendarClock } from 'lucide-svelte'; | ||||
| 
 | ||||
|   interface Props { | ||||
|     open: boolean; | ||||
| @ -63,7 +60,7 @@ | ||||
|     > | ||||
|     <!-- Copy User ID --> | ||||
|     <ContextMenu.Item class="flex cursor-pointer items-center gap-1.5" onclick={() => copy('user ID', uid)} | ||||
|       ><IDCard size={16} />Copy User ID</ContextMenu.Item | ||||
|       ><IdCard size={16} />Copy User ID</ContextMenu.Item | ||||
|     > | ||||
|     <ContextMenu.Separator /> | ||||
|     <!-- Copy Text --> | ||||
|  | ||||
| @ -1,8 +1,7 @@ | ||||
| <script lang="ts"> | ||||
|   import * as Tooltip from '$lib/components/ui/tooltip'; | ||||
|   import { buttonVariants } from '$lib/components/ui/button'; | ||||
|   import Moon from 'lucide-svelte/icons/moon-star'; | ||||
|   import Sun from 'lucide-svelte/icons/sun'; | ||||
|   import { Moon, Sun } from 'lucide-svelte'; | ||||
|   import { toggleMode } from 'mode-watcher'; | ||||
| </script> | ||||
| 
 | ||||
|  | ||||
							
								
								
									
										14
									
								
								src/lib/components/ui/avatar/avatar-fallback.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								src/lib/components/ui/avatar/avatar-fallback.svelte
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,14 @@ | ||||
| <!-- | ||||
| 	jsrepo 1.41.3 | ||||
| 	Installed from github/ieedan/shadcn-svelte-extras | ||||
| 	3-4-2025 | ||||
| --> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
|   import { Avatar as AvatarPrimitive } from 'bits-ui'; | ||||
|   import { cn } from '$lib/utils/utils.js'; | ||||
| 
 | ||||
|   let { ref = $bindable(null), class: className, ...restProps }: AvatarPrimitive.FallbackProps = $props(); | ||||
| </script> | ||||
| 
 | ||||
| <AvatarPrimitive.Fallback bind:ref class={cn('flex h-full w-full items-center justify-center rounded-full bg-muted', className)} {...restProps} /> | ||||
							
								
								
									
										14
									
								
								src/lib/components/ui/avatar/avatar-image.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								src/lib/components/ui/avatar/avatar-image.svelte
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,14 @@ | ||||
| <!-- | ||||
| 	jsrepo 1.41.3 | ||||
| 	Installed from github/ieedan/shadcn-svelte-extras | ||||
| 	3-4-2025 | ||||
| --> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
|   import { Avatar as AvatarPrimitive } from 'bits-ui'; | ||||
|   import { cn } from '$lib/utils/utils.js'; | ||||
| 
 | ||||
|   let { ref = $bindable(null), class: className, ...restProps }: AvatarPrimitive.ImageProps = $props(); | ||||
| </script> | ||||
| 
 | ||||
| <AvatarPrimitive.Image bind:ref class={cn('aspect-square h-full w-full', className)} {...restProps} /> | ||||
							
								
								
									
										14
									
								
								src/lib/components/ui/avatar/avatar.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								src/lib/components/ui/avatar/avatar.svelte
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,14 @@ | ||||
| <!-- | ||||
| 	jsrepo 1.41.3 | ||||
| 	Installed from github/ieedan/shadcn-svelte-extras | ||||
| 	3-4-2025 | ||||
| --> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
|   import { Avatar as AvatarPrimitive } from 'bits-ui'; | ||||
|   import { cn } from '$lib/utils/utils.js'; | ||||
| 
 | ||||
|   let { ref = $bindable(null), class: className, ...restProps }: AvatarPrimitive.RootProps = $props(); | ||||
| </script> | ||||
| 
 | ||||
| <AvatarPrimitive.Root bind:ref class={cn('relative flex size-10 shrink-0 overflow-hidden rounded-full', className)} {...restProps} /> | ||||
							
								
								
									
										19
									
								
								src/lib/components/ui/avatar/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								src/lib/components/ui/avatar/index.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,19 @@ | ||||
| /* | ||||
| 	jsrepo 1.41.3 | ||||
| 	Installed from github/ieedan/shadcn-svelte-extras | ||||
| 	3-4-2025 | ||||
| */ | ||||
| 
 | ||||
| import Root from './avatar.svelte'; | ||||
| import Image from './avatar-image.svelte'; | ||||
| import Fallback from './avatar-fallback.svelte'; | ||||
| 
 | ||||
| export { | ||||
|   Root, | ||||
|   Image, | ||||
|   Fallback, | ||||
|   //
 | ||||
|   Root as Avatar, | ||||
|   Image as AvatarImage, | ||||
|   Fallback as AvatarFallback, | ||||
| }; | ||||
| @ -1,7 +1,6 @@ | ||||
| <script lang="ts"> | ||||
|   import { ContextMenu as ContextMenuPrimitive, type WithoutChildrenOrChild } from 'bits-ui'; | ||||
|   import Check from 'lucide-svelte/icons/check'; | ||||
|   import Minus from 'lucide-svelte/icons/minus'; | ||||
|   import { Check, Minus } from 'lucide-svelte'; | ||||
|   import type { Snippet } from 'svelte'; | ||||
|   import { cn } from '$lib/utils.js'; | ||||
| 
 | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| <script lang="ts"> | ||||
|   import { ContextMenu as ContextMenuPrimitive, type WithoutChild } from 'bits-ui'; | ||||
|   import Circle from 'lucide-svelte/icons/circle'; | ||||
|   import { Circle } from 'lucide-svelte'; | ||||
|   import { cn } from '$lib/utils.js'; | ||||
| 
 | ||||
|   let { ref = $bindable(null), class: className, children: childrenProp, ...restProps }: WithoutChild<ContextMenuPrimitive.RadioItemProps> = $props(); | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| <script lang="ts"> | ||||
|   import { ContextMenu as ContextMenuPrimitive, type WithoutChild } from 'bits-ui'; | ||||
|   import ChevronRight from 'lucide-svelte/icons/chevron-right'; | ||||
|   import { ChevronRight } from 'lucide-svelte'; | ||||
|   import { cn } from '$lib/utils.js'; | ||||
| 
 | ||||
|   let { | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| <script lang="ts"> | ||||
|   import { Dialog as DialogPrimitive, type WithoutChildrenOrChild } from 'bits-ui'; | ||||
|   import X from 'lucide-svelte/icons/x'; | ||||
|   import { X } from 'lucide-svelte'; | ||||
|   import type { Snippet } from 'svelte'; | ||||
|   import * as Dialog from './index.js'; | ||||
|   import { cn } from '$lib/utils.js'; | ||||
|  | ||||
| @ -1,7 +1,6 @@ | ||||
| <script lang="ts"> | ||||
|   import { DropdownMenu as DropdownMenuPrimitive, type WithoutChildrenOrChild } from 'bits-ui'; | ||||
|   import Check from 'lucide-svelte/icons/check'; | ||||
|   import Minus from 'lucide-svelte/icons/minus'; | ||||
|   import { Check, Minus } from 'lucide-svelte'; | ||||
|   import { cn } from '$lib/utils.js'; | ||||
|   import type { Snippet } from 'svelte'; | ||||
| 
 | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| <script lang="ts"> | ||||
|   import { DropdownMenu as DropdownMenuPrimitive, type WithoutChild } from 'bits-ui'; | ||||
|   import Circle from 'lucide-svelte/icons/circle'; | ||||
|   import { Circle } from 'lucide-svelte'; | ||||
|   import { cn } from '$lib/utils.js'; | ||||
| 
 | ||||
|   let { | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| <script lang="ts"> | ||||
|   import { DropdownMenu as DropdownMenuPrimitive, type WithoutChild } from 'bits-ui'; | ||||
|   import ChevronRight from 'lucide-svelte/icons/chevron-right'; | ||||
|   import { ChevronRight } from 'lucide-svelte'; | ||||
|   import { cn } from '$lib/utils.js'; | ||||
| 
 | ||||
|   let { | ||||
|  | ||||
| @ -0,0 +1,21 @@ | ||||
| <!-- | ||||
| 	jsrepo 1.41.3 | ||||
| 	Installed from github/ieedan/shadcn-svelte-extras | ||||
| 	3-4-2025 | ||||
| --> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
|   import { type ButtonProps, Button } from '$lib/components/ui/button'; | ||||
|   import type { WithoutChildren } from 'bits-ui'; | ||||
|   import { useImageCropperCancel } from './image-cropper.svelte.js'; | ||||
|   import { Trash2 } from 'lucide-svelte'; | ||||
| 
 | ||||
|   let { variant = 'outline', size = 'sm', ...rest }: Omit<WithoutChildren<ButtonProps>, 'onclick'> = $props(); | ||||
| 
 | ||||
|   const cancelState = useImageCropperCancel(); | ||||
| </script> | ||||
| 
 | ||||
| <Button {...rest} {size} {variant} onclick={cancelState.onclick}> | ||||
|   <Trash2 /> | ||||
|   <span>Cancel</span> | ||||
| </Button> | ||||
| @ -0,0 +1,16 @@ | ||||
| <!-- | ||||
| 	jsrepo 1.41.3 | ||||
| 	Installed from github/ieedan/shadcn-svelte-extras | ||||
| 	3-4-2025 | ||||
| --> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
|   import { cn } from '$lib/utils/utils'; | ||||
|   import type { HTMLAttributes } from 'svelte/elements'; | ||||
| 
 | ||||
|   let { class: className, children, ...rest }: HTMLAttributes<HTMLDivElement> = $props(); | ||||
| </script> | ||||
| 
 | ||||
| <div {...rest} class={cn('flex w-full place-items-center justify-center gap-2', className)}> | ||||
|   {@render children?.()} | ||||
| </div> | ||||
| @ -0,0 +1,21 @@ | ||||
| <!-- | ||||
| 	jsrepo 1.41.3 | ||||
| 	Installed from github/ieedan/shadcn-svelte-extras | ||||
| 	3-4-2025 | ||||
| --> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
|   import { type ButtonProps, Button } from '$lib/components/ui/button'; | ||||
|   import type { WithoutChildren } from 'bits-ui'; | ||||
|   import { useImageCropperCrop } from './image-cropper.svelte.js'; | ||||
|   import { ImageUp } from 'lucide-svelte'; | ||||
| 
 | ||||
|   let { variant = 'default', size = 'sm', ...rest }: Omit<WithoutChildren<ButtonProps>, 'onclick'> = $props(); | ||||
| 
 | ||||
|   const cropState = useImageCropperCrop(); | ||||
| </script> | ||||
| 
 | ||||
| <Button {...rest} {size} {variant} onclick={cropState.onclick} data-testid="crop"> | ||||
|   <ImageUp /> | ||||
|   <span>Upload</span> | ||||
| </Button> | ||||
| @ -0,0 +1,19 @@ | ||||
| <!-- | ||||
| 	jsrepo 1.41.3 | ||||
| 	Installed from github/ieedan/shadcn-svelte-extras | ||||
| 	3-4-2025 | ||||
| --> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
|   import Cropper, { type CropperProps } from 'svelte-easy-crop'; | ||||
|   import { useImageCropperCropper } from './image-cropper.svelte.js'; | ||||
| 
 | ||||
|   let { cropShape = 'round', aspect = 1, showGrid = false, ...rest }: Omit<Partial<CropperProps>, 'oncropcomplete' | 'image'> = $props(); | ||||
| 
 | ||||
|   const cropperState = useImageCropperCropper(); | ||||
| </script> | ||||
| 
 | ||||
| <!-- This needs to be relative https://github.com/ValentinH/svelte-easy-crop#basic-usage --> | ||||
| <div class="relative h-full w-full"> | ||||
|   <Cropper {...rest} {cropShape} {aspect} {showGrid} image={cropperState.rootState.tempUrl} oncropcomplete={cropperState.onCropComplete} /> | ||||
| </div> | ||||
| @ -0,0 +1,24 @@ | ||||
| <!-- | ||||
| 	jsrepo 1.41.3 | ||||
| 	Installed from github/ieedan/shadcn-svelte-extras | ||||
| 	3-4-2025 | ||||
| --> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
|   import * as Dialog from '$lib/components/ui/dialog'; | ||||
|   import { cn } from '$lib/utils/utils'; | ||||
|   import { useImageCropperDialog } from './image-cropper.svelte.js'; | ||||
|   import type { ImageCropperDialogProps } from './types'; | ||||
| 
 | ||||
|   let { children, class: className, ...rest }: ImageCropperDialogProps = $props(); | ||||
| 
 | ||||
|   const dialogState = useImageCropperDialog(); | ||||
| </script> | ||||
| 
 | ||||
| <Dialog.Root bind:open={dialogState.rootState.open}> | ||||
|   <Dialog.Content {...rest} hideClose class={cn('min-h-96 max-w-full rounded-none border-x-0 sm:max-w-lg sm:rounded-lg sm:border-x', className)}> | ||||
|     <div class="flex flex-col gap-4"> | ||||
|       {@render children?.()} | ||||
|     </div> | ||||
|   </Dialog.Content> | ||||
| </Dialog.Root> | ||||
| @ -0,0 +1,29 @@ | ||||
| <!-- | ||||
| 	jsrepo 1.41.3 | ||||
| 	Installed from github/ieedan/shadcn-svelte-extras | ||||
| 	3-4-2025 | ||||
| --> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
|   import * as Avatar from '$lib/components/ui/avatar'; | ||||
|   import type { ImageCropperPreviewProps } from './types'; | ||||
|   import { useImageCropperPreview } from './image-cropper.svelte.js'; | ||||
|   import { Upload } from 'lucide-svelte'; | ||||
|   import { cn } from '$lib/utils/utils'; | ||||
| 
 | ||||
|   let { child, class: className }: ImageCropperPreviewProps = $props(); | ||||
| 
 | ||||
|   const previewState = useImageCropperPreview(); | ||||
| </script> | ||||
| 
 | ||||
| {#if child} | ||||
|   {@render child({ src: previewState.rootState.src })} | ||||
| {:else} | ||||
|   <Avatar.Root class={cn('size-20 ring-2 ring-accent ring-offset-2 ring-offset-background', className)}> | ||||
|     <Avatar.Image src={previewState.rootState.src} /> | ||||
|     <Avatar.Fallback> | ||||
|       <Upload class="size-4" /> | ||||
|       <span class="sr-only">Upload image</span> | ||||
|     </Avatar.Fallback> | ||||
|   </Avatar.Root> | ||||
| {/if} | ||||
| @ -0,0 +1,18 @@ | ||||
| <!-- | ||||
| 	jsrepo 1.41.3 | ||||
| 	Installed from github/ieedan/shadcn-svelte-extras | ||||
| 	3-4-2025 | ||||
| --> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
|   import { type WithChildren } from 'bits-ui'; | ||||
|   import { useImageCropperTrigger } from './image-cropper.svelte.js'; | ||||
| 
 | ||||
|   let { children }: WithChildren = $props(); | ||||
| 
 | ||||
|   const triggerState = useImageCropperTrigger(); | ||||
| </script> | ||||
| 
 | ||||
| <label for={triggerState.rootState.id} class="hover:cursor-pointer"> | ||||
|   {@render children?.()} | ||||
| </label> | ||||
							
								
								
									
										54
									
								
								src/lib/components/ui/image-cropper/image-cropper.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								src/lib/components/ui/image-cropper/image-cropper.svelte
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,54 @@ | ||||
| <!-- | ||||
| 	jsrepo 1.41.3 | ||||
| 	Installed from github/ieedan/shadcn-svelte-extras | ||||
| 	3-4-2025 | ||||
| --> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
|   import { box } from 'svelte-toolbelt'; | ||||
|   import { useImageCropperRoot } from './image-cropper.svelte.js'; | ||||
|   import type { ImageCropperRootProps } from './types'; | ||||
|   import { onDestroy } from 'svelte'; | ||||
|   import { useId } from 'bits-ui'; | ||||
| 
 | ||||
|   let { | ||||
|     id = useId(), | ||||
|     src = $bindable(''), | ||||
|     onCropped = () => {}, | ||||
|     children, | ||||
|     error = $bindable(null), | ||||
|     ...rest | ||||
|   }: ImageCropperRootProps & { error?: string | null } = $props(); | ||||
| 
 | ||||
|   const rootState = useImageCropperRoot({ | ||||
|     id: box.with(() => id), | ||||
|     src: box.with( | ||||
|       () => src, | ||||
|       (v) => (src = v), | ||||
|     ), | ||||
|     onCropped, | ||||
|   }); | ||||
| 
 | ||||
|   onDestroy(() => rootState.dispose()); | ||||
| </script> | ||||
| 
 | ||||
| {@render children?.()} | ||||
| <input | ||||
|   {...rest} | ||||
|   onchange={(e) => { | ||||
|     const file = e.currentTarget.files?.[0]; | ||||
|     if (!file) return; | ||||
|     // Prevent the user from uploading non-image files | ||||
|     if (file.type.split('/')[0] !== 'image') { | ||||
|       error = 'Please upload a valid image.'; | ||||
|       return; | ||||
|     } | ||||
|     error = null; | ||||
|     rootState.onUpload(file); | ||||
|     // reset so that we can reupload the same file | ||||
|     (e.target! as HTMLInputElement).value = ''; | ||||
|   }} | ||||
|   type="file" | ||||
|   {id} | ||||
|   style="display: none;" | ||||
| /> | ||||
							
								
								
									
										154
									
								
								src/lib/components/ui/image-cropper/image-cropper.svelte.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										154
									
								
								src/lib/components/ui/image-cropper/image-cropper.svelte.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,154 @@ | ||||
| /* | ||||
| 	jsrepo 1.41.3 | ||||
| 	Installed from github/ieedan/shadcn-svelte-extras | ||||
| 	3-4-2025 | ||||
| */ | ||||
| 
 | ||||
| import type { ReadableBoxedValues, WritableBoxedValues } from '$lib/utils/box'; | ||||
| import { Context } from 'runed'; | ||||
| import type { CropArea, DispatchEvents } from 'svelte-easy-crop'; | ||||
| import { getCroppedImg } from './utils'; | ||||
| 
 | ||||
| export type ImageCropperRootProps = WritableBoxedValues<{ | ||||
|   src: string; | ||||
| }> & | ||||
|   ReadableBoxedValues<{ | ||||
|     id: string; | ||||
|   }> & { | ||||
|     onCropped: (url: string) => void; | ||||
|   }; | ||||
| 
 | ||||
| class ImageCropperRootState { | ||||
|   #createdUrls = $state<string[]>([]); | ||||
|   open = $state(false); | ||||
|   tempUrl = $state<string>(); | ||||
|   pixelCrop = $state<CropArea>(); | ||||
| 
 | ||||
|   constructor(readonly opts: ImageCropperRootProps) { | ||||
|     this.onUpload = this.onUpload.bind(this); | ||||
|     this.onCancel = this.onCancel.bind(this); | ||||
|     this.onCrop = this.onCrop.bind(this); | ||||
|     this.dispose = this.dispose.bind(this); | ||||
|   } | ||||
| 
 | ||||
|   onUpload(file: File) { | ||||
|     this.tempUrl = URL.createObjectURL(file); | ||||
|     this.#createdUrls.push(this.tempUrl); | ||||
|     this.open = true; | ||||
|   } | ||||
| 
 | ||||
|   onCancel() { | ||||
|     this.tempUrl = undefined; | ||||
|     this.open = false; | ||||
|     this.pixelCrop = undefined; | ||||
|   } | ||||
| 
 | ||||
|   async onCrop() { | ||||
|     if (!this.pixelCrop || !this.tempUrl) return; | ||||
|     this.opts.src.current = await getCroppedImg(this.tempUrl, this.pixelCrop); | ||||
|     this.open = false; | ||||
|     this.opts.onCropped(this.opts.src.current); | ||||
|   } | ||||
| 
 | ||||
|   get src() { | ||||
|     return this.opts.src.current; | ||||
|   } | ||||
| 
 | ||||
|   get id() { | ||||
|     return this.opts.id.current; | ||||
|   } | ||||
| 
 | ||||
|   dispose() { | ||||
|     for (const url of this.#createdUrls) { | ||||
|       URL.revokeObjectURL(url); | ||||
|     } | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| export type ImageCropperTriggerProps = ReadableBoxedValues<{ | ||||
|   id?: string; | ||||
| }>; | ||||
| 
 | ||||
| class ImageCropperTriggerState { | ||||
|   constructor(readonly rootState: ImageCropperRootState) {} | ||||
| } | ||||
| 
 | ||||
| class ImageCropperPreviewState { | ||||
|   constructor(readonly rootState: ImageCropperRootState) {} | ||||
| } | ||||
| 
 | ||||
| class ImageCropperDialogState { | ||||
|   constructor(readonly rootState: ImageCropperRootState) {} | ||||
| } | ||||
| 
 | ||||
| class ImageCropperCropperState { | ||||
|   constructor(readonly rootState: ImageCropperRootState) { | ||||
|     this.onCropComplete = this.onCropComplete.bind(this); | ||||
|   } | ||||
| 
 | ||||
|   onCropComplete(e: DispatchEvents['cropcomplete']) { | ||||
|     this.rootState.pixelCrop = e.pixels; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| class ImageCropperCropState { | ||||
|   constructor(readonly rootState: ImageCropperRootState) { | ||||
|     this.onclick = this.onclick.bind(this); | ||||
|   } | ||||
| 
 | ||||
|   onclick() { | ||||
|     this.rootState.onCrop(); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| class ImageCropperCancelState { | ||||
|   constructor(readonly rootState: ImageCropperRootState) { | ||||
|     this.onclick = this.onclick.bind(this); | ||||
|   } | ||||
| 
 | ||||
|   onclick() { | ||||
|     this.rootState.onCancel(); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| const ImageCropperRootContext = new Context<ImageCropperRootState>('ImageCropper.Root'); | ||||
| 
 | ||||
| export const useImageCropperRoot = (props: ImageCropperRootProps) => { | ||||
|   return ImageCropperRootContext.set(new ImageCropperRootState(props)); | ||||
| }; | ||||
| 
 | ||||
| export const useImageCropperTrigger = () => { | ||||
|   const rootState = ImageCropperRootContext.get(); | ||||
| 
 | ||||
|   return new ImageCropperTriggerState(rootState); | ||||
| }; | ||||
| 
 | ||||
| export const useImageCropperPreview = () => { | ||||
|   const rootState = ImageCropperRootContext.get(); | ||||
| 
 | ||||
|   return new ImageCropperPreviewState(rootState); | ||||
| }; | ||||
| 
 | ||||
| export const useImageCropperDialog = () => { | ||||
|   const rootState = ImageCropperRootContext.get(); | ||||
| 
 | ||||
|   return new ImageCropperDialogState(rootState); | ||||
| }; | ||||
| 
 | ||||
| export const useImageCropperCropper = () => { | ||||
|   const rootState = ImageCropperRootContext.get(); | ||||
| 
 | ||||
|   return new ImageCropperCropperState(rootState); | ||||
| }; | ||||
| 
 | ||||
| export const useImageCropperCrop = () => { | ||||
|   const rootState = ImageCropperRootContext.get(); | ||||
| 
 | ||||
|   return new ImageCropperCropState(rootState); | ||||
| }; | ||||
| 
 | ||||
| export const useImageCropperCancel = () => { | ||||
|   const rootState = ImageCropperRootContext.get(); | ||||
| 
 | ||||
|   return new ImageCropperCancelState(rootState); | ||||
| }; | ||||
							
								
								
									
										17
									
								
								src/lib/components/ui/image-cropper/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								src/lib/components/ui/image-cropper/index.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,17 @@ | ||||
| /* | ||||
| 	jsrepo 1.41.3 | ||||
| 	Installed from github/ieedan/shadcn-svelte-extras | ||||
| 	3-4-2025 | ||||
| */ | ||||
| 
 | ||||
| import Root from './image-cropper.svelte'; | ||||
| import UploadTrigger from './image-cropper-upload-trigger.svelte'; | ||||
| import Preview from './image-cropper-preview.svelte'; | ||||
| import Dialog from './image-cropper-dialog.svelte'; | ||||
| import Cropper from './image-cropper-cropper.svelte'; | ||||
| import Controls from './image-cropper-controls.svelte'; | ||||
| import Crop from './image-cropper-crop.svelte'; | ||||
| import Cancel from './image-cropper-cancel.svelte'; | ||||
| import { getFileFromUrl } from './utils'; | ||||
| 
 | ||||
| export { Root, UploadTrigger, Preview, Dialog, Cropper, Controls, Crop, Cancel, getFileFromUrl }; | ||||
							
								
								
									
										22
									
								
								src/lib/components/ui/image-cropper/types.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								src/lib/components/ui/image-cropper/types.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,22 @@ | ||||
| /* | ||||
| 	jsrepo 1.41.3 | ||||
| 	Installed from github/ieedan/shadcn-svelte-extras | ||||
| 	3-4-2025 | ||||
| */ | ||||
| 
 | ||||
| import type { AvatarRootProps, DialogContentProps, WithChildren } from 'bits-ui'; | ||||
| import type { Snippet } from 'svelte'; | ||||
| import type { HTMLInputAttributes } from 'svelte/elements'; | ||||
| 
 | ||||
| export type ImageCropperRootProps = HTMLInputAttributes & | ||||
|   WithChildren<{ | ||||
|     id?: string; | ||||
|     src?: string; | ||||
|     onCropped?: (url: string) => void; | ||||
|   }>; | ||||
| 
 | ||||
| export type ImageCropperDialogProps = DialogContentProps; | ||||
| 
 | ||||
| export type ImageCropperPreviewProps = Omit<AvatarRootProps, 'child'> & { | ||||
|   child?: Snippet<[{ src: string }]>; | ||||
| }; | ||||
							
								
								
									
										87
									
								
								src/lib/components/ui/image-cropper/utils.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										87
									
								
								src/lib/components/ui/image-cropper/utils.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,87 @@ | ||||
| /* | ||||
| 	jsrepo 1.41.3 | ||||
| 	Installed from github/ieedan/shadcn-svelte-extras | ||||
| 	3-4-2025 | ||||
| */ | ||||
| 
 | ||||
| import type { CropArea } from 'svelte-easy-crop'; | ||||
| 
 | ||||
| export const getFileFromUrl = async (url: string, fileName = 'cropped.png'): Promise<File> => { | ||||
|   // Fetch the file data from the URL
 | ||||
|   const response = await fetch(url); | ||||
| 
 | ||||
|   if (!response.ok) { | ||||
|     throw new Error(`Failed to fetch resource: ${response.status} ${response.statusText}`); | ||||
|   } | ||||
| 
 | ||||
|   // Convert the response into a Blob
 | ||||
|   const blob = await response.blob(); | ||||
| 
 | ||||
|   // Create and return a File. You can set a custom type if needed.
 | ||||
|   return new File([blob], fileName, { type: blob.type }); | ||||
| }; | ||||
| 
 | ||||
| const createImage = (url: string): Promise<HTMLImageElement> => { | ||||
|   return new Promise<HTMLImageElement>((resolve, reject) => { | ||||
|     const image = new Image(); | ||||
|     image.addEventListener('load', () => resolve(image)); | ||||
|     image.addEventListener('error', (error) => reject(error)); | ||||
|     image.setAttribute('crossOrigin', 'anonymous'); // needed to avoid cross-origin issues on CodeSandbox
 | ||||
|     image.src = url; | ||||
|   }); | ||||
| }; | ||||
| 
 | ||||
| const getRadianAngle = (degreeValue: number) => { | ||||
|   return (degreeValue * Math.PI) / 180; | ||||
| }; | ||||
| 
 | ||||
| /** Gets the cropped image from the src using the cropped area | ||||
|  * | ||||
|  * @param imageSrc | ||||
|  * @param pixelCrop | ||||
|  * @param rotation | ||||
|  * @returns | ||||
|  */ | ||||
| export const getCroppedImg = async (imageSrc: string, pixelCrop: CropArea, rotation = 0): Promise<string> => { | ||||
|   const image = await createImage(imageSrc); | ||||
|   const canvas = document.createElement('canvas'); | ||||
|   const ctx = canvas.getContext('2d'); | ||||
| 
 | ||||
|   if (!ctx) { | ||||
|     throw new Error('Error getting 2d rendering context'); | ||||
|   } | ||||
| 
 | ||||
|   const maxSize = Math.max(image.width, image.height); | ||||
|   const safeArea = 2 * ((maxSize / 2) * Math.sqrt(2)); | ||||
| 
 | ||||
|   // set each dimensions to double largest dimension to allow for a safe area for the
 | ||||
|   // image to rotate in without being clipped by canvas context
 | ||||
|   canvas.width = safeArea; | ||||
|   canvas.height = safeArea; | ||||
| 
 | ||||
|   // translate canvas context to a central location on image to allow rotating around the center.
 | ||||
|   ctx.translate(safeArea / 2, safeArea / 2); | ||||
|   ctx.rotate(getRadianAngle(rotation)); | ||||
|   ctx.translate(-safeArea / 2, -safeArea / 2); | ||||
| 
 | ||||
|   // draw rotated image and store data.
 | ||||
|   ctx.drawImage(image, safeArea / 2 - image.width * 0.5, safeArea / 2 - image.height * 0.5); | ||||
|   const data = ctx.getImageData(0, 0, safeArea, safeArea); | ||||
| 
 | ||||
|   // set canvas width to final desired crop size - this will clear existing context
 | ||||
|   canvas.width = pixelCrop.width; | ||||
|   canvas.height = pixelCrop.height; | ||||
| 
 | ||||
|   // paste generated rotate image with correct offsets for x,y crop values.
 | ||||
|   ctx.putImageData( | ||||
|     data, | ||||
|     Math.round(0 - safeArea / 2 + image.width * 0.5 - pixelCrop.x), | ||||
|     Math.round(0 - safeArea / 2 + image.height * 0.5 - pixelCrop.y), | ||||
|   ); | ||||
| 
 | ||||
|   return new Promise((resolve) => { | ||||
|     canvas.toBlob((file) => { | ||||
|       resolve(URL.createObjectURL(file!)); | ||||
|     }, 'image/png'); | ||||
|   }); | ||||
| }; | ||||
| @ -1,6 +1,6 @@ | ||||
| <script lang="ts"> | ||||
|   import { Select as SelectPrimitive, type WithoutChild } from 'bits-ui'; | ||||
|   import Check from 'lucide-svelte/icons/check'; | ||||
|   import { Check } from 'lucide-svelte'; | ||||
|   import { cn } from '$lib/utils.js'; | ||||
| 
 | ||||
|   let { | ||||
|  | ||||
| @ -1,5 +1,5 @@ | ||||
| <script lang="ts"> | ||||
|   import ChevronDown from 'lucide-svelte/icons/chevron-down'; | ||||
|   import { ChevronDown } from 'lucide-svelte'; | ||||
|   import { Select as SelectPrimitive, type WithoutChildrenOrChild } from 'bits-ui'; | ||||
|   import { cn } from '$lib/utils.js'; | ||||
| 
 | ||||
|  | ||||
| @ -1,5 +1,5 @@ | ||||
| <script lang="ts"> | ||||
|   import ChevronUp from 'lucide-svelte/icons/chevron-up'; | ||||
|   import { ChevronUp } from 'lucide-svelte'; | ||||
|   import { Select as SelectPrimitive, type WithoutChildrenOrChild } from 'bits-ui'; | ||||
|   import { cn } from '$lib/utils.js'; | ||||
| 
 | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| <script lang="ts"> | ||||
|   import { Select as SelectPrimitive, type WithoutChild } from 'bits-ui'; | ||||
|   import ChevronDown from 'lucide-svelte/icons/chevron-down'; | ||||
|   import { ChevronDown } from 'lucide-svelte'; | ||||
|   import { cn } from '$lib/utils.js'; | ||||
| 
 | ||||
|   let { ref = $bindable(null), class: className, children, ...restProps }: WithoutChild<SelectPrimitive.TriggerProps> = $props(); | ||||
|  | ||||
| @ -21,7 +21,7 @@ | ||||
| 
 | ||||
| <script lang="ts"> | ||||
|   import { Dialog as SheetPrimitive, type WithoutChildrenOrChild } from 'bits-ui'; | ||||
|   import X from 'lucide-svelte/icons/x'; | ||||
|   import { X } from 'lucide-svelte'; | ||||
|   import type { Snippet } from 'svelte'; | ||||
|   import SheetOverlay from './sheet-overlay.svelte'; | ||||
|   import { cn } from '$lib/utils.js'; | ||||
|  | ||||
| @ -1,7 +1,7 @@ | ||||
| <script lang="ts"> | ||||
|   import { Button } from '$lib/components/ui/button/index'; | ||||
|   import * as Tooltip from '$lib/components/ui/tooltip'; | ||||
|   import Cog from 'lucide-svelte/icons/cog'; | ||||
|   import { Cog } from 'lucide-svelte'; | ||||
|   import type { PageData } from '../../routes/(main)/$types'; | ||||
|   const { data }: { data: PageData } = $props(); | ||||
| 
 | ||||
|  | ||||
							
								
								
									
										17
									
								
								src/lib/utils/box.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								src/lib/utils/box.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,17 @@ | ||||
| /* | ||||
| 	jsrepo 1.41.3 | ||||
| 	Installed from github/ieedan/shadcn-svelte-extras | ||||
| 	3-4-2025 | ||||
| */ | ||||
| 
 | ||||
| import type { ReadableBox, WritableBox } from 'svelte-toolbelt'; | ||||
| 
 | ||||
| export type Box<T> = ReadableBox<T> | WritableBox<T>; | ||||
| 
 | ||||
| export type WritableBoxedValues<T> = { | ||||
|   [K in keyof T]: WritableBox<T[K]>; | ||||
| }; | ||||
| 
 | ||||
| export type ReadableBoxedValues<T> = { | ||||
|   [K in keyof T]: ReadableBox<T[K]>; | ||||
| }; | ||||
							
								
								
									
										12
									
								
								src/lib/utils/utils.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								src/lib/utils/utils.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,12 @@ | ||||
| /* | ||||
| 	jsrepo 1.41.3 | ||||
| 	Installed from github/ieedan/shadcn-svelte-extras | ||||
| 	3-4-2025 | ||||
| */ | ||||
| 
 | ||||
| import { type ClassValue, clsx } from 'clsx'; | ||||
| import { twMerge } from 'tailwind-merge'; | ||||
| 
 | ||||
| export function cn(...inputs: ClassValue[]) { | ||||
|   return twMerge(clsx(inputs)); | ||||
| } | ||||
| @ -2,7 +2,7 @@ | ||||
|   import { page } from '$app/state'; | ||||
|   import { Button } from '$lib/components/ui/button'; | ||||
|   import * as Card from '$lib/components/ui/card'; | ||||
|   import BrokenHeart from 'lucide-svelte/icons/heart-crack'; | ||||
|   import { BrokenHeart } from 'lucide-svelte'; | ||||
| </script> | ||||
| 
 | ||||
| <main class="relative size-full"> | ||||
|  | ||||
| @ -2,6 +2,7 @@ | ||||
|   import { Button } from '$lib/components/ui/button/index'; | ||||
|   import * as Dialog from '$lib/components/ui/dialog'; | ||||
|   import type { PageData } from './$types'; | ||||
|   import { Trash2, LogOut } from 'lucide-svelte'; | ||||
| 
 | ||||
|   let { data }: { data: PageData } = $props(); | ||||
| 
 | ||||
| @ -20,16 +21,16 @@ | ||||
|   <div class="relative grid w-full grid-cols-1 gap-3 md:grid-cols-2"> | ||||
|     <UpdatePassword data={data.newpassForm} /> | ||||
|     <UpdateUsername data={data.newuserForm} /> | ||||
|     <UpdatePfp /> | ||||
|     <UpdatePfp {data} /> | ||||
| 
 | ||||
|     <!-- Account Actions --> | ||||
|     <div class="grid w-full items-start gap-3"> | ||||
|       <fieldset class="flex size-full flex-col justify-center gap-3 rounded-lg border p-4"> | ||||
|         <legend class="-ml-1 px-1 text-sm font-medium"> Account Actions </legend> | ||||
|         <form method="POST" action="?/signOut"> | ||||
|           <Button type="submit" class="w-full">Sign Out</Button> | ||||
|           <Button type="submit" class="w-full"><LogOut /> Sign Out</Button> | ||||
|         </form> | ||||
|         <Button variant="destructive" class="w-full" onclick={() => (open = !open)}>Delete Account</Button> | ||||
|         <Button variant="destructive" class="w-full" onclick={() => (open = !open)}><Trash2 /> Delete Account</Button> | ||||
|       </fieldset> | ||||
|     </div> | ||||
|   </div> | ||||
| @ -43,7 +44,7 @@ | ||||
|         This action cannot be undone. This will permanently delete your account and remove your data from our database. | ||||
|         <form class="mt-2 flex gap-2" method="POST" action="?/deleteAccount"> | ||||
|           <Button class="w-1/2" onclick={() => (open = !open)}>I changed my mind!</Button> | ||||
|           <Button variant="destructive" class="w-1/2" type="submit">Delete Account</Button> | ||||
|           <Button variant="destructive" class="w-1/2" type="submit"><Trash2 /> Delete Account</Button> | ||||
|         </form> | ||||
|       </Dialog.Description> | ||||
|     </Dialog.Header> | ||||
|  | ||||
| @ -6,7 +6,7 @@ | ||||
|   import { buttonVariants } from '$lib/components/ui/button'; | ||||
|   import { autoResize } from '$lib/functions/autoresize.svelte'; | ||||
|   import Websocket from '$lib/functions/clientWebsocket.svelte'; | ||||
|   import Send from 'lucide-svelte/icons/send'; | ||||
|   import { Send } from 'lucide-svelte'; | ||||
|   import { io } from 'socket.io-client'; | ||||
|   import { onMount } from 'svelte'; | ||||
|   import type { PageData } from './$types'; | ||||
|  | ||||
| @ -9,7 +9,6 @@ test.describe('Profile Photo Update', () => { | ||||
|   let page: Page; | ||||
|   let fileInput: Locator; | ||||
|   let profileImage: Locator; | ||||
|   let submitButton: Locator; | ||||
| 
 | ||||
|   test.beforeEach(async ({ browser }) => { | ||||
|     page = await browser.newPage(); | ||||
| @ -19,7 +18,6 @@ test.describe('Profile Photo Update', () => { | ||||
|     await page.goto('/account', { timeout: 30000, waitUntil: 'domcontentloaded' }); | ||||
| 
 | ||||
|     // Initialize locators
 | ||||
|     submitButton = page.getByRole('button', { name: 'Update Profile Photo' }); | ||||
|     profileImage = page.locator('img#userimage'); | ||||
|     fileInput = page.locator('input[type="file"]'); | ||||
|   }); | ||||
| @ -32,7 +30,8 @@ test.describe('Profile Photo Update', () => { | ||||
| 
 | ||||
|     // Upload the new image
 | ||||
|     await fileInput.setInputFiles(['./static/freakybear.jpg']); | ||||
|     await submitButton.click(); | ||||
|     await page.waitForTimeout(500); | ||||
|     await page.getByTestId('crop').click(); | ||||
| 
 | ||||
|     // Wait for upload to complete
 | ||||
|     const response = await page.waitForResponse((response) => response.request().method() === 'POST', { timeout: 30000 }); | ||||
| @ -50,11 +49,11 @@ test.describe('Profile Photo Update', () => { | ||||
| 
 | ||||
|     // Upload the new image
 | ||||
|     await fileInput.setInputFiles(['./README.md']); | ||||
|     await submitButton.click(); | ||||
|     await page.waitForTimeout(500); | ||||
| 
 | ||||
|     // Wait for upload to complete
 | ||||
|     const response = await page.waitForResponse((response) => response.request().method() === 'POST', { timeout: 30000 }); | ||||
|     expect(response.status()).toBe(500); | ||||
|     // Check for error
 | ||||
|     const errorMessageLocator = page.locator(`.text-sm.text-red-500:has-text("Please upload a valid image.")`); | ||||
|     await expect(errorMessageLocator).toBeVisible(); | ||||
| 
 | ||||
|     // Make sure the src is the same as the original
 | ||||
|     expect(await getImgSrc(profileImage)).toEqual(initalSrc); | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user