feat: Usable todolist app (not the prettiest yet but...)

main
phga 2 years ago
parent 9be964ef8f
commit 9d4019f3e4
Signed by: phga
GPG Key ID: 5249548AA705F019

@ -10,7 +10,8 @@
"license": "MIT",
"dependencies": {
"@solidjs/router": "^0.5.1",
"solid-js": "^1.5.1"
"solid-js": "^1.5.1",
"uuid": "^9.0.0"
},
"devDependencies": {
"autoprefixer": "^10.4.13",
@ -2136,6 +2137,14 @@
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"dev": true
},
"node_modules/uuid": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz",
"integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==",
"bin": {
"uuid": "dist/bin/uuid"
}
},
"node_modules/vite": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/vite/-/vite-3.2.4.tgz",
@ -3605,6 +3614,11 @@
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"dev": true
},
"uuid": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz",
"integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg=="
},
"vite": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/vite/-/vite-3.2.4.tgz",

@ -19,6 +19,7 @@
},
"dependencies": {
"@solidjs/router": "^0.5.1",
"solid-js": "^1.5.1"
"solid-js": "^1.5.1",
"uuid": "^9.0.0"
}
}

@ -1,13 +1,47 @@
import { Route, Routes } from '@solidjs/router';
import { Component, lazy } from 'solid-js';
import { Component, createEffect, createSignal, lazy } from 'solid-js';
import { loggedInUser } from './pages/Login';
import { createStore, Store, SetStoreFunction } from 'solid-js/store';
import Navbar from './ui/Navbar';
import { Todo } from './pages/Home';
// Only load the components if we are navigating to them
const Home = lazy(() => import('./pages/Home'));
const Login = lazy(() => import('./pages/Login'));
const Test = lazy(() => import('./pages/Test'));
export type User = {
id: string;
login: string;
};
// Helper funciton to get a global state
// https://stackoverflow.com/a/72339551
export const createGlobalStore = <T extends object>(
init: T
): [Store<T>, SetStoreFunction<T>] => {
const [state, setState] = createStore(init);
if (localStorage.globalStore) {
try {
setState(JSON.parse(localStorage.globalStore));
} catch (err) {
setState(() => init);
}
}
createEffect(() => {
localStorage.globalStore = JSON.stringify(state);
});
return [state, setState];
};
const [store, setStore] = createGlobalStore({
user: { id: '', login: '' } as User,
todos: [] as Todo[],
});
const App: Component = () => {
return (
<>
<Navbar />
<Routes>
<Route path={'test'} component={Test} />
<Route path={['login', 'register']} component={Login} />
@ -17,4 +51,5 @@ const App: Component = () => {
);
};
export { store, setStore };
export default App;

@ -1,9 +1,220 @@
import { Component } from "solid-js";
import {
Component,
createEffect,
createResource,
createSignal,
For,
onMount,
Show,
} from 'solid-js';
import RestClient from '../api/RestClient';
import { store, setStore } from '../App';
import Button from '../ui/Button';
import Table, { TableData, TableRow } from '../ui/Table';
import { v4 as uuidv4 } from 'uuid';
export type Todo = {
id: string;
user_id: string;
title: string;
priority: Priority;
status: Status;
description: string;
};
type TodoModalProps = {
todo: Todo;
};
export enum Status {
Todo = 'Todo',
Doing = 'Doing',
Done = 'Done',
}
export enum Priority {
High = 'High',
Normal = 'Normal',
Low = 'Low',
}
const createNewTodo = (): Todo => ({
id: uuidv4(),
user_id: store.user.id,
title: 'New Title',
description: 'Some description.',
status: Status.Todo,
priority: Priority.Normal,
});
const TodoModal: Component<TodoModalProps> = (props) => {
const [show, setShow] = createSignal(false);
const [todo, setTodo] = createSignal(props.todo);
const inputClass =
'form-control block w-full px-3 py-1.5 text-base font-normal text-gray-700 bg-white bg-clip-padding border border-solid border-gray-300 rounded transition ease-in-out m-0 focus:text-gray-700 focus:bg-white focus:border-blue-600 focus:outline-none';
const selectClass =
'form-select block w-full px-3 py-1.5 text-base font-normal text-gray-700 bg-white bg-clip-padding bg-no-repeat border border-solid border-gray-300 rounded transition ease-in-out m-0 focus:text-gray-700 focus:bg-white focus:border-blue-600 focus:outline-none';
const labelClass = 'form-label inline-block mb-2 text-gray-700';
return (
<>
<Button onClick={() => setShow(true)}>🖉</Button>
<Show when={show()} fallback={<></>}>
<div class='fixed top-0 left-0 w-screen h-screen flex items-center justify-center bg-sh-bgP2 bg-opacity-50 transform transition-transform duration-300'>
<div class='p-6 rounded-lg bg-white w-1/2'>
<form>
<div class='form-group mb-6'>
<label for='title' class={labelClass}>
Title
</label>
<input
type='text'
class={inputClass}
id='title'
onInput={(e) =>
setTodo((t) => ({ ...t, title: e.currentTarget.value } as Todo))
}
value={todo()?.title}
/>
</div>
<div class='form-group mb-6'>
<label for='description' class={labelClass}>
Description
</label>
<textarea
class={inputClass}
id='description'
onInput={(e) =>
setTodo((t) => ({ ...t, description: e.currentTarget.value } as Todo))
}
value={todo()?.description}
/>
</div>
<div class='form-group mb-6'>
<label for='status' class={labelClass}>
Status
</label>
<select
onChange={(e) =>
setTodo((t) => ({ ...t, status: e.currentTarget.value } as Todo))
}
class={selectClass}
>
{Object.keys(Status).map((k) => {
if (todo().status === Status[k as Status]) {
return (
<option selected value={Status[k as Status]}>
{k}
</option>
);
} else {
return <option value={Status[k as Status]}>{k}</option>;
}
})}
</select>
<div class='form-group mb-6'>
<label for='priority' class={labelClass}>
Priority
</label>
<select
onChange={(e) =>
setTodo((t) => ({ ...t, priority: e.currentTarget.value } as Todo))
}
class={selectClass}
>
{Object.keys(Priority).map((k) => {
if (todo().priority === Priority[k as Priority]) {
return (
<option selected value={Priority[k as Priority]}>
{k}
</option>
);
} else {
return <option value={Priority[k as Priority]}>{k}</option>;
}
})}
</select>
</div>
</div>
</form>
<Button
backgroundColor='green'
onClick={() =>
RestClient.PUT('/todo', JSON.stringify(todo())).then(() => {
fetchTodos();
setShow(false);
})
}
>
Save
</Button>{' '}
<Button backgroundColor='gray' onClick={() => setShow(false)}>
Close
</Button>{' '}
<Button
backgroundColor='red'
onClick={() => {
setTodo({ ...props.todo } as Todo);
setShow(false);
}}
>
Cancel
</Button>
</div>
</div>
</Show>
</>
);
};
const fetchTodos = async () => {
const todos = (await RestClient.GET('/todo')) as Todo[];
setStore({
...store,
todos,
});
};
const Home: Component = () => {
onMount(fetchTodos);
return (
<>
<h1 class="text-4xl">Home</h1>
<Show when={store.todos.length > 0} fallback={<></>}>
<Table
data={store.todos.map((todo) => ({
Title: todo.title,
Description: todo.description,
Status: todo.status,
Priority: todo.priority,
Remove: (
<Button
backgroundColor='red'
onClick={() => {
RestClient.DELETE(`/todo/${todo.id}`).then(
() => (window.location.href = '/')
);
}}
>
🗑
</Button>
),
Edit: <TodoModal todo={todo}></TodoModal>,
}))}
></Table>
</Show>
<div class='mt-10 ml-10'>
<Button
backgroundColor='green'
onClick={() =>
RestClient.PUT('/todo', JSON.stringify(createNewTodo())).then(() => {
fetchTodos();
})
}
>
New
</Button>
</div>
</>
);
};

@ -1,7 +1,123 @@
import { Component } from 'solid-js';
import { Component, createEffect, createSignal } from 'solid-js';
import RestClient from '../api/RestClient';
import { setStore, store, User } from '../App';
import Button from '../ui/Button';
type LoginRequest = {
login: string;
password: string;
};
const [loggedInUser, setLoggedInUser] = createSignal();
export { loggedInUser };
const Login: Component = () => {
return <h1 class='text-4xl'>Login</h1>;
const [loginRequest, setLoginRequest] = createSignal<LoginRequest>({
login: '',
password: '',
});
const [login, setLogin] = createSignal('');
const [password, setPassword] = createSignal('');
// Populate the current user outside the JSX (we need createEffect for this!)
createEffect(async () => {
if (loginRequest().login.trim() === '' || loginRequest().password.trim() === '') {
return;
}
const user = (await RestClient.POST(
'/login',
JSON.stringify(loginRequest())
)) as User;
if (user.id === undefined) {
console.log(user);
return;
}
setStore({
...store,
user,
});
window.location.href = '/';
});
return (
<div class='grid h-screen place-items-center'>
<div class='block p-6 rounded-lg shadow-lg bg-white max-w-sm'>
<form>
<div class='form-group mb-6'>
<label for='username' class='form-label inline-block mb-2 text-gray-700'>
Login
</label>
<input
type='text'
class='form-control
block
w-full
px-3
py-1.5
text-base
font-normal
text-gray-700
bg-white bg-clip-padding
border border-solid border-gray-300
rounded
transition
ease-in-out
m-0
focus:text-gray-700 focus:bg-white focus:border-blue-600 focus:outline-none'
id='username'
placeholder='Username'
onInput={(e) => setLogin(e.currentTarget.value)}
value={login()}
/>
</div>
<div class='form-group mb-6'>
<label for='Password' class='form-label inline-block mb-2 text-gray-700'>
Password
</label>
<input
type='password'
class='form-control block
w-full
px-3
py-1.5
text-base
font-normal
text-gray-700
bg-white bg-clip-padding
border border-solid border-gray-300
rounded
transition
ease-in-out
m-0
focus:text-gray-700 focus:bg-white focus:border-blue-600 focus:outline-none'
id='Password'
placeholder='Password'
onInput={(e) => setPassword(e.currentTarget.value)}
value={password()}
/>
</div>
<Button
fullWidth
onClick={() => {
console.log(login(), password());
setLoginRequest({ login: login(), password: password() });
}}
>
Sign in
</Button>
<p class='text-gray-800 mt-6 text-center'>
Want to track your todo{"'"}s?{' '}
<a
href='#!'
class='text-blue-600 hover:text-blue-700 focus:text-blue-700 transition duration-200 ease-in-out'
>
Register
</a>
</p>
</form>
</div>
</div>
);
};
export default Login;

@ -1,5 +1,6 @@
import { Component } from 'solid-js';
import RestClient from '../api/RestClient';
import Button from '../ui/Button';
const Test: Component = () => {
const TEST_USER = {
@ -9,27 +10,18 @@ const Test: Component = () => {
salt: 'MEIN_SALZ',
};
const btnPrimary =
'inline-block px-6 py-2.5 bg-blue-600 text-white font-medium text-xs leading-tight uppercase rounded-full shadow-md hover:bg-blue-700 hover:shadow-lg focus:bg-blue-700 focus:shadow-lg focus:outline-none focus:ring-0 active:bg-blue-800 active:shadow-lg transition duration-150 ease-in-out';
return (
<>
<h1 class='text-4xl'>TEST</h1>
<button class={btnPrimary} onClick={() => RestClient.GET('/')}>
GET_HOME
</button>
<button
class={btnPrimary}
<Button onClick={() => RestClient.GET('/')}>HOME</Button>
<Button
backgroundColor='gray'
onClick={() => RestClient.POST('/login', JSON.stringify(TEST_USER))}
>
POST_LOGIN
</button>
<button class={btnPrimary} onClick={() => RestClient.GET('/user')}>
GET_USER
</button>
<button class={btnPrimary} onClick={() => RestClient.GET('/todo')}>
GET_TODO
</button>
</Button>
<Button onClick={() => RestClient.GET('/user')}>GET_USER</Button>
<Button onClick={() => RestClient.GET('/todo')}>GET_TODO</Button>
</>
);
};

@ -0,0 +1,53 @@
import { Component, JSX } from 'solid-js';
type ButtonProps = {
backgroundColor?: string;
color?: string;
fullWidth?: boolean;
onClick?: Function;
children?: JSX.Element;
};
const backgroundColors: { [key: string]: string } = {
blue: 'bg-sh-blue hover:bg-sh-blueM1 focus:bg-sh-blueM1 active:bg-sh-blueM1',
red: 'bg-red-600 hover:bg-red-700 focus:bg-red-700 active:bg-red-800',
green: 'bg-green-600 hover:bg-green-700 focus:bg-green-700 active:bg-green-800',
yellow: 'bg-sh-yellow hover:bg-sh-yellowM1 focus:bg-sh-yellowM1 active:bg-sh-yellowM1',
magenta:
'bg-sh-magenta hover:bg-sh-magentaM1 focus:bg-sh-magentaM1 active:bg-sh-magentaM1',
gray: 'bg-gray-700 hover:bg-gray-800 focus:bg-gray-800 active:bg-gray-800',
cyan: 'bg-cyan-700 hover:bg-cyan-900 focus:bg-cyan-900 active:bg-cyan-800',
};
const colors: { [key: string]: string } = {
white: 'text-white',
black: 'text-black',
};
const Button: Component<ButtonProps> = (props) => {
const backgroundColor = backgroundColors[props.backgroundColor || 'blue'];
const color = colors[props.color || 'white'];
const fullWidth = props.fullWidth ? 'w-full' : '';
const fun =
props.onClick !== undefined
? (e: MouseEvent) => {
e.preventDefault();
(props.onClick as () => void)();
}
: (e: MouseEvent) => {
e.preventDefault();
console.log(
'Default behavior of Button component is invoked!',
'You might want to change the onClick function?'
);
};
const btnStyle = `${color} ${backgroundColor} ${fullWidth} inline-block px-6 py-2.5 font-medium text-xs leading-tight uppercase rounded-full shadow-md hover:shadow-lg focus:shadow-lg focus:outline-none focus:ring-0 active:shadow-lg transition duration-150 ease-in-out`;
return (
<button class={btnStyle} onClick={fun}>
{props.children}
</button>
);
};
export default Button;

@ -0,0 +1,51 @@
import { A } from '@solidjs/router';
import { Component, Show } from 'solid-js';
import RestClient from '../api/RestClient';
import { setStore, store, User } from '../App';
import Button from './Button';
const Navbar: Component = () => {
return (
<div class='flex justify-between items-center bg-sh-bg p-3'>
<div class='flex items-center'>
<a
href='/'
class='pl-2 text-xl font-bold no-underline text-sh-yellow hover:text-sh-yellowM1'
>
Just todo it!
</a>
</div>
<h1 class='flex -ml-20 text-xl font-bold no-underline text-sh-yellow'>
Hey {store.user.login}!
</h1>
<Show
when={store.user.login !== ''}
fallback={
<>
<Button
onClick={() => (window.location.href = '/login')}
backgroundColor='yellow'
color='black'
>
Login
</Button>
</>
}
>
<Button
onClick={() => {
RestClient.DELETE('/logout');
setStore({ todos: [], user: { id: '', login: '' } });
window.location.href = '/login';
}}
backgroundColor='magenta'
color='black'
>
Logout
</Button>
</Show>
</div>
);
};
export default Navbar;

@ -0,0 +1,65 @@
import { Component, For, JSX } from 'solid-js';
import { Priority, Status, Todo } from '../pages/Home';
type TableProps = {
data: Todo[];
};
type TableRowProps = {
children: JSX.Element;
};
type TableDataProps = {
children?: JSX.Element;
};
type TableHeadProps = {
children?: JSX.Element;
};
export const TableHead: Component<TableHeadProps> = (props) => {
return (
<th scope='col' class='text-sm font-medium px-6 py-4'>
{props.children}
</th>
);
};
export const TableData: Component<TableDataProps> = (props) => {
return <td class='text-sm text-gray-900 font-light px-6 py-4 '>{props.children}</td>;
};
export const TableRow: Component<TableRowProps> = (props) => {
const rowClass = 'border-b';
return <tr class={rowClass}>{props.children}</tr>;
};
const Table: Component<TableProps> = (props) => {
if (props.data.length < 1) {
return <></>;
}
return (
<div class='overflow-hidden mt-10 mx-10'>
<table class='min-w-full text-center'>
<thead class='border-b text-sh-bgM1 bg-sh-yellow'>
{Object.keys(props.data[0]).map((key) => (
<TableHead>{key}</TableHead>
))}
</thead>
<tbody>
<For each={props.data}>
{(child) => (
<TableRow>
{Object.values(child).map((val) => (
<TableData>{val}</TableData>
))}
</TableRow>
)}
</For>
</tbody>
</table>
</div>
);
};
export default Table;

@ -1,8 +1,48 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ["./src/**/*.{js,jsx,ts,tsx}"],
content: ['./src/**/*.{js,jsx,ts,tsx}'],
theme: {
extend: {},
extend: {
colors: {
'sh-bgM2': '#000306',
'sh-bgM1': '#020E18',
'sh-bg': '#0D1F2D',
'sh-bgP1': '#1E3141',
'sh-bgP2': '#3C5161',
'sh-fgM2': '#546A7B',
'sh-fgM1': '#9EA3B0',
'sh-fg': '#C3C9E9',
'sh-fgP1': '#BDD3DD',
'sh-white': '#FEFEFE',
'sh-black': '#000306',
'sh-yellowM1': '#DA7F05',
'sh-yellow': '#FDAA3A',
'sh-yellowP1': '#FFC16E',
'sh-orangeM1': '#C45A00',
'sh-orange': '#FF7F11',
'sh-orangeP1': '#FFA251',
'sh-redM2': '#960004',
'sh-redM1': '#CD1419',
'sh-red': '#ED474A',
'sh-redP1': '#FD7E81',
'sh-greenM1': '#739F2F',
'sh-green': '#A5CC69',
'sh-greenP1': '#D5EEAE',
'sh-greenP2': '#A2DFED',
'sh-blueM2': '#0683A0',
'sh-blueM1': '#36A1BB',
'sh-blue': '#64BFD6',
'sh-blueP1': '#A2DFED',
'sh-magentaM2': '#690635',
'sh-magentaM1': '#902B5B',
'sh-magenta': '#B95F8A',
'sh-magentaP1': '#DBA1BC',
'sh-purpleM2': '#630DAE',
'sh-purpleM1': '#863FC4',
'sh-purple': '#A86CDC',
'sh-purpleP1': '#CEA7F0',
},
},
},
plugins: [],
};

Loading…
Cancel
Save