Compare commits
3 Commits
16b5aba457
...
2911f05dcd
| Author | SHA1 | Date |
|---|---|---|
|
|
2911f05dcd | |
|
|
b0b6557464 | |
|
|
d7dcbb3560 |
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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>
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue