feat: added tags on upload and view tweaked navbar #36

Merged
al4423 merged 4 commits from feat/upload-tags into master 2025-06-06 03:16:07 +00:00
9 changed files with 241 additions and 17 deletions

View File

@@ -21,11 +21,11 @@
display: flex; display: flex;
position: sticky; position: sticky;
width: 100%; width: 100%;
height: 4rem; height: 3.5rem;
top: 0; top: 0;
left: 0; left: 0;
right: 0; right: 0;
background: linear-gradient(-77deg, #2e4653, #3a5b56); background: linear-gradient(-77deg, #2e4653, #223a37);
box-shadow: 0rem 0rem 0.5rem #182125; box-shadow: 0rem 0rem 0.5rem #182125;
align-items: center; align-items: center;
overflow: hidden; overflow: hidden;

View File

@@ -15,7 +15,11 @@
<img src={imgSrc} {alt} /> <img src={imgSrc} {alt} />
<div class="description"> <div class="description">
<h1>{space.location}</h1> <h1>{space.location}</h1>
<p>{space.description}</p> <div class="tagContainer">
{#each space.tags as tag (tag)}
<span class="tag">{tag}</span>
{/each}
</div>
</div> </div>
</a> </a>
@@ -33,7 +37,6 @@
.description { .description {
padding: 0.5rem; padding: 0.5rem;
color: #edebe9; color: #edebe9;
font-size: 0.875rem;
} }
img { img {
width: 100%; width: 100%;
@@ -41,4 +44,27 @@
aspect-ratio: 1 / 1; aspect-ratio: 1 / 1;
object-fit: cover; object-fit: cover;
} }
h1 {
margin-bottom: 0.5rem;
}
.tagContainer {
display: flex;
flex-wrap: wrap;
gap: 0.4rem;
border-radius: 0.5rem;
background: none;
}
.tag {
display: flex;
align-items: center;
border-radius: 0.25rem;
background-color: #2e4653;
color: #eaffeb;
font-size: 0.875rem;
cursor: pointer;
padding: 0.2rem 0.6rem;
}
</style> </style>

View File

@@ -22,6 +22,11 @@
font-size: 1rem; font-size: 1rem;
} }
::placeholder {
color: #859a90;
opacity: 1;
}
input:focus { input:focus {
border-color: #007bff; border-color: #007bff;
outline: none; outline: none;

View File

@@ -25,6 +25,11 @@
font-size: 1rem; font-size: 1rem;
} }
::placeholder {
color: #859a90;
opacity: 1;
}
textarea:focus { textarea:focus {
border-color: #007bff; border-color: #007bff;
outline: none; outline: none;

View File

@@ -70,6 +70,7 @@ export type Database = {
description: string | null description: string | null
id: string id: string
location: string | null location: string | null
tags: string[]
updated_at: string | null updated_at: string | null
} }
Insert: { Insert: {
@@ -78,6 +79,7 @@ export type Database = {
description?: string | null description?: string | null
id?: string id?: string
location?: string | null location?: string | null
tags?: string[]
updated_at?: string | null updated_at?: string | null
} }
Update: { Update: {
@@ -86,6 +88,7 @@ export type Database = {
description?: string | null description?: string | null
id?: string id?: string
location?: string | null location?: string | null
tags?: string[]
updated_at?: string | null updated_at?: string | null
} }
Relationships: [] Relationships: []

View File

@@ -23,5 +23,6 @@ export const availableStudySpaceTags = [
"Hot", "Hot",
"Air conditioned", "Air conditioned",
"Cold", "Cold",
"Cringe" "Cringe",
"PCs"
]; ];

View File

@@ -7,7 +7,7 @@
import Button from "$lib/components/Button.svelte"; import Button from "$lib/components/Button.svelte";
import Images from "$lib/components/inputs/Images.svelte"; import Images from "$lib/components/inputs/Images.svelte";
import type { Table } from "$lib"; import type { Table } from "$lib";
import { availableStudySpaceTags } from "$lib";
const { data } = $props(); const { data } = $props();
const { supabase } = $derived(data); const { supabase } = $derived(data);
@@ -16,7 +16,8 @@
let studySpaceData = $state<Omit<Table<"study_spaces">, "id" | "created_at" | "updated_at">>({ let studySpaceData = $state<Omit<Table<"study_spaces">, "id" | "created_at" | "updated_at">>({
description: "", description: "",
building_location: "", building_location: "",
location: "" location: "",
tags: []
}); });
async function uploadStudySpace() { async function uploadStudySpace() {
@@ -63,6 +64,31 @@
invalidate: ["db:study_spaces"] invalidate: ["db:study_spaces"]
}); });
} }
// Tag
let tagFilter = $state("");
let tagFilterElem = $state<HTMLInputElement>();
let filteredTags = $derived(
availableStudySpaceTags
.filter((tag) => tag.toLowerCase().includes(tagFilter.toLowerCase()))
.filter((tag) => !studySpaceData.tags.includes(tag))
);
let dropdownVisible = $state(false);
function deleteTag(tagName: string) {
return () => {
studySpaceData.tags = studySpaceData.tags.filter((tag) => tag !== tagName);
};
}
function addTag(tagName: string) {
return () => {
if (!studySpaceData.tags.includes(tagName)) {
studySpaceData.tags.push(tagName);
}
tagFilter = "";
};
}
</script> </script>
<Navbar> <Navbar>
@@ -89,13 +115,60 @@
required required
/> />
<label for="description">Add a description:</label> <label for="tags">Add tags:</label>
<div class="tagDisplay">
{#each studySpaceData.tags as tagName (tagName)}
<button class="tag" onclick={deleteTag(tagName)} type="button">
{tagName}
<img src={crossUrl} alt="delete" /></button
>
{/each}
<input
type="text"
name="tagInput"
class="tagInput"
bind:value={tagFilter}
bind:this={tagFilterElem}
onfocus={() => {
dropdownVisible = true;
}}
onblur={() => {
dropdownVisible = false;
}}
onkeypress={(event) => {
if (event.key === "Enter") {
const tag = filteredTags[0];
if (tag) addTag(tag)();
}
}}
placeholder="Add tags..."
/>
{#if dropdownVisible}
<div class="tagDropdown">
{#each filteredTags as avaliableTag (avaliableTag)}
<button
class="avaliableTag"
onclick={addTag(avaliableTag)}
onmousedown={(e) => {
// Keep input focused
e.preventDefault();
e.stopPropagation();
}}
type="button"
>
{avaliableTag}
</button>
{/each}
</div>
{/if}
</div>
<label for="description">Optional brief description:</label>
<Textarea <Textarea
name="description" name="description"
bind:value={studySpaceData.description} bind:value={studySpaceData.description}
placeholder="A quiet and well-lit, but small space..." placeholder="A quiet room with a lovely view of the park."
rows={5} rows={2}
required
/> />
<label for="building-location">Add the building location:</label> <label for="building-location">Add the building location:</label>
@@ -111,7 +184,7 @@
type="submit" type="submit"
disabled={(spaceImgs?.length || 0) === 0 || disabled={(spaceImgs?.length || 0) === 0 ||
!studySpaceData.location || !studySpaceData.location ||
!studySpaceData.description || studySpaceData.tags.length === 0 ||
!studySpaceData.building_location || !studySpaceData.building_location ||
uploading} uploading}
> >
@@ -143,4 +216,82 @@
margin-left: -0.5rem; margin-left: -0.5rem;
width: calc(100% + 1rem); width: calc(100% + 1rem);
} }
.tagDisplay {
display: flex;
gap: 0.4rem;
flex-wrap: wrap;
align-items: left;
justify-content: left;
position: relative;
width: 100%;
height: auto;
padding: 0.5rem;
border-radius: 0.5rem;
border: 2px solid #eaffeb;
background: none;
color: #eaffeb;
font-size: 1rem;
}
.tagInput {
width: 100%;
height: 100%;
background: none;
color: #eaffeb;
font-size: 1rem;
border: none;
outline: none;
}
::placeholder {
color: #859a90;
opacity: 1;
}
.tag {
display: flex;
align-items: center;
border-radius: 0.25rem;
background-color: #2e4653;
color: #eaffeb;
font-size: 0.9rem;
cursor: pointer;
border-width: 0rem;
}
.tag img {
width: 1rem;
height: 1rem;
margin-left: 0.2rem;
}
.tagDropdown {
width: 100%;
display: flex;
gap: 0.4rem;
flex-wrap: wrap;
position: absolute;
background-color: #2e4653;
box-shadow: 1px 1px 0.5rem rgba(0, 0, 0, 0.5);
border-radius: 0.5rem;
overflow-y: auto;
max-height: 10rem;
top: 100%;
left: 50%;
transform: translateX(-50%);
}
.avaliableTag {
width: 100%;
text-align: left;
background: none;
border: none;
color: #eaffeb;
font-size: 0.9rem;
margin: 0%;
padding: 0 0.8rem 0.4rem;
}
.avaliableTag:first-child {
padding-top: 0.6rem;
background-color: hsl(201, 26%, 60%);
}
.avaliableTag:last-child {
padding-bottom: 0.6rem;
}
</style> </style>

View File

@@ -29,11 +29,21 @@
<div class="nameContainer"> <div class="nameContainer">
{space.location} {space.location}
</div> </div>
<p class="descContainer"> {#if space.description != null && space.description.length > 0}
{space.description} <p class="descContainer">
</p> {space.description}
</p>
<hr />
{/if}
<div class="tagContainer">
{#each space.tags as tag (tag)}
<span class="tag">
{tag}
</span>
{/each}
</div>
<hr /> <hr />
<div class="whereSubtitle">Where it is:</div> <div class="subtitle">Where it is:</div>
<p class="addrContainer"> <p class="addrContainer">
{space.building_location} {space.building_location}
</p> </p>
@@ -87,7 +97,7 @@
font-size: 1.2rem; font-size: 1.2rem;
} }
.whereSubtitle { .subtitle {
font-size: 1.2rem; font-size: 1.2rem;
font-weight: bold; font-weight: bold;
color: #ffffff; color: #ffffff;
@@ -98,4 +108,24 @@
font-size: 1.2rem; font-size: 1.2rem;
padding: 0rem 1.4rem; padding: 0rem 1.4rem;
} }
.tagContainer {
display: flex;
flex-wrap: wrap;
gap: 0.4rem;
padding: 1.4rem;
border-radius: 0.5rem;
background: none;
}
.tag {
display: flex;
align-items: center;
border-radius: 0.25rem;
background-color: #2e4653;
color: #eaffeb;
font-size: 1.1rem;
cursor: pointer;
padding: 0.2rem 0.6rem;
}
</style> </style>

View File

@@ -0,0 +1,3 @@
alter table "public"."study_spaces" add column "tags" text[] not null default ARRAY[]::text[];