Compare commits
20 Commits
feat/initi
...
feat/study
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c7d8ed433d | ||
| ebf33d47a2 | |||
|
|
ff414f242d | ||
| 485063f8d2 | |||
|
|
0e074e9301 | ||
| 6e45851892 | |||
| d4a9d5559e | |||
| a180e49466 | |||
|
|
a03a80a186 | ||
| 11a040a677 | |||
| f85adf9edc | |||
|
|
6e72580a6a | ||
|
|
1d1bd940bf | ||
|
|
f9878d1e48 | ||
|
|
4ee33398c1 | ||
|
|
55d9646b07 | ||
| b49f937dcb | |||
| c1de092525 | |||
| 2ef82a5d41 | |||
| b17b9ddb82 |
3
src/lib/assets/arrow_right.svg
Normal file
3
src/lib/assets/arrow_right.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10 24H38M38 24L24 10M38 24L24 38" stroke="white" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 228 B |
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 7.9 KiB After Width: | Height: | Size: 6.6 KiB |
@@ -27,4 +27,8 @@
|
||||
button:focus {
|
||||
outline: 2px solid #007bff;
|
||||
}
|
||||
button:disabled {
|
||||
background: linear-gradient(-18deg, #66697b, #4e4e5e);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
</style>
|
||||
|
||||
145
src/lib/components/Carousel.svelte
Normal file
145
src/lib/components/Carousel.svelte
Normal file
@@ -0,0 +1,145 @@
|
||||
<script lang="ts">
|
||||
import arrowRightUrl from "$lib/assets/arrow_right.svg";
|
||||
import crossUrl from "$lib/assets/cross.svg";
|
||||
import { onMount } from "svelte";
|
||||
|
||||
interface Props {
|
||||
urls?: string[];
|
||||
ondelete?: (idx: number) => void;
|
||||
}
|
||||
|
||||
const { urls = [], ondelete }: Props = $props();
|
||||
let carousel = $state<HTMLDivElement>();
|
||||
let scrollPosition = $state(0);
|
||||
let scrollWidth = $state(0);
|
||||
let clientWidth = $state(1);
|
||||
function updateScroll() {
|
||||
scrollPosition = carousel?.scrollLeft || 0;
|
||||
scrollWidth = carousel?.scrollWidth || 0;
|
||||
clientWidth = carousel?.clientWidth || 1;
|
||||
}
|
||||
onMount(() => {
|
||||
const id = setInterval(() => {
|
||||
if (carousel) {
|
||||
updateScroll();
|
||||
}
|
||||
}, 1000);
|
||||
return () => clearInterval(id);
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="controls">
|
||||
<div class="carousel" bind:this={carousel} onscroll={updateScroll} onscrollend={updateScroll}>
|
||||
{#each urls as url, idx (`${idx}|${url}`)}
|
||||
<div class="item">
|
||||
<img src={url} alt="carousel item" />
|
||||
{#if ondelete}
|
||||
<button class="delete" onclick={() => ondelete(idx)}>
|
||||
<img src={crossUrl} alt="delete item" />
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{#if scrollPosition > clientWidth / 2}
|
||||
<button
|
||||
class="arrow left"
|
||||
onclick={(e) => {
|
||||
e.preventDefault();
|
||||
if (carousel) carousel.scrollLeft -= carousel.clientWidth;
|
||||
}}
|
||||
>
|
||||
<img src={arrowRightUrl} alt="go to previous" />
|
||||
</button>
|
||||
{/if}
|
||||
{#if scrollPosition < scrollWidth - clientWidth * 1.5}
|
||||
<button
|
||||
class="arrow right"
|
||||
onclick={(e) => {
|
||||
e.preventDefault();
|
||||
if (carousel) carousel.scrollLeft += carousel.clientWidth;
|
||||
}}
|
||||
>
|
||||
<img src={arrowRightUrl} alt="go to next" />
|
||||
</button>
|
||||
{/if}
|
||||
<span class="position">
|
||||
{Math.round(scrollPosition / clientWidth) + 1} / {urls.length}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.carousel {
|
||||
display: flex;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
scroll-snap-type: x mandatory;
|
||||
scroll-behavior: smooth;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
.controls {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.item {
|
||||
position: relative;
|
||||
min-width: 100%;
|
||||
max-width: 100%;
|
||||
scroll-snap-align: center;
|
||||
}
|
||||
.item img {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
.delete,
|
||||
.position,
|
||||
.arrow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
border: none;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
border-radius: 9999px;
|
||||
}
|
||||
.arrow,
|
||||
.delete {
|
||||
cursor: pointer;
|
||||
width: 2rem;
|
||||
aspect-ratio: 1 / 1;
|
||||
}
|
||||
.arrow img,
|
||||
.delete img {
|
||||
width: 1.4rem;
|
||||
}
|
||||
.arrow:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
.arrow.left {
|
||||
left: 0.2rem;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
.arrow.left img {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
.arrow.right {
|
||||
right: 0.2rem;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
.delete {
|
||||
top: 0.4rem;
|
||||
right: 0.2rem;
|
||||
}
|
||||
.position {
|
||||
font-size: 0.8rem;
|
||||
top: 0.4rem;
|
||||
border-radius: 1rem;
|
||||
left: 0.2rem;
|
||||
padding: 0.3rem 0.5rem;
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
}
|
||||
</style>
|
||||
@@ -24,15 +24,19 @@
|
||||
height: 4rem;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: linear-gradient(-77deg, #2e4653, #3a5b56);
|
||||
box-shadow: 0rem 0rem 0.5rem #182125;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.logo {
|
||||
display: block;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.logo img {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
@@ -1,20 +1,21 @@
|
||||
<script lang="ts">
|
||||
import type { Snippet } from "svelte";
|
||||
import type { Table } from "$lib";
|
||||
|
||||
interface Props {
|
||||
space: Table<"study_spaces">;
|
||||
alt: string;
|
||||
imgSrc: string;
|
||||
description?: Snippet;
|
||||
href?: string;
|
||||
}
|
||||
|
||||
const { alt, imgSrc, description, href }: Props = $props();
|
||||
const { space, alt, imgSrc, href }: Props = $props();
|
||||
</script>
|
||||
|
||||
<a class="card" {href}>
|
||||
<img src={imgSrc} {alt} />
|
||||
<div class="description">
|
||||
{@render description?.()}
|
||||
<h1>{space.location}</h1>
|
||||
<p>{space.description}</p>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
@@ -22,7 +23,12 @@
|
||||
.card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: #38353f;
|
||||
background-color: #49bd85;
|
||||
width: 100%;
|
||||
max-width: 20rem;
|
||||
border-radius: 0.5rem;
|
||||
overflow: hidden;
|
||||
text-decoration: none;
|
||||
}
|
||||
.description {
|
||||
padding: 0.5rem;
|
||||
@@ -30,7 +36,9 @@
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
img {
|
||||
width: 16rem;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
aspect-ratio: 1 / 1;
|
||||
object-fit: cover;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import cameraUrl from "$lib/assets/camera.svg";
|
||||
import Carousel from "../Carousel.svelte";
|
||||
|
||||
interface Props {
|
||||
name: string;
|
||||
@@ -18,14 +19,37 @@
|
||||
class:no-bg={files && files.length > 0}
|
||||
>
|
||||
{#if files && files.length > 0}
|
||||
<img src={URL.createObjectURL(files[0])} alt="uploaded study space" class="preview" />
|
||||
<Carousel
|
||||
urls={files
|
||||
? Array(files.length)
|
||||
.keys()
|
||||
.map((i) => URL.createObjectURL(files![i]))
|
||||
.toArray()
|
||||
: []}
|
||||
ondelete={(idx) => {
|
||||
if (!files) return;
|
||||
const dt = new DataTransfer();
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
if (i !== idx) dt.items.add(files[i]);
|
||||
}
|
||||
files = dt.files;
|
||||
}}
|
||||
/>
|
||||
{: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 />
|
||||
<input
|
||||
type="file"
|
||||
id={name}
|
||||
{name}
|
||||
multiple
|
||||
accept=".png, .jpg, .jpeg, .svg"
|
||||
{...rest}
|
||||
bind:files
|
||||
/>
|
||||
</label>
|
||||
|
||||
<style>
|
||||
@@ -11,7 +11,7 @@
|
||||
let { value = $bindable(), name, ...rest }: Props = $props();
|
||||
</script>
|
||||
|
||||
<textarea id={name} {name} {...rest}></textarea>
|
||||
<textarea id={name} {name} bind:value {...rest}></textarea>
|
||||
|
||||
<style>
|
||||
textarea {
|
||||
|
||||
6
src/lib/database.d.ts
vendored
6
src/lib/database.d.ts
vendored
@@ -65,7 +65,7 @@ export type Database = {
|
||||
}
|
||||
study_spaces: {
|
||||
Row: {
|
||||
building_address: string | null
|
||||
building_location: string | null
|
||||
created_at: string | null
|
||||
description: string | null
|
||||
id: string
|
||||
@@ -73,7 +73,7 @@ export type Database = {
|
||||
updated_at: string | null
|
||||
}
|
||||
Insert: {
|
||||
building_address?: string | null
|
||||
building_location?: string | null
|
||||
created_at?: string | null
|
||||
description?: string | null
|
||||
id?: string
|
||||
@@ -81,7 +81,7 @@ export type Database = {
|
||||
updated_at?: string | null
|
||||
}
|
||||
Update: {
|
||||
building_address?: string | null
|
||||
building_location?: string | null
|
||||
created_at?: string | null
|
||||
description?: string | null
|
||||
id?: string
|
||||
|
||||
@@ -3,3 +3,25 @@ import type { Database } from "./database.d.ts";
|
||||
export type Table<T extends keyof Database["public"]["Tables"]> =
|
||||
Database["public"]["Tables"][T]["Row"];
|
||||
export type Enum<T extends keyof Database["public"]["Enums"]> = Database["public"]["Enums"][T];
|
||||
|
||||
export const availableStudySpaceTags = [
|
||||
"Quiet",
|
||||
"Loud",
|
||||
"Silent",
|
||||
"Group study",
|
||||
"Individual study",
|
||||
"Power outlets",
|
||||
"No power outlets",
|
||||
"24/7",
|
||||
"Food allowed",
|
||||
"No food allowed",
|
||||
"Good wifi",
|
||||
"Bad wifi",
|
||||
"No wifi",
|
||||
"Whiteboard",
|
||||
"Restricted access",
|
||||
"Hot",
|
||||
"Air conditioned",
|
||||
"Cold",
|
||||
"Cringe"
|
||||
];
|
||||
|
||||
@@ -26,24 +26,30 @@
|
||||
alt="Photo of {studySpace.description}"
|
||||
href="/space/{studySpace.id}"
|
||||
imgSrc={imgUrl}
|
||||
>
|
||||
{#snippet description()}
|
||||
<p>{studySpace.description}</p>
|
||||
{/snippet}
|
||||
</SpaceCard>
|
||||
space={studySpace}
|
||||
/>
|
||||
{/each}
|
||||
</main>
|
||||
|
||||
<style>
|
||||
main {
|
||||
display: grid;
|
||||
box-sizing: border-box;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 1rem;
|
||||
padding: 1rem;
|
||||
width: min(600px, 100vw);
|
||||
max-width: 600px;
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.new-space {
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
@media (max-width: 20rem) {
|
||||
main {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -5,22 +5,22 @@
|
||||
import Navbar from "$lib/components/Navbar.svelte";
|
||||
import crossUrl from "$lib/assets/cross.svg";
|
||||
import Button from "$lib/components/Button.svelte";
|
||||
import Image from "$lib/components/inputs/Image.svelte";
|
||||
import Images from "$lib/components/inputs/Images.svelte";
|
||||
import type { Table } from "$lib";
|
||||
import { availableStudySpaceTags } from "$lib/index"
|
||||
|
||||
const { data } = $props();
|
||||
const { supabase } = $derived(data);
|
||||
|
||||
let spaceImg = $state<FileList>();
|
||||
let spaceImgs = $state<FileList>();
|
||||
let studySpaceData = $state<Omit<Table<"study_spaces">, "id" | "created_at" | "updated_at">>({
|
||||
description: "",
|
||||
building_address: "",
|
||||
building_location: "",
|
||||
location: ""
|
||||
});
|
||||
|
||||
async function uploadStudySpace() {
|
||||
const imageFile = spaceImg?.[0];
|
||||
if (!imageFile) return alert("Please select an image file.");
|
||||
if (!spaceImgs || spaceImgs.length < 1) return alert("Please select an image file.");
|
||||
|
||||
const { data: studySpaceInsert, error: studySpaceError } = await supabase
|
||||
.from("study_spaces")
|
||||
@@ -30,21 +30,31 @@
|
||||
if (studySpaceError)
|
||||
return alert(`Error uploading study space: ${studySpaceError.message}`);
|
||||
|
||||
const { data: imgUpload, error: imageError } = await supabase.storage
|
||||
.from("files_bucket")
|
||||
.upload(`public/${studySpaceInsert.id}-${imageFile.name}`, imageFile, {
|
||||
contentType: imageFile.type
|
||||
});
|
||||
const imgUploads = await Promise.all(
|
||||
Array(spaceImgs.length)
|
||||
.keys()
|
||||
.map(async (i) => {
|
||||
const imageFile = spaceImgs![i];
|
||||
const resp = await supabase.storage
|
||||
.from("files_bucket")
|
||||
.upload(`public/${studySpaceInsert.id}-${imageFile.name}`, imageFile, {
|
||||
contentType: imageFile.type
|
||||
});
|
||||
return resp;
|
||||
})
|
||||
);
|
||||
const imageError = imgUploads.find(({ error }) => error)?.error;
|
||||
if (imageError) return alert(`Error uploading image: ${imageError.message}`);
|
||||
|
||||
const { error: imageInsertError } = await supabase
|
||||
.from("study_space_images")
|
||||
.insert({
|
||||
study_space_id: studySpaceInsert.id,
|
||||
image_path: imgUpload.path
|
||||
})
|
||||
.select()
|
||||
.single();
|
||||
.insert(
|
||||
imgUploads.map(({ data }) => ({
|
||||
study_space_id: studySpaceInsert.id,
|
||||
image_path: data!.path
|
||||
}))
|
||||
)
|
||||
.select();
|
||||
if (imageInsertError) return alert(`Error creating image: ${imageInsertError.message}`);
|
||||
|
||||
alert("Thank you for your contribution!");
|
||||
@@ -53,6 +63,17 @@
|
||||
invalidate: ["db:study_spaces"]
|
||||
});
|
||||
}
|
||||
|
||||
let selectedTags = $state<string[]>([]);
|
||||
|
||||
function toggleTag(tag: string) {
|
||||
if (selectedTags.includes(tag)) {
|
||||
selectedTags = selectedTags.filter(t => t !== tag);
|
||||
} else {
|
||||
selectedTags = [...selectedTags, tag];
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<Navbar>
|
||||
@@ -67,32 +88,156 @@
|
||||
await uploadStudySpace();
|
||||
}}
|
||||
>
|
||||
<Image name="study-space-image" minHeight="16rem" bind:files={spaceImg} />
|
||||
<Images name="study-space-image" minHeight="16rem" bind:files={spaceImgs} required />
|
||||
|
||||
<label for="location">Enter the name:</label>
|
||||
<Text name="location" bind:value={studySpaceData.location} placeholder="Room 123, Floor 1" />
|
||||
<Text
|
||||
name="location"
|
||||
bind:value={studySpaceData.location}
|
||||
placeholder="Room 123, Floor 1"
|
||||
required
|
||||
/>
|
||||
|
||||
<fieldset>
|
||||
<legend>Select applicable tags:</legend>
|
||||
<div class="tag-list">
|
||||
{#each availableStudySpaceTags as tag}
|
||||
<label class="tag-checkbox">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedTags.includes(tag)}
|
||||
onchange={() => toggleTag(tag)}
|
||||
/>
|
||||
{tag}
|
||||
</label>
|
||||
{/each}
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<label for="description">Add a description:</label>
|
||||
<Textarea
|
||||
name="description"
|
||||
bind:value={studySpaceData.description}
|
||||
placeholder="A quiet, but small study space..."
|
||||
placeholder="A quiet and well-lit, but small space..."
|
||||
rows={5}
|
||||
required
|
||||
/>
|
||||
|
||||
<label for="address">Add an address:</label>
|
||||
<label for="building-location">Add the building location:</label>
|
||||
<Text
|
||||
name="address"
|
||||
bind:value={studySpaceData.building_address}
|
||||
placeholder="180 Queen's Gate, London, SW7 5HF"
|
||||
name="building-location"
|
||||
bind:value={studySpaceData.building_location}
|
||||
placeholder="Huxley Building, Imperial South Kensington Campus"
|
||||
required
|
||||
/>
|
||||
|
||||
<div class="submit">
|
||||
<Button type="submit">Share this study space!</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={(spaceImgs?.length || 0) === 0 ||
|
||||
!studySpaceData.location ||
|
||||
!studySpaceData.description ||
|
||||
!studySpaceData.building_location}
|
||||
>
|
||||
Share this study space!
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<style>
|
||||
/* .tag-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.tag-checkbox {
|
||||
background: #2e4653;
|
||||
color: white;
|
||||
border-radius: 0.5rem;
|
||||
padding: 0.25rem 0.75rem;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
input[type="checkbox"] {
|
||||
accent-color: #49bd85;
|
||||
} */
|
||||
|
||||
|
||||
fieldset {
|
||||
border: none;
|
||||
padding: 0;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
legend {
|
||||
font-size: 1rem;
|
||||
color: #ffffff;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.tag-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.tag-checkbox {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background-color: #2e4653;
|
||||
color: #ffffff;
|
||||
padding: 0.4rem 0.8rem;
|
||||
border-radius: 9999px; /* pill shape */
|
||||
font-size: 0.85rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease, transform 0.1s ease;
|
||||
}
|
||||
|
||||
.tag-checkbox:hover {
|
||||
background-color: #3a5b56;
|
||||
transform: scale(1.03);
|
||||
}
|
||||
|
||||
.tag-checkbox input[type="checkbox"] {
|
||||
appearance: none;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
margin-right: 0.5rem;
|
||||
border-radius: 0.25rem;
|
||||
border: 2px solid #49bd85;
|
||||
background-color: transparent;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.tag-checkbox input[type="checkbox"]:checked {
|
||||
background-color: #49bd85;
|
||||
border-color: #49bd85;
|
||||
}
|
||||
|
||||
.tag-checkbox input[type="checkbox"]::after {
|
||||
content: "";
|
||||
display: block;
|
||||
width: 0.25rem;
|
||||
height: 0.5rem;
|
||||
border: solid #1f2a2f;
|
||||
border-width: 0 0.125rem 0.125rem 0;
|
||||
transform: rotate(45deg);
|
||||
position: absolute;
|
||||
top: 0.15rem;
|
||||
left: 0.32rem;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.tag-checkbox input[type="checkbox"]:checked::after {
|
||||
opacity: 1;
|
||||
}
|
||||
form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -2,16 +2,19 @@
|
||||
import Navbar from "$lib/components/Navbar.svelte";
|
||||
import crossUrl from "$lib/assets/cross.svg";
|
||||
import placeholder from "$lib/assets/study_space.png";
|
||||
import Carousel from "$lib/components/Carousel.svelte";
|
||||
|
||||
const { data } = $props();
|
||||
const { space, supabase } = $derived(data);
|
||||
|
||||
const imgUrl = $derived(
|
||||
space.study_space_images.length > 0
|
||||
? supabase.storage
|
||||
.from("files_bucket")
|
||||
.getPublicUrl(space.study_space_images[0].image_path).data.publicUrl
|
||||
: placeholder
|
||||
const imgUrls = $derived(
|
||||
space.study_space_images.length === 0
|
||||
? [placeholder]
|
||||
: space.study_space_images.map(
|
||||
(img) =>
|
||||
supabase.storage.from("files_bucket").getPublicUrl(img.image_path).data
|
||||
.publicUrl
|
||||
)
|
||||
);
|
||||
</script>
|
||||
|
||||
@@ -22,7 +25,7 @@
|
||||
</Navbar>
|
||||
|
||||
<main>
|
||||
<img src={imgUrl} alt="the study space" />
|
||||
<Carousel urls={imgUrls} />
|
||||
<div class="nameContainer">
|
||||
{space.location}
|
||||
</div>
|
||||
@@ -32,7 +35,7 @@
|
||||
<hr />
|
||||
<div class="whereSubtitle">Where it is:</div>
|
||||
<p class="addrContainer">
|
||||
{space.building_address}
|
||||
{space.building_location}
|
||||
</p>
|
||||
</main>
|
||||
|
||||
@@ -61,6 +64,7 @@
|
||||
margin: 0 auto;
|
||||
}
|
||||
.nameContainer {
|
||||
z-index: 10;
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 0.6rem;
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE study_spaces RENAME COLUMN "building_address" TO "building_location";
|
||||
@@ -11,7 +11,8 @@ CREATE TABLE study_spaces (
|
||||
description text,
|
||||
-- Location within building, e.g., "Room 101"
|
||||
location text,
|
||||
building_address text,
|
||||
building_location text,
|
||||
tags text[] NOT NULL DEFAULT array[]::text[],
|
||||
created_at timestamp with time zone DEFAULT now(),
|
||||
updated_at timestamp with time zone DEFAULT now()
|
||||
);
|
||||
@@ -24,6 +25,7 @@ CREATE TABLE study_space_images (
|
||||
PRIMARY KEY (study_space_id, image_path)
|
||||
);
|
||||
|
||||
|
||||
-- Triggers
|
||||
CREATE TRIGGER study_spaces_updated_at
|
||||
AFTER UPDATE ON study_spaces
|
||||
|
||||
Reference in New Issue
Block a user