Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ node_modules
!.env.example
vite.config.js.timestamp-*
vite.config.ts.timestamp-*
.vscode
39 changes: 39 additions & 0 deletions src/lib/assets/share-icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
90 changes: 90 additions & 0 deletions src/lib/components/ShareButton.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
<script lang="ts">
import { locale } from "$lib/stores/locale"
import { fade } from "svelte/transition"

let { sequence } = $props<{ sequence: number[] }>()

// track if url has been copied to clipboard
let urlCopied: boolean = $state(false)

const getShareUrl = () =>
window.location.origin + window.location.pathname + "#" + sequence.join("")
Comment thread
v-ji marked this conversation as resolved.
Outdated
// function to copy url including sequence to clipboard
async function copyUrlToClipboard(event: MouseEvent) {
try {
const url = getShareUrl()
console.log("URL: ", url)
Comment thread
v-ji marked this conversation as resolved.
Outdated
await navigator.clipboard.writeText(url)
urlCopied = true

setTimeout(() => {
urlCopied = false
;(event.target as HTMLButtonElement).blur() // remove focus ring from the clicked button
}, 3000)
} catch (err) {
console.error("Failed to copy URL to clipboard:", err)
}
}

let showDialogue: boolean = $state(false)
function toggleDialogue(event: MouseEvent) {
showDialogue = !showDialogue
;(event.target as HTMLButtonElement).blur()
}
</script>

<!-- Share Button that opens dialogue for copying url with current sequence -->
<div class="fixed right-6 bottom-6 flex items-center space-x-2">
<!-- {#if urlCopied}
<div class="rounded-full bg-sky-800 px-3 py-2 text-white shadow-lg">
{$locale === "de" ? "Link zum Teilen kopiert!" : "Link copied for sharing!"}
</div>
{/if} -->
{#if showDialogue}
<div
transition:fade={{ duration: 200 }}
class="absolute right-20 bottom-0 rounded-lg border border-gray-300 bg-white p-3 shadow-lg"
>
<button
class="absolute top-2 right-2 text-gray-500 hover:text-gray-700 focus:outline-none"
onclick={toggleDialogue}
>
</button>
<p class="text-gray-700">
{$locale === "de"
? "Teile deinen gewürfelten Einakter:"
: "Share your randomised one-act play:"}
</p>
<div
class="mt-2 flex flex-col items-center space-y-2 space-x-2 sm:flex-row sm:space-y-0 sm:space-x-2"
>
<input type="text" class="rounded border bg-gray-100 p-1" value={getShareUrl()} readonly />
<button
class="w-32 rounded bg-sky-800 p-1 text-white transition hover:bg-sky-700 focus:ring-2 focus:ring-sky-500 focus:ring-offset-2"
onclick={copyUrlToClipboard}
>
{urlCopied
? $locale === "de"
? "Link kopiert!"
: "Link copied!"
: $locale === "de"
? "Link kopieren"
: "Copy link"}
</button>
</div>
</div>
{/if}

<!-- Share Button -->
<button
onclick={toggleDialogue}
transition:fade={{ duration: 300 }}
class="flex h-18 w-18 cursor-pointer items-center justify-center rounded-full bg-sky-800 text-white shadow-lg transition-all hover:bg-sky-700 focus:ring-2 focus:ring-sky-500 focus:ring-offset-2 focus:outline-none"
aria-label={$locale === "de" ? "Dialog zum Teilen öffnen" : "Open share dialogue"}
title={$locale === "de" ? "Dialog zum Teilen öffnen" : "Open share dialogue"}
>
<!-- Share Icon -->
<img src="/src/lib/assets/share-icon.svg" alt="Share" class="h-5 w-5" />
</button>
</div>
94 changes: 58 additions & 36 deletions src/routes/+page.svelte
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
<script lang="ts">
import { replaceState } from "$app/navigation"
import { page } from "$app/state"
import Carousel from "$lib/components/Carousel.svelte"
import Dice3D from "$lib/components/Dice3D.svelte"
import ShareButton from "$lib/components/ShareButton.svelte"
import { generateRandomSequence } from "$lib/dice"
import { locale } from "$lib/stores/locale"
import emblaCarouselSvelte from "embla-carousel-svelte"
Expand All @@ -13,47 +15,73 @@
skipSnaps: true // Allow the carousel to skip scroll snaps if it's dragged vigorously
}

let sequence: number[] = $state([])
let isRolling = $state(false)

function toggleLanguage() {
$locale = $locale === "de" ? "en" : "de"
}

// On change to isRolling, set a new sequence
let sequence: number[] = $state([])
const sequenceLength = 200
const sequenceRegex = new RegExp(`^[1-6]{${sequenceLength}}$`)

// Mount gate
let mounted = $state(false)
onMount(() => {
mounted = true
})

// Deferred scroll flag
let wantsScroll = $state(false)

// Whenever the url hash changes, derive sequence
$effect(() => {
if (isRolling) {
const hash = page.url.hash.slice(1)

if (hash && sequenceRegex.test(hash)) {
sequence = hash.split("").map(Number)

// Reset hash
requestAnimationFrame(() => {
const url = new URL(page.url)
url.hash = ""
replaceState(url, page.state)
})

wantsScroll = true
} else if (sequence.length === 0) {
// Only set once on init
sequence = generateRandomSequence()
}
})

function initialiseSequence() {
// On load, check if there's a sequence in the URL hash
const hashString = page.url.hash.slice(1)
if (!hashString) return generateRandomSequence()

// Check if it’s a valid sequence
const sequenceLength = 200
const sequenceRegex = new RegExp(`^[1-6]{${sequenceLength}}$`)
if (!sequenceRegex.test(hashString)) {
return generateRandomSequence()
}
// Perform the scroll once everything exists
$effect(() => {
if (!mounted || !wantsScroll || !mainElement) return
// Wait for the next frame to ensure the DOM is ready
requestAnimationFrame(() => {
scrollToMain()
wantsScroll = false
})
})

// Valid, apply
const hashSequence = hashString.split("").map(Number)
// Reset hash
history.pushState("", document.title, window.location.pathname + window.location.search)
return hashSequence
// Reference to the main element
let mainElement: HTMLElement
function scrollToMain() {
mainElement?.scrollIntoView({ behavior: "smooth", block: "start" })
}

let isRolling = $state(false)
// On change to isRolling, set a new sequence
$effect(() => {
if (isRolling) {
sequence = generateRandomSequence()
}
})

// Track scroll position
let scrollY = $state(0)
let innerHeight = $state(0)
let showBackToTop = $derived(scrollY > innerHeight)

onMount(() => {
sequence = initialiseSequence()
})
let showShareButton = $derived(scrollY > innerHeight * 0.1)

// Add function to scroll back to top
function scrollToTop() {
Expand All @@ -62,16 +90,6 @@
behavior: "smooth"
})
}

// Reference to the main element
let mainElement: HTMLElement

// Add function to scroll to main content
function scrollToMain() {
mainElement?.scrollIntoView({
behavior: "smooth"
})
}
</script>

<svelte:window bind:scrollY bind:innerHeight />
Expand Down Expand Up @@ -173,7 +191,7 @@
<button
onclick={scrollToTop}
transition:fade={{ duration: 300 }}
class="fixed right-6 bottom-6 flex h-18 w-18 cursor-pointer items-center justify-center rounded-full bg-sky-800 text-white shadow-lg transition-all hover:bg-sky-700 focus:ring-2 focus:ring-sky-500 focus:ring-offset-2 focus:outline-none"
class="fixed right-6 bottom-25 flex h-18 w-18 cursor-pointer items-center justify-center rounded-full bg-sky-800 text-white shadow-lg transition-all hover:bg-sky-700 focus:ring-2 focus:ring-sky-500 focus:ring-offset-2 focus:outline-none"
aria-label={$locale === "de" ? "Zurück nach oben" : "Back to top"}
title={$locale === "de" ? "Zurück nach oben" : "Back to top"}
>
Expand All @@ -188,6 +206,10 @@
</svg>
</button>
{/if}
<div style="display: {showShareButton ? 'block' : 'none'};">
Comment thread
v-ji marked this conversation as resolved.
Outdated
<!-- Share Button Component -->
<ShareButton {sequence} />
</div>

<style>
@keyframes pulse {
Expand Down
5 changes: 5 additions & 0 deletions svelte.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ const config = {
strict: true,
path: {
base: process.argv.includes("dev") ? "" : process.env.BASE_PATH
},
resolve: {
alias: {
$lib: "/src/lib" // Define your aliases here
}
Comment thread
v-ji marked this conversation as resolved.
Outdated
}
})
}
Expand Down
3 changes: 2 additions & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
"skipLibCheck": true,
"sourceMap": true,
"strict": true,
"moduleResolution": "bundler"
"moduleResolution": "bundler",
"inlineSources": true
}
// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias
//
Expand Down
5 changes: 4 additions & 1 deletion vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,8 @@ import { defineConfig } from "vite"

export default defineConfig({
plugins: [tailwindcss(), sveltekit()],
assetsInclude: "src/lib/assets/**/*.xsl"
assetsInclude: "src/lib/assets/**/*.xsl",
build: {
sourcemap: true // enable sourcemaps for debugging
}
Comment thread
v-ji marked this conversation as resolved.
Outdated
})