feat(backend): add fake data generators
This commit is contained in:
parent
97c9a75ed3
commit
335a97740d
17 changed files with 3336 additions and 98 deletions
|
@ -1,6 +1,7 @@
|
|||
alembic==1.10.4
|
||||
anyio==3.6.2
|
||||
click==8.1.3
|
||||
Faker==18.9.0
|
||||
fastapi==0.95.1
|
||||
greenlet==2.0.2
|
||||
h11==0.14.0
|
||||
|
@ -9,6 +10,8 @@ Mako==1.2.4
|
|||
MarkupSafe==2.1.2
|
||||
psycopg2-binary==2.9.6
|
||||
pydantic==1.10.7
|
||||
python-dateutil==2.8.2
|
||||
six==1.16.0
|
||||
sniffio==1.3.0
|
||||
SQLAlchemy==2.0.13
|
||||
starlette==0.26.1
|
||||
|
|
|
@ -7,7 +7,7 @@ from sqlalchemy.orm import Session
|
|||
from todo.models import todos as todomodel
|
||||
from todo.models import users as usermodel
|
||||
from todo.schemas import todos as todoschema
|
||||
from todo.exceptions import NotFoundException, InvalidFilterParameterException
|
||||
from todo.utils.exceptions import NotFoundException, InvalidFilterParameterException
|
||||
|
||||
|
||||
def create_todo(db: Session, todo: todoschema.TodoItemCreate, user_id: int) -> todoschema.TodoItem:
|
||||
|
@ -56,6 +56,16 @@ def read_todos_for_user(db: Session, user_id: int, skip: int = 0, limit: int = 1
|
|||
return [todoschema.TodoItem.from_orm(db_todo) for db_todo in db_todos]
|
||||
|
||||
|
||||
def read_todos_count_for_user(db: Session, user_id: int) -> int:
|
||||
"""Returns the total number of todo-items of the user with the specified user_id."""
|
||||
|
||||
db_user = db.query(usermodel.User).filter(usermodel.User.id == user_id).first()
|
||||
if not db_user:
|
||||
raise NotFoundException(f"User with id '{user_id}' not found.")
|
||||
|
||||
return db.query(todomodel.TodoItem).filter(todomodel.TodoItem.user_id == user_id).count()
|
||||
|
||||
|
||||
def update_todo(db: Session, todo: todoschema.TodoItemUpdate, todo_id: int) -> todoschema.TodoItem:
|
||||
"""Updates the todo-item with the provided id with all non-None fields from the input todo-item."""
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ from sqlalchemy.orm import Session
|
|||
|
||||
from todo.models import users as usermodel
|
||||
from todo.schemas import users as userschema
|
||||
from todo.exceptions import NotFoundException, InvalidFilterParameterException
|
||||
from todo.utils.exceptions import NotFoundException, InvalidFilterParameterException
|
||||
|
||||
|
||||
def hash_password(password: str) -> str:
|
||||
|
@ -70,6 +70,12 @@ def read_users(db: Session, skip: int = 0, limit: int = 100) -> list[userschema.
|
|||
return [userschema.User.from_orm(db_user) for db_user in db_users]
|
||||
|
||||
|
||||
def read_users_count(db: Session) -> int:
|
||||
"""Returns the total number of users currently the database."""
|
||||
|
||||
return db.query(usermodel.User).count()
|
||||
|
||||
|
||||
def update_user(db: Session, user: userschema.UserUpdate, id: int) -> userschema.User:
|
||||
"""Updates the user with the provided id with all non-None fields from the input user."""
|
||||
|
||||
|
|
|
@ -1,19 +0,0 @@
|
|||
"""This module is a collection of project-wide exceptions."""
|
||||
|
||||
|
||||
class NotFoundException(Exception):
|
||||
"""Raised when a resource was unexpectedly not found."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class DataIntegrityException(Exception):
|
||||
"""Raised to prevent a semantically invalid database operation."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class InvalidFilterParameterException(Exception):
|
||||
"""Raised when a query filter parameter is invalid."""
|
||||
|
||||
pass
|
|
@ -6,7 +6,8 @@ from sqlalchemy.orm import Session
|
|||
from todo.database.engine import get_db
|
||||
from todo.schemas import todos as todoschema
|
||||
from todo.crud import todos as todocrud
|
||||
from todo.exceptions import NotFoundException, InvalidFilterParameterException
|
||||
from todo.utils.exceptions import NotFoundException, InvalidFilterParameterException
|
||||
from todo.utils.exceptions import create_exception_dict as fmt
|
||||
|
||||
|
||||
router = APIRouter(
|
||||
|
@ -25,7 +26,7 @@ def create_todo(todo: todoschema.TodoItemCreate, user_id: int, db: Session = Dep
|
|||
try:
|
||||
return todocrud.create_todo(db=db, todo=todo, user_id=user_id)
|
||||
except NotFoundException as e:
|
||||
raise HTTPException(404, str(e))
|
||||
raise HTTPException(404, fmt(str(e)))
|
||||
|
||||
|
||||
@router.get("/{todo_id}", response_model=todoschema.TodoItem)
|
||||
|
@ -33,7 +34,7 @@ def read_todo(todo_id: int, db: Session = Depends(get_db)):
|
|||
try:
|
||||
return todocrud.read_todo(db=db, todo_id=todo_id)
|
||||
except NotFoundException as e:
|
||||
raise HTTPException(404, str(e))
|
||||
raise HTTPException(404, fmt(str(e)))
|
||||
|
||||
|
||||
@router.get("/user/{user_id}", response_model=list[todoschema.TodoItem])
|
||||
|
@ -41,9 +42,17 @@ def read_todos(user_id: int, skip: int = 0, limit: int = 100, db: Session = Depe
|
|||
try:
|
||||
return todocrud.read_todos_for_user(db=db, user_id=user_id, skip=skip, limit=limit)
|
||||
except InvalidFilterParameterException as e:
|
||||
raise HTTPException(400, str(e))
|
||||
raise HTTPException(400, fmt(str(e)))
|
||||
except NotFoundException as e:
|
||||
raise HTTPException(404, str(e))
|
||||
raise HTTPException(404, fmt(str(e)))
|
||||
|
||||
|
||||
@router.get("/user/{user_id}/total", response_model=dict[str, int])
|
||||
def read_todos_count(user_id: int, db: Session = Depends(get_db)):
|
||||
try:
|
||||
return {"total": todocrud.read_todos_count_for_user(db=db, user_id=user_id)}
|
||||
except NotFoundException as e:
|
||||
raise HTTPException(404, fmt(str(e)))
|
||||
|
||||
|
||||
@router.patch("/{todo_id}", response_model=todoschema.TodoItem)
|
||||
|
@ -51,7 +60,7 @@ def update_todo(todo_id: int, todo: todoschema.TodoItemUpdate, db: Session = Dep
|
|||
try:
|
||||
return todocrud.update_todo(db=db, todo=todo, todo_id=todo_id)
|
||||
except NotFoundException as e:
|
||||
raise HTTPException(404, str(e))
|
||||
raise HTTPException(404, fmt(str(e)))
|
||||
|
||||
|
||||
@router.delete("/{todo_id}", response_model=todoschema.TodoItem)
|
||||
|
@ -59,4 +68,4 @@ def delete_todo(todo_id: int, db: Session = Depends(get_db)):
|
|||
try:
|
||||
return todocrud.delete_todo(db=db, todo_id=todo_id)
|
||||
except NotFoundException as e:
|
||||
raise HTTPException(404, str(e))
|
||||
raise HTTPException(404, fmt(str(e)))
|
||||
|
|
|
@ -6,7 +6,8 @@ from sqlalchemy.orm import Session
|
|||
from todo.database.engine import get_db
|
||||
from todo.schemas import users as userschema
|
||||
from todo.crud import users as usercrud
|
||||
from todo.exceptions import NotFoundException
|
||||
from todo.utils.exceptions import NotFoundException, InvalidFilterParameterException
|
||||
from todo.utils.exceptions import create_exception_dict as fmt
|
||||
|
||||
|
||||
router = APIRouter(
|
||||
|
@ -35,12 +36,20 @@ def read_user(id: int, db: Session = Depends(get_db)):
|
|||
try:
|
||||
return usercrud.read_user(db=db, id=id)
|
||||
except NotFoundException as e:
|
||||
raise HTTPException(404, str(e))
|
||||
raise HTTPException(404, fmt(str(e)))
|
||||
|
||||
|
||||
@router.get("/", response_model=list[userschema.User])
|
||||
def read_users(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)):
|
||||
return usercrud.read_users(db=db, skip=skip, limit=limit)
|
||||
try:
|
||||
return usercrud.read_users(db=db, skip=skip, limit=limit)
|
||||
except InvalidFilterParameterException as e:
|
||||
raise HTTPException(400, fmt(str(e)))
|
||||
|
||||
|
||||
@router.get("/total/", response_model=dict[str, int])
|
||||
def read_users_count(db: Session = Depends(get_db)):
|
||||
return {"total": usercrud.read_users_count(db=db)}
|
||||
|
||||
|
||||
@router.patch("/{id}", response_model=userschema.User)
|
||||
|
@ -48,7 +57,7 @@ def update_user(id: int, user: userschema.UserUpdate, db: Session = Depends(get_
|
|||
try:
|
||||
return usercrud.update_user(db=db, user=user, id=id)
|
||||
except NotFoundException as e:
|
||||
raise HTTPException(404, str(e))
|
||||
raise HTTPException(404, fmt(str(e)))
|
||||
|
||||
|
||||
@router.delete("/{id}", response_model=userschema.User)
|
||||
|
@ -56,4 +65,4 @@ def delete_user(id: int, db: Session = Depends(get_db)):
|
|||
try:
|
||||
return usercrud.delete_user(db=db, id=id)
|
||||
except NotFoundException as e:
|
||||
raise HTTPException(404, str(e))
|
||||
raise HTTPException(404, fmt(str(e)))
|
||||
|
|
0
backend/todo/utils/__init__.py
Normal file
0
backend/todo/utils/__init__.py
Normal file
40
backend/todo/utils/exceptions.py
Normal file
40
backend/todo/utils/exceptions.py
Normal file
|
@ -0,0 +1,40 @@
|
|||
"""This module is a collection of project-wide exceptions."""
|
||||
|
||||
|
||||
class NotFoundException(Exception):
|
||||
"""Raised when a resource was unexpectedly not found."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class DataIntegrityException(Exception):
|
||||
"""Raised to prevent a semantically invalid database operation."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class InvalidFilterParameterException(Exception):
|
||||
"""Raised when a query filter parameter is invalid."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
def create_exception_dict(message: str):
|
||||
"""Creates a list for returning API error messages to the client.
|
||||
|
||||
The list has the following format:
|
||||
[
|
||||
{
|
||||
"msg": "Something went wrong!.",
|
||||
},
|
||||
]
|
||||
|
||||
This is useful to return consistent API error messages.
|
||||
"""
|
||||
|
||||
if not isinstance(message, str):
|
||||
raise TypeError(f"Expected a string but got '{type(message)}'.")
|
||||
if not len(message):
|
||||
message = "An unknown error occurred."
|
||||
|
||||
return [{"msg": message}]
|
83
backend/todo/utils/fakery.py
Normal file
83
backend/todo/utils/fakery.py
Normal file
|
@ -0,0 +1,83 @@
|
|||
"""This module contains utilities for creating fake database entries."""
|
||||
|
||||
from random import randint
|
||||
|
||||
from faker import Faker
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from todo.schemas.users import UserCreate, User
|
||||
from todo.crud.users import create_user
|
||||
from todo.schemas.todos import TodoItemCreate, TodoItem
|
||||
from todo.crud.todos import create_todo
|
||||
from todo.database.engine import get_db
|
||||
|
||||
|
||||
def _get_faker() -> Faker:
|
||||
"""Creates and returns a Faker object."""
|
||||
|
||||
return Faker()
|
||||
|
||||
|
||||
def get_fake_user_details() -> UserCreate:
|
||||
"""Returns a set of fake details for creating a new user."""
|
||||
|
||||
fk = _get_faker()
|
||||
|
||||
password = fk.password(length=randint(6, 16))
|
||||
email = fk.profile(fields=['mail'])['mail']
|
||||
first_name = fk.first_name()
|
||||
last_name = fk.last_name()
|
||||
|
||||
return UserCreate(
|
||||
email=email,
|
||||
first_name=first_name,
|
||||
last_name=last_name,
|
||||
password=password,
|
||||
password_confirmation=password,
|
||||
)
|
||||
|
||||
|
||||
def get_fake_todo_details() -> TodoItemCreate:
|
||||
"""Returns a set of fake details for creating a new todo-item."""
|
||||
|
||||
fk = _get_faker()
|
||||
|
||||
title = fk.sentence(nb_words=randint(4, 6))
|
||||
|
||||
# Generate some sentences and concatenate them with a randomized delimiter
|
||||
description_sentences = [fk.sentence(nb_words=randint(4, 6)) for i in range(randint(1, 5))]
|
||||
if randint(0, 1):
|
||||
delimiter = '\n- '
|
||||
elif randint(0, 1):
|
||||
delimiter = '\n'
|
||||
else:
|
||||
delimiter = ' '
|
||||
description = delimiter.join(description_sentences)
|
||||
|
||||
return TodoItemCreate(
|
||||
title=title,
|
||||
description=description,
|
||||
)
|
||||
|
||||
|
||||
def create_fake_user(db: Session = next(get_db())) -> User:
|
||||
"""Creates a fake user and saves them to the database."""
|
||||
|
||||
return create_user(db=db, user=get_fake_user_details())
|
||||
|
||||
|
||||
def create_fake_todo(user_id: int, db: Session = next(get_db())) -> TodoItem:
|
||||
"""Creates a fake todo-item for the specified user and saves it to the database."""
|
||||
|
||||
return create_todo(db=db, todo=get_fake_todo_details(), user_id=user_id)
|
||||
|
||||
|
||||
def populate_database(num_users: int = 100, max_todos_per_user: int = 100) -> None:
|
||||
"""Creates the specified number of users, each with between 0 and max_todos_per_user todo-items."""
|
||||
|
||||
db = next(get_db())
|
||||
|
||||
for i in range(num_users):
|
||||
user = create_fake_user(db)
|
||||
for j in range(randint(0, max_todos_per_user)):
|
||||
create_fake_todo(user.id, db)
|
|
@ -88,6 +88,6 @@ services:
|
|||
ports:
|
||||
- "8001:8081"
|
||||
environment:
|
||||
DATABASE_URL: "postgres://todo:todo@todo-db:5432/todo?sslmode=disable"
|
||||
PGWEB_DATABASE_URL: "postgres://todo:todo@todo-db:5432/todo?sslmode=disable"
|
||||
|
||||
...
|
||||
|
|
2926
frontend/package-lock.json
generated
2926
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -10,13 +10,14 @@
|
|||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/adapter-auto": "^2.0.0",
|
||||
"@sveltejs/adapter-node": "^1.2.4",
|
||||
"@sveltejs/kit": "^1.5.0",
|
||||
"autoprefixer": "^10.4.14",
|
||||
"postcss": "^8.4.23",
|
||||
"sass": "^1.62.1",
|
||||
"svelte": "^3.54.0",
|
||||
"svelte-check": "^3.0.1",
|
||||
"svelte-material-ui": "^7.0.0-beta.8",
|
||||
"tailwindcss": "^3.3.2",
|
||||
"tslib": "^2.4.1",
|
||||
"typescript": "^5.0.0",
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
/* This file imports all styles defined in other files. */
|
||||
@import "./fonts.css";
|
||||
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
/* Global defaults */
|
||||
|
||||
@layer base {
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
@apply font-bold text-primary;
|
||||
|
@ -48,4 +51,42 @@
|
|||
button:focus {
|
||||
@apply ring-1 ring-neutral-400 ring-offset-1;
|
||||
}
|
||||
|
||||
table {
|
||||
@apply border-collapse;
|
||||
}
|
||||
|
||||
th:first-of-type {
|
||||
@apply rounded-tl-md;
|
||||
}
|
||||
th:last-of-type {
|
||||
@apply rounded-tr-md;
|
||||
}
|
||||
tr:last-of-type td:first-of-type {
|
||||
@apply rounded-bl-md;
|
||||
}
|
||||
tr:last-of-type td:last-of-type {
|
||||
@apply rounded-br-md;
|
||||
}
|
||||
|
||||
tr {
|
||||
@apply hover:bg-secondary-300/50;
|
||||
@apply border-y border-primary/50;
|
||||
@apply bg-secondary-100/10;
|
||||
}
|
||||
tr:first-of-type, tr:last-of-type {
|
||||
@apply border-y-0;
|
||||
}
|
||||
|
||||
th {
|
||||
@apply bg-primary font-semibold text-secondary-100;
|
||||
}
|
||||
caption, th, td {
|
||||
@apply p-1;
|
||||
}
|
||||
@media screen(sm) {
|
||||
caption, th, td {
|
||||
@apply p-2 text-start;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,27 +0,0 @@
|
|||
<script lang='ts'>
|
||||
function handleClick() {
|
||||
promise = getUsers();
|
||||
}
|
||||
|
||||
async function getUsers() {
|
||||
const res = await fetch("/api/users/");
|
||||
const json = await res.text();
|
||||
|
||||
if (res.ok) {
|
||||
return json;
|
||||
} else {
|
||||
throw new Error(json);
|
||||
}
|
||||
}
|
||||
let promise = getUsers();
|
||||
</script>
|
||||
|
||||
<h1>TODOs</h1>
|
||||
<button on:click={handleClick}>Click Me</button>
|
||||
{#await promise}
|
||||
<p>Waiting</p>
|
||||
{:then users}
|
||||
<p>{users}</p>
|
||||
{:catch error}
|
||||
<p style="color: red">{error}</p>
|
||||
{/await}
|
178
frontend/src/routes/users/+page.svelte
Normal file
178
frontend/src/routes/users/+page.svelte
Normal file
|
@ -0,0 +1,178 @@
|
|||
<script lang='ts'>
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
const errorMessages : String[] = [];
|
||||
let error = false;
|
||||
let loading = true;
|
||||
|
||||
type User = {
|
||||
id: number;
|
||||
email: string;
|
||||
first_name: string;
|
||||
last_name: string;
|
||||
created: Date;
|
||||
updated: Date;
|
||||
}
|
||||
let currentItems: User[] = [];
|
||||
let totalItemCount = 0;
|
||||
let limit = 10;
|
||||
let currentPage = 1;
|
||||
let skip = 0;
|
||||
$: totalPageCount = Math.ceil(totalItemCount / limit);
|
||||
|
||||
async function getUsersCount() {
|
||||
const response = await fetch(`/api/users/total/`);
|
||||
const responseJson = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
if (responseJson) {
|
||||
if (responseJson.hasOwnProperty('total')) {
|
||||
if (typeof responseJson.total === 'number') {
|
||||
return responseJson.total;
|
||||
}
|
||||
}
|
||||
}
|
||||
error = true;
|
||||
} else {
|
||||
// Try to parse the error messages
|
||||
if (responseJson) {
|
||||
if (responseJson.hasOwnProperty('detail')) {
|
||||
if (Array.isArray(responseJson.detail)) {
|
||||
for (let errorObj of responseJson.detail) {
|
||||
if (errorObj.hasOwnProperty('msg')) {
|
||||
if (typeof errorObj.msg === 'string') {
|
||||
errorMessages.push(errorObj.msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (errorMessages.length === 0) errorMessages.push("Error during API call.")
|
||||
error = true;
|
||||
}
|
||||
}
|
||||
|
||||
async function getUsers(skip=0, limit=100) {
|
||||
loading = true;
|
||||
|
||||
totalItemCount = await getUsersCount();
|
||||
if (error) return;
|
||||
|
||||
const response = await fetch(`/api/users/?skip=${skip}&limit=${limit}`);
|
||||
const responseJson = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
currentItems = responseJson;
|
||||
loading = false;
|
||||
return responseJson;
|
||||
} else {
|
||||
// Try to parse the error messages
|
||||
if (responseJson) {
|
||||
if (responseJson.hasOwnProperty('detail')) {
|
||||
if (Array.isArray(responseJson.detail)) {
|
||||
for (let errorObj of responseJson.detail) {
|
||||
if (errorObj.hasOwnProperty('msg')) {
|
||||
if (typeof errorObj.msg === 'string') {
|
||||
errorMessages.push(errorObj.msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
loading = false;
|
||||
error = true;
|
||||
}
|
||||
}
|
||||
|
||||
function handlePrevClick() {
|
||||
currentPage -= 1;
|
||||
skip -= limit;
|
||||
getUsers(skip, limit);
|
||||
}
|
||||
|
||||
function handleNextClick() {
|
||||
currentPage += 1;
|
||||
skip += limit;
|
||||
getUsers(skip, limit);
|
||||
}
|
||||
|
||||
function handleFirstClick() {
|
||||
currentPage = 1;
|
||||
skip = 0;
|
||||
getUsers(skip, limit);
|
||||
}
|
||||
|
||||
function handleLastClick() {
|
||||
currentPage = totalPageCount;
|
||||
skip = limit * (totalPageCount - 1);
|
||||
getUsers(skip, limit);
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
getUsers(skip, limit);
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col items-center">
|
||||
<h1 class="text-center">Users</h1>
|
||||
{#if loading}
|
||||
<div class="min-h-screen flex flex-col items-center justify-center">
|
||||
<p>Loading...</p>
|
||||
</div>
|
||||
{:else if error}
|
||||
{#if errorMessages.length}
|
||||
{#each errorMessages as errorMessage}
|
||||
<p style="color: red">{errorMessage}</p>
|
||||
{/each}
|
||||
{:else}
|
||||
<p style="color: red">An unknown error occurred.</p>
|
||||
{/if}
|
||||
{:else}
|
||||
<div>
|
||||
<table>
|
||||
<caption>List of users</caption>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Email</th>
|
||||
<th>First Name</th>
|
||||
<th>Last Name</th>
|
||||
<th>Created</th>
|
||||
<th>Updated</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each currentItems as user}
|
||||
<tr>
|
||||
<td>{user.id}</td>
|
||||
<td>{user.email}</td>
|
||||
<td>{user.first_name}</td>
|
||||
<td>{user.last_name}</td>
|
||||
<td>{new Date(user.created).toLocaleDateString()}</td>
|
||||
<td>{new Date(user.updated).toLocaleDateString()}</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<select bind:value={limit} on:change={getUsers(skip, limit)}>
|
||||
<option value="10">
|
||||
10
|
||||
</option>
|
||||
<option value="25">
|
||||
25
|
||||
</option>
|
||||
<option value="50">
|
||||
50
|
||||
</option>
|
||||
</select>
|
||||
<button on:click={handleFirstClick} disabled={currentPage === 1} class="disabled:opacity-10">first</button>
|
||||
<button on:click={handlePrevClick} disabled={currentPage === 1} class="disabled:opacity-10">prev</button>
|
||||
<button on:click={handleNextClick} disabled={currentPage === totalPageCount} class="disabled:opacity-10">next</button>
|
||||
<button on:click={handleLastClick} disabled={currentPage === totalPageCount} class="disabled:opacity-10">last</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
25
frontend/src/theme/_smui-theme.scss
Normal file
25
frontend/src/theme/_smui-theme.scss
Normal file
|
@ -0,0 +1,25 @@
|
|||
@use 'sass:color';
|
||||
|
||||
@use '@material/theme/color-palette';
|
||||
|
||||
// Svelte Colors!
|
||||
@use '@material/theme/index' as theme with (
|
||||
$primary: #ff3e00,
|
||||
$secondary: #676778,
|
||||
$surface: #fff,
|
||||
$background: #fff,
|
||||
$error: color-palette.$red-900
|
||||
);
|
||||
|
||||
html,
|
||||
body {
|
||||
background-color: theme.$surface;
|
||||
color: theme.$on-surface;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #40b3ff;
|
||||
}
|
||||
a:visited {
|
||||
color: color.scale(#40b3ff, $lightness: -35%);
|
||||
}
|
25
frontend/src/theme/dark/_smui-theme.scss
Normal file
25
frontend/src/theme/dark/_smui-theme.scss
Normal file
|
@ -0,0 +1,25 @@
|
|||
@use 'sass:color';
|
||||
|
||||
@use '@material/theme/color-palette';
|
||||
|
||||
// Svelte Colors! (Dark Theme)
|
||||
@use '@material/theme/index' as theme with (
|
||||
$primary: #ff3e00,
|
||||
$secondary: color.scale(#676778, $whiteness: -10%),
|
||||
$surface: color.adjust(color-palette.$grey-900, $blue: +4),
|
||||
$background: #000,
|
||||
$error: color-palette.$red-700
|
||||
);
|
||||
|
||||
html,
|
||||
body {
|
||||
background-color: #000;
|
||||
color: theme.$on-surface;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #40b3ff;
|
||||
}
|
||||
a:visited {
|
||||
color: color.scale(#40b3ff, $lightness: -35%);
|
||||
}
|
Loading…
Add table
Reference in a new issue