restructure: flatten workspace nesting, move devcontainer to root

- backend/workspaces/backend/ → backend/
- backend/workspaces/frontend/ → frontend/
- backend/.devcontainer/ + .vscode/ → repo root (where VS Code expects them)
- loose scripts/SQL files → scripts/
- replace nested git repo with single repo at project root
- update docker-compose.yml build context and devcontainer.json path
- add root .gitignore

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-03-15 11:47:58 +01:00
parent 7e725090fe
commit e63adb964d
155 changed files with 650 additions and 29 deletions

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="107" height="128" viewBox="0 0 107 128"><title>svelte-logo</title><path d="M94.157 22.819c-10.4-14.885-30.94-19.297-45.792-9.835L22.282 29.608A29.92 29.92 0 0 0 8.764 49.65a31.5 31.5 0 0 0 3.108 20.231 30 30 0 0 0-4.477 11.183 31.9 31.9 0 0 0 5.448 24.116c10.402 14.887 30.942 19.297 45.791 9.835l26.083-16.624A29.92 29.92 0 0 0 98.235 78.35a31.53 31.53 0 0 0-3.105-20.232 30 30 0 0 0 4.474-11.182 31.88 31.88 0 0 0-5.447-24.116" style="fill:#ff3e00"/><path d="M45.817 106.582a20.72 20.72 0 0 1-22.237-8.243 19.17 19.17 0 0 1-3.277-14.503 18 18 0 0 1 .624-2.435l.49-1.498 1.337.981a33.6 33.6 0 0 0 10.203 5.098l.97.294-.09.968a5.85 5.85 0 0 0 1.052 3.878 6.24 6.24 0 0 0 6.695 2.485 5.8 5.8 0 0 0 1.603-.704L69.27 76.28a5.43 5.43 0 0 0 2.45-3.631 5.8 5.8 0 0 0-.987-4.371 6.24 6.24 0 0 0-6.698-2.487 5.7 5.7 0 0 0-1.6.704l-9.953 6.345a19 19 0 0 1-5.296 2.326 20.72 20.72 0 0 1-22.237-8.243 19.17 19.17 0 0 1-3.277-14.502 17.99 17.99 0 0 1 8.13-12.052l26.081-16.623a19 19 0 0 1 5.3-2.329 20.72 20.72 0 0 1 22.237 8.243 19.17 19.17 0 0 1 3.277 14.503 18 18 0 0 1-.624 2.435l-.49 1.498-1.337-.98a33.6 33.6 0 0 0-10.203-5.1l-.97-.294.09-.968a5.86 5.86 0 0 0-1.052-3.878 6.24 6.24 0 0 0-6.696-2.485 5.8 5.8 0 0 0-1.602.704L37.73 51.72a5.42 5.42 0 0 0-2.449 3.63 5.79 5.79 0 0 0 .986 4.372 6.24 6.24 0 0 0 6.698 2.486 5.8 5.8 0 0 0 1.602-.704l9.952-6.342a19 19 0 0 1 5.295-2.328 20.72 20.72 0 0 1 22.237 8.242 19.17 19.17 0 0 1 3.277 14.503 18 18 0 0 1-8.13 12.053l-26.081 16.622a19 19 0 0 1-5.3 2.328" style="fill:#fff"/></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -0,0 +1,116 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte';
// Props
export let name: string;
export let label: string;
export let value: string = "";
export let initialName: string = "";
const dispatch = createEventDispatcher();
// Lokaler State
let searchTerm = initialName;
// Sync mit externen Änderungen (z.B. Reset Button)
$: searchTerm = initialName;
let results: any[] = [];
let showDropdown = false;
let loading = false;
let debounceTimer: any;
function handleInput() {
// Wenn der User tippt, ist die alte ID ungültig -> Reset
if (value && searchTerm !== initialName) {
value = "";
dispatch('change', { value: "" }); // Bescheid geben: Auswahl aufgehoben
}
showDropdown = true;
clearTimeout(debounceTimer);
debounceTimer = setTimeout(async () => {
if (searchTerm.length < 1) {
results = [];
return;
}
loading = true;
try {
const res = await fetch(`/api/persons?q=${encodeURIComponent(searchTerm)}`);
if (res.ok) {
results = await res.json();
} else {
results = [];
}
} catch (e) {
console.error("Suche fehlgeschlagen", e);
results = [];
} finally {
loading = false;
}
}, 300);
}
function selectPerson(person: any) {
value = person.id;
searchTerm = `${person.firstName} ${person.lastName}`;
showDropdown = false;
// --- NEU: Event feuern ---
dispatch('change', { value: person.id });
}
function clickOutside(node: HTMLElement) {
const handleClick = (event: any) => {
if (node && !node.contains(event.target) && !event.defaultPrevented) {
showDropdown = false;
}
};
document.addEventListener('click', handleClick, true);
return {
destroy() { document.removeEventListener('click', handleClick, true); }
};
}
</script>
<div class="relative" use:clickOutside>
<label for={name} class="block text-sm font-medium text-gray-700">{label}</label>
<input type="hidden" {name} bind:value={value} />
<input
type="text"
id="{name}-search"
autocomplete="off"
bind:value={searchTerm}
on:input={handleInput}
on:focus={() => showDropdown = true}
placeholder="Namen tippen..."
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm border p-2 focus:ring-blue-500 focus:border-blue-500"
/>
{#if showDropdown && (results.length > 0 || loading)}
<div class="absolute z-10 mt-1 w-full bg-white shadow-lg max-h-60 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm">
{#if loading}
<div class="p-2 text-gray-500 text-sm">Suche...</div>
{:else}
{#each results as person}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
class="cursor-pointer select-none relative py-2 pl-3 pr-9 hover:bg-blue-100 text-gray-900"
on:click={() => selectPerson(person)}
role="button"
tabindex="0"
>
<div class="flex items-center">
<span class="font-medium block truncate">
{person.lastName}, {person.firstName}
</span>
</div>
</div>
{/each}
{/if}
</div>
{/if}
</div>

View File

@@ -0,0 +1,138 @@
<script lang="ts">
export let tags: string[] = []; // Two-way binding
export let allowCreation = true;
let inputVal = '';
let suggestions: string[] = [];
let activeIndex = -1;
let showSuggestions = false;
// Fetch suggestions from backend
async function fetchSuggestions(query: string) {
if (query.length < 2) {
suggestions = [];
return;
}
try {
console.log('fetch tags')
const res = await fetch(`/api/tags?query=${encodeURIComponent(query)}`);
if (res.ok) {
const data = await res.json();
// Filter out tags already selected
suggestions = data.filter((t: string) => !tags.includes(t));
showSuggestions = true;
}
} catch (e) {
console.error("Tag fetch error", e);
}
}
function addTag(tag: string) {
const trimmed = tag.trim();
if (trimmed && !tags.includes(trimmed)) {
tags = [...tags, trimmed];
}
inputVal = '';
suggestions = [];
showSuggestions = false;
activeIndex = -1;
}
function removeTag(index: number) {
tags = tags.filter((_, i) => i !== index);
}
function handleKeydown(e: KeyboardEvent) {
console.log("keydown",e)
if (e.key === 'Enter') {
e.preventDefault();
if (activeIndex >= 0 && suggestions[activeIndex]) {
addTag(suggestions[activeIndex]);
} else if(allowCreation) {
addTag(inputVal); // Add new tag
}
} else if (e.key === 'Backspace' && inputVal === '' && tags.length > 0) {
removeTag(tags.length - 1);
} else if (e.key === 'ArrowDown') {
e.preventDefault();
activeIndex = (activeIndex + 1) % suggestions.length;
} else if (e.key === 'ArrowUp') {
e.preventDefault();
activeIndex = (activeIndex - 1 + suggestions.length) % suggestions.length;
}
}
function handleInput() {
fetchSuggestions(inputVal);
}
</script>
<div class="w-full">
<!-- Tag Container -->
<div
class="flex flex-wrap gap-2 p-2 border border-gray-300 rounded focus-within:ring-1 focus-within:ring-brand-navy focus-within:border-brand-navy bg-white min-h-[42px]"
>
<!-- Render Selected Tags -->
{#each tags as tag, i}
<span
class="bg-brand-sand/30 text-brand-navy text-sm font-medium px-2 py-1 rounded flex items-center gap-1"
>
{tag}
<button
type="button"
on:click={() => removeTag(i)}
class="text-brand-navy/50 hover:text-red-500 focus:outline-none"
>
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"
><path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18L18 6M6 6l12 12"
/></svg
>
</button>
</span>
{/each}
<!-- Input Field -->
<div class="relative flex-1 min-w-[120px]">
<input
type="text"
bind:value={inputVal}
on:input={handleInput}
on:keydown={handleKeydown}
on:focus={() => handleInput()}
on:blur={() => setTimeout(() => (showSuggestions = false), 200)}
placeholder={tags.length === 0
? allowCreation
? 'Schlagworte hinzufügen...'
: 'Nach Schlagworten filtern...'
: ''}
class="w-full h-full border-none p-1 focus:ring-0 text-sm bg-transparent outline-none"
/>
<!-- Typeahead Dropdown -->
{#if showSuggestions && suggestions.length > 0}
<ul
class="absolute left-0 top-full mt-1 w-full bg-white border border-gray-200 rounded shadow-lg z-50 max-h-48 overflow-y-auto"
>
{#each suggestions as suggestion, i}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<li
class="px-3 py-2 text-sm cursor-pointer hover:bg-brand-sand/20 {i === activeIndex
? 'bg-brand-sand/20 text-brand-navy font-bold'
: 'text-gray-700'}"
on:click={() => addTag(suggestion)}
>
{suggestion}
</li>
{/each}
</ul>
{/if}
</div>
</div>
{#if allowCreation}
<p class="text-xs text-gray-400 mt-1">Enter drücken um Schlagwort zu erstellen.</p>
{/if}
</div>

View File

@@ -0,0 +1 @@
// place files you want to import through the `$lib` alias in this folder.