Compare commits
No commits in common. "4fbec8744aa68e1d92bdf2c61824761f5175b965" and "3c3a5b172df9b2061206f58470e6b3ad86ccb87a" have entirely different histories.
4fbec8744a
...
3c3a5b172d
22
README.md
22
README.md
|
|
@ -5,18 +5,18 @@ Sukurti react typescript aplikaciją.
|
|||
Sukurti du puslapius, kuriuos būtų galima perjungti (pasirinkimo laisvė, kaip tai įgyvendinti).
|
||||
|
||||
Pirmas puslapis:
|
||||
+ Sukurti sąrašą (TODO list'ą, shopping list'ą, t.t.). Sąrašo elementai turi turėti bent 3 reikšmes
|
||||
+ Į sarašą turi būti galima pridėti įrašus (naudojant formas, galima naudoti libus)
|
||||
+ Prieš pridedant, leisti pasirinkti, ar pridėti į sąrašo galą, ar į priekį
|
||||
+ Galimybė redaguoti sąrašo narius (taip pat naudojant formas)
|
||||
+ Ištrinti elementus
|
||||
+ Keisti elementų pozicijas (pasirinkimo laisvė, kaip tai įgyvendinti)
|
||||
* Sukurti sąrašą (TODO list'ą, shopping list'ą, t.t.). Sąrašo elementai turi turėti bent 3 reikšmes
|
||||
* Į sarašą turi būti galima pridėti įrašus (naudojant formas, galima naudoti libus)
|
||||
* Prieš pridedant, leisti pasirinkti, ar pridėti į sąrašo galą, ar į priekį
|
||||
* Galimybė redaguoti sąrašo narius (taip pat naudojant formas)
|
||||
* Ištrinti elementus
|
||||
* Keisti elementų pozicijas (pasirinkimo laisvė, kaip tai įgyvendinti)
|
||||
|
||||
Antras puslapis:
|
||||
+ Gauti duomenis iš public API (https://api.chucknorris.io/jokes/random?category=dev) ir juos parodyti ekrane
|
||||
+ Kol puslapis atidarytas, kas 15s atnaujinti duomenis
|
||||
+ Taip pat reikia pavaizduoti datą ir laiką, kada duomenys buvo paskutinį kartą gauti
|
||||
+ Išjungus puslapį, nustoti duomenų gavimą
|
||||
+ Atidarius, iš karto atnaujinti
|
||||
* Gauti duomenis iš public API (https://api.chucknorris.io/jokes/random?category=dev) ir juos parodyti ekrane
|
||||
* Kol puslapis atidarytas, kas 15s atnaujinti duomenis
|
||||
* Taip pat reikia pavaizduoti datą ir laiką, kada duomenys buvo paskutinį kartą gauti
|
||||
* Išjungus puslapį, nustoti duomenų gavimą
|
||||
* Atidarius, iš karto atnaujinti
|
||||
|
||||
Stiliai ir aplikacijos išvaizda yra laisva forma, bet dizaino libų nenaudoti (material-ui, antd ir pan.)
|
||||
|
|
|
|||
60
flake.nix
60
flake.nix
|
|
@ -13,65 +13,5 @@
|
|||
tmux
|
||||
];
|
||||
};
|
||||
|
||||
packages."${system}".default = buildNpmPackage {
|
||||
pname = "todo-list";
|
||||
version = "1.0.0";
|
||||
src = ./.;
|
||||
npmDepsHash = "sha256-CAV5SbdfW0+eOeCHPquAJioFRqaxMY7MMMkip+1y0Nc=";
|
||||
installPhase = ''
|
||||
mkdir -p $out
|
||||
cp -r dist/. $out
|
||||
'';
|
||||
};
|
||||
|
||||
nixosModules.default = { config, lib, ... }:
|
||||
let
|
||||
inherit (lib) mkEnableOption mkIf mkOption mkMerge types recursiveUpdate mkForce;
|
||||
cfg = config.services.todo-list;
|
||||
in
|
||||
{
|
||||
options.services.todo-list = {
|
||||
enable = mkEnableOption "Todo list";
|
||||
|
||||
hostname = mkOption {
|
||||
type = types.str;
|
||||
example = "todo.example.com";
|
||||
description = "The hostname to serve site on.";
|
||||
};
|
||||
|
||||
nginx = mkOption {
|
||||
type = types.submodule (
|
||||
recursiveUpdate
|
||||
(import (nixpkgs + "/nixos/modules/services/web-servers/nginx/vhost-options.nix") { inherit config lib; })
|
||||
{ }
|
||||
);
|
||||
default = { };
|
||||
example = ''
|
||||
{
|
||||
serverAliases = [
|
||||
"list.''${config.networking.domain}"
|
||||
];
|
||||
# To enable encryption and let let's encrypt take care of certificate
|
||||
forceSSL = true;
|
||||
enableACME = true;
|
||||
}
|
||||
'';
|
||||
description = ''
|
||||
With this option, you can customize the nginx virtualHost settings.
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
services.nginx.virtualHosts.${cfg.hostname} = mkMerge [
|
||||
cfg.nginx
|
||||
{
|
||||
root = mkForce self.packages."${system}".default;
|
||||
locations."/".tryFiles = "$uri $uri/ /index.html";
|
||||
}
|
||||
];
|
||||
};
|
||||
};
|
||||
}) [ "x86_64-linux" "aarch64-linux" ]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<!doctype html>
|
||||
<html lang="en" class="bg-neutral-200 dark:bg-neutral-800">
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
|
|
|
|||
|
|
@ -1,17 +1,15 @@
|
|||
import { useState } from 'react'
|
||||
|
||||
export function AccordionItem({ title, children, isOpen }: { title: string; children: JSX.Element; isOpen?: boolean }) {
|
||||
const [open, setOpen] = useState(isOpen);
|
||||
import { } from 'react'
|
||||
|
||||
export function AccordionItem({ title, children, isOpen, onClick }: { title: string; children: JSX.Element; isOpen: boolean, onClick: () => void; }) {
|
||||
return (
|
||||
<>
|
||||
<button type="button" className="accordion-title" onClick={() => setOpen(!open)}>
|
||||
<button type="button" className="accordion-title" onClick={onClick}>
|
||||
<span>{title}</span>
|
||||
<svg className={"w-3 h-3 shrink-0" + (open ? "" : " rotate-180")} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 10 6">
|
||||
<svg className={"w-3 h-3 shrink-0" + (isOpen ? "" : " rotate-180")} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 10 6">
|
||||
<path stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 5 5 1 1 5" />
|
||||
</svg>
|
||||
</button>
|
||||
{open && <div className="accordion-body">{children}</div>}
|
||||
{isOpen && <div className="accordion-body">{children}</div>}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { } from 'react'
|
|||
|
||||
export function EnumSelect({ name, entries, defaultValue }: { name: string; entries: Record<string, string>, defaultValue?: string }): JSX.Element {
|
||||
return (
|
||||
<select id={name} name={name} defaultValue={defaultValue} className="w-full">
|
||||
<select id={name} name={name} defaultValue={defaultValue}>
|
||||
{Object.entries(entries).map(([v, name]) => <option key={v} value={v}>{name}</option>)}
|
||||
</select>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -2,12 +2,26 @@ import { } from 'react'
|
|||
import { taskTypes, taskStatuses } from './consts';
|
||||
import { EnumSelect } from './enum-select';
|
||||
|
||||
export function TaskForm({ createTask }: { createTask: (t: Record<string, FormDataEntryValue>) => void }): JSX.Element {
|
||||
export function TaskForm({ tasks, setTasks, setExpanded }: { tasks: TodoTasks, setTasks: (t: TodoTasks) => void, setExpanded: (p: number) => void }): JSX.Element {
|
||||
return (
|
||||
<form className="grid grid-cols-1 gap-y-8" onSubmit={event => {
|
||||
event.preventDefault();
|
||||
createTask(Object.fromEntries(new FormData(event.currentTarget)));
|
||||
event.currentTarget.reset();
|
||||
|
||||
const data = Object.fromEntries(new FormData(event.currentTarget));
|
||||
const newTask = {
|
||||
id: tasks.reduce((max, row) => row.id > max ? row.id : max, 1) + 1,
|
||||
type: data.type as TaskType,
|
||||
title: data.title as string,
|
||||
status: data.status as TaskStatus,
|
||||
};
|
||||
|
||||
if (data.insert) {
|
||||
setTasks([newTask, ...tasks]);
|
||||
} else {
|
||||
setTasks([...tasks, newTask]);
|
||||
}
|
||||
|
||||
setExpanded(0);
|
||||
}}>
|
||||
<div>
|
||||
<label htmlFor="type">Tipas</label>
|
||||
|
|
@ -18,7 +32,7 @@ export function TaskForm({ createTask }: { createTask: (t: Record<string, FormDa
|
|||
<div>
|
||||
<label htmlFor="title">Pavadinimas</label>
|
||||
<div className="mt-2">
|
||||
<input type="text" id="title" name="title" className="w-full" />
|
||||
<input type="text" id="title" name="title" className="" />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
|
|
@ -32,6 +46,9 @@ export function TaskForm({ createTask }: { createTask: (t: Record<string, FormDa
|
|||
<label htmlFor="insert" className="ms-2">Įterpti pradžioje</label>
|
||||
</div>
|
||||
<div className="mt-2 flex items-center justify-end gap-x-6">
|
||||
<button type="button" className="font-semibold" onClick={() => setExpanded(0)}>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="bg-indigo-500 rounded-lg px-3 py-2 font-semibold text-white hover:bg-indigo-600"
|
||||
|
|
|
|||
|
|
@ -2,14 +2,11 @@ import { useState } from 'react'
|
|||
import { taskTypes, taskStatuses } from './consts';
|
||||
import { EnumSelect } from './enum-select';
|
||||
|
||||
export function TaskRow({ task, updateTask, deleteTask, swapTasks }: TaskProps & { task: TodoTask }) {
|
||||
export function TaskRow({ task, updateTask }: { task: TodoTask, updateTask: (task: TodoTask) => void }): JSX.Element {
|
||||
const [edit, setEdit] = useState(false);
|
||||
const [classnames, setClassnames] = useState<string[]>([]);
|
||||
|
||||
return (
|
||||
<form
|
||||
className={["tr", ...classnames].join(' ')}
|
||||
onSubmit={event => {
|
||||
<form className="tr" onSubmit={event => {
|
||||
event.preventDefault();
|
||||
|
||||
const data = Object.fromEntries(new FormData(event.currentTarget));
|
||||
|
|
@ -22,40 +19,7 @@ export function TaskRow({ task, updateTask, deleteTask, swapTasks }: TaskProps &
|
|||
});
|
||||
|
||||
setEdit(false)
|
||||
}}
|
||||
draggable
|
||||
onDragStart={event => {
|
||||
event.dataTransfer.setData('text/plain', task.id.toString());
|
||||
event.dataTransfer.effectAllowed = 'move';
|
||||
setClassnames(['dragged'])
|
||||
}}
|
||||
onDragOver={event => {
|
||||
event.preventDefault();
|
||||
const fromId = +event.dataTransfer.getData('text/plain');
|
||||
if (fromId !== task.id) {
|
||||
setClassnames(['dropover']);
|
||||
}
|
||||
}}
|
||||
onDragLeave={event => {
|
||||
const fromId = +event.dataTransfer.getData('text/plain');
|
||||
if (fromId !== task.id) {
|
||||
setClassnames([])
|
||||
}
|
||||
}}
|
||||
onDrop={event => {
|
||||
event.stopPropagation();
|
||||
setClassnames([])
|
||||
swapTasks(+event.dataTransfer.getData('text/plain'), task.id);
|
||||
}}
|
||||
onDragEnd={() => {
|
||||
setClassnames([])
|
||||
}}
|
||||
>
|
||||
<div className="td">
|
||||
<svg className="w-5 h-5 stroke-neutral-800 dark:stroke-neutral-200" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M21 10L3 10M21 14L3 14M12 4L12 10M12 14L12 20M15 18L12 21L9 18M15 6L12 3L9 6" strokeWidth="1" strokeLinecap="round" strokeLinejoin="round"></path>
|
||||
</svg>
|
||||
</div>
|
||||
}}>
|
||||
<div className="td">
|
||||
{edit
|
||||
? <EnumSelect entries={taskTypes} name="type" defaultValue={task.type} />
|
||||
|
|
@ -76,30 +40,23 @@ export function TaskRow({ task, updateTask, deleteTask, swapTasks }: TaskProps &
|
|||
<div className="td">
|
||||
{edit ? <>
|
||||
<button type="submit" className="mr-3">
|
||||
<svg className="w-5 h-5 fill-indigo-500" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 50 50">
|
||||
<path d="M 25 2 C 12.317 2 2 12.317 2 25 C 2 37.683 12.317 48 25 48 C 37.683 48 48 37.683 48 25 C 48 20.44 46.660281 16.189328 44.363281 12.611328 L 42.994141 14.228516 C 44.889141 17.382516 46 21.06 46 25 C 46 36.579 36.579 46 25 46 C 13.421 46 4 36.579 4 25 C 4 13.421 13.421 4 25 4 C 30.443 4 35.393906 6.0997656 39.128906 9.5097656 L 40.4375 7.9648438 C 36.3525 4.2598437 30.935 2 25 2 z M 43.236328 7.7539062 L 23.914062 30.554688 L 15.78125 22.96875 L 14.417969 24.431641 L 24.083984 33.447266 L 44.763672 9.046875 L 43.236328 7.7539062 z"></path>
|
||||
<svg className="w-4 h-4 fill-white" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 490 490">
|
||||
<polygon points="452.253,28.326 197.831,394.674 29.044,256.875 0,292.469 207.253,461.674 490,54.528 "></polygon>
|
||||
</svg>
|
||||
</button>
|
||||
<button type="button" onClick={event => {
|
||||
event.preventDefault()
|
||||
setEdit(false)
|
||||
}}>
|
||||
<svg className="w-5 h-5 fill-indigo-500" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 50 50">
|
||||
<path d="M 25 2 C 12.309534 2 2 12.309534 2 25 C 2 37.690466 12.309534 48 25 48 C 37.690466 48 48 37.690466 48 25 C 48 12.309534 37.690466 2 25 2 z M 25 4 C 36.609534 4 46 13.390466 46 25 C 46 36.609534 36.609534 46 25 46 C 13.390466 46 4 36.609534 4 25 C 4 13.390466 13.390466 4 25 4 z M 32.990234 15.986328 A 1.0001 1.0001 0 0 0 32.292969 16.292969 L 25 23.585938 L 17.707031 16.292969 A 1.0001 1.0001 0 0 0 16.990234 15.990234 A 1.0001 1.0001 0 0 0 16.292969 17.707031 L 23.585938 25 L 16.292969 32.292969 A 1.0001 1.0001 0 1 0 17.707031 33.707031 L 25 26.414062 L 32.292969 33.707031 A 1.0001 1.0001 0 1 0 33.707031 32.292969 L 26.414062 25 L 33.707031 17.707031 A 1.0001 1.0001 0 0 0 32.990234 15.986328 z"></path>
|
||||
<button type="button" onClick={() => setEdit(false)}>
|
||||
<svg className="w-4 h-4 fill-white" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 490 490">
|
||||
<polygon points="456.851,0 245,212.564 33.149,0 0.708,32.337 212.669,245.004 0.708,457.678 33.149,490 245,277.443 456.851,490 489.292,457.678 277.331,245.004 489.292,32.337 ">
|
||||
</polygon>
|
||||
</svg>
|
||||
</button>
|
||||
</> : <>
|
||||
<button type="button" className="mr-3" onClick={event => {
|
||||
<button type="button" onClick={event => {
|
||||
event.preventDefault()
|
||||
setEdit(true)
|
||||
}}>
|
||||
<svg className="w-5 h-5 fill-indigo-500" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 50 50">
|
||||
<path d="M 43.125 2 C 41.878906 2 40.636719 2.488281 39.6875 3.4375 L 38.875 4.25 L 45.75 11.125 C 45.746094 11.128906 46.5625 10.3125 46.5625 10.3125 C 48.464844 8.410156 48.460938 5.335938 46.5625 3.4375 C 45.609375 2.488281 44.371094 2 43.125 2 Z M 37.34375 6.03125 C 37.117188 6.0625 36.90625 6.175781 36.75 6.34375 L 4.3125 38.8125 C 4.183594 38.929688 4.085938 39.082031 4.03125 39.25 L 2.03125 46.75 C 1.941406 47.09375 2.042969 47.457031 2.292969 47.707031 C 2.542969 47.957031 2.90625 48.058594 3.25 47.96875 L 10.75 45.96875 C 10.917969 45.914063 11.070313 45.816406 11.1875 45.6875 L 43.65625 13.25 C 44.054688 12.863281 44.058594 12.226563 43.671875 11.828125 C 43.285156 11.429688 42.648438 11.425781 42.25 11.8125 L 9.96875 44.09375 L 5.90625 40.03125 L 38.1875 7.75 C 38.488281 7.460938 38.578125 7.011719 38.410156 6.628906 C 38.242188 6.246094 37.855469 6.007813 37.4375 6.03125 C 37.40625 6.03125 37.375 6.03125 37.34375 6.03125 Z"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<button type="button" onClick={() => deleteTask(task.id)}>
|
||||
<svg className="w-5 h-5 fill-indigo-500" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 50 50">
|
||||
<path d="M 21 2 C 19.354545 2 18 3.3545455 18 5 L 18 7 L 10.154297 7 A 1.0001 1.0001 0 0 0 9.984375 6.9863281 A 1.0001 1.0001 0 0 0 9.8398438 7 L 8 7 A 1.0001 1.0001 0 1 0 8 9 L 9 9 L 9 45 C 9 46.645455 10.354545 48 12 48 L 38 48 C 39.645455 48 41 46.645455 41 45 L 41 9 L 42 9 A 1.0001 1.0001 0 1 0 42 7 L 40.167969 7 A 1.0001 1.0001 0 0 0 39.841797 7 L 32 7 L 32 5 C 32 3.3545455 30.645455 2 29 2 L 21 2 z M 21 4 L 29 4 C 29.554545 4 30 4.4454545 30 5 L 30 7 L 20 7 L 20 5 C 20 4.4454545 20.445455 4 21 4 z M 11 9 L 18.832031 9 A 1.0001 1.0001 0 0 0 19.158203 9 L 30.832031 9 A 1.0001 1.0001 0 0 0 31.158203 9 L 39 9 L 39 45 C 39 45.554545 38.554545 46 38 46 L 12 46 C 11.445455 46 11 45.554545 11 45 L 11 9 z M 18.984375 13.986328 A 1.0001 1.0001 0 0 0 18 15 L 18 40 A 1.0001 1.0001 0 1 0 20 40 L 20 15 A 1.0001 1.0001 0 0 0 18.984375 13.986328 z M 24.984375 13.986328 A 1.0001 1.0001 0 0 0 24 15 L 24 40 A 1.0001 1.0001 0 1 0 26 40 L 26 15 A 1.0001 1.0001 0 0 0 24.984375 13.986328 z M 30.984375 13.986328 A 1.0001 1.0001 0 0 0 30 15 L 30 40 A 1.0001 1.0001 0 1 0 32 40 L 32 15 A 1.0001 1.0001 0 0 0 30.984375 13.986328 z"></path>
|
||||
<svg className="w-4 h-4 fill-white" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 30 30">
|
||||
<path d="M 22.828125 3 C 22.316375 3 21.804562 3.1954375 21.414062 3.5859375 L 19 6 L 24 11 L 26.414062 8.5859375 C 27.195062 7.8049375 27.195062 6.5388125 26.414062 5.7578125 L 24.242188 3.5859375 C 23.851688 3.1954375 23.339875 3 22.828125 3 z M 17 8 L 5.2597656 19.740234 C 5.2597656 19.740234 6.1775313 19.658 6.5195312 20 C 6.8615312 20.342 6.58 22.58 7 23 C 7.42 23.42 9.6438906 23.124359 9.9628906 23.443359 C 10.281891 23.762359 10.259766 24.740234 10.259766 24.740234 L 22 13 L 17 8 z M 4 23 L 3.0566406 25.671875 A 1 1 0 0 0 3 26 A 1 1 0 0 0 4 27 A 1 1 0 0 0 4.328125 26.943359 A 1 1 0 0 0 4.3378906 26.939453 L 4.3632812 26.931641 A 1 1 0 0 0 4.3691406 26.927734 L 7 26 L 5.5 24.5 L 4 23 z"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</>}
|
||||
|
|
|
|||
|
|
@ -1,17 +1,16 @@
|
|||
import { } from 'react'
|
||||
import { TaskRow } from './task-row';
|
||||
|
||||
export function TaskTable({ tasks, ...rest }: TaskProps & { tasks: TodoTasks }) {
|
||||
export function TaskTable({ tasks, updateTask }: { tasks: TodoTasks, updateTask: (task: TodoTask) => void }): JSX.Element {
|
||||
return (
|
||||
<div className="w-full">
|
||||
<div className="tr">
|
||||
<div className="td"></div>
|
||||
<div className="td">Tipas</div>
|
||||
<div className="td">Pavadinimas</div>
|
||||
<div className="td">Statusas</div>
|
||||
<div className="td">Veiksmai</div>
|
||||
</div>
|
||||
{tasks.map(task => <TaskRow key={task.id} task={task} {...rest} />)}
|
||||
{tasks.map(task => <TaskRow key={task.id} task={task} updateTask={updateTask} />)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { useRouteError } from "react-router-dom";
|
||||
|
||||
export function ErrorPage() {
|
||||
export default function ErrorPage() {
|
||||
const error = useRouteError() as {
|
||||
status?: number,
|
||||
statusText?: string,
|
||||
|
|
|
|||
|
|
@ -9,40 +9,36 @@
|
|||
@apply my-4;
|
||||
}
|
||||
|
||||
.nav-button {
|
||||
@apply cursor-pointer px-5 py-2 rounded-lg ring-1 ring-inset ring-gray-300 hover:ring-indigo-500 focus:ring-2 focus:ring-indigo-500 transition-shadow duration-500;
|
||||
a.nav-button {
|
||||
@apply bg-neutral-100 dark:bg-neutral-900 cursor-pointer px-5 py-2 rounded-lg border border-gray-300 hover:border-indigo-500 transition-colors duration-700;
|
||||
}
|
||||
|
||||
.tr {
|
||||
@apply flex border-b pb-3 border-slate-100 dark:border-slate-700 text-neutral-700 dark:text-neutral-100;
|
||||
@apply flex border-b pb-3 border-slate-100 dark:border-slate-700 text-slate-500 dark:text-slate-400;
|
||||
|
||||
.td {
|
||||
@apply pl-4 pt-4 content-center;
|
||||
}
|
||||
|
||||
.td:nth-child(1) {
|
||||
@apply flex-auto w-2 pl-0;
|
||||
@apply flex-auto w-32;
|
||||
}
|
||||
|
||||
.td:nth-child(2) {
|
||||
@apply flex-auto w-28;
|
||||
}
|
||||
|
||||
.td:nth-child(3) {
|
||||
@apply flex-auto w-64;
|
||||
}
|
||||
|
||||
.td:nth-child(4) {
|
||||
@apply flex-auto w-36;
|
||||
.td:nth-child(3) {
|
||||
@apply flex-auto w-32;
|
||||
}
|
||||
|
||||
.td:nth-child(5) {
|
||||
@apply flex-auto w-24 flex justify-center;
|
||||
.td:nth-child(4) {
|
||||
@apply flex-auto w-16 flex justify-center;
|
||||
}
|
||||
}
|
||||
|
||||
.tr:first-child {
|
||||
@apply dark:border-slate-600 font-bold;
|
||||
@apply dark:border-slate-600 text-slate-400 dark:text-slate-200;
|
||||
|
||||
.td {
|
||||
@apply pt-0;
|
||||
|
|
@ -50,27 +46,16 @@
|
|||
}
|
||||
|
||||
.tr:last-child {
|
||||
@apply border-b-0;
|
||||
}
|
||||
|
||||
.dropover, .dragged {
|
||||
@apply ring-2 ring-inset ring-indigo-500;
|
||||
}
|
||||
|
||||
.dragged {
|
||||
@apply border-b-transparent;
|
||||
@apply pb-0 border-b-0;
|
||||
}
|
||||
|
||||
select,
|
||||
input {
|
||||
@apply bg-white dark:bg-neutral-800 h-10 rounded-lg border-0 px-3 ring-1 ring-inset ring-neutral-200 focus:ring-2 focus:ring-indigo-500 focus:outline-none hover:ring-indigo-500 transition-shadow duration-700;
|
||||
}
|
||||
input:disabled {
|
||||
@apply bg-neutral-100 dark:bg-neutral-700 hover:ring-neutral-200 transition-none;
|
||||
@apply h-14 w-full rounded-lg border-0 px-3 py-4 ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-indigo-500 focus:outline-none hover:ring-indigo-500 transition-shadow duration-700;
|
||||
}
|
||||
|
||||
.accordion-title {
|
||||
@apply text-2xl flex items-center justify-between w-full p-5 font-medium border border-b-0 border-neutral-200 dark:focus:ring-slate-800 dark:border-slate-700 hover:bg-neutral-100 dark:hover:bg-neutral-700 gap-3;
|
||||
@apply text-2xl flex items-center justify-between w-full p-5 font-medium border border-b-0 border-slate-200 dark:focus:ring-slate-800 dark:border-slate-700 hover:bg-slate-100 dark:hover:bg-slate-800 gap-3;
|
||||
}
|
||||
|
||||
.accordion-title:first-child {
|
||||
|
|
|
|||
|
|
@ -5,10 +5,9 @@ import {
|
|||
RouterProvider,
|
||||
} from "react-router-dom";
|
||||
import './index.css'
|
||||
import { List } from './routes/list.tsx'
|
||||
import { Fetch } from './routes/fetch.tsx'
|
||||
import { Root } from './routes/root.tsx'
|
||||
import { ErrorPage } from './error-page.tsx';
|
||||
import List from './routes/list.tsx'
|
||||
import Root from './routes/root.tsx'
|
||||
import ErrorPage from './error-page.tsx';
|
||||
|
||||
const router = createBrowserRouter([
|
||||
{
|
||||
|
|
@ -22,7 +21,7 @@ const router = createBrowserRouter([
|
|||
},
|
||||
{
|
||||
path: "/fetch",
|
||||
element: <Fetch />,
|
||||
element: <div/>,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,86 +0,0 @@
|
|||
import { useState, useEffect, useRef, useCallback } from 'react'
|
||||
|
||||
const URL = 'https://api.chucknorris.io/jokes/random?category=dev';
|
||||
|
||||
export function Fetch() {
|
||||
// const [seconds, setSeconds] = useState(15);
|
||||
const [last, setLast] = useState(0);
|
||||
const [now, setNow] = useState(0);
|
||||
const [response, setResponse] = useState<undefined | {
|
||||
icon_url: string;
|
||||
value: string;
|
||||
}>(undefined);
|
||||
|
||||
|
||||
const fetchRef = useRef(0);
|
||||
const countdownRef = useRef(0);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const fetchData = async () => {
|
||||
setLast(Date.now());
|
||||
setResponse(undefined);
|
||||
|
||||
try {
|
||||
const response = await fetch(URL);
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setResponse(data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
};
|
||||
|
||||
const stopFetching = () => {
|
||||
clearInterval(fetchRef.current);
|
||||
clearInterval(countdownRef.current);
|
||||
if (inputRef.current) {
|
||||
inputRef.current.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
const startFetching = useCallback(() => {
|
||||
if (inputRef.current) {
|
||||
stopFetching();
|
||||
inputRef.current.disabled = true;
|
||||
fetchRef.current = setInterval(fetchData, 1000 * Number(inputRef.current.value));
|
||||
countdownRef.current = setInterval(() => setNow(Date.now()), 1000);
|
||||
setNow(Date.now());
|
||||
fetchData();
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
startFetching();
|
||||
return stopFetching;
|
||||
}, [startFetching])
|
||||
|
||||
const localizedLast = new Intl.DateTimeFormat('lt-LT', {
|
||||
dateStyle: 'short',
|
||||
timeStyle: 'long',
|
||||
}).format(last);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button className="nav-button mr-5" onClick={stopFetching}>
|
||||
Sustabdyti
|
||||
</button>
|
||||
<button className="nav-button mr-5" onClick={startFetching}>
|
||||
Paleisti
|
||||
</button>
|
||||
gauti kas <input ref={inputRef} className="w-12" defaultValue="15" /> sek. Iki sekančio liko {inputRef.current ? Number(inputRef.current.value) - Math.floor((now - last) / 1000) : 0} sek.
|
||||
<p>Paskutinis gavimas: {localizedLast}</p>
|
||||
<div className="flex">
|
||||
{
|
||||
response ? <>
|
||||
<div className="flex-auto w-1/12 mr-3"><img src={response.icon_url} /></div>
|
||||
<div className="flex-auto w-11/12">{response.value}</div>
|
||||
</> : <svg aria-hidden="true" className="w-8 h-8 text-gray-200 animate-spin dark:text-gray-600 fill-blue-600" viewBox="0 0 100 101" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z" fill="currentColor" />
|
||||
<path d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z" fill="currentFill" />
|
||||
</svg>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -3,7 +3,7 @@ import { AccordionItem } from '../components/accordion-item';
|
|||
import { TaskTable } from '../components/task-table';
|
||||
import { TaskForm } from '../components/task-form';
|
||||
|
||||
export function List () {
|
||||
function App() {
|
||||
const [tasks, setTasks] = useState<TodoTasks>([
|
||||
{
|
||||
id: 1,
|
||||
|
|
@ -25,53 +25,23 @@ export function List () {
|
|||
},
|
||||
]);
|
||||
|
||||
const createTask = (data: Record<string, FormDataEntryValue>) => {
|
||||
const task = {
|
||||
id: tasks.reduce((max, row) => row.id > max ? row.id : max, 1) + 1,
|
||||
type: data.type as TaskType,
|
||||
title: data.title as string,
|
||||
status: data.status as TaskStatus,
|
||||
};
|
||||
const [expanded, setExpanded] = useState(0);
|
||||
|
||||
setTasks(data.insert ? [task, ...tasks] : [...tasks, task]);
|
||||
|
||||
scroll(0, 0);
|
||||
}
|
||||
|
||||
const updateTask = (task: TodoTask) => {
|
||||
const index = tasks.findIndex(t => t.id == task.id);
|
||||
setTasks([...tasks.slice(0, index), task, ...tasks.slice(index + 1)]);
|
||||
};
|
||||
|
||||
const deleteTask = (id: number) => {
|
||||
setTasks(tasks.filter(t => t.id != id));
|
||||
};
|
||||
|
||||
const swapTasks = (fromId: number, toId: number) => {
|
||||
const from = tasks.findIndex(t => t.id == fromId);
|
||||
const to = tasks.findIndex(t => t.id == toId);
|
||||
const length = tasks.length;
|
||||
const task = tasks[from]
|
||||
|
||||
if (from === to || from > length || to > length) {
|
||||
return;
|
||||
}
|
||||
|
||||
setTasks(tasks.flatMap((item, index) => {
|
||||
if (index === from) return [];
|
||||
if (index === to) return from < to ? [item, task] : [task, item];
|
||||
return item;
|
||||
}));
|
||||
};
|
||||
const handleAccordion = (panel: number) => setExpanded(expanded == panel ? -1 : panel);
|
||||
|
||||
return (
|
||||
<>
|
||||
<AccordionItem title='Užduočių sąrašas' isOpen>
|
||||
<TaskTable {...{ tasks, updateTask, deleteTask, swapTasks }} />
|
||||
<AccordionItem title='Užduočių sąrašas' isOpen={expanded == 0} onClick={() => handleAccordion(0)}>
|
||||
<TaskTable tasks={tasks} updateTask={updated => {
|
||||
const index = tasks.findIndex(t => t.id == updated.id);
|
||||
setTasks([...tasks.slice(0, index), updated, ...tasks.slice(index + 1)]);
|
||||
}} />
|
||||
</AccordionItem>
|
||||
<AccordionItem title='Nauja užduotis'>
|
||||
<TaskForm {...{ createTask }} />
|
||||
<AccordionItem title='Nauja užduotis' isOpen={expanded == 1} onClick={() => handleAccordion(1)}>
|
||||
<TaskForm tasks={tasks} setTasks={setTasks} setExpanded={setExpanded} />
|
||||
</AccordionItem>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default App
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
import { Outlet, Link } from "react-router-dom";
|
||||
|
||||
export function Root() {
|
||||
export default function Root() {
|
||||
return (
|
||||
<>
|
||||
<nav className="mx-auto flex items-center justify-center gap-x-5 py-6 px-8">
|
||||
<Link to={`/list`} className="nav-button">Sąrašas</Link>
|
||||
<Link to={`/fetch`} className="nav-button">Gavimas</Link>
|
||||
<Link to={`/fetch`} className="nav-button">Duomenys</Link>
|
||||
</nav>
|
||||
<main className="my-10">
|
||||
<Outlet />
|
||||
|
|
|
|||
|
|
@ -2,16 +2,12 @@
|
|||
|
||||
type TaskType = 'task' | 'bug';
|
||||
type TaskStatus = 'paused' | 'in progress' | 'testing' | 'released';
|
||||
interface TodoTask {
|
||||
class TodoTask {
|
||||
id: number;
|
||||
type: TaskType;
|
||||
title: string;
|
||||
status: TaskStatus;
|
||||
}
|
||||
|
||||
type TodoTasks = TodoTask[];
|
||||
|
||||
interface TaskProps {
|
||||
updateTask: (task: TodoTask) => void;
|
||||
deleteTask: (id: number) => void;
|
||||
swapTasks: (from: number, to: number) => void;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue