Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Changed

- [memorandum] Ordner-Erstellung über FAB + Modal statt direkter Erstellung; FAB immer sichtbar; Zero-State öffnet ebenfalls das Modal.
- [jokes] Inline-Witz-Formular durch FAB + Modal ersetzt; Seite zeigt nur noch den Zufallswitz-Bereich.
- [hitstar] Remove Spotify embed (full iframe) from the reveal phase; only round tracker, abort button, flipped track card and next-round button are shown after guessing.
- [hitstar] Spotify track search now uses a single random year (1955–2025) per request instead of a fixed range to improve year distribution across decades.
- [hitstar] Add `genre:pop` filter to Spotify search query to increase the share of well-known tracks.
Expand Down
13 changes: 13 additions & 0 deletions docs/features/jokes.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,19 @@ Keys under `jokes.*` in translation files.

---

## Frontend Components

| Component | Description |
|-----------|-------------|
| `frontend/src/lib/components/jokes/Jokes.svelte` | Main page component — shows random joke area only |
| `frontend/src/lib/components/jokes/CreateJokeOverlay.svelte` | Carbon Modal for creating a new joke (FAB-triggered) |

### Create Joke Pattern

Joke creation uses a FAB (fixed primary button, bottom-right) that opens a Carbon Modal (`CreateJokeOverlay.svelte`). The modal contains a `TextArea` for the joke text and a `Select` for language (`de`/`en`). On success: `celebrate()` + `InlineNotification` + auto-close after ~1.5 s. The inline form that was previously embedded directly on the page has been removed.

---

## Key Implementation Notes

- `persistDailyJoke()` sets `verified: true` on all auto-fetched jokes.
Expand Down
20 changes: 20 additions & 0 deletions docs/features/memorandum.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,26 @@ Keys under `memorandum.*` in translation files.

---

## Frontend Components

| Component | Description |
|-----------|-------------|
| `frontend/src/lib/components/memorandum/FolderArea.svelte` | Main folder grid; FAB always visible |
| `frontend/src/lib/components/memorandum/FolderOverlay.svelte` | Dual-mode Carbon Modal: **create** (FAB) and **edit** (context menu) |
| `frontend/src/lib/components/memorandum/LinkOverlay.svelte` | Modal for creating/editing links |
| `frontend/src/lib/components/memorandum/Startup.svelte` | Zero-state screen with "create folder" CTA |

### Folder Create/Edit Pattern

`FolderOverlay.svelte` handles both modes via `type = $derived(currentFolderId ? 'edit' : 'new')`:

- **Create** (FAB in `FolderArea.svelte`): `bind:open={createFolderModalOpen}`, no `currentFolderId` → pre-fills default name, resets color sliders, pushes new `FolderClass` to `$localPresetStore`, calls `celebrate()`.
- **Edit** (context menu in `Folder.svelte`): `bind:open={$folderOverlayOptionsStore.showOverlay}` in `+page.svelte`, `currentFolderId` is set → loads persisted name and background color.

The FAB is **always visible** (not gated on `Folders.length > 0`). The zero-state screen also opens the create modal via its CTA button.

---

## Key Implementation Notes

- Memorandum has **no dedicated MongoDB schema** — all data lives in the shared Keystore collection.
Expand Down
108 changes: 108 additions & 0 deletions frontend/src/lib/components/jokes/CreateJokeOverlay.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
<script lang="ts">
// 1. IMPORTS
import { createJoke } from '$lib/api/jokes.api';
import { celebrate } from '$lib/util/stores/stores-global';
import { initialized, t } from '$lib/util/translations';
import Modal from 'carbon-components-svelte/src/Modal/Modal.svelte';
import InlineNotification from 'carbon-components-svelte/src/Notification/InlineNotification.svelte';
import Select from 'carbon-components-svelte/src/Select/Select.svelte';
import SelectItem from 'carbon-components-svelte/src/Select/SelectItem.svelte';
import TextArea from 'carbon-components-svelte/src/TextArea/TextArea.svelte';
import Save from 'carbon-icons-svelte/lib/Save.svelte';
import { fade } from 'svelte/transition';

// 2. PROPS
let { open = $bindable(false) } = $props();

// 3. CONST
const languages = ['de', 'en'];

// 4. STATE
let jokeText = $state('');
let jokeLanguage = $state('de');
let timeout = $state<number | undefined>(undefined);

// 5. DERIVED
const submittable = $derived(jokeText && jokeLanguage);
const showNotification = $derived(timeout !== undefined);

// 6. EFFECTS
$effect(() => {
if (open) {
jokeText = '';
jokeLanguage = 'de';
timeout = undefined;
}
});

// 8. FUNCTIONS
const submitJoke = async () => {
const createResponse = await createJoke({
text: jokeText,
language: jokeLanguage,
categories: [],
});

if (createResponse?._id) {
celebrate();
timeout = 1500;
jokeText = '';
jokeLanguage = 'de';
}
};

const closeOverlay = () => {
open = false;
timeout = undefined;
};
</script>

<Modal
bind:open
modalHeading={$t('page.jokes.createJokeModalTitle')}
primaryButtonText={$t('page.shared.save')}
primaryButtonIcon={Save}
primaryButtonDisabled={!submittable}
secondaryButtonText={$t('page.shared.abort')}
on:click:button--primary={submitJoke}
on:click:button--secondary={closeOverlay}
>
{#if $initialized}
<TextArea
helperText={$t('page.jokes.newJokeHelperText')}
placeholder={$t('page.jokes.newJokeTextInput')}
rows={5}
cols={60}
bind:value={jokeText}
/>

<div class="mt2">
<Select
labelText={$t('page.jokes.newJokeLanguage')}
bind:selected={jokeLanguage}
>
{#each languages as language}
<SelectItem value={language} />
{/each}
</Select>
</div>

{#if showNotification}
<div transition:fade>
<InlineNotification
{timeout}
kind="success"
lowContrast
subtitle={$t('page.jokes.jokeCreatedSnackbar')}
class="mt2"
on:close={() => {
timeout = undefined;
open = false;
}}
/>
</div>
{/if}
{:else}
<p>Locale initializing...</p>
{/if}
</Modal>
118 changes: 27 additions & 91 deletions frontend/src/lib/components/jokes/Jokes.svelte
Original file line number Diff line number Diff line change
@@ -1,32 +1,18 @@
<script lang="ts">
// 1. IMPORTS
import { createJoke, getRandomJoke } from '$lib/api/jokes.api';
import { getRandomJoke } from '$lib/api/jokes.api';
import type { JokeDto } from '$lib/types/jokes.dto';
import { celebrate } from '$lib/util/stores/stores-global';
import { initialized, t } from '$lib/util/translations';
import Button from 'carbon-components-svelte/src/Button/Button.svelte';
import InlineNotification from 'carbon-components-svelte/src/Notification/InlineNotification.svelte';
import Select from 'carbon-components-svelte/src/Select/Select.svelte';
import SelectItem from 'carbon-components-svelte/src/Select/SelectItem.svelte';
import TextArea from 'carbon-components-svelte/src/TextArea/TextArea.svelte';
import Tile from 'carbon-components-svelte/src/Tile/Tile.svelte';
import Add from 'carbon-icons-svelte/lib/Add.svelte';
import CloudDownload from 'carbon-icons-svelte/lib/CloudDownload.svelte';
import Save from 'carbon-icons-svelte/lib/Save.svelte';
import { onMount } from 'svelte';
import { fade } from 'svelte/transition';

// 2. CONST (non-reactive constants)
const languages = ['de', 'en'];
import CreateJokeOverlay from './CreateJokeOverlay.svelte';

// 4. STATE
let randomJoke = $state<JokeDto | null>(null);
let newJokeText = $state('');
let newJokeLanguage = $state('de');
let timeout = $state(undefined);

// 5. DERIVED
const showNotification = $derived(timeout !== undefined);
const submittable = $derived(newJokeText && newJokeLanguage);
let createJokeModalOpen = $state(false);

// 7. LIFECYCLE
onMount(() => {
Expand All @@ -37,99 +23,42 @@
const setRandomJoke = async () => {
randomJoke = await getRandomJoke();
};

const createNewJoke = async () => {
// TODO: Add categories
const createResponse = await createJoke({
text: newJokeText,
language: newJokeLanguage,
categories: [],
});

if (createResponse?._id) {
timeout = 3_000;
celebrate();
}

newJokeText = '';
newJokeLanguage = 'de';
};
</script>

{#if $initialized}
<section>
<h1>{$t('page.jokes.titleRandomJoke')}</h1>
<p>{$t('page.jokes.descriptionRandomJoke')}</p>

<Button
iconDescription={$t('page.jokes.loadRandomJoke')}
icon={CloudDownload}
on:click={setRandomJoke}
class="mt2"
>
{$t('page.jokes.newRandomJoke')}
</Button>

{#if randomJoke}
<Tile class="random_joke mt2">
<i>{randomJoke.text}</i>
<p>📅{new Date(randomJoke.created).toLocaleString('DE-de')}</p>
</Tile>
{/if}

<h2 class="mt2">{$t('page.jokes.titleNewJoke')}</h2>
<p>{$t('page.jokes.descriptionNewJoke')}</p>

<div class="mt1">
<TextArea
helperText={$t('page.jokes.newJokeHelperText')}
placeholder={$t('page.jokes.newJokeTextInput')}
rows={5}
cols={60}
bind:value={newJokeText}
/>
</div>

<div class="mt2">
<Select
labelText={$t('page.jokes.newJokeLanguage')}
bind:selected={newJokeLanguage}
>
{#each languages as language}
<SelectItem value={language} />
{/each}
</Select>
</div>

<Button
kind="ghost"
iconDescription={$t('page.jokes.saveJoke')}
icon={Save}
on:click={createNewJoke}
disabled={!submittable}
class="save_button mt2"
iconDescription={$t('page.jokes.loadRandomJoke')}
icon={CloudDownload}
on:click={setRandomJoke}
class="mt2"
>
{$t('page.jokes.createJokeButtonText')}
{$t('page.jokes.newRandomJoke')}
</Button>

{#if showNotification}
<div transition:fade>
<InlineNotification
{timeout}
kind="success"
lowContrast
subtitle={$t('page.jokes.jokeCreatedSnackbar')}
class="inline_notification"
on:close={(e) => {
timeout = undefined;
}}
/>
</div>
{/if}
</section>

<Button
kind="primary"
iconDescription={$t('page.jokes.addJoke')}
icon={Add}
id="add_joke_button"
tooltipPosition="left"
on:click={() => (createJokeModalOpen = true)}
/>

<CreateJokeOverlay bind:open={createJokeModalOpen} />
{:else}
<section>Locale initializing...</section>
d
{/if}

<style lang="scss">
Expand All @@ -154,4 +83,11 @@
:global(.random_joke) {
padding: 2rem;
}

:global(#add_joke_button) {
position: fixed;
bottom: calc(var(--default_padding) + env(safe-area-inset-bottom, 0px));
right: var(--default_padding);
z-index: 100;
}
</style>
Loading
Loading