feat: initial uploads and single study space view

Co-Authored-By: Alex Ling <al443@ic.ac.uk>
This commit is contained in:
2025-06-04 18:10:45 +01:00
parent b02f2b2303
commit 40435df5e2
16 changed files with 486 additions and 86 deletions

11
src/lib/assets/camera.svg Normal file
View File

@@ -0,0 +1,11 @@
<svg width="38" height="38" viewBox="0 0 38 38" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_117_282)">
<path d="M36.4168 30.0833C36.4168 30.9232 36.0832 31.7286 35.4893 32.3225C34.8955 32.9164 34.09 33.25 33.2502 33.25H4.75016C3.91031 33.25 3.10486 32.9164 2.51099 32.3225C1.91713 31.7286 1.5835 30.9232 1.5835 30.0833V12.6667C1.5835 11.8268 1.91713 11.0214 2.51099 10.4275C3.10486 9.83363 3.91031 9.5 4.75016 9.5H11.0835L14.2502 4.75H23.7502L26.9168 9.5H33.2502C34.09 9.5 34.8955 9.83363 35.4893 10.4275C36.0832 11.0214 36.4168 11.8268 36.4168 12.6667V30.0833Z" stroke="#49BD85" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M19.0002 26.9167C22.498 26.9167 25.3335 24.0811 25.3335 20.5833C25.3335 17.0855 22.498 14.25 19.0002 14.25C15.5024 14.25 12.6668 17.0855 12.6668 20.5833C12.6668 24.0811 15.5024 26.9167 19.0002 26.9167Z" stroke="#49BD85" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
</g>
<defs>
<clipPath id="clip0_117_282">
<rect width="38" height="38" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

3
src/lib/assets/cross.svg Normal file
View File

@@ -0,0 +1,3 @@
<svg width="68" height="68" viewBox="0 0 68 68" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M43.8404 24.0416L24.0414 43.8406M24.0414 24.0416L43.8404 43.8406" stroke="white" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 259 B

View File

@@ -0,0 +1,30 @@
<script lang="ts">
import type { Snippet } from "svelte";
interface Props {
onclick?: (event: MouseEvent) => void;
disabled?: boolean;
type?: "button" | "submit" | "reset";
children?: Snippet;
}
const { children, ...rest }: Props = $props();
</script>
<button {...rest}>
{@render children?.()}
</button>
<style>
button {
padding: 0.5rem 1rem;
border-radius: 0.5rem;
background: linear-gradient(-83deg, #3fb095, #49bd85);
box-shadow: 0rem 0rem 0.5rem #182125;
color: #eaffeb;
border: none;
cursor: pointer;
}
button:focus {
outline: 2px solid #007bff;
}
</style>

View File

@@ -0,0 +1,45 @@
<script lang="ts">
import logoUrl from "$lib/assets/logo.svg";
import type { Snippet } from "svelte";
interface Props {
children?: Snippet;
}
const { children }: Props = $props();
</script>
<nav>
<a href="/" class="logo">
<img src={logoUrl} alt="logo" />
</a>
<div class="rightButton">{@render children?.()}</div>
</nav>
<style>
nav {
display: flex;
position: sticky;
width: 100%;
height: 4rem;
top: 0;
left: 0;
background: linear-gradient(-77deg, #2e4653, #3a5b56);
box-shadow: 0rem 0rem 0.5rem #182125;
align-items: center;
}
.logo {
display: block;
height: 100%;
}
.logo img {
height: 100%;
}
.rightButton {
display: flex;
flex-direction: row-reverse;
flex: 1;
}
</style>

View File

@@ -5,17 +5,18 @@
alt: string;
imgSrc: string;
description?: Snippet;
href?: string;
}
const { alt, imgSrc, description }: Props = $props();
const { alt, imgSrc, description, href }: Props = $props();
</script>
<div class="card">
<a class="card" {href}>
<img src={imgSrc} {alt} />
<div class="description">
{@render description?.()}
</div>
</div>
</a>
<style>
.card {

View File

@@ -0,0 +1,64 @@
<script lang="ts">
import cameraUrl from "$lib/assets/camera.svg";
interface Props {
name: string;
height?: string;
minHeight?: string;
files?: FileList;
required?: boolean;
}
let { name, height, minHeight, files = $bindable(), ...rest }: Props = $props();
</script>
<label
for={name}
style="height: {height || 'auto'}; min-height: {minHeight || 'auto'};"
class:no-bg={files && files.length > 0}
>
{#if files && files.length > 0}
<img src={URL.createObjectURL(files[0])} alt="uploaded study space" class="preview" />
{:else}
<div class="message">
<img src={cameraUrl} class="icon" alt="camera icon" />
<span>Click to upload a photo</span>
</div>
{/if}
<input type="file" id={name} {name} accept=".png, .jpg, .jpeg, .svg" {...rest} bind:files />
</label>
<style>
label {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
padding: 0.5rem;
border-radius: 0.5rem;
background-color: #eaffeb;
cursor: pointer;
}
label.no-bg {
background-color: transparent;
padding: 0;
}
label input {
display: none;
}
.message {
display: flex;
flex-direction: column;
align-items: center;
color: #49bd85;
}
.preview {
max-height: 100%;
max-width: 100%;
}
.message .icon {
width: 2rem;
}
</style>

View File

@@ -0,0 +1,29 @@
<script lang="ts">
interface Props {
name: string;
value?: string | null;
placeholder?: string;
required?: boolean;
}
let { value = $bindable(), name, ...rest }: Props = $props();
</script>
<input type="text" id={name} {name} bind:value {...rest} />
<style>
input {
width: 100%;
padding: 0.5rem;
border-radius: 0.5rem;
border: 2px solid #eaffeb;
background: none;
color: #eaffeb;
font-size: 1rem;
}
input:focus {
border-color: #007bff;
outline: none;
}
</style>

View File

@@ -0,0 +1,32 @@
<script lang="ts">
interface Props {
name: string;
value?: string | null;
placeholder?: string;
required?: boolean;
rows?: number;
cols?: number;
}
let { value = $bindable(), name, ...rest }: Props = $props();
</script>
<textarea id={name} {name} {...rest}></textarea>
<style>
textarea {
width: 100%;
padding: 0.5rem;
border-radius: 0.5rem;
resize: vertical;
border: 2px solid #eaffeb;
background: none;
color: #eaffeb;
font-size: 1rem;
}
textarea:focus {
border-color: #007bff;
outline: none;
}
</style>