Initial commit for the React-based admin panel.
Some checks are pending
CI/CD / build-and-test (push) Waiting to run
Some checks are pending
CI/CD / build-and-test (push) Waiting to run
This commit is contained in:
parent
eaecace48d
commit
6f1cc7e2a6
19 changed files with 4972 additions and 0 deletions
24
src/Guestbooky-admin/.gitignore
vendored
Normal file
24
src/Guestbooky-admin/.gitignore
vendored
Normal file
|
@ -0,0 +1,24 @@
|
|||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
38
src/Guestbooky-admin/eslint.config.js
Normal file
38
src/Guestbooky-admin/eslint.config.js
Normal file
|
@ -0,0 +1,38 @@
|
|||
import js from '@eslint/js'
|
||||
import globals from 'globals'
|
||||
import react from 'eslint-plugin-react'
|
||||
import reactHooks from 'eslint-plugin-react-hooks'
|
||||
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||
|
||||
export default [
|
||||
{ ignores: ['dist'] },
|
||||
{
|
||||
files: ['**/*.{js,jsx}'],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2020,
|
||||
globals: globals.browser,
|
||||
parserOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
ecmaFeatures: { jsx: true },
|
||||
sourceType: 'module',
|
||||
},
|
||||
},
|
||||
settings: { react: { version: '18.3' } },
|
||||
plugins: {
|
||||
react,
|
||||
'react-hooks': reactHooks,
|
||||
'react-refresh': reactRefresh,
|
||||
},
|
||||
rules: {
|
||||
...js.configs.recommended.rules,
|
||||
...react.configs.recommended.rules,
|
||||
...react.configs['jsx-runtime'].rules,
|
||||
...reactHooks.configs.recommended.rules,
|
||||
'react/jsx-no-target-blank': 'off',
|
||||
'react-refresh/only-export-components': [
|
||||
'warn',
|
||||
{ allowConstantExport: true },
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
13
src/Guestbooky-admin/index.html
Normal file
13
src/Guestbooky-admin/index.html
Normal file
|
@ -0,0 +1,13 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/png" href="/src/assets/guestbooky.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Guestbooky Admin Panel</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.jsx"></script>
|
||||
</body>
|
||||
</html>
|
4391
src/Guestbooky-admin/package-lock.json
generated
Normal file
4391
src/Guestbooky-admin/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
30
src/Guestbooky-admin/package.json
Normal file
30
src/Guestbooky-admin/package.json
Normal file
|
@ -0,0 +1,30 @@
|
|||
{
|
||||
"name": "guestbooky-admin",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.7.7",
|
||||
"guestbooky-admin": "file:",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.13.0",
|
||||
"@types/react": "^18.3.11",
|
||||
"@types/react-dom": "^18.3.1",
|
||||
"@vitejs/plugin-react": "^4.3.3",
|
||||
"eslint": "^9.13.0",
|
||||
"eslint-plugin-react": "^7.37.1",
|
||||
"eslint-plugin-react-hooks": "^5.0.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.13",
|
||||
"globals": "^15.11.0",
|
||||
"vite": "^5.4.9"
|
||||
}
|
||||
}
|
24
src/Guestbooky-admin/src/App.css
Normal file
24
src/Guestbooky-admin/src/App.css
Normal file
|
@ -0,0 +1,24 @@
|
|||
.header{
|
||||
font-size: 3em;
|
||||
word-spacing: 0.3em;
|
||||
letter-spacing: -0.05em;
|
||||
background: linear-gradient(180deg, hsl(15, 80%, 10%) 0%, hsl(15, 50%, 60%) 100%);
|
||||
width: 100%;
|
||||
padding: 2ch;
|
||||
border-bottom: 0.1em solid hsl(15, 80%, 20%);
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
margin-inline: auto;
|
||||
width: 1200px;
|
||||
color: hsl(15, 100%, 90%);
|
||||
text-shadow: 0.01em 0.01em 0.01em hsl(15, 0%, 20%);
|
||||
font-family: "Carattere", cursive;
|
||||
}
|
||||
|
||||
.content {
|
||||
width: 1200px;
|
||||
max-width: 1200px;
|
||||
margin-inline: auto;
|
||||
padding: 2ch;
|
||||
}
|
24
src/Guestbooky-admin/src/App.jsx
Normal file
24
src/Guestbooky-admin/src/App.jsx
Normal file
|
@ -0,0 +1,24 @@
|
|||
import {useState} from 'react'
|
||||
import AdminPage from './components/AdminPage'
|
||||
import LoginPage from './components/LoginPage'
|
||||
|
||||
import './App.css'
|
||||
|
||||
function App() {
|
||||
const [isLoggedIn, setIsLoggedIn] = useState(false)
|
||||
|
||||
const handleLoggedIn = (newValue) => {
|
||||
setIsLoggedIn(newValue)
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<div className="header"><h1>Guestbooky Admin</h1></div>
|
||||
<div className="content">
|
||||
{isLoggedIn ? <AdminPage className="content"/> :
|
||||
<LoginPage onLoggedIn={handleLoggedIn} className="content"/>}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default App
|
BIN
src/Guestbooky-admin/src/assets/guestbooky.png
Normal file
BIN
src/Guestbooky-admin/src/assets/guestbooky.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 11 KiB |
111
src/Guestbooky-admin/src/components/AdminPage.css
Normal file
111
src/Guestbooky-admin/src/components/AdminPage.css
Normal file
|
@ -0,0 +1,111 @@
|
|||
@keyframes smallFadeIn {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.list-item {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
animation: smallFadeIn 0.5s ease-out forwards;
|
||||
animation-delay: calc(0.2s * var(--child-index));
|
||||
}
|
||||
|
||||
.warning{
|
||||
background-color: hsla(15, 100%, 90%, 0.75);
|
||||
border: 0.2em solid hsla(15, 80%, 70%, 1);
|
||||
border-radius: 0.3em;
|
||||
padding: 2ch 1ch;
|
||||
width: fit-content;
|
||||
font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace;
|
||||
}
|
||||
|
||||
.navigation{
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.navigation button {
|
||||
font-size: 1em;
|
||||
margin: 2em;
|
||||
background: hsla(15, 60%, 80%, 1);
|
||||
border-radius: 0.2em;
|
||||
width: fit-content;
|
||||
padding: 0.8em 0.5em;
|
||||
font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace;
|
||||
}
|
||||
|
||||
.navigation button:hover {
|
||||
background: hsla(15, 80%, 90%, 0.75);
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style-type: none;
|
||||
}
|
||||
|
||||
ul li{
|
||||
line-height: 1.6em;
|
||||
display: grid;
|
||||
grid-row-gap: 2em;
|
||||
padding: 1em;
|
||||
grid-template-rows: auto 1fr;
|
||||
grid-template-columns: 1fr auto auto;
|
||||
font-family: 'Solway', serif;
|
||||
}
|
||||
|
||||
ul li h2{
|
||||
align-self: center;
|
||||
grid-row: 1;
|
||||
background: linear-gradient(to right, hsl(15, 40%, 85%), hsla(15, 40%, 85%, 0));
|
||||
border-radius: 0.2em;
|
||||
padding: 0.5em 0 0.5em 0.5em;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
ul li h3{
|
||||
align-self: center;
|
||||
grid-row: 1;
|
||||
justify-self: right;
|
||||
padding-right: 0.5em;
|
||||
vertical-align: bottom;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
|
||||
ul li button{
|
||||
grid-row: 1;
|
||||
justify-self: right;
|
||||
width: fit-content;
|
||||
border-radius: 0.5em;
|
||||
padding: 0.5em;
|
||||
font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace;
|
||||
background: hsla(15, 60%, 80%, 0.75);
|
||||
}
|
||||
ul li button:hover {
|
||||
background: hsla(15, 80%, 90%, 0.75);
|
||||
}
|
||||
.message-text {
|
||||
grid-row: 2;
|
||||
grid-column: 1 / 4;
|
||||
background-color: hsl(15, 50%, 90%);
|
||||
margin: 1em 0.5em;
|
||||
padding: 1em 0.5em;
|
||||
font-weight: 300;
|
||||
border-radius: 0.5em;
|
||||
box-shadow: 0.05em 0.05em 0.05em hsla(15, 20%, 20%, 0.5);
|
||||
}
|
||||
|
||||
.message-text p{
|
||||
line-height: 1.5em;
|
||||
text-align: justify;
|
||||
text-justify: inter-word;
|
||||
text-align-last: left;
|
||||
}
|
51
src/Guestbooky-admin/src/components/AdminPage.jsx
Normal file
51
src/Guestbooky-admin/src/components/AdminPage.jsx
Normal file
|
@ -0,0 +1,51 @@
|
|||
import useMessages from "../hooks/useMessages.js";
|
||||
|
||||
import './AdminPage.css'
|
||||
|
||||
|
||||
const AdminPage = () => {
|
||||
const {messages, totalMessages, loading, error, removeMessage, page, nextPage, previousPage} = useMessages();
|
||||
const localeOptions = {
|
||||
weekday: 'long',
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
hour: 'numeric',
|
||||
minute: 'numeric',
|
||||
second: 'numeric',
|
||||
timeZoneName: 'short'
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (<div className='warning'><p>Loading...</p></div>);
|
||||
}
|
||||
|
||||
if (messages.length === 0 && page > 1) {
|
||||
previousPage();
|
||||
}
|
||||
|
||||
if (messages.length === 0) {
|
||||
return (<div className='warning'><p>No messages.</p></div>);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='warning'><p>{error ? 'Error : ' + error : 'Total messages: ' + totalMessages }</p></div>
|
||||
<ul>
|
||||
{messages.length > 0 ? messages.map((message, index) => (
|
||||
<li key={message.id} className='list-item' style={{ '--child-index': index}} >
|
||||
<h2>{message.author}</h2>
|
||||
<h3>{new Date(message.timestamp).toLocaleString(navigator.language, localeOptions)}</h3>
|
||||
<button onClick={() => removeMessage(message.id)}>Delete</button>
|
||||
<div className='message-text'><p>{message.message}</p></div>
|
||||
</li>
|
||||
)) : ''}
|
||||
</ul>
|
||||
<div className='navigation'>
|
||||
<button onClick={() => previousPage()}>Previous Page</button>
|
||||
<button onClick={() => nextPage()}>Next Page</button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
export default AdminPage;
|
64
src/Guestbooky-admin/src/components/LoginPage.css
Normal file
64
src/Guestbooky-admin/src/components/LoginPage.css
Normal file
|
@ -0,0 +1,64 @@
|
|||
.login-form{
|
||||
display: flex;
|
||||
margin-inline: auto;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
vertical-align: center;
|
||||
margin-top: 20vh;
|
||||
background-color: hsla(15, 100%, 93%);
|
||||
width: fit-content;
|
||||
padding: 2em;
|
||||
border-radius: 1em;
|
||||
box-shadow: 0.05em 0.05em 0.05em hsla(15, 20%, 20%, 0.5);
|
||||
}
|
||||
|
||||
.fade-in{
|
||||
opacity: 0;
|
||||
transform: translateY(10vh);
|
||||
animation: fadeIn 0.5s ease-out forwards;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translateY(20vh);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
form {
|
||||
display: grid;
|
||||
grid-template-columns: auto 20ch;
|
||||
grid-column-gap: 2ch;
|
||||
grid-row-gap: 2ch;
|
||||
align-items: center;
|
||||
font-family: 'Solway', serif;
|
||||
}
|
||||
|
||||
form label {
|
||||
font-size: 1.4em;
|
||||
text-align: right;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
form input[type=text], form input[type=password] {
|
||||
font-size: 1.4em;
|
||||
border: 0.2em hsl(15, 100%, 50%) outset;
|
||||
border-radius: 0.2em;
|
||||
padding: 0.2em 0.2em;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
form button {
|
||||
margin-top: 1em;
|
||||
grid-column: 2 / 3;
|
||||
padding: 1em 2em;
|
||||
font-size: 1.4em;
|
||||
border-radius: 0.2em;
|
||||
background-color: hsl(15, 50%, 85%);
|
||||
font-family: 'Solway', serif;
|
||||
font-weight: 300;
|
||||
}
|
40
src/Guestbooky-admin/src/components/LoginPage.jsx
Normal file
40
src/Guestbooky-admin/src/components/LoginPage.jsx
Normal file
|
@ -0,0 +1,40 @@
|
|||
import React, { useRef } from 'react';
|
||||
import useAuth from '../hooks/useAuth.js';
|
||||
|
||||
import './LoginPage.css'
|
||||
|
||||
const LoginPage = ({onLoggedIn}) =>
|
||||
{
|
||||
const userInputRef = useRef(null);
|
||||
const passInputRef = useRef(null);
|
||||
const { authenticate, error } = useAuth();
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault()
|
||||
const user = userInputRef.current.value;
|
||||
const pass = passInputRef.current.value;
|
||||
|
||||
authenticate({user, pass})
|
||||
.then(() => {
|
||||
if (error === null) {
|
||||
onLoggedIn(true)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return(
|
||||
<>
|
||||
<div className="login-form fade-in">
|
||||
<form action="post" onSubmit={handleSubmit}>
|
||||
<label>User: </label><input type='text' name='username' placeholder=''
|
||||
ref={userInputRef}></input>
|
||||
<label>Password: </label><input type='password' name='password' placeholder=''
|
||||
ref={passInputRef}></input>
|
||||
<button type='submit' className='login-button'>Login</button>
|
||||
</form>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default LoginPage;
|
1
src/Guestbooky-admin/src/environment/constants.js
Normal file
1
src/Guestbooky-admin/src/environment/constants.js
Normal file
|
@ -0,0 +1 @@
|
|||
export const API_URL = 'http://localhost:8080';
|
19
src/Guestbooky-admin/src/hooks/useAuth.js
Normal file
19
src/Guestbooky-admin/src/hooks/useAuth.js
Normal file
|
@ -0,0 +1,19 @@
|
|||
import { useState } from "react";
|
||||
import { post } from '../services/httpService.js'
|
||||
|
||||
const useAuth = () => {
|
||||
const [error, setError] = useState(null);
|
||||
|
||||
const authenticate = async ({user, pass}) => {
|
||||
try {
|
||||
await post('/auth/login', {username: user, password: pass});
|
||||
}
|
||||
catch(err){
|
||||
setError(err);
|
||||
}
|
||||
}
|
||||
|
||||
return { authenticate, error };
|
||||
}
|
||||
|
||||
export default useAuth;
|
67
src/Guestbooky-admin/src/hooks/useMessages.js
Normal file
67
src/Guestbooky-admin/src/hooks/useMessages.js
Normal file
|
@ -0,0 +1,67 @@
|
|||
import { useState, useEffect } from "react";
|
||||
import { get, del } from '../services/httpService.js'
|
||||
|
||||
const useMessages = () => {
|
||||
const [messages, setMessages] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState(null);
|
||||
const [totalMessages, setTotalMessages] = useState(0);
|
||||
const [page, setPage] = useState(1);
|
||||
const amountPerPage = 10;
|
||||
|
||||
useEffect(() => {
|
||||
const fetchMessages = async () => {
|
||||
try{
|
||||
setLoading(true);
|
||||
const data = await get('/message', {
|
||||
headers: {
|
||||
'Range': 'messages=' + ((page - 1) * amountPerPage) + '-' + (((page - 1) * amountPerPage) + amountPerPage),
|
||||
},
|
||||
});
|
||||
|
||||
const [, rangeInfo] = data.headers['content-range'].split(' ');
|
||||
const [, total] = rangeInfo.split('/');
|
||||
|
||||
setTotalMessages(parseInt(total));
|
||||
setMessages(data.data);
|
||||
} catch(err) {
|
||||
setError(err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
fetchMessages();
|
||||
}, [page]);
|
||||
|
||||
const removeMessage = async (id) => {
|
||||
try{
|
||||
await del('/message', {id: id});
|
||||
setMessages((prevlist) => prevlist.filter(message => message.id !== id));
|
||||
setTotalMessages(totalMessages - 1)
|
||||
}catch(err){
|
||||
setError(err);
|
||||
}
|
||||
};
|
||||
|
||||
const nextPage = () => {
|
||||
if(page * amountPerPage < totalMessages)
|
||||
setPage((page + 1));
|
||||
}
|
||||
const previousPage = () => {
|
||||
if (page > 1)
|
||||
setPage((page - 1));
|
||||
}
|
||||
|
||||
return {
|
||||
messages,
|
||||
totalMessages,
|
||||
loading,
|
||||
error,
|
||||
removeMessage,
|
||||
page,
|
||||
nextPage,
|
||||
previousPage
|
||||
}
|
||||
}
|
||||
|
||||
export default useMessages;
|
40
src/Guestbooky-admin/src/index.css
Normal file
40
src/Guestbooky-admin/src/index.css
Normal file
|
@ -0,0 +1,40 @@
|
|||
@import url(https://fonts.bunny.net/css?family=carattere:400|solway:300,500);
|
||||
|
||||
@media (prefers-reduced-motion: reduce){
|
||||
html:focus-within {
|
||||
scroll-behavior: auto;
|
||||
}
|
||||
*, *::before, *::after {
|
||||
animation-duration: 0.01ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition: none;
|
||||
transition-duration: 0.01ms !important;
|
||||
scroll-behavior: auto !important;
|
||||
}
|
||||
}
|
||||
|
||||
*, *::before, *::after{
|
||||
box-sizing: border-box;
|
||||
transition: all 0.05s ease;
|
||||
animation-duration: initial;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
html:focus-within{
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
img, picture, svg, video, canvas{
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
body {
|
||||
background: linear-gradient(90deg, hsl(15, 80%, 90%) 0%, hsl(15, 80%, 98%) 5%, hsl(15, 80%, 98%) 95%, hsl(15, 80%, 90%) 100%);
|
||||
color: hsl(15, 70%, 5%);
|
||||
}
|
7
src/Guestbooky-admin/src/main.jsx
Normal file
7
src/Guestbooky-admin/src/main.jsx
Normal file
|
@ -0,0 +1,7 @@
|
|||
import { createRoot } from 'react-dom/client'
|
||||
import './index.css'
|
||||
import App from './App.jsx'
|
||||
|
||||
createRoot(document.getElementById('root')).render(
|
||||
<App />
|
||||
)
|
21
src/Guestbooky-admin/src/services/httpService.js
Normal file
21
src/Guestbooky-admin/src/services/httpService.js
Normal file
|
@ -0,0 +1,21 @@
|
|||
import axios from 'axios';
|
||||
import { API_URL } from '../environment/constants.js'
|
||||
|
||||
const httpClient = axios.create({
|
||||
baseURL: API_URL,
|
||||
withCredentials: true,
|
||||
timeout: 5000,
|
||||
headers: {'Content-Type': 'application/json',}
|
||||
});
|
||||
|
||||
export const get = (endpoint, params) => {
|
||||
return httpClient.get(endpoint, { headers: params.headers, data: params.data});
|
||||
}
|
||||
|
||||
export const post = (endpoint, data) => {
|
||||
return httpClient.post(endpoint, data);
|
||||
}
|
||||
|
||||
export const del = (endpoint, data) => {
|
||||
return httpClient.delete(endpoint, {data: data });
|
||||
}
|
7
src/Guestbooky-admin/vite.config.js
Normal file
7
src/Guestbooky-admin/vite.config.js
Normal file
|
@ -0,0 +1,7 @@
|
|||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
})
|
Loading…
Reference in a new issue