feat: Usable todolist app (not the prettiest yet but...)
This commit is contained in:
parent
9be964ef8f
commit
9d4019f3e4
16
rust_solid_cassandra/frontend/package-lock.json
generated
16
rust_solid_cassandra/frontend/package-lock.json
generated
@ -10,7 +10,8 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@solidjs/router": "^0.5.1",
|
"@solidjs/router": "^0.5.1",
|
||||||
"solid-js": "^1.5.1"
|
"solid-js": "^1.5.1",
|
||||||
|
"uuid": "^9.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"autoprefixer": "^10.4.13",
|
"autoprefixer": "^10.4.13",
|
||||||
@ -2136,6 +2137,14 @@
|
|||||||
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
||||||
"dev": true
|
"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": {
|
"node_modules/vite": {
|
||||||
"version": "3.2.4",
|
"version": "3.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-3.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-3.2.4.tgz",
|
||||||
@ -3605,6 +3614,11 @@
|
|||||||
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
||||||
"dev": true
|
"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": {
|
"vite": {
|
||||||
"version": "3.2.4",
|
"version": "3.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-3.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-3.2.4.tgz",
|
||||||
|
@ -19,6 +19,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@solidjs/router": "^0.5.1",
|
"@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 { 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
|
// Only load the components if we are navigating to them
|
||||||
const Home = lazy(() => import('./pages/Home'));
|
const Home = lazy(() => import('./pages/Home'));
|
||||||
const Login = lazy(() => import('./pages/Login'));
|
const Login = lazy(() => import('./pages/Login'));
|
||||||
const Test = lazy(() => import('./pages/Test'));
|
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 = () => {
|
const App: Component = () => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<Navbar />
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path={'test'} component={Test} />
|
<Route path={'test'} component={Test} />
|
||||||
<Route path={['login', 'register']} component={Login} />
|
<Route path={['login', 'register']} component={Login} />
|
||||||
@ -17,4 +51,5 @@ const App: Component = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export { store, setStore };
|
||||||
export default App;
|
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';
|
||||||
|
|
||||||
const Home: Component = () => {
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<h1 class="text-4xl">Home</h1>
|
<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 (
|
||||||
|
<>
|
||||||
|
<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 = () => {
|
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;
|
export default Login;
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { Component } from 'solid-js';
|
import { Component } from 'solid-js';
|
||||||
import RestClient from '../api/RestClient';
|
import RestClient from '../api/RestClient';
|
||||||
|
import Button from '../ui/Button';
|
||||||
|
|
||||||
const Test: Component = () => {
|
const Test: Component = () => {
|
||||||
const TEST_USER = {
|
const TEST_USER = {
|
||||||
@ -9,27 +10,18 @@ const Test: Component = () => {
|
|||||||
salt: 'MEIN_SALZ',
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<h1 class='text-4xl'>TEST</h1>
|
<h1 class='text-4xl'>TEST</h1>
|
||||||
<button class={btnPrimary} onClick={() => RestClient.GET('/')}>
|
<Button onClick={() => RestClient.GET('/')}>HOME</Button>
|
||||||
GET_HOME
|
<Button
|
||||||
</button>
|
backgroundColor='gray'
|
||||||
<button
|
|
||||||
class={btnPrimary}
|
|
||||||
onClick={() => RestClient.POST('/login', JSON.stringify(TEST_USER))}
|
onClick={() => RestClient.POST('/login', JSON.stringify(TEST_USER))}
|
||||||
>
|
>
|
||||||
POST_LOGIN
|
POST_LOGIN
|
||||||
</button>
|
</Button>
|
||||||
<button class={btnPrimary} onClick={() => RestClient.GET('/user')}>
|
<Button onClick={() => RestClient.GET('/user')}>GET_USER</Button>
|
||||||
GET_USER
|
<Button onClick={() => RestClient.GET('/todo')}>GET_TODO</Button>
|
||||||
</button>
|
|
||||||
<button class={btnPrimary} onClick={() => RestClient.GET('/todo')}>
|
|
||||||
GET_TODO
|
|
||||||
</button>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
53
rust_solid_cassandra/frontend/src/ui/Button.tsx
Normal file
53
rust_solid_cassandra/frontend/src/ui/Button.tsx
Normal file
@ -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;
|
51
rust_solid_cassandra/frontend/src/ui/Navbar.tsx
Normal file
51
rust_solid_cassandra/frontend/src/ui/Navbar.tsx
Normal file
@ -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;
|
65
rust_solid_cassandra/frontend/src/ui/Table.tsx
Normal file
65
rust_solid_cassandra/frontend/src/ui/Table.tsx
Normal file
@ -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} */
|
/** @type {import('tailwindcss').Config} */
|
||||||
module.exports = {
|
module.exports = {
|
||||||
content: ["./src/**/*.{js,jsx,ts,tsx}"],
|
content: ['./src/**/*.{js,jsx,ts,tsx}'],
|
||||||
theme: {
|
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: [],
|
plugins: [],
|
||||||
};
|
};
|
||||||
|
Loading…
x
Reference in New Issue
Block a user