Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
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
21 changes: 21 additions & 0 deletions frontend/src/components/trees/TreeDescription.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<script lang="ts">
import { Notice, PanelSection } from '$components/ui';
import parseMarkdown from '$lib/utils/parseMarkdown';
export let description: string | null = null;

const defaultDescription =
'**Wow – du hast einen von ganz wenigen entdeckt!**<br><br>Ich bin eine echte *Rarität* in Bielefeld. Aber wer mich einmal sieht, erinnert sich.<br><br>Vielleicht entdeckst du ja noch mehr meiner Art?';

let renderedDescription = '';

$: description,
(async () => {
renderedDescription = await parseMarkdown(description ?? defaultDescription);
})();
</script>

<PanelSection>
<Notice tone="info">
{@html renderedDescription}
</Notice>
</PanelSection>
134 changes: 86 additions & 48 deletions frontend/src/components/trees/TreeMetric.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -8,56 +8,94 @@
$: percent = Math.min(100, Math.max(0, (value / max) * 100));
</script>

<div class="relative flex flex-col items-center p-3 text-center gap-2">
<!-- Baum-Icon mit Strich -->
<div class="relative w-14 h-14 text-green-600">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
class="w-full h-full"
stroke="currentColor"
>
<path
d="M12 22.25L12 15.25M12 10V14.25M12 15.25L14.5 12.75M12 15.25L12 14.25M12 14.25L9.75 12.5"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M17.9653 6.50909V6.53612L17.9667 6.56312C18.1359 9.68991 17.6603 12.6963 16.5425 14.8368C15.4492 16.9303 13.8005 18.1265 11.4806 17.9893C10.9592 17.9584 10.3515 17.6981 9.69557 17.1696C9.04588 16.6462 8.4103 15.9076 7.84918 15.0444C6.71093 13.2934 6 11.2225 6 9.76145C6 8.30523 6.70627 6.30887 7.83436 4.65928C8.3906 3.84589 9.01891 3.16298 9.65873 2.69222C10.3021 2.21888 10.9002 2 11.4215 2C12.8361 2 14.5348 2.46694 15.857 3.3114C17.1826 4.15805 17.9653 5.26409 17.9653 6.50909Z"
stroke-width="2"
/>
</svg>

{#if position === 'right'}
<!-- Vertikaler Balken rechts -->
<div class="absolute top-0 bottom-0 right-[-14px] w-[6px] bg-black/20 rounded">
<div
class="absolute bottom-0 left-0 w-full bg-green-600 rounded"
style="height: {percent}%;"
<div class="snap-center shrink-0 w-52 sm:w-56 md:w-auto">
<div
class="relative grid grid-cols-2 items-center gap-4 px-4 py-5 rounded-xl border border-zinc-200 bg-white text-left shadow-sm"
>
<!-- Linke Spalte: Icon + Balken -->
<div class="relative flex justify-center items-center w-full h-14 text-green-600">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
class="w-full h-full animate-pulse-slow"
stroke="currentColor"
>
<path
d="M12 22.25L12 15.25M12 10V14.25M12 15.25L14.5 12.75M12 15.25L12 14.25M12 14.25L9.75 12.5"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
</div>
{:else if position === 'top'}
<!-- Horizontaler Balken oben -->
<div class="absolute top-[-12px] left-0 right-0 h-[6px] bg-black/20 rounded">
<div
class="absolute top-0 left-0 h-full bg-green-600 rounded"
style="width: {percent}%; left: {50 - percent / 2}%;"
<path
d="M17.9653 6.50909V6.53612L17.9667 6.56312C18.1359 9.68991 17.6603 12.6963 16.5425 14.8368C15.4492 16.9303 13.8005 18.1265 11.4806 17.9893C10.9592 17.9584 10.3515 17.6981 9.69557 17.1696C9.04588 16.6462 8.4103 15.9076 7.84918 15.0444C6.71093 13.2934 6 11.2225 6 9.76145C6 8.30523 6.70627 6.30887 7.83436 4.65928C8.3906 3.84589 9.01891 3.16298 9.65873 2.69222C10.3021 2.21888 10.9002 2 11.4215 2C12.8361 2 14.5348 2.46694 15.857 3.3114C17.1826 4.15805 17.9653 5.26409 17.9653 6.50909Z"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
</div>
{:else if position === 'bottom'}
<!-- Horizontaler Balken unten (nun volle Breite, wie oben) -->
<div class="absolute bottom-[-12px] left-0 right-0 h-[6px] bg-black/20 rounded">
</svg>

<!-- Fortschrittsbalken je nach Position -->
{#if position === 'right'}
<div class="absolute top-0 bottom-0 right-[10px] w-[5px] h-14 bg-gray-200 rounded">
<div
class="absolute bottom-0 left-0 w-full bg-green-600 rounded"
style="height: {percent}%;"
/>
</div>
{:else if position === 'top'}
<div
class="absolute top-0 left-0 h-full bg-green-600 rounded"
style="width: {percent}%; left: {50 - percent / 2}%;"
/>
</div>
{/if}
</div>
class="absolute top-[-10px] left-1/2 transform -translate-x-1/2 h-[5px] w-14 bg-gray-200 rounded"
>
<div
class="absolute top-0 left-0 h-full bg-green-600 rounded"
style="width: {percent}%; left: {50 - percent / 2}%;"
/>
</div>
{:else if position === 'bottom'}
<div
class="absolute bottom-[-10px] left-1/2 transform -translate-x-1/2 h-[5px] w-14 bg-gray-200 rounded"
>
<div
class="absolute top-0 left-0 h-full bg-green-600 rounded"
style="width: {percent}%; left: {50 - percent / 2}%;"
/>
</div>
{/if}
</div>

<!-- Label + Wert -->
<p class="mt-4 text-sm text-gray-600">{label}</p>
<p class="text-base font-semibold">{value} {unit}</p>
<!-- Rechte Spalte: Text -->
<div class="flex flex-col items-center justify-center text-center w-full">
<p class="text-xs text-gray-500 leading-tight">
{#if label === 'Kronendurchmesser'}
Kronen-<br />durchmesser
{:else if label === 'Stammdurchmesser'}
Stamm-<br />durchmesser
{:else}
{label}
{/if}
</p>
<p class="text-sm font-semibold text-gray-800 mt-1">
{value}
{unit}
</p>
</div>
</div>
</div>

<style>
@keyframes pulseSlow {
0%,
100% {
transform: scale(1);
opacity: 1;
}
50% {
transform: scale(1.06);
opacity: 0.85;
}
}
.animate-pulse-slow {
animation: pulseSlow 3s ease-in-out infinite;
}
</style>
47 changes: 47 additions & 0 deletions frontend/src/components/trees/TreeMetricsView.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<script lang="ts">
import TreeMetric from './TreeMetric.svelte';
import { isMobile } from '$lib/utils/media';

export let height: number;
export let crown_diameter: number;
export let trunk_diameter: number;
</script>

{#if $isMobile}
<!-- 📱 Mobile: Carousel -->
<div class="relative">
<div
class="flex gap-4 overflow-x-auto scroll-smooth snap-x snap-mandatory px-1 pb-2"
style="scrollbar-width: thin;"
>
<TreeMetric label="Höhe" value={height} unit="m" max={39} position="right" />
<TreeMetric
label="Kronendurchmesser"
value={crown_diameter}
unit="m"
max={29}
position="top"
/>
<TreeMetric
label="Stammdurchmesser"
value={trunk_diameter}
unit="cm"
max={297}
position="bottom"
/>
</div>
</div>
{:else}
<!-- 💻 Desktop: 3-Spalten-Grid -->
<div class="grid grid-cols-3 gap-4 text-sm text-gray-800">
<TreeMetric label="Höhe" value={height} unit="m" max={39} position="right" />
<TreeMetric label="Kronendurchmesser" value={crown_diameter} unit="m" max={29} position="top" />
<TreeMetric
label="Stammdurchmesser"
value={trunk_diameter}
unit="cm"
max={297}
position="bottom"
/>
</div>
{/if}
5 changes: 1 addition & 4 deletions frontend/src/components/trees/TreeWaterings.svelte
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
<script lang="ts">
import { onMount, createEventDispatcher } from 'svelte';
import { onMount } from 'svelte';
import { getWateringsForTree, getCurrentUser } from '$lib/supabase';
import { WateringHistory } from '$components/waterings';
import { Notice } from '$components/ui';
import type { Watering } from '$types/watering';

export let treeId: string;

const dispatch = createEventDispatcher();

let currentUserId: string | null = null;
let waterings: Watering[] = [];
let loading = true;
Expand All @@ -24,7 +22,6 @@
error = 'Fehler beim Laden der Gießungen.';
} finally {
loading = false;
dispatch('contentChanged');
}
}

Expand Down
3 changes: 2 additions & 1 deletion frontend/src/components/trees/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export { default as AdoptTreeButton } from './AdoptTreeButton.svelte';
export { default as TreeMetric } from './TreeMetric.svelte';
export { default as TreeDescription } from './TreeDescription.svelte';
export { default as TreeMetricsView } from './TreeMetricsView.svelte';
export { default as TreeWaterings } from './TreeWaterings.svelte';
export { default as WaterColumn } from './WaterColumn.svelte';
export { default as AddWateringForm } from './add-watering/AddWateringForm.svelte';
Expand Down
24 changes: 18 additions & 6 deletions frontend/src/components/ui/Accordion.svelte
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
<!--Übernommen aus https://svelte.dev/playground/c109f83f3c114cb7829f04fe2440ef94?version=5.28.2, dann modifiziert ⚒️-->
<script lang="ts">
import { onMount, afterUpdate, tick, createEventDispatcher } from 'svelte';
import { onMount, onDestroy, tick, createEventDispatcher } from 'svelte';

export let open = false;
const dispatch = createEventDispatcher();
Expand All @@ -9,24 +8,37 @@
let contentEl: HTMLDivElement;
let contentHeight = 0;

let resizeObserver: ResizeObserver;

const handleClick = () => (open = !open);

const updateHeight = async () => {
if (contentEl) {
await tick(); // DOM muss aktualisiert sein
await tick();
contentHeight = contentEl.scrollHeight;
dispatch('heightchange', { height: contentHeight });
}
};

onMount(updateHeight);
afterUpdate(updateHeight);
onMount(() => {
updateHeight();

if (contentEl) {
resizeObserver = new ResizeObserver(() => {
updateHeight();
});
resizeObserver.observe(contentEl);
}
});

onDestroy(() => {
resizeObserver?.disconnect();
});

$: if (open) {
updateHeight();
}

// Optional: von außen triggerbar
export function updateHeightExternally() {
updateHeight();
}
Expand Down
37 changes: 37 additions & 0 deletions frontend/src/lib/supabase/trees.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { supabase } from './client';


/**
* Holt den deutschen Baumtyp eines Baumes anhand seiner UUID.
*/
Expand Down Expand Up @@ -37,4 +38,40 @@ export async function loadSpeciesMap(treeIds: string[]): Promise<Map<string, str
);

return speciesMap;
}

export async function getTreeSpeciesDescription(treeTypeBotanic: string) {
const field = Math.random() < 0.5 ? 'description_emotional' : 'description_neutral';


const { data, error } = await supabase
.from('tree_species')
.select(field)
.eq('tree_type_botanic', treeTypeBotanic)
.maybeSingle();

if (error) {
console.error('Failed to load species description:', error.message);
return null;
}

return (data as Record<string, string | null>)?.[field] ?? null;
}

/**
* Holt alle Baumdaten für eine bestimmte UUID aus der trees-Tabelle.
*/
export async function getTreeById(treeId: string) {
const { data, error } = await supabase
.from('trees')
.select()
.eq('uuid', treeId)
.maybeSingle();

if (error) {
console.error('Failed to load tree:', error.message);
return null;
}

return data;
}
12 changes: 12 additions & 0 deletions frontend/src/lib/supabase/waterings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,16 @@ export async function getWateringsForTree(tree_uuid: string): Promise<Watering[]
return data ?? [];
}

export async function loadWateringsForTree(tree_uuid: string): Promise<{ data: Watering[]; error: string | null }> {
try {
const waterings = await getWateringsForTree(tree_uuid);
return { data: waterings, error: null };
} catch (err) {
console.error('Fehler beim Abrufen der Gießungen:', err);
return { data: [], error: 'Fehler beim Laden der Gießungen.' };
}
}

export async function getWateringsForUser(user_uuid: string): Promise<Watering[]> {
const { data, error } = await supabase
.from('waterings')
Expand All @@ -55,3 +65,5 @@ export async function deleteWatering(uuid: string): Promise<void> {
throw new Error(`Fehler beim Löschen: ${error.message}`);
}
}


Loading