Compare commits

..

10 Commits

Author SHA1 Message Date
fd06713555 link topic in topics page to topic page 2025-12-16 01:04:24 +01:00
e8c8779536 sort topics based on amount of tasks 2025-12-15 22:26:54 +01:00
1d53921a73 update docker compose 2025-12-15 22:26:34 +01:00
f28c0f56ad show topics list 2025-08-25 20:40:10 +02:00
c55c266f44 update default port 2025-08-25 20:23:55 +02:00
Stan
a27ec15124 Merge pull request #21 from stanrunge/codex/preserve-original-indices-in-filtered-view
Fix indexing for filtered progress table
2025-07-02 14:23:24 +02:00
Stan
e77509b909 Fix progress index after search 2025-07-02 14:23:12 +02:00
1a2c7b5f55 explicit funciton return types 2025-06-30 03:21:49 +02:00
3a1877375d add index to tasks 2025-05-25 23:57:16 +02:00
fe3329fc37 add basic search bar 2025-03-24 18:49:20 +01:00
18 changed files with 379 additions and 101 deletions

View File

@@ -8,16 +8,16 @@ services:
ports:
- "5432:5432"
volumes:
- db-data:/var/lib/postgresql/data
- db-data:/var/lib/postgresql
sveltekit:
image: oven/bun
volumes:
- ./packages/web:/app
working_dir: /app
command: bun run dev --host
command: sh -c "bun install & bun run dev --host"
ports:
- "80:5173"
- "3000:5173"
env_file: .env
develop:
watch:

Binary file not shown.

View File

@@ -1,14 +1,16 @@
{
"$schema": "https://shadcn-svelte.com/schema.json",
"style": "default",
"tailwind": {
"config": "tailwind.config.js",
"css": "src/app.css",
"baseColor": "slate"
},
"aliases": {
"components": "$lib/components",
"utils": "$lib/utils"
"utils": "$lib/utils",
"ui": "$lib/components/ui",
"hooks": "$lib/hooks",
"lib": "$lib"
},
"typescript": true
}
"typescript": true,
"registry": "https://tw3.shadcn-svelte.com/registry/default"
}

View File

@@ -21,12 +21,38 @@ export default [
},
{
files: ['**/*.svelte'],
rules: {
'@typescript-eslint/explicit-function-return-type': [
'error',
{
allowExpressions: false,
allowTypedFunctionExpressions: true
}
]
},
languageOptions: {
parserOptions: {
parser: ts.parser
}
}
},
{
files: ['**/*.ts'],
languageOptions: {
globals: globals.node
},
rules: {
'@typescript-eslint/explicit-function-return-type': [
'error',
{
allowExpressions: false,
allowTypedFunctionExpressions: true
}
]
}
},
{
ignores: ['build/', '.svelte-kit/', 'dist/']
}

View File

@@ -25,7 +25,7 @@
"@types/eslint": "9.6.1",
"@types/pg": "^8.11.10",
"autoprefixer": "^10.4.20",
"bits-ui": "^0.22.0",
"bits-ui": "^1.4.7",
"clsx": "^2.1.1",
"cmdk-sv": "^0.0.18",
"drizzle-kit": "^0.30.1",

View File

@@ -0,0 +1,28 @@
import Root from "./table.svelte";
import Body from "./table-body.svelte";
import Caption from "./table-caption.svelte";
import Cell from "./table-cell.svelte";
import Footer from "./table-footer.svelte";
import Head from "./table-head.svelte";
import Header from "./table-header.svelte";
import Row from "./table-row.svelte";
export {
Root,
Body,
Caption,
Cell,
Footer,
Head,
Header,
Row,
//
Root as Table,
Body as TableBody,
Caption as TableCaption,
Cell as TableCell,
Footer as TableFooter,
Head as TableHead,
Header as TableHeader,
Row as TableRow,
};

View File

@@ -0,0 +1,16 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import type { WithElementRef } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLTableSectionElement>> = $props();
</script>
<tbody bind:this={ref} class={cn("[&_tr:last-child]:border-0", className)} {...restProps}>
{@render children?.()}
</tbody>

View File

@@ -0,0 +1,16 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import type { WithElementRef } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLElement>> = $props();
</script>
<caption bind:this={ref} class={cn("text-muted-foreground mt-4 text-sm", className)} {...restProps}>
{@render children?.()}
</caption>

View File

@@ -0,0 +1,20 @@
<script lang="ts">
import type { HTMLTdAttributes } from "svelte/elements";
import type { WithElementRef } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLTdAttributes> = $props();
</script>
<td
bind:this={ref}
class={cn("p-4 align-middle [&:has([role=checkbox])]:pr-0", className)}
{...restProps}
>
{@render children?.()}
</td>

View File

@@ -0,0 +1,16 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import type { WithElementRef } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLTableSectionElement>> = $props();
</script>
<tfoot bind:this={ref} class={cn("bg-muted/50 font-medium", className)} {...restProps}>
{@render children?.()}
</tfoot>

View File

@@ -0,0 +1,23 @@
<script lang="ts">
import type { HTMLThAttributes } from "svelte/elements";
import type { WithElementRef } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLThAttributes> = $props();
</script>
<th
bind:this={ref}
class={cn(
"text-muted-foreground h-12 px-4 text-left align-middle font-medium [&:has([role=checkbox])]:pr-0",
className
)}
{...restProps}
>
{@render children?.()}
</th>

View File

@@ -0,0 +1,16 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import type { WithElementRef } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLTableSectionElement>> = $props();
</script>
<thead bind:this={ref} class={cn("[&_tr]:border-b", className)} {...restProps}>
{@render children?.()}
</thead>

View File

@@ -0,0 +1,23 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import type { WithElementRef } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLTableRowElement>> = $props();
</script>
<tr
bind:this={ref}
class={cn(
"hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors",
className
)}
{...restProps}
>
{@render children?.()}
</tr>

View File

@@ -0,0 +1,18 @@
<script lang="ts">
import type { HTMLTableAttributes } from "svelte/elements";
import type { WithElementRef } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLTableAttributes> = $props();
</script>
<div class="relative w-full overflow-auto">
<table bind:this={ref} class={cn("w-full caption-bottom text-sm", className)} {...restProps}>
{@render children?.()}
</table>
</div>

View File

@@ -2,7 +2,7 @@ import { db } from '$lib/db';
import { tasks } from '$lib/db/schema.js';
import { desc } from 'drizzle-orm';
export const load = async ({ cookies }) => {
export const load = async () => {
return {
tasks: await db.query.tasks.findMany({
with: {

View File

@@ -1,5 +1,16 @@
<script lang="ts">
let { data } = $props();
let searchTerm = $state('');
let filteredTasks = $derived(
data.tasks.filter(
(task) =>
task.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
task.tasksToTopics.some((t) =>
t.topic.name.toLowerCase().includes(searchTerm.toLowerCase())
)
)
);
</script>
<div class="flex flex-col items-center p-4">
@@ -8,6 +19,13 @@
<h1 class="font-bold text-2xl">
Tasks ({data.tasks.length})
</h1>
<a href="/progress/topics">
<button class="px-4 py-2 bg-gray-800 text-white rounded hover:bg-gray-600 transition">
Topics
</button>
</a>
{#if data.editable}
<a href="/progress/create">
<button class="px-4 py-2 bg-green-500 text-white rounded hover:bg-green-600 transition">
@@ -16,6 +34,15 @@
</a>
{/if}
</div>
<!-- Search Bar -->
<div class="w-full max-w-md">
<input
type="text"
bind:value={searchTerm}
placeholder="Search tasks or topics..."
class="w-full px-4 py-2 rounded bg-gray-800 border border-gray-700 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
Total progress: {data.tasks.reduce((acc, task) => acc + task.currentPoints, 0)} / {data.tasks.reduce(
(acc, task) => acc + task.totalPoints,
0
@@ -26,103 +53,101 @@
).toFixed(3)}%)
</div>
{#if data.tasks.length > 0}
{#if filteredTasks.length > 0}
<div class="overflow-x-auto w-full">
{#if data.tasks.length > 0}
<!-- Desktop Table View -->
<div class="hidden md:block">
<table class="min-w-full">
<thead>
<tr class="border-b border-white">
<th class="px-6 py-3 text-left text-sm font-semibold">Name</th>
<th class="px-6 py-3 text-left text-sm font-semibold">Topics</th>
<th class="px-6 py-3 text-left text-sm font-semibold min-w-[200px]">Progress</th>
</tr>
</thead>
<tbody>
{#each data.tasks as task, index}
<tr class="{index % 2 === 0 ? 'bg-black' : 'bg-gray-950'} hover:bg-gray-800">
<td class="px-6 py-2 border-b">
<a href={`/progress/${task.id}`} class="text-blue-500 hover:underline">
{task.name}
</a>
</td>
<td class="px-6 py-2 border-b">
<a href={`/progress/${task.id}`} class="flex gap-2 flex-wrap">
{#each task.tasksToTopics as topic}
<div
class="relative bg-blue-500 text-white rounded-full px-2 py-1 shadow font-medium hover:bg-blue-600 transition cursor-pointer"
title="Click for more details about {topic.topic.name}"
>
{topic.topic.name}
</div>
{/each}
</a>
</td>
<td class="px-6 py-2 border-b min-w-[200px]">
<div class="relative w-full h-6 bg-gray-700 rounded-full overflow-hidden">
<!-- Desktop Table View -->
<div class="hidden md:block">
<table class="min-w-full">
<thead>
<tr class="border-b border-white">
<th class="px-6 py-3 text-left text-sm font-semibold">Name</th>
<th class="px-6 py-3 text-left text-sm font-semibold">Topics</th>
<th class="px-6 py-3 text-left text-sm font-semibold min-w-[200px]">Progress</th>
</tr>
</thead>
<tbody>
{#each filteredTasks as task, index}
<tr class="{index % 2 === 0 ? 'bg-black' : 'bg-gray-950'} hover:bg-gray-800">
<td class="px-6 py-2 border-b">
<a href={`/progress/${task.id}`} class="text-blue-500 hover:underline">
{data.tasks.indexOf(task) + 1}. {task.name}
</a>
</td>
<td class="px-6 py-2 border-b">
<a href={`/progress/${task.id}`} class="flex gap-2 flex-wrap">
{#each task.tasksToTopics as topic}
<div
class="absolute h-full bg-green-500"
style="width: {(task.currentPoints / task.totalPoints) * 100}%"
></div>
<span class="absolute inset-0 flex items-center justify-center text-sm">
{task.currentPoints} / {task.totalPoints} ({(
(task.currentPoints / task.totalPoints) *
100
).toFixed(1)}%)
</span>
</div>
</td>
</tr>
{/each}
</tbody>
</table>
</div>
<!-- Mobile Card View -->
<div class="md:hidden space-y-4">
{#each data.tasks as task}
<div class="bg-gray-800 rounded-lg p-2 w-full">
<a
href={`/progress/${task.id}`}
class="text-blue-500 hover:underline text-lg font-bold"
>
{task.name}
</a>
<div class="mt-1">
<div class="mb-1"></div>
<span class="font-medium">Topics:</span>
<div class="flex flex-wrap gap-1 mt-1">
{#each task.tasksToTopics as topic}
class="relative bg-blue-500 text-white rounded-full px-2 py-1 shadow font-medium hover:bg-blue-600 transition cursor-pointer"
title="Click for more details about {topic.topic.name}"
>
{topic.topic.name}
</div>
{/each}
</a>
</td>
<td class="px-6 py-2 border-b min-w-[200px]">
<div class="relative w-full h-6 bg-gray-700 rounded-full overflow-hidden">
<div
class="bg-blue-500 text-white rounded-full px-2 py-1 shadow font-medium hover:bg-blue-600 transition cursor-pointer"
title="Click for more details about {topic.topic.name}"
>
{topic.topic.name}
</div>
{/each}
</div>
</div>
<div class="w-full h-4 bg-gray-700 rounded-full relative overflow-hidden">
<div
class="absolute h-full bg-green-500"
style="width: {(task.currentPoints / task.totalPoints) * 100}%"
></div>
<span class="absolute inset-0 flex items-center justify-center text-xs">
{task.currentPoints} / {task.totalPoints} ({(
(task.currentPoints / task.totalPoints) *
100
).toFixed(1)}%)
</span>
class="absolute h-full bg-green-500"
style="width: {(task.currentPoints / task.totalPoints) * 100}%"
></div>
<span class="absolute inset-0 flex items-center justify-center text-sm">
{task.currentPoints} / {task.totalPoints} ({(
(task.currentPoints / task.totalPoints) *
100
).toFixed(1)}%)
</span>
</div>
</td>
</tr>
{/each}
</tbody>
</table>
</div>
<!-- Mobile Card View -->
<div class="md:hidden space-y-4">
{#each filteredTasks as task}
<div class="bg-gray-800 rounded-lg p-2 w-full">
<a
href={`/progress/${task.id}`}
class="text-blue-500 hover:underline text-lg font-bold"
>
{task.name}
</a>
<div class="mt-1">
<div class="mb-1"></div>
<span class="font-medium">Topics:</span>
<div class="flex flex-wrap gap-1 mt-1">
{#each task.tasksToTopics as topic}
<div
class="bg-blue-500 text-white rounded-full px-2 py-1 shadow font-medium hover:bg-blue-600 transition cursor-pointer"
title="Click for more details about {topic.topic.name}"
>
{topic.topic.name}
</div>
{/each}
</div>
</div>
{/each}
</div>
{:else}
<p class="text-gray-500">No tasks available. Click "Create" to add a new task.</p>
{/if}
<div class="w-full h-4 bg-gray-700 rounded-full relative overflow-hidden">
<div
class="absolute h-full bg-green-500"
style="width: {(task.currentPoints / task.totalPoints) * 100}%"
></div>
<span class="absolute inset-0 flex items-center justify-center text-xs">
{task.currentPoints} / {task.totalPoints} ({(
(task.currentPoints / task.totalPoints) *
100
).toFixed(1)}%)
</span>
</div>
</div>
{/each}
</div>
</div>
{:else}
<p class="text-gray-500">No tasks available. Click "Create" to add a new task.</p>
<p class="text-gray-500">
No tasks found matching your search. Try a different keyword or clear the search.
</p>
{/if}
</div>

View File

@@ -0,0 +1,20 @@
import { db } from '$lib/db';
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async () => {
const topics = await db.query.topics.findMany({
with: {
topicsToTasks: {
with: {
task: true
}
}
}
});
topics.sort((a, b) => b.topicsToTasks.length - a.topicsToTasks.length);
return {
topics
};
};

View File

@@ -0,0 +1,29 @@
<script lang="ts">
import { goto } from '$app/navigation';
import * as Table from '$lib/components/ui/table/index.js';
let { data } = $props();
</script>
<Table.Root>
<Table.Header>
<Table.Row>
<Table.Head class="w-[100px]">Name</Table.Head>
<Table.Head>Amount of Tasks</Table.Head>
<Table.Head class="text-right">Actions</Table.Head>
</Table.Row>
</Table.Header>
<Table.Body>
{#each data.topics as topic}
<Table.Row
onclick={() => goto(`/progress/topics/${topic.id}`)}
class="cursor-pointer hover:bg-muted/50"
>
<Table.Cell class="font-medium">{topic.name}</Table.Cell>
<Table.Cell>{topic.topicsToTasks.length}</Table.Cell>
<Table.Cell class="text-right"></Table.Cell>
</Table.Row>
{/each}
</Table.Body>
</Table.Root>