Compare commits

...

3 Commits

Author SHA1 Message Date
Patrick 2911f05dcd started impl of task ui 2025-10-22 13:54:40 +02:00
Patrick b0b6557464 added basic task endpoint 2025-10-19 17:12:04 +02:00
Patrick d7dcbb3560 Added login bypass 2025-10-16 15:07:04 +02:00
9 changed files with 397 additions and 2 deletions

View File

@ -0,0 +1,99 @@
<script lang="ts">
import { onMount } from "svelte";
let { checked = $bindable(), ...props} = $props()
let loaded = $state(false)
onMount(() => {
loaded = true
})
</script>
<div class="shell" class:loaded={loaded}>
<label>
<input type="checkbox" bind:checked/>
<svg width="10px" height="10px" viewBox="0 0 12 9">
<polyline points="1,5 4,8 11,1" fill="none"></polyline>
</svg>
</label>
</div>
<style>
label {
display: block;
width: 16px;
height: 16px;
position: relative;
top: 50%;
left: 50%;
transform: translateY(-50%) translateX(-50%);
border: 1px solid black;
border-radius: 50%;
user-select: none;
cursor: pointer;
}
label:hover {
border-color: gray;
}
label:has(>input:checked) {
background: green;
}
.loaded label:has(>input:checked) {
animation: pop 0.6s ease;
}
.loaded label::before {
content: "";
position: absolute;
inset: 0;
background: white;
border-radius: 50%;
opacity: 1;
transform: scale(0);
transition-delay: 0.2s;
}
.loaded label:has(>input:checked)::before {
transform: scale(2.2);
opacity: 0;
transition: all 0.6s ease;
}
@keyframes pop {
50% {
transform: scale(1.2);
}
}
input {
display: none;
}
svg {
position: absolute;
top: 3px;
left: 3px;
stroke: white;
stroke-width: 1.5px;
stroke-dasharray: 16px;
stroke-dashoffset: 16px;
}
.loaded svg {
transition: all 0.5s ease;
}
label:has(> input:checked) > svg {
stroke-dashoffset: 0px;
}
</style>

View File

@ -0,0 +1,117 @@
<script module>
import type { Task } from "@prisma/client"
import Checkbox from "./checkbox.svelte"
import { onMount } from "svelte";
export interface TaskComponentProps {
task: Task
}
</script>
<script lang="ts">
const { task }: TaskComponentProps = $props()
let loaded = $state(false)
onMount(() => { loaded = true })
let checked = $state(task.checked)
</script>
<div class="container">
<div class="main-task">
<div class="checkbox">
<Checkbox bind:checked={checked}/>
</div>
<div class="taskheader">
<div><h2 class="taskheader">{task.content}</h2></div>
</div>
{#if true}
<div class="taskcontent">{@html [task.content, "<br>"].join().repeat(5)}</div>
{/if}
<div class="due">
Due: <br/>
{new Date(task.created_at).toLocaleDateString()} <br/>
{new Date(task.created_at).toLocaleTimeString()}
</div>
</div>
</div>
<style>
.container {
width: var(--width, 100%);
min-height: var(--min-height, 50px);
margin: 2px;
padding: 10px;
border: 1px solid gray;
display: flex;
flex-direction: column;
justify-items: center;
justify-content: stretch;
align-content: flex-start;
}
.main-task {
width: 100%;
display: grid;
grid-template-columns: 25px auto fit-content(11ch);
grid-template-areas:
'checkboxArea headerArea dueArea';
column-gap: 10px;
align-content: stretch;
}
.main-task:has(.taskcontent) {
grid-template-areas:
'checkboxArea headerArea dueArea'
'. contentArea dueArea';
}
.checkbox {
grid-area: checkboxArea;
display: flex;
justify-content: center;
align-content: center;
}
.taskheader {
grid-area: headerArea;
width: fit-content;
transform: translateY(0.075em);
}
.taskheader h2 {
margin: 0;
}
.taskcontent {
grid-area: contentArea;
width: 100%;
text-align: left;
margin-top: 5px;
}
.due {
grid-area: dueArea;
min-width: 11ch;
}
.loaded {
transition: none;
}
.loaded.checked {
transition: all 0.3s ease;
}
</style>

View File

@ -1,3 +1,4 @@
import Bun from "bun"
import path from "node:path"
const to_absolute_path = (p: string): string => {
@ -40,6 +41,8 @@ class Config {
private _session_timeout: number = 15 * 60 * 1000
private _session_refresh_grace: number = 5 * 60 * 1000 // time until expiration
readonly bypass_login = this.is_debug && process.env.BYPASS_LOGIN == "true"
get log_dir(): string {
return this._log_dir
}

View File

@ -158,6 +158,46 @@ class UserMgmt {
}
}
const _manager = new UserMgmt()
class _LOGIN_BYPASS_Mgmt extends UserMgmt {
global_session: SessionData | null = null
constructor() {
super()
db.user.findFirst().then((user) => {
if (!user) {
return
}
this.global_session = {
token: "",
user: user,
expires: new Date(8640000000000000), // Max Time
issued: new Date()
}
})
}
async login(): Promise<SessionData | null> {
if (!this.global_session) {
const user = await db.user.findFirst()
if (user) {
this.global_session = {
token: "",
user: user,
expires: new Date(8640000000000000), // Max Time
issued: new Date()
}
}
}
return this.global_session
}
async session_login(): Promise<SessionData | null> {
return this.login()
}
}
const _manager = (Config.is_debug && Config.bypass_login) ? new _LOGIN_BYPASS_Mgmt() : new UserMgmt()
export default _manager

View File

@ -0,0 +1,24 @@
import type { Actions, PageServerLoad } from "./$types"
export const load: PageServerLoad = async ({ locals, fetch }) => {
const response = await fetch("/api/users/tasks", {
method: "GET"
})
return { tasks: await response.json() }
}
export const actions = {
create: async ({ request, fetch }) => {
const data = await request.formData()
const response = await fetch("/api/users/tasks/create", {
method: "POST",
body: data
})
return { }
}
} satisfies Actions

View File

@ -0,0 +1,42 @@
<script lang="ts">
import { enhance } from '$app/forms'
import Task from '$lib/components/task.svelte'
const { data } = $props()
</script>
<div class="container">
<h1>Tasks</h1>
<div class="task">
<form method="POST" action="?/create" use:enhance>
<input name="content" />
</form>
</div>
{#each data.tasks as task}
<Task task={task} --width="75%" />
{/each}
</div>
<style>
.container {
width: 100%;
text-align: center;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.task {
width: 75%;
margin: 5px;
padding: 5px;
border: 1px solid black;
}
</style>

View File

@ -0,0 +1,31 @@
import type { RequestHandler } from "./$types";
import { json, error } from "@sveltejs/kit"
import { Error401Cause } from "$lib/errors";
import db from "$lib/server/database"
enum StatusFilterValues {
all,
open,
done,
}
export const GET: RequestHandler = async ({ request, locals, url }) => {
const filter_param = url.searchParams.get("status")
const filter = (filter_param && filter_param in StatusFilterValues)
? StatusFilterValues[filter_param as keyof typeof StatusFilterValues]
: StatusFilterValues.all
const tasks = await db.task.findMany({
where: {
userId: {
equals: locals.user.id
}
}
})
return json(tasks)
}

View File

@ -0,0 +1,29 @@
import type { RequestHandler } from "./$types";
import { json, error } from "@sveltejs/kit"
import db from "$lib/server/database"
export const POST: RequestHandler = async ({ request, locals }) => {
const data = await request.formData()
const content = data.get("content")
if (!content || typeof content !== "string") {
return error(400, { message: "content must be specified" })
}
const task = await db.task.create({
data: {
content: content,
user: {
connect: {
id: locals.user.id
}
}
}
})
return json(task)
}

View File

@ -1,3 +1,13 @@
body {
body {
--primary-bg-color: white;
--primary-text-color: black;
width: 100%;
background-color: var(--primary-bg-color, white);
}
* {
color: var(--primary-text-color, black);
}