Compare commits
10 Commits
16186baf7f
...
fd06713555
| Author | SHA1 | Date | |
|---|---|---|---|
| fd06713555 | |||
| e8c8779536 | |||
| 1d53921a73 | |||
| f28c0f56ad | |||
| c55c266f44 | |||
|
|
a27ec15124 | ||
|
|
e77509b909 | ||
| 1a2c7b5f55 | |||
| 3a1877375d | |||
| fe3329fc37 |
@@ -8,16 +8,16 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- "5432:5432"
|
- "5432:5432"
|
||||||
volumes:
|
volumes:
|
||||||
- db-data:/var/lib/postgresql/data
|
- db-data:/var/lib/postgresql
|
||||||
|
|
||||||
sveltekit:
|
sveltekit:
|
||||||
image: oven/bun
|
image: oven/bun
|
||||||
volumes:
|
volumes:
|
||||||
- ./packages/web:/app
|
- ./packages/web:/app
|
||||||
working_dir: /app
|
working_dir: /app
|
||||||
command: bun run dev --host
|
command: sh -c "bun install & bun run dev --host"
|
||||||
ports:
|
ports:
|
||||||
- "80:5173"
|
- "3000:5173"
|
||||||
env_file: .env
|
env_file: .env
|
||||||
develop:
|
develop:
|
||||||
watch:
|
watch:
|
||||||
|
|||||||
Binary file not shown.
@@ -1,14 +1,16 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://shadcn-svelte.com/schema.json",
|
"$schema": "https://shadcn-svelte.com/schema.json",
|
||||||
"style": "default",
|
|
||||||
"tailwind": {
|
"tailwind": {
|
||||||
"config": "tailwind.config.js",
|
|
||||||
"css": "src/app.css",
|
"css": "src/app.css",
|
||||||
"baseColor": "slate"
|
"baseColor": "slate"
|
||||||
},
|
},
|
||||||
"aliases": {
|
"aliases": {
|
||||||
"components": "$lib/components",
|
"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"
|
||||||
}
|
}
|
||||||
@@ -21,12 +21,38 @@ export default [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
files: ['**/*.svelte'],
|
files: ['**/*.svelte'],
|
||||||
|
|
||||||
|
rules: {
|
||||||
|
'@typescript-eslint/explicit-function-return-type': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
allowExpressions: false,
|
||||||
|
allowTypedFunctionExpressions: true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
languageOptions: {
|
languageOptions: {
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
parser: ts.parser
|
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/']
|
ignores: ['build/', '.svelte-kit/', 'dist/']
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
"@types/eslint": "9.6.1",
|
"@types/eslint": "9.6.1",
|
||||||
"@types/pg": "^8.11.10",
|
"@types/pg": "^8.11.10",
|
||||||
"autoprefixer": "^10.4.20",
|
"autoprefixer": "^10.4.20",
|
||||||
"bits-ui": "^0.22.0",
|
"bits-ui": "^1.4.7",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk-sv": "^0.0.18",
|
"cmdk-sv": "^0.0.18",
|
||||||
"drizzle-kit": "^0.30.1",
|
"drizzle-kit": "^0.30.1",
|
||||||
|
|||||||
28
packages/web/src/lib/components/ui/table/index.ts
Normal file
28
packages/web/src/lib/components/ui/table/index.ts
Normal 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,
|
||||||
|
};
|
||||||
16
packages/web/src/lib/components/ui/table/table-body.svelte
Normal file
16
packages/web/src/lib/components/ui/table/table-body.svelte
Normal 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>
|
||||||
@@ -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>
|
||||||
20
packages/web/src/lib/components/ui/table/table-cell.svelte
Normal file
20
packages/web/src/lib/components/ui/table/table-cell.svelte
Normal 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>
|
||||||
16
packages/web/src/lib/components/ui/table/table-footer.svelte
Normal file
16
packages/web/src/lib/components/ui/table/table-footer.svelte
Normal 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>
|
||||||
23
packages/web/src/lib/components/ui/table/table-head.svelte
Normal file
23
packages/web/src/lib/components/ui/table/table-head.svelte
Normal 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>
|
||||||
16
packages/web/src/lib/components/ui/table/table-header.svelte
Normal file
16
packages/web/src/lib/components/ui/table/table-header.svelte
Normal 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>
|
||||||
23
packages/web/src/lib/components/ui/table/table-row.svelte
Normal file
23
packages/web/src/lib/components/ui/table/table-row.svelte
Normal 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>
|
||||||
18
packages/web/src/lib/components/ui/table/table.svelte
Normal file
18
packages/web/src/lib/components/ui/table/table.svelte
Normal 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>
|
||||||
@@ -2,7 +2,7 @@ import { db } from '$lib/db';
|
|||||||
import { tasks } from '$lib/db/schema.js';
|
import { tasks } from '$lib/db/schema.js';
|
||||||
import { desc } from 'drizzle-orm';
|
import { desc } from 'drizzle-orm';
|
||||||
|
|
||||||
export const load = async ({ cookies }) => {
|
export const load = async () => {
|
||||||
return {
|
return {
|
||||||
tasks: await db.query.tasks.findMany({
|
tasks: await db.query.tasks.findMany({
|
||||||
with: {
|
with: {
|
||||||
|
|||||||
@@ -1,5 +1,16 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
let { data } = $props();
|
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>
|
</script>
|
||||||
|
|
||||||
<div class="flex flex-col items-center p-4">
|
<div class="flex flex-col items-center p-4">
|
||||||
@@ -8,6 +19,13 @@
|
|||||||
<h1 class="font-bold text-2xl">
|
<h1 class="font-bold text-2xl">
|
||||||
Tasks ({data.tasks.length})
|
Tasks ({data.tasks.length})
|
||||||
</h1>
|
</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}
|
{#if data.editable}
|
||||||
<a href="/progress/create">
|
<a href="/progress/create">
|
||||||
<button class="px-4 py-2 bg-green-500 text-white rounded hover:bg-green-600 transition">
|
<button class="px-4 py-2 bg-green-500 text-white rounded hover:bg-green-600 transition">
|
||||||
@@ -16,6 +34,15 @@
|
|||||||
</a>
|
</a>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</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(
|
Total progress: {data.tasks.reduce((acc, task) => acc + task.currentPoints, 0)} / {data.tasks.reduce(
|
||||||
(acc, task) => acc + task.totalPoints,
|
(acc, task) => acc + task.totalPoints,
|
||||||
0
|
0
|
||||||
@@ -26,9 +53,8 @@
|
|||||||
).toFixed(3)}%)
|
).toFixed(3)}%)
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if data.tasks.length > 0}
|
{#if filteredTasks.length > 0}
|
||||||
<div class="overflow-x-auto w-full">
|
<div class="overflow-x-auto w-full">
|
||||||
{#if data.tasks.length > 0}
|
|
||||||
<!-- Desktop Table View -->
|
<!-- Desktop Table View -->
|
||||||
<div class="hidden md:block">
|
<div class="hidden md:block">
|
||||||
<table class="min-w-full">
|
<table class="min-w-full">
|
||||||
@@ -40,11 +66,11 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{#each data.tasks as task, index}
|
{#each filteredTasks as task, index}
|
||||||
<tr class="{index % 2 === 0 ? 'bg-black' : 'bg-gray-950'} hover:bg-gray-800">
|
<tr class="{index % 2 === 0 ? 'bg-black' : 'bg-gray-950'} hover:bg-gray-800">
|
||||||
<td class="px-6 py-2 border-b">
|
<td class="px-6 py-2 border-b">
|
||||||
<a href={`/progress/${task.id}`} class="text-blue-500 hover:underline">
|
<a href={`/progress/${task.id}`} class="text-blue-500 hover:underline">
|
||||||
{task.name}
|
{data.tasks.indexOf(task) + 1}. {task.name}
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-2 border-b">
|
<td class="px-6 py-2 border-b">
|
||||||
@@ -81,7 +107,7 @@
|
|||||||
|
|
||||||
<!-- Mobile Card View -->
|
<!-- Mobile Card View -->
|
||||||
<div class="md:hidden space-y-4">
|
<div class="md:hidden space-y-4">
|
||||||
{#each data.tasks as task}
|
{#each filteredTasks as task}
|
||||||
<div class="bg-gray-800 rounded-lg p-2 w-full">
|
<div class="bg-gray-800 rounded-lg p-2 w-full">
|
||||||
<a
|
<a
|
||||||
href={`/progress/${task.id}`}
|
href={`/progress/${task.id}`}
|
||||||
@@ -118,11 +144,10 @@
|
|||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
|
||||||
<p class="text-gray-500">No tasks available. Click "Create" to add a new task.</p>
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{: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}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
20
packages/web/src/routes/progress/topics/+page.server.ts
Normal file
20
packages/web/src/routes/progress/topics/+page.server.ts
Normal 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
|
||||||
|
};
|
||||||
|
};
|
||||||
29
packages/web/src/routes/progress/topics/+page.svelte
Normal file
29
packages/web/src/routes/progress/topics/+page.svelte
Normal 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>
|
||||||
Reference in New Issue
Block a user