Compare commits
5 Commits
feature/im
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
264caa2686 | ||
|
|
aa6a313262 | ||
|
|
081812d678 | ||
|
|
dd1d01ba1e | ||
|
|
1535d450f3 |
19
Dockerfile
Executable file
19
Dockerfile
Executable file
@@ -0,0 +1,19 @@
|
|||||||
|
FROM node:22
|
||||||
|
|
||||||
|
# Set working directory inside container
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy package files first (better layer caching)
|
||||||
|
COPY package*.json ./
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
RUN npm install
|
||||||
|
|
||||||
|
# Copy the rest of the project
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Expose Vite dev server port
|
||||||
|
EXPOSE 5173
|
||||||
|
|
||||||
|
# Default command
|
||||||
|
CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0"]
|
||||||
34
README.md
Normal file → Executable file
34
README.md
Normal file → Executable file
@@ -0,0 +1,34 @@
|
|||||||
|
## BOOKMETER PLUS
|
||||||
|
|
||||||
|
|
||||||
|
For a start let get the docker container going
|
||||||
|
```bash
|
||||||
|
docker compose up -d --remove-orphans
|
||||||
|
```
|
||||||
|
|
||||||
|
### Vite
|
||||||
|
Then for development purposes execute:
|
||||||
|
```bash
|
||||||
|
bin/dev
|
||||||
|
```
|
||||||
|
This command will start server on port 5173. http://localhost:5173
|
||||||
|
|
||||||
|
|
||||||
|
### Tailwind
|
||||||
|
If we want to rebuild tailwind styles then lets start this command in another terminal window:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bin/tailwindbuild
|
||||||
|
```
|
||||||
|
|
||||||
|
To download bookmeter json files there is a PHP script
|
||||||
|
|
||||||
|
```bash
|
||||||
|
php download_posts.ph
|
||||||
|
```
|
||||||
|
|
||||||
|
To compile production version execute this command:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bin/build
|
||||||
|
```
|
||||||
|
|||||||
2
bin/build
Executable file
2
bin/build
Executable file
@@ -0,0 +1,2 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
docker compose run --rm node npm run build
|
||||||
2
bin/dev
Executable file
2
bin/dev
Executable file
@@ -0,0 +1,2 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
docker compose run --rm node npm run dev -- --host 0.0.0.0
|
||||||
2
bin/tailwindbuild
Executable file
2
bin/tailwindbuild
Executable file
@@ -0,0 +1,2 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
docker compose run --rm node npm run tailwind:build
|
||||||
10
docker-compose.yml
Executable file
10
docker-compose.yml
Executable file
@@ -0,0 +1,10 @@
|
|||||||
|
services:
|
||||||
|
node:
|
||||||
|
build: .
|
||||||
|
working_dir: /app
|
||||||
|
volumes:
|
||||||
|
- .:/app
|
||||||
|
- /app/node_modules
|
||||||
|
ports:
|
||||||
|
- "5173:5173"
|
||||||
|
command: npm run dev -- --host 0.0.0.0
|
||||||
26
download_posts.php
Normal file → Executable file
26
download_posts.php
Normal file → Executable file
@@ -1,9 +1,29 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
if (!file_exists('public/posts_2023.json')) {
|
if (!file_exists('public/posts_2023.json')) {
|
||||||
file_put_contents('public/posts_2023.json', file_get_contents('https://bookmeter.xyz/api/posts?edition=2023'));
|
file_put_contents('public/posts_2023.json', download_posts('https://bookmeter.xyz/api/posts?edition=2023'));
|
||||||
}
|
}
|
||||||
if (!file_exists('public/posts_2024.json')) {
|
if (!file_exists('public/posts_2024.json')) {
|
||||||
file_put_contents('public/posts_2024.json', file_get_contents('https://bookmeter.xyz/api/posts?edition=2024'));
|
file_put_contents('public/posts_2024.json', download_posts('https://bookmeter.xyz/api/posts?edition=2024'));
|
||||||
|
}
|
||||||
|
if (!file_exists('public/posts_2025.json')) {
|
||||||
|
file_put_contents('public/posts_2025.json', download_posts('https://bookmeter.xyz/api/posts?edition=2025'));
|
||||||
|
}
|
||||||
|
file_put_contents('public/posts_2026.json', download_posts('https://bookmeter.xyz/api/posts?edition=2026'));
|
||||||
|
|
||||||
|
if (is_dir('dist')) {
|
||||||
|
copy('public/posts_2026.json', 'dist/posts_2026.json');
|
||||||
|
copy('public/posts_2025.json', 'dist/posts_2025.json');
|
||||||
|
copy('public/posts_2024.json', 'dist/posts_2024.json');
|
||||||
|
copy('public/posts_2023.json', 'dist/posts_2023.json');
|
||||||
|
}
|
||||||
|
function download_posts(string $url): false|string
|
||||||
|
{
|
||||||
|
$posts = file_get_contents($url);
|
||||||
|
$posts = json_decode($posts, true);
|
||||||
|
array_walk($posts, function (&$item) {
|
||||||
|
unset($item['image']);
|
||||||
|
});
|
||||||
|
|
||||||
|
return json_encode($posts);
|
||||||
}
|
}
|
||||||
file_put_contents('public/posts_2025.json', file_get_contents('https://bookmeter.xyz/api/posts?edition=2025'));
|
|
||||||
243
index.html
Normal file → Executable file
243
index.html
Normal file → Executable file
@@ -1,42 +1,85 @@
|
|||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html lang="pl">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
|
<script>
|
||||||
|
(() => {
|
||||||
|
const theme = localStorage.getItem('theme');
|
||||||
|
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||||
|
|
||||||
|
if (theme === 'dark' || (!theme && prefersDark)) {
|
||||||
|
document.documentElement.classList.add('dark');
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
<style>
|
||||||
|
html {
|
||||||
|
background-color: #030712;
|
||||||
|
transition: background-color 0.7s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
html:not(.dark) {
|
||||||
|
background-color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
[x-cloak] {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>bookmeterplus Project</title>
|
<title>bookmeterplus Project</title>
|
||||||
<style>
|
|
||||||
[x-cloak] {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
</head>
|
||||||
<body class="dark:text-gray-300 dark:bg-stone-950">
|
<body class="dark:text-gray-300 dark:bg-gray-950">
|
||||||
|
|
||||||
|
|
||||||
<div x-data="bookmeterList">
|
<div x-data="bookmeterList">
|
||||||
|
<!-- <div class="p-10">-->
|
||||||
|
<!-- <div class="chart-container" style="position: relative; height:30vh; width:80vw">-->
|
||||||
|
<!-- <canvas id="statsChart" height="100px"></canvas>-->
|
||||||
|
<!-- </div>-->
|
||||||
|
<!-- <div class="chart-container" style="position: relative; height:50vh; width:80vw">-->
|
||||||
|
<!-- <canvas id="chartPoints" height="100px"></canvas>-->
|
||||||
|
<!-- </div>-->
|
||||||
|
<!-- </div>-->
|
||||||
|
<!-- <button @click="updatechartnew()" class="text-red-500">click me</button>-->
|
||||||
<div x-cloak>
|
<div x-cloak>
|
||||||
<form class="flex items-center m-2" x-show="!advancedSearch">
|
<form class="flex items-center m-2" x-show="!advancedSearch">
|
||||||
<div class="relative w-full d-block max-w-md ">
|
<div class="relative w-full d-block max-w-md ">
|
||||||
<div class="absolute inset-y-0 start-0 flex items-center ps-3 pointer-events-none">
|
<div class="absolute inset-y-0 start-0 flex items-center ps-3 pointer-events-none">
|
||||||
<svg class="w-4 h-4 text-gray-500" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 21 21">
|
<svg class="w-4 h-4 text-gray-500" aria-hidden="true" xmlns="http://www.w3.org/2000/svg"
|
||||||
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m19 19-4-4m0-7A7 7 0 1 1 1 8a7 7 0 0 1 14 0Z"/>
|
viewBox="0 0 21 21">
|
||||||
|
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
|
d="m19 19-4-4m0-7A7 7 0 1 1 1 8a7 7 0 0 1 14 0Z"/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<input type="text" class="peer bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:outline focus:outline-sky-500 block w-full ps-10 p-2.5 pr-8 dark:bg-stone-900 dark:border-stone-700 dark:text-gray-100" x-model="nameFilter" placeholder="wyszukiwarka"/>
|
<input type="text"
|
||||||
<button x-show="nameFilter.length" @click="nameFilter=''" type="button" class="absolute inset-y-0 end-0 flex items-center pe-3 invisible peer-valid:visible">
|
class="peer bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:outline focus:outline-sky-500 block w-full ps-10 p-2.5 pr-8 dark:bg-gray-900 dark:border-stone-700 dark:text-gray-100"
|
||||||
<svg class="w-4 h-4 text-gray-500 " xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
|
x-model="nameFilter" placeholder="wyszukiwarka"/>
|
||||||
|
<button x-show="nameFilter.length" @click="nameFilter=''" type="button"
|
||||||
|
class="absolute inset-y-0 end-0 flex items-center pe-3 invisible peer-valid:visible">
|
||||||
|
<svg class="w-4 h-4 text-gray-500 " xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"
|
||||||
|
stroke-width="2" stroke="currentColor">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12"/>
|
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12"/>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-block max-w-xs mr-2">
|
<div class="d-block max-w-xs mr-2">
|
||||||
<label for="basic-year-2025" class="p-2.5 inline-block" >
|
<label for="basic-year-2026" class="p-2.5 inline-block">
|
||||||
<input @input.debounce="updateFilter" x-model="year" type="checkbox" id="basic-year-2025" value="2025" /> 2025
|
<input @input.debounce="updateFilter" x-model="year" type="checkbox" id="basic-year-2026"
|
||||||
|
value="2026"/> 2026
|
||||||
</label>
|
</label>
|
||||||
<label for="year-2024" class="p-2.5 inline-block" >
|
<label for="basic-year-2025" class="p-2.5 inline-block">
|
||||||
<input @input.debounce="updateFilter" x-model="year" type="checkbox" id="basic-year-2024" value="2024" /> 2024
|
<input @input.debounce="updateFilter" x-model="year" type="checkbox" id="basic-year-2025"
|
||||||
|
value="2025"/> 2025
|
||||||
</label>
|
</label>
|
||||||
<label for="year-2023" class="p-2.5 inline-block" >
|
<label for="year-2024" class="p-2.5 inline-block">
|
||||||
<input @input.debounce="updateFilter" x-model="year" type="checkbox" id="basic-year-2023" value="2023" /> 2023
|
<input @input.debounce="updateFilter" x-model="year" type="checkbox" id="basic-year-2024"
|
||||||
|
value="2024"/> 2024
|
||||||
|
</label>
|
||||||
|
<label for="year-2023" class="p-2.5 inline-block">
|
||||||
|
<input @input.debounce="updateFilter" x-model="year" type="checkbox" id="basic-year-2023"
|
||||||
|
value="2023"/> 2023
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="relative inline-block m-4 text-blue-400 inline cursor-pointer">
|
<div class="relative inline-block m-4 text-blue-400 inline cursor-pointer">
|
||||||
@@ -45,46 +88,63 @@
|
|||||||
</form>
|
</form>
|
||||||
<div x-show="advancedSearch" class="flex m-4">
|
<div x-show="advancedSearch" class="flex m-4">
|
||||||
<div class="relative w-full d-block max-w-xs mr-2">
|
<div class="relative w-full d-block max-w-xs mr-2">
|
||||||
<input @input.debounce="updateFilter" x-model="username" placeholder="użytkownik" type="text" class="peer mr-2 max-w-xs bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:outline focus:outline-sky-500 block w-full p-2.5 px-4 dark:bg-stone-900 dark:border-stone-700 dark:text-gray-100" />
|
<input @input.debounce="updateFilter" x-model.debounce.250ms="username" placeholder="użytkownik"
|
||||||
<button x-show="username.length" @click="username=''" type="button" class="absolute inset-y-0 end-0 flex items-center pe-3 invisible peer-valid:visible">
|
type="text"
|
||||||
<svg class="w-4 h-4 text-gray-500 " xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
|
class="peer mr-2 max-w-xs bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:outline focus:outline-sky-500 block w-full p-2.5 px-4 dark:bg-gray-900 dark:border-stone-700 dark:text-gray-100"/>
|
||||||
|
<button x-show="username.length" @click="username=''" type="button"
|
||||||
|
class="absolute inset-y-0 end-0 flex items-center pe-3 invisible peer-valid:visible">
|
||||||
|
<svg class="w-4 h-4 text-gray-500 " xmlns="http://www.w3.org/2000/svg" fill="none"
|
||||||
|
viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12"/>
|
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12"/>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="relative w-full d-block max-w-xs mr-2">
|
<div class="relative w-full d-block max-w-xs mr-2">
|
||||||
<input @input.debounce="updateFilter" x-model="title" placeholder="tytuł" type="text" class="peer mr-2 max-w-xs bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:outline focus:outline-sky-500 block w-full p-2.5 px-4 dark:bg-stone-900 dark:border-stone-700 dark:text-gray-100" />
|
<input @input.debounce="updateFilter" x-model.debounce.250ms="title" placeholder="tytuł" type="text"
|
||||||
<button x-show="title.length" @click="title=''" type="button" class="absolute inset-y-0 end-0 flex items-center pe-3 invisible peer-valid:visible">
|
class="peer mr-2 max-w-xs bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:outline focus:outline-sky-500 block w-full p-2.5 px-4 dark:bg-gray-900 dark:border-stone-700 dark:text-gray-100"/>
|
||||||
<svg class="w-4 h-4 text-gray-500 " xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
|
<button x-show="title.length" @click="title=''" type="button"
|
||||||
|
class="absolute inset-y-0 end-0 flex items-center pe-3 invisible peer-valid:visible">
|
||||||
|
<svg class="w-4 h-4 text-gray-500 " xmlns="http://www.w3.org/2000/svg" fill="none"
|
||||||
|
viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12"/>
|
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12"/>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="relative w-full d-block max-w-xs mr-2">
|
<div class="relative w-full d-block max-w-xs mr-2">
|
||||||
<input @input.debounce="updateFilter" x-model="author" placeholder="autor" type="text" class="peer mr-2 max-w-xs bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:outline focus:outline-sky-500 block w-full p-2.5 px-4 dark:bg-stone-900 dark:border-stone-700 dark:text-gray-100" />
|
<input @input.debounce="updateFilter" x-model.debounce.250ms="author" placeholder="autor" type="text"
|
||||||
<button x-show="author.length" @click="author=''" type="button" class="absolute inset-y-0 end-0 flex items-center pe-3 invisible peer-valid:visible">
|
class="peer mr-2 max-w-xs bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:outline focus:outline-sky-500 block w-full p-2.5 px-4 dark:bg-gray-900 dark:border-stone-700 dark:text-gray-100"/>
|
||||||
<svg class="w-4 h-4 text-gray-500 " xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
|
<button x-show="author.length" @click="author=''" type="button"
|
||||||
|
class="absolute inset-y-0 end-0 flex items-center pe-3 invisible peer-valid:visible">
|
||||||
|
<svg class="w-4 h-4 text-gray-500 " xmlns="http://www.w3.org/2000/svg" fill="none"
|
||||||
|
viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12"/>
|
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12"/>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="relative w-full d-block max-w-xs mr-2">
|
<div class="relative w-full d-block max-w-xs mr-2">
|
||||||
<input @input.debounce="updateFilter" x-model="publisher" placeholder="wydawca" type="text" class="peer mr-2 max-w-xs bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:outline focus:outline-sky-500 block w-full p-2.5 px-4 dark:bg-stone-900 dark:border-stone-700 dark:text-gray-100" />
|
<input @input.debounce="updateFilter" x-model.debounce.250ms="publisher" placeholder="wydawca"
|
||||||
<button x-show="publisher.length" @click="publisher=''" type="button" class="absolute inset-y-0 end-0 flex items-center pe-3 invisible peer-valid:visible">
|
type="text"
|
||||||
<svg class="w-4 h-4 text-gray-500 " xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
|
class="peer mr-2 max-w-xs bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:outline focus:outline-sky-500 block w-full p-2.5 px-4 dark:bg-gray-900 dark:border-stone-700 dark:text-gray-100"/>
|
||||||
|
<button x-show="publisher.length" @click="publisher=''" type="button"
|
||||||
|
class="absolute inset-y-0 end-0 flex items-center pe-3 invisible peer-valid:visible">
|
||||||
|
<svg class="w-4 h-4 text-gray-500 " xmlns="http://www.w3.org/2000/svg" fill="none"
|
||||||
|
viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12"/>
|
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12"/>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-block max-w-xs mr-2">
|
<div class="d-block max-w-xs mr-2">
|
||||||
<label for="year-2025" class="p-2.5 inline-block" >
|
<label for="year-2026" class="p-2.5 inline-block">
|
||||||
<input @input.debounce="updateFilter" x-model="year" type="checkbox" id="year-2025" value="2025" /> 2025
|
<input @input.debounce="updateFilter" x-model="year" type="checkbox" id="year-2026" value="2026"/> 2026
|
||||||
</label>
|
</label>
|
||||||
<label for="year-2024" class="p-2.5 inline-block" >
|
<label for="year-2025" class="p-2.5 inline-block">
|
||||||
<input @input.debounce="updateFilter" x-model="year" type="checkbox" id="year-2024" value="2024" /> 2024
|
<input @input.debounce="updateFilter" x-model="year" type="checkbox" id="year-2025" value="2025"/> 2025
|
||||||
</label>
|
</label>
|
||||||
<label for="year-2023" class="p-2.5 inline-block" >
|
<label for="year-2024" class="p-2.5 inline-block">
|
||||||
<input @input.debounce="updateFilter" x-model="year" type="checkbox" id="year-2023" value="2023" /> 2023
|
<input @input.debounce="updateFilter" x-model="year" type="checkbox" id="year-2024" value="2024"/> 2024
|
||||||
|
</label>
|
||||||
|
<label for="year-2023" class="p-2.5 inline-block">
|
||||||
|
<input @input.debounce="updateFilter" x-model="year" type="checkbox" id="year-2023" value="2023"/> 2023
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -94,45 +154,103 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<template x-ref="sortIcons">
|
||||||
|
<span>
|
||||||
|
<svg x-show="orderColumn[1] === -1 && orderColumn[0] === columnName" xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-5">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="m19.5 8.25-7.5 7.5-7.5-7.5"/>
|
||||||
|
</svg>
|
||||||
|
<svg x-show="orderColumn[1] === 1 && orderColumn[0] === columnName" xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-5">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="m4.5 15.75 7.5-7.5 7.5 7.5"/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
<table class="table table-fixed w-full text-sm">
|
<table class="table table-fixed w-full text-sm">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th class="text-right pl-1 w-[4rem]">#</th>
|
<th class="text-right pl-1 w-[4rem]">#</th>
|
||||||
<th class="text-left pl-1 cursor-pointer" @click="orderBy('username')">Uzytkownik</th>
|
<th class="text-left pl-1 cursor-pointer" @click="orderBy('username')">
|
||||||
<th class="text-left pl-1 cursor-pointer" @click="orderBy('title')">Tytuł</th>
|
<div class="flex" x-data="{ columnName: 'username' }">
|
||||||
<th class="text-left pl-1 cursor-pointer" @click="orderBy('author')">Autor</th>
|
<span>Użytkownik</span>
|
||||||
<th class="text-left pl-1 cursor-pointer" @click="orderBy('publisher')">Wydawca</th>
|
<div x-html="$refs.sortIcons.innerHTML"></div>
|
||||||
<th class="text-right pr-4 w-[7rem] cursor-pointer" @click="orderBy('pages')">Stron</th>
|
</div>
|
||||||
<th class="text-left pl-1 w-[12rem] cursor-pointer" @click="orderBy('format')">Format</th>
|
</th>
|
||||||
<th class="text-right w-[5rem] cursor-pointer" @click="orderBy('rating')">Ocena</th>
|
<th class="text-left pl-1 cursor-pointer" @click="orderBy('title')">
|
||||||
<th class="text-right pr-4 w-[5rem] cursor-pointer" @click="orderBy('likes')">⚡</th>
|
<div class="flex" x-data="{ columnName: 'title' }">
|
||||||
|
<span>Tytuł</span>
|
||||||
|
<div x-html="$refs.sortIcons.innerHTML"></div>
|
||||||
|
</div>
|
||||||
|
</th>
|
||||||
|
<th class="text-left pl-1 cursor-pointer" @click="orderBy('author')">
|
||||||
|
<div class="flex" x-data="{ columnName: 'author' }">
|
||||||
|
<span>Autor</span>
|
||||||
|
<div x-html="$refs.sortIcons.innerHTML"></div>
|
||||||
|
</div>
|
||||||
|
</th>
|
||||||
|
<th class="text-left pl-1 cursor-pointer" @click="orderBy('publisher')">
|
||||||
|
<div class="flex" x-data="{ columnName: 'publisher' }">
|
||||||
|
<span>Wydawca</span>
|
||||||
|
<div x-html="$refs.sortIcons.innerHTML"></div>
|
||||||
|
</div>
|
||||||
|
</th>
|
||||||
|
<th class="text-right pr-4 w-[7rem] cursor-pointer" @click="orderBy('pages')">
|
||||||
|
<div class="flex" x-data="{ columnName: 'pages' }">
|
||||||
|
<span>Stron</span>
|
||||||
|
<div x-html="$refs.sortIcons.innerHTML"></div>
|
||||||
|
</div>
|
||||||
|
</th>
|
||||||
|
<th class="text-left pl-1 w-[12rem] cursor-pointer" @click="orderBy('format')">
|
||||||
|
<div class="flex" x-data="{ columnName: 'format' }">
|
||||||
|
<span>Format</span>
|
||||||
|
<div x-html="$refs.sortIcons.innerHTML"></div>
|
||||||
|
</div>
|
||||||
|
</th>
|
||||||
|
<th class="text-right w-[5rem] cursor-pointer" @click="orderBy('rating')">
|
||||||
|
<div class="flex" x-data="{ columnName: 'rating' }">
|
||||||
|
<span>Ocena</span>
|
||||||
|
<div x-html="$refs.sortIcons.innerHTML"></div>
|
||||||
|
</div>
|
||||||
|
</th>
|
||||||
|
<th class="text-right pr-4 w-[5rem] cursor-pointer" @click="orderBy('likes')">
|
||||||
|
<div class="flex" x-data="{ columnName: 'likes' }">
|
||||||
|
<span>⚡</span>
|
||||||
|
<div x-html="$refs.sortIcons.innerHTML"></div>
|
||||||
|
</div>
|
||||||
|
</th>
|
||||||
<th class="text-left pl-1 w-[5rem] cursor-pointer">Link</th>
|
<th class="text-left pl-1 w-[5rem] cursor-pointer">Link</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<template x-for="(item, key) in filteredItems" :key="item.uuid">
|
<template x-for="(item, key) in filteredItems" :key="item.uuid">
|
||||||
<tr class="border-b border-slate-100 dark:border-stone-900 m-3 w-auto hover:bg-gray-100 dark:hover:bg-stone-900">
|
<tr class="border-b border-slate-100 dark:border-stone-900 m-3 w-auto hover:bg-gray-100 dark:hover:bg-gray-900">
|
||||||
<td class="pl-1 text-right" x-text="filteredItems.length-key"></td>
|
<td class="pl-1 text-right" x-text="filteredItems.length-key"></td>
|
||||||
<td class="pl-1 w-3 py-2">
|
<td class="pl-1 w-3 py-2">
|
||||||
<a @click="advancedSearch ? username=item.username : nameFilter='@'+item.username" class="cursor-pointer" x-text="item.username"></a>
|
<a @click="advancedSearch ? username=item.username : nameFilter='@'+item.username"
|
||||||
|
class="cursor-pointer" x-text="item.username"></a>
|
||||||
</td>
|
</td>
|
||||||
<td class="pl-1 w-12">
|
<td class="pl-1 w-12">
|
||||||
<a @click="advancedSearch ? title=item.book.title : nameFilter=item.book.title" class="cursor-pointer" x-text="item.book.title"></a>
|
<a @click="advancedSearch ? title=item.book.title : nameFilter=item.book.title"
|
||||||
|
class="cursor-pointer" x-text="item.book.title"></a>
|
||||||
</td>
|
</td>
|
||||||
<td class="pl-1 w-8">
|
<td class="pl-1 w-8">
|
||||||
<a @click="advancedSearch ? author=item.book.author : nameFilter=item.book.author" class="cursor-pointer" x-text="item.book.author"></a>
|
<a @click="advancedSearch ? author=item.book.author : nameFilter=item.book.author"
|
||||||
|
class="cursor-pointer" x-text="item.book.author"></a>
|
||||||
</td>
|
</td>
|
||||||
<td class="pl-1 w-8">
|
<td class="pl-1 w-8">
|
||||||
<a @click="advancedSearch ? publisher=item.book.publisher : nameFilter=item.book.publisher" class="cursor-pointer" x-text="item.book.publisher"></a>
|
<a @click="advancedSearch ? publisher=item.book.publisher : nameFilter=item.book.publisher"
|
||||||
|
class="cursor-pointer" x-text="item.book.publisher"></a>
|
||||||
|
</td>
|
||||||
|
<td class="pr-4 text-right w-[5rem] w-[5rem]"><a :href="item.book.pages" x-text="item.book.pages"></a>
|
||||||
</td>
|
</td>
|
||||||
<td class="pr-4 text-right w-[5rem] w-[5rem]"><a :href="item.book.pages" x-text="item.book.pages"></a></td>
|
|
||||||
<td class="pl-1"><a :href="item.book.format" x-text="item.book.format"></a></td>
|
<td class="pl-1"><a :href="item.book.format" x-text="item.book.format"></a></td>
|
||||||
<td class="text-right"><a :href="item.book.rating" x-text="item.book.rating"></a></td>
|
<td class="text-right"><a :href="item.book.rating" x-text="item.book.rating"></a></td>
|
||||||
<td class="pr-4 text-right" x-text="item.likes"></td>
|
<td class="pr-4 text-right" x-text="item.likes"></td>
|
||||||
<td class="pl-1 py-2">
|
<td class="pl-1 py-2">
|
||||||
<a :href="'https://www.hejto.pl/wpis/'+item.slug" target="_blank">
|
<a :href="'https://www.hejto.pl/wpis/'+item.slug" target="_blank">
|
||||||
link
|
link
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-4 h-4 text-gray-500 inline">
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2"
|
||||||
|
stroke="currentColor" class="w-4 h-4 text-gray-500 inline">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round"
|
<path stroke-linecap="round" stroke-linejoin="round"
|
||||||
d="M13.5 6H5.25A2.25 2.25 0 0 0 3 8.25v10.5A2.25 2.25 0 0 0 5.25 21h10.5A2.25 2.25 0 0 0 18 18.75V10.5m-10.5 6L21 3m0 0h-5.25M21 3v5.25"/>
|
d="M13.5 6H5.25A2.25 2.25 0 0 0 3 8.25v10.5A2.25 2.25 0 0 0 5.25 21h10.5A2.25 2.25 0 0 0 18 18.75V10.5m-10.5 6L21 3m0 0h-5.25M21 3v5.25"/>
|
||||||
</svg>
|
</svg>
|
||||||
@@ -150,7 +268,10 @@
|
|||||||
</td>
|
</td>
|
||||||
<td class="align-top">
|
<td class="align-top">
|
||||||
<template x-for="(item, index) in stats().formats">
|
<template x-for="(item, index) in stats().formats">
|
||||||
<p x-text="index+': '+item"></p>
|
<div class="flex">
|
||||||
|
<p x-text="index+':'"></p>
|
||||||
|
<p x-text="item" class="flex-1 text-right"></p>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</td>
|
</td>
|
||||||
<td class="align-top">
|
<td class="align-top">
|
||||||
@@ -165,6 +286,22 @@
|
|||||||
</tfoot>
|
</tfoot>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
<div x-data="darkmodetoggle" x-cloak="" class="flex m-5 border-t py-4 px-4 dark:bg-gray-800 dark:border-t-gray-500">
|
||||||
|
<div class="cursor-pointer text-yellow-500 px-4 dark:text-yellow-200" @click="mode('light')" title="Light mode">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="size-6">
|
||||||
|
<path d="M12 2.25a.75.75 0 0 1 .75.75v2.25a.75.75 0 0 1-1.5 0V3a.75.75 0 0 1 .75-.75ZM7.5 12a4.5 4.5 0 1 1 9 0 4.5 4.5 0 0 1-9 0ZM18.894 6.166a.75.75 0 0 0-1.06-1.06l-1.591 1.59a.75.75 0 1 0 1.06 1.061l1.591-1.59ZM21.75 12a.75.75 0 0 1-.75.75h-2.25a.75.75 0 0 1 0-1.5H21a.75.75 0 0 1 .75.75ZM17.834 18.894a.75.75 0 0 0 1.06-1.06l-1.59-1.591a.75.75 0 1 0-1.061 1.06l1.59 1.591ZM12 18a.75.75 0 0 1 .75.75V21a.75.75 0 0 1-1.5 0v-2.25A.75.75 0 0 1 12 18ZM7.758 17.303a.75.75 0 0 0-1.061-1.06l-1.591 1.59a.75.75 0 0 0 1.06 1.061l1.591-1.59ZM6 12a.75.75 0 0 1-.75.75H3a.75.75 0 0 1 0-1.5h2.25A.75.75 0 0 1 6 12ZM6.697 7.757a.75.75 0 0 0 1.06-1.06l-1.59-1.591a.75.75 0 0 0-1.061 1.06l1.59 1.591Z"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="cursor-pointer text-yellow-500 px-4 dark:text-yellow-200" @click="mode('dark')" title="Dark mode">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="size-6">
|
||||||
|
<path fill-rule="evenodd"
|
||||||
|
d="M9.528 1.718a.75.75 0 0 1 .162.819A8.97 8.97 0 0 0 9 6a9 9 0 0 0 9 9 8.97 8.97 0 0 0 3.463-.69.75.75 0 0 1 .981.98 10.503 10.503 0 0 1-9.694 6.46c-5.799 0-10.5-4.7-10.5-10.5 0-4.368 2.667-8.112 6.46-9.694a.75.75 0 0 1 .818.162Z"
|
||||||
|
clip-rule="evenodd"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!--<script type="module" src="/src/chart.js"></script>-->
|
||||||
<script type="module" src="/src/main.js"></script>
|
<script type="module" src="/src/main.js"></script>
|
||||||
|
<script type="module" src="/src/darkmodetoggle.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
848
package-lock.json
generated
Normal file → Executable file
848
package-lock.json
generated
Normal file → Executable file
File diff suppressed because it is too large
Load Diff
6
package.json
Normal file → Executable file
6
package.json
Normal file → Executable file
@@ -18,13 +18,13 @@
|
|||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@alpinejs/persist": "^3.14.9",
|
"@alpinejs/persist": "^3.14.9",
|
||||||
"@tailwindcss/cli": "^4.1.11",
|
"alpinejs": "^3.14.9",
|
||||||
"alpinejs": "^3.14.9"
|
"chart.js": "^4.5.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"autoprefixer": "^10.4.21",
|
"autoprefixer": "^10.4.21",
|
||||||
"postcss": "^8.5.6",
|
"postcss": "^8.5.6",
|
||||||
"tailwindcss": "^3.4.17",
|
"tailwindcss": "^3.4.17",
|
||||||
"vite": "^7.0.5"
|
"vite": "7.1.5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
34
src/chart.js
Executable file
34
src/chart.js
Executable file
@@ -0,0 +1,34 @@
|
|||||||
|
import Chart from 'chart.js/auto'
|
||||||
|
|
||||||
|
const data = [
|
||||||
|
{year: 2009, count: 10},
|
||||||
|
{year: 2010, count: NaN},
|
||||||
|
{year: 2011, count: 20},
|
||||||
|
{year: 2012, count: 15},
|
||||||
|
{year: 2013, count: 25},
|
||||||
|
{year: 2014, count: 22},
|
||||||
|
{year: 2015, count: 30},
|
||||||
|
{year: 2016, count: 28},
|
||||||
|
];
|
||||||
|
|
||||||
|
// linechart.data.labels.push(label);
|
||||||
|
// linechart.data.datasets.forEach((dataset) => {
|
||||||
|
// dataset.data.push(newData);
|
||||||
|
// });
|
||||||
|
// linechart.update();
|
||||||
|
|
||||||
|
window.linechart = new Chart(
|
||||||
|
document.getElementById('acquisitions'),
|
||||||
|
{
|
||||||
|
type: 'bar',
|
||||||
|
data: {
|
||||||
|
labels: data.map(row => row.year),
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
label: 'Acquisitions by year',
|
||||||
|
data: data.map(row => row.count)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
6
src/darkmodetoggle.js
Executable file
6
src/darkmodetoggle.js
Executable file
@@ -0,0 +1,6 @@
|
|||||||
|
export default () => ({
|
||||||
|
mode(modeName) {
|
||||||
|
localStorage.theme = modeName;
|
||||||
|
document.documentElement.classList.toggle("dark", modeName==='dark');
|
||||||
|
}
|
||||||
|
});
|
||||||
120
src/generated.css
Normal file → Executable file
120
src/generated.css
Normal file → Executable file
@@ -595,6 +595,10 @@ video {
|
|||||||
margin: 1rem;
|
margin: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.m-5 {
|
||||||
|
margin: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
.mr-2 {
|
.mr-2 {
|
||||||
margin-right: 0.5rem;
|
margin-right: 0.5rem;
|
||||||
}
|
}
|
||||||
@@ -619,6 +623,16 @@ video {
|
|||||||
display: table;
|
display: table;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.size-5 {
|
||||||
|
width: 1.25rem;
|
||||||
|
height: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.size-6 {
|
||||||
|
width: 1.5rem;
|
||||||
|
height: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
.h-4 {
|
.h-4 {
|
||||||
height: 1rem;
|
height: 1rem;
|
||||||
}
|
}
|
||||||
@@ -671,6 +685,10 @@ video {
|
|||||||
max-width: 20rem;
|
max-width: 20rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.flex-1 {
|
||||||
|
flex: 1 1 0%;
|
||||||
|
}
|
||||||
|
|
||||||
.table-fixed {
|
.table-fixed {
|
||||||
table-layout: fixed;
|
table-layout: fixed;
|
||||||
}
|
}
|
||||||
@@ -695,6 +713,10 @@ video {
|
|||||||
border-bottom-width: 1px;
|
border-bottom-width: 1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.border-t {
|
||||||
|
border-top-width: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
.border-gray-300 {
|
.border-gray-300 {
|
||||||
--tw-border-opacity: 1;
|
--tw-border-opacity: 1;
|
||||||
border-color: rgb(209 213 219 / var(--tw-border-opacity, 1));
|
border-color: rgb(209 213 219 / var(--tw-border-opacity, 1));
|
||||||
@@ -710,6 +732,10 @@ video {
|
|||||||
background-color: rgb(249 250 251 / var(--tw-bg-opacity, 1));
|
background-color: rgb(249 250 251 / var(--tw-bg-opacity, 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.p-10 {
|
||||||
|
padding: 2.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
.p-2\.5 {
|
.p-2\.5 {
|
||||||
padding: 0.625rem;
|
padding: 0.625rem;
|
||||||
}
|
}
|
||||||
@@ -724,6 +750,11 @@ video {
|
|||||||
padding-bottom: 0.5rem;
|
padding-bottom: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.py-4 {
|
||||||
|
padding-top: 1rem;
|
||||||
|
padding-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
.pe-3 {
|
.pe-3 {
|
||||||
padding-inline-end: 0.75rem;
|
padding-inline-end: 0.75rem;
|
||||||
}
|
}
|
||||||
@@ -780,10 +811,28 @@ video {
|
|||||||
color: rgb(17 24 39 / var(--tw-text-opacity, 1));
|
color: rgb(17 24 39 / var(--tw-text-opacity, 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.text-red-500 {
|
||||||
|
--tw-text-opacity: 1;
|
||||||
|
color: rgb(239 68 68 / var(--tw-text-opacity, 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-yellow-500 {
|
||||||
|
--tw-text-opacity: 1;
|
||||||
|
color: rgb(234 179 8 / var(--tw-text-opacity, 1));
|
||||||
|
}
|
||||||
|
|
||||||
.filter {
|
.filter {
|
||||||
filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow);
|
filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.transition {
|
||||||
|
transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter;
|
||||||
|
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
transition-duration: 150ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
@custom-variant dark (&:where([data-theme=dark], [data-theme=dark] *));
|
||||||
|
|
||||||
[x-cloak] {
|
[x-cloak] {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
@@ -805,39 +854,52 @@ video {
|
|||||||
visibility: visible;
|
visibility: visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
.dark\:border-stone-700:is(.dark *) {
|
||||||
.dark\:border-stone-700 {
|
--tw-border-opacity: 1;
|
||||||
--tw-border-opacity: 1;
|
border-color: rgb(68 64 60 / var(--tw-border-opacity, 1));
|
||||||
border-color: rgb(68 64 60 / var(--tw-border-opacity, 1));
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.dark\:border-stone-900 {
|
.dark\:border-stone-900:is(.dark *) {
|
||||||
--tw-border-opacity: 1;
|
--tw-border-opacity: 1;
|
||||||
border-color: rgb(28 25 23 / var(--tw-border-opacity, 1));
|
border-color: rgb(28 25 23 / var(--tw-border-opacity, 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark\:bg-stone-900 {
|
.dark\:border-t-gray-500:is(.dark *) {
|
||||||
--tw-bg-opacity: 1;
|
--tw-border-opacity: 1;
|
||||||
background-color: rgb(28 25 23 / var(--tw-bg-opacity, 1));
|
border-top-color: rgb(107 114 128 / var(--tw-border-opacity, 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark\:bg-stone-950 {
|
.dark\:bg-gray-800:is(.dark *) {
|
||||||
--tw-bg-opacity: 1;
|
--tw-bg-opacity: 1;
|
||||||
background-color: rgb(12 10 9 / var(--tw-bg-opacity, 1));
|
background-color: rgb(31 41 55 / var(--tw-bg-opacity, 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark\:text-gray-100 {
|
.dark\:bg-gray-900:is(.dark *) {
|
||||||
--tw-text-opacity: 1;
|
--tw-bg-opacity: 1;
|
||||||
color: rgb(243 244 246 / var(--tw-text-opacity, 1));
|
background-color: rgb(17 24 39 / var(--tw-bg-opacity, 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark\:text-gray-300 {
|
.dark\:bg-gray-950:is(.dark *) {
|
||||||
--tw-text-opacity: 1;
|
--tw-bg-opacity: 1;
|
||||||
color: rgb(209 213 219 / var(--tw-text-opacity, 1));
|
background-color: rgb(3 7 18 / var(--tw-bg-opacity, 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark\:hover\:bg-stone-900:hover {
|
.dark\:text-gray-100:is(.dark *) {
|
||||||
--tw-bg-opacity: 1;
|
--tw-text-opacity: 1;
|
||||||
background-color: rgb(28 25 23 / var(--tw-bg-opacity, 1));
|
color: rgb(243 244 246 / var(--tw-text-opacity, 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dark\:text-gray-300:is(.dark *) {
|
||||||
|
--tw-text-opacity: 1;
|
||||||
|
color: rgb(209 213 219 / var(--tw-text-opacity, 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark\:text-yellow-200:is(.dark *) {
|
||||||
|
--tw-text-opacity: 1;
|
||||||
|
color: rgb(254 240 138 / var(--tw-text-opacity, 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark\:hover\:bg-gray-900:hover:is(.dark *) {
|
||||||
|
--tw-bg-opacity: 1;
|
||||||
|
background-color: rgb(17 24 39 / var(--tw-bg-opacity, 1));
|
||||||
}
|
}
|
||||||
209
src/main.js
Normal file → Executable file
209
src/main.js
Normal file → Executable file
@@ -2,12 +2,17 @@ import './generated.css'
|
|||||||
|
|
||||||
import Alpine from 'alpinejs'
|
import Alpine from 'alpinejs'
|
||||||
import persist from '@alpinejs/persist'
|
import persist from '@alpinejs/persist'
|
||||||
|
import Chart from 'chart.js/auto'
|
||||||
|
import darkmodetoggle from './darkmodetoggle';
|
||||||
|
|
||||||
window.Alpine = Alpine;
|
window.Alpine = Alpine;
|
||||||
Alpine.plugin(persist)
|
Alpine.plugin(persist)
|
||||||
|
Alpine.data('darkmodetoggle', darkmodetoggle);
|
||||||
Alpine.data('bookmeterList', function () {
|
Alpine.data('bookmeterList', function () {
|
||||||
return {
|
return {
|
||||||
|
chart: null,
|
||||||
|
chartPoints: null,
|
||||||
|
chartPublisher: null,
|
||||||
items: {},
|
items: {},
|
||||||
filteredItems: [],
|
filteredItems: [],
|
||||||
username: '',
|
username: '',
|
||||||
@@ -15,32 +20,156 @@ Alpine.data('bookmeterList', function () {
|
|||||||
title: '',
|
title: '',
|
||||||
author: '',
|
author: '',
|
||||||
nameFilter: '',
|
nameFilter: '',
|
||||||
years: [2023, 2024, 2025],
|
years: [2023, 2024, 2025, 2026],
|
||||||
year: this.$persist(['2025']).as('year-filter'),
|
year: this.$persist(['2026']).as('year-filter'),
|
||||||
orderColumn: ['order', 1],
|
orderColumn: ['order', 0],
|
||||||
advancedSearch: this.$persist(0).as('advanced-search'),
|
advancedSearch: this.$persist(0).as('advanced-search'),
|
||||||
async init() {
|
async init() {
|
||||||
this.$watch('nameFilter', () => this.updateFilter());
|
|
||||||
this.$watch('username', () => this.updateFilter());
|
this.$watch(() => JSON.stringify([
|
||||||
this.$watch('title', () => this.updateFilter());
|
this.nameFilter,
|
||||||
this.$watch('author', () => this.updateFilter());
|
this.username,
|
||||||
this.$watch('publisher', () => this.updateFilter());
|
this.title,
|
||||||
this.$watch('year', () => this.updateFilter());
|
this.author,
|
||||||
|
this.publisher,
|
||||||
|
this.year,
|
||||||
|
this.advancedSearch
|
||||||
|
]), () => this.updateFilter());
|
||||||
|
|
||||||
let qp = new URLSearchParams(window.location.search);
|
let qp = new URLSearchParams(window.location.search);
|
||||||
if(qp.get('filter')) this.nameFilter = qp.get('filter');
|
if(qp.get('filter')) this.nameFilter = qp.get('filter');
|
||||||
|
|
||||||
const [data1, data2, data3] = await Promise.all([
|
const [data1, data2, data3, data4] = await Promise.all([
|
||||||
|
fetch('/posts_2026.json').then(res => res.json()),
|
||||||
fetch('/posts_2025.json').then(res => res.json()),
|
fetch('/posts_2025.json').then(res => res.json()),
|
||||||
fetch('/posts_2024.json').then(res => res.json()),
|
fetch('/posts_2024.json').then(res => res.json()),
|
||||||
fetch('/posts_2023.json').then(res => res.json())
|
fetch('/posts_2023.json').then(res => res.json())
|
||||||
])
|
])
|
||||||
|
|
||||||
this.items = [...data1, ...data2, ...data3]
|
this.items = [...data1, ...data2, ...data3, ...data4]
|
||||||
|
.filter(i => i.book.title.length > 0)
|
||||||
|
.map(i => ({
|
||||||
|
...i,
|
||||||
|
_year: i.created_at.substring(0, 4),
|
||||||
|
_username: i.username.toLowerCase(),
|
||||||
|
_title: i.book.title.toLowerCase(),
|
||||||
|
_author: i.book.author.toLowerCase(),
|
||||||
|
_publisher: i.book.publisher.toLowerCase(),
|
||||||
|
_search: (
|
||||||
|
i.username +
|
||||||
|
' ' + i.book.title +
|
||||||
|
' ' + i.book.author +
|
||||||
|
' ' + i.book.publisher
|
||||||
|
).toLowerCase()
|
||||||
|
}));
|
||||||
this.updateFilter();
|
this.updateFilter();
|
||||||
|
// this.initChart();
|
||||||
|
},
|
||||||
|
updatechartnew(){
|
||||||
|
this.chart.data.datasets[0].data[0] = Math.random()*50;
|
||||||
|
Alpine.raw(this.chart).update('none');
|
||||||
|
this.updateChart()
|
||||||
|
},
|
||||||
|
// initChart() {
|
||||||
|
// const ctx = document.getElementById('statsChart').getContext('2d')
|
||||||
|
//
|
||||||
|
// this.chart = Alpine.raw(new Chart(ctx, {
|
||||||
|
// type: 'bar',
|
||||||
|
// data: {
|
||||||
|
// labels: ['Red', 'Blue', 'Yellow', 'Green', 'Purple', 'Orange'],
|
||||||
|
// datasets: [{
|
||||||
|
// label: '# of Votes',
|
||||||
|
// data: [12, 19, 3, 5, 2, 3],
|
||||||
|
// borderWidth: 1
|
||||||
|
// }]
|
||||||
|
// }
|
||||||
|
// }));
|
||||||
|
//
|
||||||
|
// const chartPoints = document.getElementById('chartPoints').getContext('2d')
|
||||||
|
//
|
||||||
|
// const bubbleData = this.filteredItems.map(item => ({
|
||||||
|
// x: item.book.pages,
|
||||||
|
// y: item.book.rating,
|
||||||
|
// r: Math.sqrt(item.likes || 1) * 2, // scale likes
|
||||||
|
// format: item.book.format
|
||||||
|
// }));
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// this.chartPoints = Alpine.raw(new Chart(chartPoints, {
|
||||||
|
// type: 'bubble',
|
||||||
|
// data: {
|
||||||
|
// labels: [],
|
||||||
|
// datasets: [{
|
||||||
|
// label: 'points',
|
||||||
|
// // data: this.pointsChartData,
|
||||||
|
// data: bubbleData,
|
||||||
|
// // backgroundColor: 'rgb(255, 99, 132)'
|
||||||
|
// backgroundColor: bubbleData.map(d =>
|
||||||
|
// d.format === 'audiobook' ? 'rgba(255,99,132,0.5)' :
|
||||||
|
// d.format === 'ebook' ? 'rgba(54,162,235,0.5)' :
|
||||||
|
// 'rgba(75,192,192,0.5)'
|
||||||
|
// )
|
||||||
|
// }]
|
||||||
|
// },
|
||||||
|
// options: {
|
||||||
|
// scales: {
|
||||||
|
// x: { title: { display: true, text: 'Pages' } },
|
||||||
|
// y: { title: { display: true, text: 'Rating' }, min: 0, max: 10 }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }));
|
||||||
|
//
|
||||||
|
// },
|
||||||
|
get pointsChartData(){
|
||||||
|
let data = [];
|
||||||
|
for(let i in this.filteredItems){
|
||||||
|
// console.info(Alpine.raw(this.filteredItems[i]));
|
||||||
|
if(!(this.filteredItems[i]['book']['rating'] in data)){
|
||||||
|
data[this.filteredItems[i]['book']['rating']] = []
|
||||||
|
}
|
||||||
|
if(!(this.filteredItems[i]['likes'] in data[this.filteredItems[i]['book']['rating']])){
|
||||||
|
data[this.filteredItems[i]['book']['rating']][this.filteredItems[i]['likes']] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
data[this.filteredItems[i]['book']['rating']][this.filteredItems[i]['likes']]++;
|
||||||
|
}
|
||||||
|
|
||||||
|
let data2 = [];
|
||||||
|
for(let i in data){
|
||||||
|
for(let j in data[i]){
|
||||||
|
data2.push({x: j, y: i, r: data[i][j], label: 'label'});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return data2;
|
||||||
|
},
|
||||||
|
updateChart() {
|
||||||
|
if (!this.chart) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// console.info( Alpine.raw(this.filteredItems));
|
||||||
|
const topItems = this.filteredItems.slice(0, 20).map(item => ({
|
||||||
|
title: item.book?.title || 'Unknown',
|
||||||
|
likes: item.likes || 0,
|
||||||
|
// date: (new Date(item.created_at)).getFullYear()+'-'+((new Date(item.created_at)).getMonth()+1)
|
||||||
|
}));
|
||||||
|
|
||||||
|
let counts = {};
|
||||||
|
|
||||||
|
// topItems.forEach(item => counts[item.date] = 1 + ())
|
||||||
|
|
||||||
|
|
||||||
|
console.info(topItems);
|
||||||
|
this.chart.data.labels = topItems.map(i => i.title)
|
||||||
|
this.chart.data.datasets[0].data = topItems.map(i => i.likes)
|
||||||
|
|
||||||
|
Alpine.raw(this.chart).update('none');
|
||||||
},
|
},
|
||||||
orderBy(property) {
|
orderBy(property) {
|
||||||
let direction = (this.orderColumn[0] === property) ? this.orderColumn[1] * -1 : 1;
|
let direction = (this.orderColumn[0] === property) ? ((this.orderColumn[1] === -1) ? 0 : this.orderColumn[1] * -1) : 1;
|
||||||
|
if (!direction) {
|
||||||
|
property = 'order';
|
||||||
|
}
|
||||||
this.orderColumn = [property, direction];
|
this.orderColumn = [property, direction];
|
||||||
this.sort();
|
this.sort();
|
||||||
},
|
},
|
||||||
@@ -59,6 +188,10 @@ Alpine.data('bookmeterList', function () {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
sort() {
|
sort() {
|
||||||
|
if (this.orderColumn[1] === 0) {
|
||||||
|
this.initFilteredData();
|
||||||
|
return;
|
||||||
|
}
|
||||||
let sortFunction = ({
|
let sortFunction = ({
|
||||||
username: (a,b) => ((a.username < b.username) ? -1 : (a.username > b.username) ? 1 : 0) * this.orderColumn[1],
|
username: (a,b) => ((a.username < b.username) ? -1 : (a.username > b.username) ? 1 : 0) * this.orderColumn[1],
|
||||||
title: (a,b) => ((a.book.title < b.book.title) ? -1 : (a.book.title > b.book.title) ? 1 : 0) * this.orderColumn[1],
|
title: (a,b) => ((a.book.title < b.book.title) ? -1 : (a.book.title > b.book.title) ? 1 : 0) * this.orderColumn[1],
|
||||||
@@ -70,29 +203,41 @@ Alpine.data('bookmeterList', function () {
|
|||||||
likes: (a,b) => ((a.likes < b.likes) ? -1 : (a.likes > b.likes) ? 1 : 0) * this.orderColumn[1],
|
likes: (a,b) => ((a.likes < b.likes) ? -1 : (a.likes > b.likes) ? 1 : 0) * this.orderColumn[1],
|
||||||
})[this.orderColumn[0]];
|
})[this.orderColumn[0]];
|
||||||
this.filteredItems = this.filteredItems.sort(sortFunction);
|
this.filteredItems = this.filteredItems.sort(sortFunction);
|
||||||
|
if (this.orderColumn[0] === 'order') {
|
||||||
|
this.initFilteredData();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
updateFilter(){
|
initFilteredData(){
|
||||||
let filter = this.nameFilter.toLowerCase();
|
let filter = this.nameFilter.toLowerCase();
|
||||||
this.filteredItems = this.items.filter(i => this.year.includes(i.created_at.substring(0,4)))
|
|
||||||
.filter(i => {
|
|
||||||
if(this.advancedSearch) {
|
|
||||||
return (
|
|
||||||
(!this.username || i.username.toLowerCase().includes(this.username.toLowerCase())) &&
|
|
||||||
(!this.title || i.book.title.toLowerCase().includes(this.title.toLowerCase())) &&
|
|
||||||
(!this.author || i.book.author.toLowerCase().includes(this.author.toLowerCase())) &&
|
|
||||||
(!this.publisher || i.book.publisher.toLowerCase().includes(this.publisher.toLowerCase()))
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
if(filter.startsWith('@') && i.username.toLowerCase().includes(filter.substring(1))) return true;
|
|
||||||
if (i.username.toLowerCase().includes(filter) ||
|
|
||||||
i.book.title.toLowerCase().includes(filter) || i.book.author.toLowerCase().includes(filter) || i.book.publisher.toLowerCase().includes(filter)) return true;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
this.sort();
|
|
||||||
|
|
||||||
this.updateURL();
|
let username = this.username.toLowerCase().trim();
|
||||||
|
let author = this.author.toLowerCase().trim();
|
||||||
|
let publisher = this.publisher.toLowerCase().trim();
|
||||||
|
let title = this.title.toLowerCase().trim();
|
||||||
|
|
||||||
|
this.filteredItems = this.items
|
||||||
|
.filter(i => this.year.includes(i._year))
|
||||||
|
.filter(i => {
|
||||||
|
if (this.advancedSearch) {
|
||||||
|
return (
|
||||||
|
(!this.username || i._username.includes(username)) &&
|
||||||
|
(!this.title || i._title.includes(title)) &&
|
||||||
|
(!this.author || i._author.includes(author)) &&
|
||||||
|
(!this.publisher || i._publisher.includes(publisher))
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
if (filter.startsWith('@')) {
|
||||||
|
return i.username.toLowerCase().includes(filter.slice(1));
|
||||||
|
}
|
||||||
|
return i._search.includes(filter);
|
||||||
|
}
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
updateFilter: Alpine.debounce(function () {
|
||||||
|
this.initFilteredData();
|
||||||
|
this.sort();
|
||||||
|
this.updateURL();
|
||||||
|
}, 250),
|
||||||
updateURL() {
|
updateURL() {
|
||||||
let qp = new URLSearchParams();
|
let qp = new URLSearchParams();
|
||||||
if(this.nameFilter !== '') qp.set('filter', this.nameFilter);
|
if(this.nameFilter !== '') qp.set('filter', this.nameFilter);
|
||||||
|
|||||||
1
src/style.css
Normal file → Executable file
1
src/style.css
Normal file → Executable file
@@ -1,6 +1,7 @@
|
|||||||
@tailwind base;
|
@tailwind base;
|
||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
@custom-variant dark (&:where([data-theme=dark], [data-theme=dark] *));
|
||||||
|
|
||||||
[x-cloak] {
|
[x-cloak] {
|
||||||
display: none;
|
display: none;
|
||||||
|
|||||||
1
tailwind.config.js
Normal file → Executable file
1
tailwind.config.js
Normal file → Executable file
@@ -8,5 +8,6 @@ module.exports = {
|
|||||||
extend: {},
|
extend: {},
|
||||||
},
|
},
|
||||||
plugins: [],
|
plugins: [],
|
||||||
|
darkMode: 'class'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user