Big formatting spree
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 3m50s
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 3m50s
This commit is contained in:
@@ -51,12 +51,12 @@ type graphMessagesResponse struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type graphMessage struct {
|
type graphMessage struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Subject string `json:"subject"`
|
Subject string `json:"subject"`
|
||||||
ReceivedDateTime string `json:"receivedDateTime"`
|
ReceivedDateTime string `json:"receivedDateTime"`
|
||||||
From graphFrom `json:"from"`
|
From graphFrom `json:"from"`
|
||||||
Body graphBody `json:"body"`
|
Body graphBody `json:"body"`
|
||||||
BodyPreview string `json:"bodyPreview"`
|
BodyPreview string `json:"bodyPreview"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type graphFrom struct {
|
type graphFrom struct {
|
||||||
|
|||||||
@@ -41,8 +41,8 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
rateLimitWindow = time.Second
|
rateLimitWindow = time.Second
|
||||||
rateLimitMaxMsgs = 10
|
rateLimitMaxMsgs = 10
|
||||||
)
|
)
|
||||||
|
|
||||||
func InitWebSocket(database *gorm.DB, domain string) {
|
func InitWebSocket(database *gorm.DB, domain string) {
|
||||||
|
|||||||
@@ -1,198 +1,201 @@
|
|||||||
networks:
|
networks:
|
||||||
app-network:
|
app-network:
|
||||||
driver: bridge
|
driver: bridge
|
||||||
ipam:
|
ipam:
|
||||||
config:
|
config:
|
||||||
- subnet: 172.28.0.0/16
|
- subnet: 172.28.0.0/16
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
dbdata:
|
# Postgres database
|
||||||
uploads:
|
dbdata:
|
||||||
vue_dist:
|
# File upload
|
||||||
|
uploads:
|
||||||
searxng_data:
|
# Vue build
|
||||||
|
vue_dist:
|
||||||
|
# Searxng data
|
||||||
|
searxng_data:
|
||||||
|
|
||||||
services:
|
services:
|
||||||
vue:
|
vue:
|
||||||
build:
|
build:
|
||||||
context: ./vue
|
context: ./vue
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
container_name: vue
|
container_name: vue
|
||||||
volumes:
|
volumes:
|
||||||
- vue_dist:/output
|
- vue_dist:/output
|
||||||
networks:
|
networks:
|
||||||
- app-network
|
- app-network
|
||||||
|
|
||||||
nginx:
|
nginx:
|
||||||
build:
|
build:
|
||||||
context: ./nginx
|
context: ./nginx
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
container_name: nginx
|
container_name: nginx
|
||||||
env_file: ./.env
|
env_file: ./.env
|
||||||
restart: always
|
restart: always
|
||||||
depends_on:
|
depends_on:
|
||||||
- vue
|
- vue
|
||||||
- backend
|
- backend
|
||||||
- icecast2
|
- icecast2
|
||||||
- gitea
|
- gitea
|
||||||
- hasura
|
- hasura
|
||||||
- quartz
|
- quartz
|
||||||
- searxng
|
- searxng
|
||||||
networks:
|
networks:
|
||||||
- app-network
|
- app-network
|
||||||
ports:
|
ports:
|
||||||
- 80:80
|
- 80:80
|
||||||
- 443:443
|
- 443:443
|
||||||
volumes:
|
volumes:
|
||||||
- ./certbot/conf:/etc/letsencrypt
|
- ./certbot/conf:/etc/letsencrypt
|
||||||
- ./certbot/www:/var/www/certbot
|
- ./certbot/www:/var/www/certbot
|
||||||
- uploads:/uploads
|
- uploads:/uploads
|
||||||
- vue_dist:/etc/nginx/html
|
- vue_dist:/etc/nginx/html
|
||||||
|
|
||||||
certbot:
|
certbot:
|
||||||
image: certbot/certbot:v3.1.0
|
image: certbot/certbot:v3.1.0
|
||||||
container_name: certbot
|
container_name: certbot
|
||||||
volumes:
|
volumes:
|
||||||
- ./certbot/entrypoint.sh:/entrypoint.sh
|
- ./certbot/entrypoint.sh:/entrypoint.sh
|
||||||
- ./certbot/conf:/etc/letsencrypt
|
- ./certbot/conf:/etc/letsencrypt
|
||||||
- ./certbot/www:/var/www/certbot
|
- ./certbot/www:/var/www/certbot
|
||||||
entrypoint: ["/entrypoint.sh"]
|
entrypoint: ["/entrypoint.sh"]
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
networks:
|
networks:
|
||||||
- app-network
|
- app-network
|
||||||
|
|
||||||
backend:
|
backend:
|
||||||
build:
|
build:
|
||||||
context: ./backend
|
context: ./backend
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
container_name: "${BACKEND_HOST}"
|
container_name: "${BACKEND_HOST}"
|
||||||
restart: always
|
restart: always
|
||||||
depends_on:
|
depends_on:
|
||||||
- db
|
- db
|
||||||
networks:
|
networks:
|
||||||
- app-network
|
- app-network
|
||||||
env_file:
|
env_file:
|
||||||
- ./.env
|
- ./.env
|
||||||
volumes:
|
volumes:
|
||||||
- ./backend/token/:/backend/token
|
- ./backend/token/:/backend/token
|
||||||
- ${OBSIDIAN_DIR}:/backend/notes
|
- ${OBSIDIAN_DIR}:/backend/notes
|
||||||
- ./logs:/backend/logs
|
- ./logs:/backend/logs
|
||||||
- uploads:/backend/uploads
|
- uploads:/backend/uploads
|
||||||
- ./icecast2/fallback_music:/backend/fallback_music
|
- ./icecast2/fallback_music:/backend/fallback_music
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "wget", "-qO-", "http://localhost:8080/"]
|
test: ["CMD", "wget", "-qO-", "http://localhost:8080/"]
|
||||||
interval: 30s
|
interval: 30s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 3
|
retries: 3
|
||||||
start_period: 10s
|
start_period: 10s
|
||||||
|
|
||||||
autoheal:
|
autoheal:
|
||||||
image: willfarrell/autoheal:latest
|
image: willfarrell/autoheal:latest
|
||||||
container_name: autoheal
|
container_name: autoheal
|
||||||
restart: always
|
restart: always
|
||||||
environment:
|
environment:
|
||||||
- AUTOHEAL_CONTAINER_LABEL=all
|
- AUTOHEAL_CONTAINER_LABEL=all
|
||||||
- AUTOHEAL_INTERVAL=30
|
- AUTOHEAL_INTERVAL=30
|
||||||
- AUTOHEAL_START_PERIOD=60
|
- AUTOHEAL_START_PERIOD=60
|
||||||
volumes:
|
volumes:
|
||||||
- /var/run/docker.sock:/var/run/docker.sock
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
|
||||||
db:
|
db:
|
||||||
image: postgres:16
|
image: postgres:16
|
||||||
container_name: "${POSTGRES_HOST}"
|
container_name: "${POSTGRES_HOST}"
|
||||||
restart: always
|
restart: always
|
||||||
env_file:
|
env_file:
|
||||||
- ./.env
|
- ./.env
|
||||||
networks:
|
networks:
|
||||||
- app-network
|
- app-network
|
||||||
volumes:
|
volumes:
|
||||||
- dbdata:/var/lib/postgresql/data
|
- dbdata:/var/lib/postgresql/data
|
||||||
|
|
||||||
hasura:
|
hasura:
|
||||||
image: hasura/graphql-engine:v2.44.0
|
image: hasura/graphql-engine:v2.44.0
|
||||||
container_name: "${HASURA_HOST}"
|
container_name: "${HASURA_HOST}"
|
||||||
restart: always
|
restart: always
|
||||||
depends_on:
|
depends_on:
|
||||||
- db
|
- db
|
||||||
networks:
|
networks:
|
||||||
- app-network
|
- app-network
|
||||||
environment:
|
environment:
|
||||||
HASURA_GRAPHQL_DATABASE_URL: "postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}"
|
HASURA_GRAPHQL_DATABASE_URL: "postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}"
|
||||||
HASURA_GRAPHQL_ADMIN_SECRET: "${HASURA_GRAPHQL_ADMIN_SECRET}"
|
HASURA_GRAPHQL_ADMIN_SECRET: "${HASURA_GRAPHQL_ADMIN_SECRET}"
|
||||||
HASURA_GRAPHQL_ENABLE_CONSOLE: "false"
|
HASURA_GRAPHQL_ENABLE_CONSOLE: "false"
|
||||||
HASURA_GRAPHQL_DEV_MODE: "false"
|
HASURA_GRAPHQL_DEV_MODE: "false"
|
||||||
HASURA_GRAPHQL_ENABLED_LOG_TYPES: "startup, http-log, webhook-log, websocket-log, query-log"
|
HASURA_GRAPHQL_ENABLED_LOG_TYPES: "startup, http-log, webhook-log, websocket-log, query-log"
|
||||||
|
|
||||||
icecast2:
|
icecast2:
|
||||||
build:
|
build:
|
||||||
context: ./icecast2
|
context: ./icecast2
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
container_name: "${ICECAST_HOST}"
|
container_name: "${ICECAST_HOST}"
|
||||||
restart: always
|
restart: always
|
||||||
networks:
|
networks:
|
||||||
- app-network
|
- app-network
|
||||||
env_file:
|
env_file:
|
||||||
- ./.env
|
- ./.env
|
||||||
volumes:
|
volumes:
|
||||||
- ./icecast2/fallback_music:/music:ro
|
- ./icecast2/fallback_music:/music:ro
|
||||||
ports:
|
ports:
|
||||||
- "${LIQUIDSOAP_HARBOR_PORT:-8001}:${LIQUIDSOAP_HARBOR_PORT:-8001}"
|
- "${LIQUIDSOAP_HARBOR_PORT:-8001}:${LIQUIDSOAP_HARBOR_PORT:-8001}"
|
||||||
|
|
||||||
quartz:
|
quartz:
|
||||||
build:
|
build:
|
||||||
context: ./quartz
|
context: ./quartz
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
container_name: "${QUARTZ_HOST}"
|
container_name: "${QUARTZ_HOST}"
|
||||||
restart: always
|
restart: always
|
||||||
networks:
|
networks:
|
||||||
- app-network
|
- app-network
|
||||||
env_file:
|
env_file:
|
||||||
- ./.env
|
- ./.env
|
||||||
volumes:
|
volumes:
|
||||||
- ${OBSIDIAN_DIR}:/quartz/content:ro
|
- ${OBSIDIAN_DIR}:/quartz/content:ro
|
||||||
|
|
||||||
searxng:
|
searxng:
|
||||||
build:
|
build:
|
||||||
context: ./searxng
|
context: ./searxng
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
container_name: "${SEARXNG_HOST}"
|
container_name: "${SEARXNG_HOST}"
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
networks:
|
networks:
|
||||||
- app-network
|
- app-network
|
||||||
environment:
|
environment:
|
||||||
- BASE_URL=https://www.${DOMAIN}/searxng/
|
- BASE_URL=https://www.${DOMAIN}/searxng/
|
||||||
- INSTANCE_NAME=searxng
|
- INSTANCE_NAME=searxng
|
||||||
- SEARXNG_SECRET_KEY=${SEARXNG_SECRET_KEY}
|
- SEARXNG_SECRET_KEY=${SEARXNG_SECRET_KEY}
|
||||||
volumes:
|
volumes:
|
||||||
- searxng_data:/etc/searxng
|
- searxng_data:/etc/searxng
|
||||||
|
|
||||||
gitea:
|
gitea:
|
||||||
image: docker.gitea.com/gitea:1.25.4-rootless
|
image: docker.gitea.com/gitea:1.25.4-rootless
|
||||||
container_name: "${GITEA_HOST}"
|
container_name: "${GITEA_HOST}"
|
||||||
entrypoint: ["/usr/bin/dumb-init", "--", "/etc/gitea/entrypoint.sh"]
|
entrypoint: ["/usr/bin/dumb-init", "--", "/etc/gitea/entrypoint.sh"]
|
||||||
networks:
|
networks:
|
||||||
- app-network
|
- app-network
|
||||||
environment:
|
environment:
|
||||||
- GITEA__database__DB_TYPE=postgres
|
- GITEA__database__DB_TYPE=postgres
|
||||||
- GITEA__database__HOST=${POSTGRES_HOST}
|
- GITEA__database__HOST=${POSTGRES_HOST}
|
||||||
- GITEA__database__NAME=${POSTGRES_GITEA_DB}
|
- GITEA__database__NAME=${POSTGRES_GITEA_DB}
|
||||||
- GITEA__database__USER=${POSTGRES_USER}
|
- GITEA__database__USER=${POSTGRES_USER}
|
||||||
- GITEA__database__PASSWD=${POSTGRES_PASSWORD}
|
- GITEA__database__PASSWD=${POSTGRES_PASSWORD}
|
||||||
- GITEA__server__LFS_JWT_SECRET=${GITEA_LFS_JWT_SECRET}
|
- GITEA__server__LFS_JWT_SECRET=${GITEA_LFS_JWT_SECRET}
|
||||||
- GITEA__security__INTERNAL_TOKEN=${GITEA_INTERNAL_TOKEN}
|
- GITEA__security__INTERNAL_TOKEN=${GITEA_INTERNAL_TOKEN}
|
||||||
- GITEA__oauth2__JWT_SECRET=${GITEA_OAUTH2_JWT_SECRET}
|
- GITEA__oauth2__JWT_SECRET=${GITEA_OAUTH2_JWT_SECRET}
|
||||||
- USER_UID=1000
|
- USER_UID=1000
|
||||||
- USER_GID=1000
|
- USER_GID=1000
|
||||||
restart: always
|
restart: always
|
||||||
volumes:
|
volumes:
|
||||||
- ./gitea/data:/var/lib/gitea
|
- ./gitea/data:/var/lib/gitea
|
||||||
- ./gitea/config:/etc/gitea
|
- ./gitea/config:/etc/gitea
|
||||||
- ./gitea/entrypoint.sh:/etc/gitea/entrypoint.sh:ro
|
- ./gitea/entrypoint.sh:/etc/gitea/entrypoint.sh:ro
|
||||||
- /etc/timezone:/etc/timezone:ro
|
- /etc/timezone:/etc/timezone:ro
|
||||||
- /etc/localtime:/etc/localtime:ro
|
- /etc/localtime:/etc/localtime:ro
|
||||||
ports:
|
ports:
|
||||||
- "2222:2222"
|
- "2222:2222"
|
||||||
- "3000:3000"
|
- "3000:3000"
|
||||||
depends_on:
|
depends_on:
|
||||||
- db
|
- db
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
# Check if dev mode, certificate exists, or setup mode
|
# Check if DEV_MODE
|
||||||
if [ "$DEV_MODE" = "true" ]; then
|
if [ "$DEV_MODE" = "true" ]; then
|
||||||
echo "Dev mode. Generating self-signed certificate for HTTPS."
|
echo "Dev mode. Generating self-signed certificate for HTTPS."
|
||||||
CERT_DIR="/etc/letsencrypt/live/localhost"
|
CERT_DIR="/etc/letsencrypt/live/localhost"
|
||||||
@@ -12,16 +12,19 @@ if [ "$DEV_MODE" = "true" ]; then
|
|||||||
-out "$CERT_DIR/fullchain.pem" \
|
-out "$CERT_DIR/fullchain.pem" \
|
||||||
-subj "/CN=localhost" 2>/dev/null
|
-subj "/CN=localhost" 2>/dev/null
|
||||||
fi
|
fi
|
||||||
|
# In dev mode, so use nginx_dev.conf.template
|
||||||
envsubst '${DOMAIN} ${BACKEND_HOST} ${BACKEND_PORT} ${BACKEND_ENDPOINT} ${ICECAST_HOST} ${ICECAST_PORT} ${GITEA_HOST} ${GITEA_PORT} ${HASURA_HOST} ${HASURA_PORT} ${QUARTZ_HOST} ${QUARTZ_PORT} ${UPTIMEKUMA_HOST} ${UPTIMEKUMA_PORT} ${SEARXNG_HOST} ${SEARXNG_PORT} ${WALLABAG_HOST} ${WALLABAG_PORT}' \
|
envsubst '${DOMAIN} ${BACKEND_HOST} ${BACKEND_PORT} ${BACKEND_ENDPOINT} ${ICECAST_HOST} ${ICECAST_PORT} ${GITEA_HOST} ${GITEA_PORT} ${HASURA_HOST} ${HASURA_PORT} ${QUARTZ_HOST} ${QUARTZ_PORT} ${UPTIMEKUMA_HOST} ${UPTIMEKUMA_PORT} ${SEARXNG_HOST} ${SEARXNG_PORT} ${WALLABAG_HOST} ${WALLABAG_PORT}' \
|
||||||
</etc/nginx/nginx_dev.conf.template \
|
</etc/nginx/nginx_dev.conf.template \
|
||||||
>/etc/nginx/nginx.conf
|
>/etc/nginx/nginx.conf
|
||||||
elif [ -f "/etc/letsencrypt/live/$DOMAIN/fullchain.pem" ] && [ -f "/etc/letsencrypt/live/$DOMAIN/privkey.pem" ]; then
|
elif [ -f "/etc/letsencrypt/live/$DOMAIN/fullchain.pem" ] && [ -f "/etc/letsencrypt/live/$DOMAIN/privkey.pem" ]; then
|
||||||
echo "Certificates found. Using production nginx config."
|
echo "Certificates found. Using production nginx config."
|
||||||
|
# In production with certificates already existing, so use nginx.conf.template
|
||||||
envsubst '${DOMAIN} ${BACKEND_HOST} ${BACKEND_PORT} ${BACKEND_ENDPOINT} ${ICECAST_HOST} ${ICECAST_PORT} ${GITEA_HOST} ${GITEA_PORT} ${HASURA_HOST} ${HASURA_PORT} ${QUARTZ_HOST} ${QUARTZ_PORT} ${UPTIMEKUMA_HOST} ${UPTIMEKUMA_PORT} ${SEARXNG_HOST} ${SEARXNG_PORT} ${WALLABAG_HOST} ${WALLABAG_PORT}' \
|
envsubst '${DOMAIN} ${BACKEND_HOST} ${BACKEND_PORT} ${BACKEND_ENDPOINT} ${ICECAST_HOST} ${ICECAST_PORT} ${GITEA_HOST} ${GITEA_PORT} ${HASURA_HOST} ${HASURA_PORT} ${QUARTZ_HOST} ${QUARTZ_PORT} ${UPTIMEKUMA_HOST} ${UPTIMEKUMA_PORT} ${SEARXNG_HOST} ${SEARXNG_PORT} ${WALLABAG_HOST} ${WALLABAG_PORT}' \
|
||||||
</etc/nginx/nginx.conf.template \
|
</etc/nginx/nginx.conf.template \
|
||||||
>/etc/nginx/nginx.conf
|
>/etc/nginx/nginx.conf
|
||||||
else
|
else
|
||||||
echo "Certificates NOT found. Using setup nginx config."
|
echo "Certificates NOT found. Using setup nginx config."
|
||||||
|
# In production with no certificates, so use nginx_setup.conf.template and will need restart after generation
|
||||||
envsubst '${DOMAIN}' </etc/nginx/nginx_setup.conf.template >/etc/nginx/nginx.conf
|
envsubst '${DOMAIN}' </etc/nginx/nginx_setup.conf.template >/etc/nginx/nginx.conf
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
@@ -1,33 +1,33 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<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" />
|
||||||
<meta name="description" content="Adam French's personal website">
|
<meta name="description" content="Adam French's personal website" />
|
||||||
<title>AF</title>
|
<title>AF</title>
|
||||||
<link rel="preconnect" href="https://i.scdn.co" crossorigin>
|
<link rel="preconnect" href="https://i.scdn.co" crossorigin />
|
||||||
<link
|
<link
|
||||||
rel="preconnect"
|
rel="preconnect"
|
||||||
href="https://cdn.akamai.steamstatic.com"
|
href="https://cdn.akamai.steamstatic.com"
|
||||||
crossorigin
|
crossorigin
|
||||||
>
|
/>
|
||||||
<link rel="icon" type="/img/x-icon" href="/img/favicon.ico">
|
<link rel="icon" type="/img/x-icon" href="/img/favicon.ico" />
|
||||||
<link
|
<link
|
||||||
rel="preload"
|
rel="preload"
|
||||||
href="/fonts/big_noodle_titling.woff2"
|
href="/fonts/big_noodle_titling.woff2"
|
||||||
as="font"
|
as="font"
|
||||||
type="font/woff2"
|
type="font/woff2"
|
||||||
crossorigin
|
crossorigin
|
||||||
>
|
/>
|
||||||
<link
|
<link
|
||||||
rel="preload"
|
rel="preload"
|
||||||
href="/fonts/CreatoDisplay-Bold.woff2"
|
href="/fonts/CreatoDisplay-Bold.woff2"
|
||||||
as="font"
|
as="font"
|
||||||
type="font/woff2"
|
type="font/woff2"
|
||||||
crossorigin
|
crossorigin
|
||||||
>
|
/>
|
||||||
</head>
|
</head>
|
||||||
<body id="app">
|
<body id="app">
|
||||||
<script type="module" src="/src/main.js"></script>
|
<script type="module" src="/src/main.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -3,5 +3,5 @@ import { RouterView } from "vue-router";
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<RouterView />
|
<RouterView />
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -5,128 +5,128 @@ const clock = ref("");
|
|||||||
let timer;
|
let timer;
|
||||||
|
|
||||||
function updateClock() {
|
function updateClock() {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
clock.value = now.toLocaleDateString("en-US", {
|
clock.value = now.toLocaleDateString("en-US", {
|
||||||
weekday: "short",
|
weekday: "short",
|
||||||
month: "short",
|
month: "short",
|
||||||
day: "numeric",
|
day: "numeric",
|
||||||
hour: "2-digit",
|
hour: "2-digit",
|
||||||
minute: "2-digit",
|
minute: "2-digit",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
updateClock();
|
updateClock();
|
||||||
timer = setInterval(updateClock, 1000);
|
timer = setInterval(updateClock, 1000);
|
||||||
});
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
clearInterval(timer);
|
clearInterval(timer);
|
||||||
});
|
});
|
||||||
|
|
||||||
const user = "visitor";
|
const user = "visitor";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<footer class="waybar">
|
<footer class="waybar">
|
||||||
<div class="modules-left">
|
<div class="modules-left">
|
||||||
<span class="workspace active">ツ</span>
|
<span class="workspace active">ツ</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="modules-right">
|
<div class="modules-right">
|
||||||
<span class="module greeting">Hi, {{ user }}!</span>
|
<span class="module greeting">Hi, {{ user }}!</span>
|
||||||
<span class="module cpu hide-sm">CPU 3%</span>
|
<span class="module cpu hide-sm">CPU 3%</span>
|
||||||
<span class="module mem hide-sm">MEM 42%</span>
|
<span class="module mem hide-sm">MEM 42%</span>
|
||||||
<span class="module disk hide-sm">DISK 67%</span>
|
<span class="module disk hide-sm">DISK 67%</span>
|
||||||
<span class="module network hide-sm">↑ 12K ↓ 84K</span>
|
<span class="module network hide-sm">↑ 12K ↓ 84K</span>
|
||||||
<span class="module battery hide-sm">BAT 98%</span>
|
<span class="module battery hide-sm">BAT 98%</span>
|
||||||
<span class="module clock">{{ clock }}</span>
|
<span class="module clock">{{ clock }}</span>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.waybar {
|
.waybar {
|
||||||
font-family: "URWGothic-Book", monospace;
|
font-family: "URWGothic-Book", monospace;
|
||||||
background-color: var(--bg_primary);
|
background-color: var(--bg_primary);
|
||||||
color: var(--primary);
|
color: var(--primary);
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 4px 8px;
|
padding: 4px 8px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
min-height: 36px;
|
min-height: 36px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modules-left {
|
.modules-left {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 2px;
|
gap: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.workspace {
|
.workspace {
|
||||||
background: var(--quaternary);
|
background: var(--quaternary);
|
||||||
border: none;
|
border: none;
|
||||||
border-bottom: 2px solid var(--secondary);
|
border-bottom: 2px solid var(--secondary);
|
||||||
color: var(--secondary);
|
color: var(--secondary);
|
||||||
padding: 2px 10px;
|
padding: 2px 10px;
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modules-right {
|
.modules-right {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.module {
|
.module {
|
||||||
padding: 2px 12px;
|
padding: 2px 12px;
|
||||||
border-left: 1px solid var(--tertiary);
|
border-left: 1px solid var(--tertiary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.module:first-child {
|
.module:first-child {
|
||||||
border-left: none;
|
border-left: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.greeting {
|
.greeting {
|
||||||
color: var(--secondary);
|
color: var(--secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.clock {
|
.clock {
|
||||||
color: var(--tertiary);
|
color: var(--tertiary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.cpu,
|
.cpu,
|
||||||
.mem,
|
.mem,
|
||||||
.disk {
|
.disk {
|
||||||
color: var(--primary);
|
color: var(--primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.network {
|
.network {
|
||||||
color: var(--secondary);
|
color: var(--secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.battery {
|
.battery {
|
||||||
color: var(--primary);
|
color: var(--primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 800px) {
|
@media (max-width: 800px) {
|
||||||
.waybar {
|
.waybar {
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
padding: 4px 4px;
|
padding: 4px 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.workspace {
|
.workspace {
|
||||||
padding: 2px 6px;
|
padding: 2px 6px;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.module {
|
.module {
|
||||||
padding: 2px 6px;
|
padding: 2px 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hide-sm {
|
.hide-sm {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -6,55 +6,55 @@ import { useRoute } from "vue-router";
|
|||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
|
|
||||||
const parentPath = computed(() => {
|
const parentPath = computed(() => {
|
||||||
const segments = route.path.split("/").filter(Boolean);
|
const segments = route.path.split("/").filter(Boolean);
|
||||||
if (segments.length == 1) {
|
if (segments.length == 1) {
|
||||||
return "/";
|
return "/";
|
||||||
} else {
|
} else {
|
||||||
segments.pop();
|
segments.pop();
|
||||||
return segments.length ? "/" + segments.join("/") : null;
|
return segments.length ? "/" + segments.join("/") : null;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const faces = [
|
const faces = [
|
||||||
"^_^",
|
"^_^",
|
||||||
"¯\\_(ツ)_/¯",
|
"¯\\_(ツ)_/¯",
|
||||||
"(◕‿◕✿)",
|
"(◕‿◕✿)",
|
||||||
"ಠ_ಠ",
|
"ಠ_ಠ",
|
||||||
"ʘ‿ʘ",
|
"ʘ‿ʘ",
|
||||||
"^̮^",
|
"^̮^",
|
||||||
">_>",
|
">_>",
|
||||||
"¬_¬",
|
"¬_¬",
|
||||||
"˙ ͜ʟ˙",
|
"˙ ͜ʟ˙",
|
||||||
"( ͡° ͜ʖ ͡°)",
|
"( ͡° ͜ʖ ͡°)",
|
||||||
"[̲̅$̲̅(̲̅5̲̅)̲̅$̲̅]",
|
"[̲̅$̲̅(̲̅5̲̅)̲̅$̲̅]",
|
||||||
"(ง'̀-'́)ง",
|
"(ง'̀-'́)ง",
|
||||||
"\ (•◡•) /",
|
"\ (•◡•) /",
|
||||||
"( ͡ᵔ ͜ʖ ͡ᵔ )",
|
"( ͡ᵔ ͜ʖ ͡ᵔ )",
|
||||||
"ᕙ(⇀‸↼‶)ᕗ",
|
"ᕙ(⇀‸↼‶)ᕗ",
|
||||||
"⚆ _ ⚆",
|
"⚆ _ ⚆",
|
||||||
"(。◕‿◕。)",
|
"(。◕‿◕。)",
|
||||||
"(╯°□°)╯︵ ʞooqǝɔɐɟ",
|
"(╯°□°)╯︵ ʞooqǝɔɐɟ",
|
||||||
"̿ ̿ ̿'̿'\̵͇̿̿\з=(•_•)=ε/̵͇̿̿/'̿'̿ ̿",
|
"̿ ̿ ̿'̿'\̵͇̿̿\з=(•_•)=ε/̵͇̿̿/'̿'̿ ̿",
|
||||||
"(☞゚ヮ゚)☞ ☜(゚ヮ゚☜)",
|
"(☞゚ヮ゚)☞ ☜(゚ヮ゚☜)",
|
||||||
];
|
];
|
||||||
const faces_string = faces.join(" ");
|
const faces_string = faces.join(" ");
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<nav class="flex flex-row w-full h-fit border border-primary bg-bg_primary">
|
<nav class="flex flex-row w-full h-fit border border-primary bg-bg_primary">
|
||||||
<RouterLink class="bdr-2 bg-bg_primary" v-if="parentPath" :to="parentPath">
|
<RouterLink class="bdr-2 bg-bg_primary" v-if="parentPath" :to="parentPath">
|
||||||
<span>UP</span>
|
<span>UP</span>
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
<Headline class="border flex-1 max-w-full">
|
<Headline class="border flex-1 max-w-full">
|
||||||
<code class="whitespace-pre">{{ faces_string }}</code>
|
<code class="whitespace-pre">{{ faces_string }}</code>
|
||||||
</Headline>
|
</Headline>
|
||||||
</nav>
|
</nav>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.left {
|
.left {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,57 +1,57 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<img src="/img/borders/utena.png" class="flower tl antirotate" />
|
<img src="/img/borders/utena.png" class="flower tl antirotate" />
|
||||||
<img src="/img/borders/utena.png" class="flower tr rotate" />
|
<img src="/img/borders/utena.png" class="flower tr rotate" />
|
||||||
<img src="/img/borders/utena.png" class="flower bl rotate" />
|
<img src="/img/borders/utena.png" class="flower bl rotate" />
|
||||||
<img src="/img/borders/utena.png" class="flower br antirotate" />
|
<img src="/img/borders/utena.png" class="flower br antirotate" />
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.container {
|
.container {
|
||||||
position: relative;
|
position: relative;
|
||||||
margin: 100px;
|
margin: 100px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.flower {
|
.flower {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 150px;
|
width: 150px;
|
||||||
}
|
}
|
||||||
.tl {
|
.tl {
|
||||||
top: -80px;
|
top: -80px;
|
||||||
left: -80px;
|
left: -80px;
|
||||||
--start: 0deg;
|
--start: 0deg;
|
||||||
}
|
}
|
||||||
.tr {
|
.tr {
|
||||||
top: -80px;
|
top: -80px;
|
||||||
right: -80px;
|
right: -80px;
|
||||||
--start: 90deg;
|
--start: 90deg;
|
||||||
}
|
}
|
||||||
.bl {
|
.bl {
|
||||||
bottom: -80px;
|
bottom: -80px;
|
||||||
left: -80px;
|
left: -80px;
|
||||||
--start: 180deg;
|
--start: 180deg;
|
||||||
}
|
}
|
||||||
.br {
|
.br {
|
||||||
bottom: -80px;
|
bottom: -80px;
|
||||||
right: -80px;
|
right: -80px;
|
||||||
--start: 270deg;
|
--start: 270deg;
|
||||||
}
|
}
|
||||||
.rotate {
|
.rotate {
|
||||||
animation: spin 3s linear infinite;
|
animation: spin 3s linear infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
.antirotate {
|
.antirotate {
|
||||||
animation: spin 3s linear infinite reverse;
|
animation: spin 3s linear infinite reverse;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes spin {
|
@keyframes spin {
|
||||||
from {
|
from {
|
||||||
transform: rotate(var(--start));
|
transform: rotate(var(--start));
|
||||||
}
|
}
|
||||||
to {
|
to {
|
||||||
transform: rotate(calc(var(--start) + 360deg));
|
transform: rotate(calc(var(--start) + 360deg));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -6,11 +6,11 @@ const container = ref(null);
|
|||||||
|
|
||||||
// List of (offset, width)
|
// List of (offset, width)
|
||||||
function generateOffsets(width = 100, step = 10, n = 20) {
|
function generateOffsets(width = 100, step = 10, n = 20) {
|
||||||
return Array.from({ length: n }, (_, i) => ({
|
return Array.from({ length: n }, (_, i) => ({
|
||||||
width,
|
width,
|
||||||
offset: step * i,
|
offset: step * i,
|
||||||
color: getRandomColor(),
|
color: getRandomColor(),
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
const offsets = ref(generateOffsets((150, 15, 10)));
|
const offsets = ref(generateOffsets((150, 15, 10)));
|
||||||
let rafId;
|
let rafId;
|
||||||
@@ -18,71 +18,71 @@ let rafId;
|
|||||||
const speed = 0.5; // pixels per frame
|
const speed = 0.5; // pixels per frame
|
||||||
|
|
||||||
function animate() {
|
function animate() {
|
||||||
const ctnr = container.value;
|
const ctnr = container.value;
|
||||||
for (const item of offsets.value) {
|
for (const item of offsets.value) {
|
||||||
const width = Math.max(ctnr.offsetWidth, item.width);
|
const width = Math.max(ctnr.offsetWidth, item.width);
|
||||||
|
|
||||||
console.log(ctnr.offsetWidth);
|
console.log(ctnr.offsetWidth);
|
||||||
|
|
||||||
item.offset -= speed;
|
item.offset -= speed;
|
||||||
if (item.offset <= -width) {
|
if (item.offset <= -width) {
|
||||||
item.color = getRandomColor();
|
item.color = getRandomColor();
|
||||||
item.offset = 0;
|
item.offset = 0;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
rafId = requestAnimationFrame(animate);
|
}
|
||||||
|
rafId = requestAnimationFrame(animate);
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
rafId = requestAnimationFrame(animate);
|
rafId = requestAnimationFrame(animate);
|
||||||
});
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
cancelAnimationFrame(rafId);
|
cancelAnimationFrame(rafId);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="bg-primary container" ref="container">
|
<div class="bg-primary container" ref="container">
|
||||||
<div :key="index" v-for="(item, index) in offsets">
|
<div :key="index" v-for="(item, index) in offsets">
|
||||||
<div
|
<div
|
||||||
:style="{
|
:style="{
|
||||||
width: item.width + 'px',
|
width: item.width + 'px',
|
||||||
translate: item.offset + 'px',
|
translate: item.offset + 'px',
|
||||||
backgroundColor: item.color,
|
backgroundColor: item.color,
|
||||||
}"
|
}"
|
||||||
class="item item1"
|
class="item item1"
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
:style="{
|
:style="{
|
||||||
width: item.width + 'px',
|
width: item.width + 'px',
|
||||||
right: -item.width + 'px',
|
right: -item.width + 'px',
|
||||||
translate: item.offset + 'px',
|
translate: item.offset + 'px',
|
||||||
backgroundColor: item.color,
|
backgroundColor: item.color,
|
||||||
}"
|
}"
|
||||||
class="item item2"
|
class="item item2"
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.container {
|
.container {
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
will-change: transform;
|
will-change: transform;
|
||||||
}
|
}
|
||||||
.item {
|
.item {
|
||||||
opacity: 40%;
|
opacity: 40%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
}
|
}
|
||||||
.item1 {
|
.item1 {
|
||||||
left: 0px;
|
left: 0px;
|
||||||
top: 0px;
|
top: 0px;
|
||||||
}
|
}
|
||||||
.item2 {
|
.item2 {
|
||||||
top: 0px;
|
top: 0px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
<script setup></script>
|
<script setup></script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<button
|
<button
|
||||||
class="text-primary bg-link text-center border cursor-pointer transition-colors duration-150 ease-in-out hover:bg-bg_primary active:scale-95"
|
class="text-primary bg-link text-center border cursor-pointer transition-colors duration-150 ease-in-out hover:bg-bg_primary active:scale-95"
|
||||||
>
|
>
|
||||||
<slot />
|
<slot />
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped></style>
|
<style scoped></style>
|
||||||
|
|||||||
@@ -2,30 +2,30 @@
|
|||||||
import { ref } from "vue";
|
import { ref } from "vue";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
modelValue: {
|
modelValue: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits(["update:modelValue"]);
|
const emit = defineEmits(["update:modelValue"]);
|
||||||
|
|
||||||
function toggle() {
|
function toggle() {
|
||||||
emit("update:modelValue", !props.modelValue);
|
emit("update:modelValue", !props.modelValue);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<button
|
<button
|
||||||
@click="toggle"
|
@click="toggle"
|
||||||
class="box-content border-2 border-primary w-10 h-fit rounded-full cursor-pointer"
|
class="box-content border-2 border-primary w-10 h-fit rounded-full cursor-pointer"
|
||||||
:class="[props.modelValue ? 'bg-bg_secondary' : 'bg-bg_primary']"
|
:class="[props.modelValue ? 'bg-bg_secondary' : 'bg-bg_primary']"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 40 40"
|
||||||
|
class="w-5 h-5 transition-all duration-300 ease-in-out"
|
||||||
|
:class="[props.modelValue ? 'ml-5' : 'ml-0']"
|
||||||
>
|
>
|
||||||
<svg
|
<circle class="fill-primary" cx="20" cy="20" r="20" />
|
||||||
viewBox="0 0 40 40"
|
</svg>
|
||||||
class="w-5 h-5 transition-all duration-300 ease-in-out"
|
</button>
|
||||||
:class="[props.modelValue ? 'ml-5' : 'ml-0']"
|
|
||||||
>
|
|
||||||
<circle class="fill-primary" cx="20" cy="20" r="20" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="w-full border-b border-primary pb-0.5">
|
<div class="w-full border-b border-primary pb-0.5">
|
||||||
<h1 class="p-0 m-0">
|
<h1 class="p-0 m-0">
|
||||||
<slot />
|
<slot />
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -12,74 +12,74 @@ let rafId;
|
|||||||
const speed = 0.5; // pixels per frame
|
const speed = 0.5; // pixels per frame
|
||||||
|
|
||||||
function measureWidth() {
|
function measureWidth() {
|
||||||
const ctnr = container.value;
|
const ctnr = container.value;
|
||||||
const it1 = item1.value;
|
const it1 = item1.value;
|
||||||
if (ctnr && it1) {
|
if (ctnr && it1) {
|
||||||
cachedWidth = Math.max(ctnr.offsetWidth, it1.scrollWidth);
|
cachedWidth = Math.max(ctnr.offsetWidth, it1.scrollWidth);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function animate() {
|
function animate() {
|
||||||
const ctnr = container.value;
|
const ctnr = container.value;
|
||||||
if (!ctnr || cachedWidth === 0) {
|
if (!ctnr || cachedWidth === 0) {
|
||||||
rafId = requestAnimationFrame(animate);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
offset -= speed;
|
|
||||||
|
|
||||||
if (offset <= -cachedWidth) {
|
|
||||||
offset += cachedWidth;
|
|
||||||
}
|
|
||||||
|
|
||||||
ctnr.style.transform = `translateX(${offset}px)`;
|
|
||||||
|
|
||||||
rafId = requestAnimationFrame(animate);
|
rafId = requestAnimationFrame(animate);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
offset -= speed;
|
||||||
|
|
||||||
|
if (offset <= -cachedWidth) {
|
||||||
|
offset += cachedWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctnr.style.transform = `translateX(${offset}px)`;
|
||||||
|
|
||||||
|
rafId = requestAnimationFrame(animate);
|
||||||
}
|
}
|
||||||
|
|
||||||
let resizeObserver;
|
let resizeObserver;
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
measureWidth();
|
measureWidth();
|
||||||
rafId = requestAnimationFrame(animate);
|
rafId = requestAnimationFrame(animate);
|
||||||
|
|
||||||
resizeObserver = new ResizeObserver(measureWidth);
|
resizeObserver = new ResizeObserver(measureWidth);
|
||||||
resizeObserver.observe(container.value);
|
resizeObserver.observe(container.value);
|
||||||
});
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
cancelAnimationFrame(rafId);
|
cancelAnimationFrame(rafId);
|
||||||
resizeObserver?.disconnect();
|
resizeObserver?.disconnect();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="root">
|
<div class="root">
|
||||||
<div class="container" ref="container">
|
<div class="container" ref="container">
|
||||||
<div ref="item1">
|
<div ref="item1">
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.root {
|
.root {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
width: fit-content;
|
width: fit-content;
|
||||||
height: fit-content;
|
height: fit-content;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-auto-flow: column;
|
grid-auto-flow: column;
|
||||||
grid-auto-columns: max-content;
|
grid-auto-columns: max-content;
|
||||||
/* Each column fits its content */
|
/* Each column fits its content */
|
||||||
overflow-x: visible;
|
overflow-x: visible;
|
||||||
will-change: transform;
|
will-change: transform;
|
||||||
gap: 10em;
|
gap: 10em;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -2,38 +2,44 @@
|
|||||||
import { computed } from "vue";
|
import { computed } from "vue";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
href: { type: String, default: "" },
|
href: { type: String, default: "" },
|
||||||
to: { type: String, default: "" },
|
to: { type: String, default: "" },
|
||||||
target: { type: String, default: undefined },
|
target: { type: String, default: undefined },
|
||||||
rel: { type: String, default: undefined },
|
rel: { type: String, default: undefined },
|
||||||
});
|
});
|
||||||
|
|
||||||
const computedRel = computed(() => {
|
const computedRel = computed(() => {
|
||||||
if (props.rel !== undefined) return props.rel;
|
if (props.rel !== undefined) return props.rel;
|
||||||
if (props.target === "_blank") return "noopener noreferrer";
|
if (props.target === "_blank") return "noopener noreferrer";
|
||||||
return undefined;
|
return undefined;
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<RouterLink v-if="to" :to="to" class="inline-link">
|
<RouterLink v-if="to" :to="to" class="inline-link">
|
||||||
<slot />
|
<slot />
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
<a v-else :href="href" :target="target" :rel="computedRel" class="inline-link">
|
<a
|
||||||
<slot />
|
v-else
|
||||||
</a>
|
:href="href"
|
||||||
|
:target="target"
|
||||||
|
:rel="computedRel"
|
||||||
|
class="inline-link"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</a>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.inline-link {
|
.inline-link {
|
||||||
color: var(--primary);
|
color: var(--primary);
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
transition: color 0.15s ease;
|
transition: color 0.15s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.inline-link:hover {
|
.inline-link:hover {
|
||||||
color: var(--tertiary);
|
color: var(--tertiary);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -2,37 +2,43 @@
|
|||||||
import { computed } from "vue";
|
import { computed } from "vue";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
href: { type: String, default: "" },
|
href: { type: String, default: "" },
|
||||||
to: { type: String, default: "" },
|
to: { type: String, default: "" },
|
||||||
target: { type: String, default: undefined },
|
target: { type: String, default: undefined },
|
||||||
rel: { type: String, default: undefined },
|
rel: { type: String, default: undefined },
|
||||||
bare: { type: Boolean, default: false },
|
bare: { type: Boolean, default: false },
|
||||||
});
|
});
|
||||||
|
|
||||||
const computedRel = computed(() => {
|
const computedRel = computed(() => {
|
||||||
if (props.rel !== undefined) return props.rel;
|
if (props.rel !== undefined) return props.rel;
|
||||||
if (props.target === "_blank") return "noopener noreferrer";
|
if (props.target === "_blank") return "noopener noreferrer";
|
||||||
return undefined;
|
return undefined;
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<RouterLink v-if="to" :to="to" :class="{ link: !bare }">
|
<RouterLink v-if="to" :to="to" :class="{ link: !bare }">
|
||||||
<slot />
|
<slot />
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
<a v-else :href="href" :target="target" :rel="computedRel" :class="{ link: !bare }">
|
<a
|
||||||
<slot />
|
v-else
|
||||||
</a>
|
:href="href"
|
||||||
|
:target="target"
|
||||||
|
:rel="computedRel"
|
||||||
|
:class="{ link: !bare }"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</a>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.link {
|
.link {
|
||||||
color: var(--primary);
|
color: var(--primary);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
transition: color 0.15s ease;
|
transition: color 0.15s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.link:hover {
|
.link:hover {
|
||||||
color: var(--tertiary);
|
color: var(--tertiary);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<p class="p-1">
|
<p class="p-1">
|
||||||
<slot />
|
<slot />
|
||||||
</p>
|
</p>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -3,36 +3,36 @@ import { ref } from "vue";
|
|||||||
import ToggleButton from "@/components/input/ToggleButton.vue";
|
import ToggleButton from "@/components/input/ToggleButton.vue";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
modelValue: {
|
modelValue: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits(["update:modelValue"]);
|
const emit = defineEmits(["update:modelValue"]);
|
||||||
const toggleButtonRef = ref(null);
|
const toggleButtonRef = ref(null);
|
||||||
|
|
||||||
const updateValue = (newValue) => {
|
const updateValue = (newValue) => {
|
||||||
emit("update:modelValue", newValue);
|
emit("update:modelValue", newValue);
|
||||||
};
|
};
|
||||||
const handleClick = () => {
|
const handleClick = () => {
|
||||||
toggleButtonRef.value?.$el?.click();
|
toggleButtonRef.value?.$el?.click();
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="w-full border-b border-primary cursor-pointer"
|
class="w-full border-b border-primary cursor-pointer"
|
||||||
@click="handleClick"
|
@click="handleClick"
|
||||||
>
|
>
|
||||||
<h3 class="pl-2 m-0">
|
<h3 class="pl-2 m-0">
|
||||||
<slot />
|
<slot />
|
||||||
</h3>
|
</h3>
|
||||||
<ToggleButton
|
<ToggleButton
|
||||||
class="pointer-events-none"
|
class="pointer-events-none"
|
||||||
:model-value="props.modelValue"
|
:model-value="props.modelValue"
|
||||||
@update:model-value="updateValue"
|
@update:model-value="updateValue"
|
||||||
@click.stop
|
@click.stop
|
||||||
ref="toggleButtonRef"
|
ref="toggleButtonRef"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<div ref="container" @mouseenter="onMouseEnter" @mouseleave="onMouseLeave" class="overflow-y-auto">
|
<div
|
||||||
<slot />
|
ref="container"
|
||||||
</div>
|
@mouseenter="onMouseEnter"
|
||||||
|
@mouseleave="onMouseLeave"
|
||||||
|
class="overflow-y-auto"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
@@ -20,89 +25,89 @@ let pauseTimeoutId = null;
|
|||||||
let cachedScrollHeight = 0;
|
let cachedScrollHeight = 0;
|
||||||
|
|
||||||
function measureScrollHeight() {
|
function measureScrollHeight() {
|
||||||
const el = container.value;
|
const el = container.value;
|
||||||
if (el) cachedScrollHeight = el.scrollHeight;
|
if (el) cachedScrollHeight = el.scrollHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
function stopLoop() {
|
function stopLoop() {
|
||||||
if (rafId !== null) {
|
if (rafId !== null) {
|
||||||
cancelAnimationFrame(rafId);
|
cancelAnimationFrame(rafId);
|
||||||
rafId = null;
|
rafId = null;
|
||||||
}
|
}
|
||||||
if (pauseTimeoutId !== null) {
|
if (pauseTimeoutId !== null) {
|
||||||
clearTimeout(pauseTimeoutId);
|
clearTimeout(pauseTimeoutId);
|
||||||
pauseTimeoutId = null;
|
pauseTimeoutId = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function startLoop() {
|
function startLoop() {
|
||||||
stopLoop();
|
stopLoop();
|
||||||
rafId = requestAnimationFrame(tick);
|
rafId = requestAnimationFrame(tick);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onMouseEnter() {
|
function onMouseEnter() {
|
||||||
hovered = true;
|
hovered = true;
|
||||||
stopLoop();
|
stopLoop();
|
||||||
}
|
}
|
||||||
|
|
||||||
function onMouseLeave() {
|
function onMouseLeave() {
|
||||||
hovered = false;
|
hovered = false;
|
||||||
const el = container.value;
|
const el = container.value;
|
||||||
if (el && cachedScrollHeight > 0) {
|
if (el && cachedScrollHeight > 0) {
|
||||||
pos = el.scrollTop / cachedScrollHeight;
|
pos = el.scrollTop / cachedScrollHeight;
|
||||||
}
|
}
|
||||||
startLoop();
|
startLoop();
|
||||||
}
|
}
|
||||||
|
|
||||||
function schedulePause(callback) {
|
function schedulePause(callback) {
|
||||||
stopLoop();
|
stopLoop();
|
||||||
pauseTimeoutId = setTimeout(callback, PAUSE);
|
pauseTimeoutId = setTimeout(callback, PAUSE);
|
||||||
}
|
}
|
||||||
|
|
||||||
function tick() {
|
function tick() {
|
||||||
rafId = null;
|
rafId = null;
|
||||||
const el = container.value;
|
const el = container.value;
|
||||||
if (hovered) return;
|
if (hovered) return;
|
||||||
|
|
||||||
if (!el || cachedScrollHeight === 0) {
|
|
||||||
rafId = requestAnimationFrame(tick);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const reachedBottom = pos >= 1;
|
|
||||||
const reachedTop = pos <= 0;
|
|
||||||
|
|
||||||
if (reachedBottom) {
|
|
||||||
pos = 0.999;
|
|
||||||
direction = -1;
|
|
||||||
schedulePause(startLoop);
|
|
||||||
return;
|
|
||||||
} else if (reachedTop && direction === -1) {
|
|
||||||
pos = 0.001;
|
|
||||||
direction = 1;
|
|
||||||
schedulePause(startLoop);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
pos += direction * SPEED;
|
|
||||||
|
|
||||||
el.scrollTop = pos * cachedScrollHeight;
|
|
||||||
|
|
||||||
|
if (!el || cachedScrollHeight === 0) {
|
||||||
rafId = requestAnimationFrame(tick);
|
rafId = requestAnimationFrame(tick);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const reachedBottom = pos >= 1;
|
||||||
|
const reachedTop = pos <= 0;
|
||||||
|
|
||||||
|
if (reachedBottom) {
|
||||||
|
pos = 0.999;
|
||||||
|
direction = -1;
|
||||||
|
schedulePause(startLoop);
|
||||||
|
return;
|
||||||
|
} else if (reachedTop && direction === -1) {
|
||||||
|
pos = 0.001;
|
||||||
|
direction = 1;
|
||||||
|
schedulePause(startLoop);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
pos += direction * SPEED;
|
||||||
|
|
||||||
|
el.scrollTop = pos * cachedScrollHeight;
|
||||||
|
|
||||||
|
rafId = requestAnimationFrame(tick);
|
||||||
}
|
}
|
||||||
|
|
||||||
let resizeObserver;
|
let resizeObserver;
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
measureScrollHeight();
|
measureScrollHeight();
|
||||||
schedulePause(startLoop);
|
schedulePause(startLoop);
|
||||||
|
|
||||||
resizeObserver = new ResizeObserver(measureScrollHeight);
|
resizeObserver = new ResizeObserver(measureScrollHeight);
|
||||||
resizeObserver.observe(container.value);
|
resizeObserver.observe(container.value);
|
||||||
});
|
});
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
stopLoop();
|
stopLoop();
|
||||||
resizeObserver?.disconnect();
|
resizeObserver?.disconnect();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -19,210 +19,205 @@ const SCROLL_THRESHOLD = 100;
|
|||||||
let resizeObserver = null;
|
let resizeObserver = null;
|
||||||
|
|
||||||
function scrollToBottom() {
|
function scrollToBottom() {
|
||||||
if (messagesContainer.value) {
|
if (messagesContainer.value) {
|
||||||
messagesContainer.value.scrollTop =
|
messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight;
|
||||||
messagesContainer.value.scrollHeight;
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function scrollToBottomIfNear() {
|
function scrollToBottomIfNear() {
|
||||||
if (isNearBottom.value) {
|
if (isNearBottom.value) {
|
||||||
scrollToBottom();
|
scrollToBottom();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onScroll() {
|
function onScroll() {
|
||||||
if (!messagesContainer.value) return;
|
if (!messagesContainer.value) return;
|
||||||
const { scrollHeight, scrollTop, clientHeight } = messagesContainer.value;
|
const { scrollHeight, scrollTop, clientHeight } = messagesContainer.value;
|
||||||
isNearBottom.value =
|
isNearBottom.value =
|
||||||
scrollHeight - scrollTop - clientHeight < SCROLL_THRESHOLD;
|
scrollHeight - scrollTop - clientHeight < SCROLL_THRESHOLD;
|
||||||
}
|
}
|
||||||
|
|
||||||
function goToBottom() {
|
function goToBottom() {
|
||||||
isNearBottom.value = true;
|
isNearBottom.value = true;
|
||||||
scrollToBottom();
|
scrollToBottom();
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => messages.value.length,
|
() => messages.value.length,
|
||||||
() => {
|
() => {
|
||||||
nextTick(scrollToBottomIfNear);
|
nextTick(scrollToBottomIfNear);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
function sendMessage() {
|
function sendMessage() {
|
||||||
const text = messageInput.value.trim();
|
const text = messageInput.value.trim();
|
||||||
if (!text) return;
|
if (!text) return;
|
||||||
isNearBottom.value = true;
|
isNearBottom.value = true;
|
||||||
messagesStore.sendMessage(text);
|
messagesStore.sendMessage(text);
|
||||||
messageInput.value = "";
|
messageInput.value = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onFileSelected(e) {
|
async function onFileSelected(e) {
|
||||||
const file = e.target.files[0];
|
const file = e.target.files[0];
|
||||||
if (!file) return;
|
if (!file) return;
|
||||||
isNearBottom.value = true;
|
isNearBottom.value = true;
|
||||||
await messagesStore.uploadAndSendFile(file);
|
await messagesStore.uploadAndSendFile(file);
|
||||||
fileInput.value.value = "";
|
fileInput.value.value = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
function isImageUrl(url) {
|
function isImageUrl(url) {
|
||||||
return /\.(jpg|jpeg|png|gif|webp)$/i.test(url);
|
return /\.(jpg|jpeg|png|gif|webp)$/i.test(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
function isVideoUrl(url) {
|
function isVideoUrl(url) {
|
||||||
return /\.(mp4|webm|ogg|mov)$/i.test(url);
|
return /\.(mp4|webm|ogg|mov)$/i.test(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
function isSafeFileUrl(url) {
|
function isSafeFileUrl(url) {
|
||||||
return typeof url === "string" && url.startsWith("/uploads/");
|
return typeof url === "string" && url.startsWith("/uploads/");
|
||||||
}
|
}
|
||||||
|
|
||||||
const urlRegex = /(https?:\/\/[^\s<]+)/g;
|
const urlRegex = /(https?:\/\/[^\s<]+)/g;
|
||||||
|
|
||||||
function parseMessageParts(text) {
|
function parseMessageParts(text) {
|
||||||
const parts = [];
|
const parts = [];
|
||||||
let lastIndex = 0;
|
let lastIndex = 0;
|
||||||
let match;
|
let match;
|
||||||
while ((match = urlRegex.exec(text)) !== null) {
|
while ((match = urlRegex.exec(text)) !== null) {
|
||||||
if (match.index > lastIndex) {
|
if (match.index > lastIndex) {
|
||||||
parts.push({
|
parts.push({
|
||||||
type: "text",
|
type: "text",
|
||||||
value: text.slice(lastIndex, match.index),
|
value: text.slice(lastIndex, match.index),
|
||||||
});
|
});
|
||||||
}
|
|
||||||
parts.push({ type: "link", value: match[1] });
|
|
||||||
lastIndex = urlRegex.lastIndex;
|
|
||||||
}
|
}
|
||||||
if (lastIndex < text.length) {
|
parts.push({ type: "link", value: match[1] });
|
||||||
parts.push({ type: "text", value: text.slice(lastIndex) });
|
lastIndex = urlRegex.lastIndex;
|
||||||
}
|
}
|
||||||
return parts;
|
if (lastIndex < text.length) {
|
||||||
|
parts.push({ type: "text", value: text.slice(lastIndex) });
|
||||||
|
}
|
||||||
|
return parts;
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
messagesStore.connect();
|
messagesStore.connect();
|
||||||
|
|
||||||
if (messagesContainer.value) {
|
if (messagesContainer.value) {
|
||||||
messagesContainer.value.addEventListener("scroll", onScroll, {
|
messagesContainer.value.addEventListener("scroll", onScroll, {
|
||||||
passive: true,
|
passive: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (messagesInner.value) {
|
if (messagesInner.value) {
|
||||||
resizeObserver = new ResizeObserver(scrollToBottomIfNear);
|
resizeObserver = new ResizeObserver(scrollToBottomIfNear);
|
||||||
resizeObserver.observe(messagesInner.value);
|
resizeObserver.observe(messagesInner.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
scrollToBottom();
|
scrollToBottom();
|
||||||
});
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
messagesStore.disconnect();
|
messagesStore.disconnect();
|
||||||
|
|
||||||
if (messagesContainer.value) {
|
if (messagesContainer.value) {
|
||||||
messagesContainer.value.removeEventListener("scroll", onScroll);
|
messagesContainer.value.removeEventListener("scroll", onScroll);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (resizeObserver) {
|
if (resizeObserver) {
|
||||||
resizeObserver.disconnect();
|
resizeObserver.disconnect();
|
||||||
resizeObserver = null;
|
resizeObserver = null;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="chat-root flex-col flex min-h-0">
|
<div class="chat-root flex-col flex min-h-0">
|
||||||
<Header>Chat</Header>
|
<Header>Chat</Header>
|
||||||
<div
|
<div
|
||||||
ref="messagesContainer"
|
ref="messagesContainer"
|
||||||
class="flex flex-col flex-1 min-h-0 overflow-y-auto overflow-x-hidden p-2 min-w-0"
|
class="flex flex-col flex-1 min-h-0 overflow-y-auto overflow-x-hidden p-2 min-w-0"
|
||||||
|
>
|
||||||
|
<div ref="messagesInner">
|
||||||
|
<p
|
||||||
|
v-for="message in messages"
|
||||||
|
:key="message.id"
|
||||||
|
class="break-words min-w-0 w-full"
|
||||||
>
|
>
|
||||||
<div ref="messagesInner">
|
<span class="text-tertiary">{{ message.authorId }}:</span>
|
||||||
<p
|
<template
|
||||||
v-for="message in messages"
|
v-for="(part, i) in parseMessageParts(message.text || '')"
|
||||||
:key="message.id"
|
:key="i"
|
||||||
class="break-words min-w-0 w-full"
|
>
|
||||||
>
|
<Link
|
||||||
<span class="text-tertiary">{{ message.authorId }}:</span>
|
v-if="part.type === 'link'"
|
||||||
<template
|
bare
|
||||||
v-for="(part, i) in parseMessageParts(
|
:href="part.value"
|
||||||
message.text || '',
|
target="_blank"
|
||||||
)"
|
class="text-primary underline break-all"
|
||||||
:key="i"
|
>{{ part.value }}</Link
|
||||||
>
|
>
|
||||||
<Link
|
<span v-else>{{ part.value }}</span>
|
||||||
v-if="part.type === 'link'"
|
</template>
|
||||||
bare
|
<template v-if="message.fileUrl && isSafeFileUrl(message.fileUrl)">
|
||||||
:href="part.value"
|
<img
|
||||||
target="_blank"
|
v-if="isImageUrl(message.fileUrl)"
|
||||||
class="text-primary underline break-all"
|
:src="message.fileUrl"
|
||||||
>{{ part.value }}</Link
|
alt="Uploaded image"
|
||||||
>
|
loading="lazy"
|
||||||
<span v-else>{{ part.value }}</span>
|
class="w-full max-w-full rounded block"
|
||||||
</template>
|
|
||||||
<template
|
|
||||||
v-if="message.fileUrl && isSafeFileUrl(message.fileUrl)"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
v-if="isImageUrl(message.fileUrl)"
|
|
||||||
:src="message.fileUrl"
|
|
||||||
alt="Uploaded image"
|
|
||||||
loading="lazy"
|
|
||||||
class="w-full max-w-full rounded block"
|
|
||||||
/>
|
|
||||||
<video
|
|
||||||
v-else-if="isVideoUrl(message.fileUrl)"
|
|
||||||
:src="message.fileUrl"
|
|
||||||
controls
|
|
||||||
preload="none"
|
|
||||||
class="w-full max-w-full max-h-48 rounded block"
|
|
||||||
/>
|
|
||||||
<Link
|
|
||||||
v-else
|
|
||||||
bare
|
|
||||||
:href="message.fileUrl"
|
|
||||||
target="_blank"
|
|
||||||
class="underline break-all"
|
|
||||||
>{{ message.fileUrl.split("/").pop() }}</Link
|
|
||||||
>
|
|
||||||
</template>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<input
|
|
||||||
v-model="messageInput"
|
|
||||||
@keyup.enter="sendMessage"
|
|
||||||
aria-label="Chat message"
|
|
||||||
/>
|
/>
|
||||||
<input
|
<video
|
||||||
ref="fileInput"
|
v-else-if="isVideoUrl(message.fileUrl)"
|
||||||
type="file"
|
:src="message.fileUrl"
|
||||||
class="hidden"
|
controls
|
||||||
@change="onFileSelected"
|
preload="none"
|
||||||
|
class="w-full max-w-full max-h-48 rounded block"
|
||||||
/>
|
/>
|
||||||
<div class="flex gap-2">
|
<Link
|
||||||
<Button class="flex-1" @click="sendMessage">Send</Button>
|
v-else
|
||||||
<Button
|
bare
|
||||||
v-if="authStore.user.admin"
|
:href="message.fileUrl"
|
||||||
class="flex-1"
|
target="_blank"
|
||||||
@click="fileInput.click()"
|
class="underline break-all"
|
||||||
>Attach</Button
|
>{{ message.fileUrl.split("/").pop() }}</Link
|
||||||
>
|
>
|
||||||
<Button v-if="!isNearBottom" class="flex-1" @click="goToBottom"
|
</template>
|
||||||
>Bottom</Button
|
</p>
|
||||||
>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
v-model="messageInput"
|
||||||
|
@keyup.enter="sendMessage"
|
||||||
|
aria-label="Chat message"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
ref="fileInput"
|
||||||
|
type="file"
|
||||||
|
class="hidden"
|
||||||
|
@change="onFileSelected"
|
||||||
|
/>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<Button class="flex-1" @click="sendMessage">Send</Button>
|
||||||
|
<Button
|
||||||
|
v-if="authStore.user.admin"
|
||||||
|
class="flex-1"
|
||||||
|
@click="fileInput.click()"
|
||||||
|
>Attach</Button
|
||||||
|
>
|
||||||
|
<Button v-if="!isNearBottom" class="flex-1" @click="goToBottom"
|
||||||
|
>Bottom</Button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@media (max-width: 850px) {
|
@media (max-width: 850px) {
|
||||||
.chat-root {
|
.chat-root {
|
||||||
max-height: none;
|
max-height: none;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -9,37 +9,42 @@ const { gitFeed: feed, loaded } = storeToRefs(homeData);
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col text-center min-h-0 h-full overflow-x-hidden">
|
<div class="flex flex-col text-center min-h-0 h-full overflow-x-hidden">
|
||||||
<Header class="text-left">Commits</Header>
|
<Header class="text-left">Commits</Header>
|
||||||
|
|
||||||
<div v-if="!loaded" class="flex-1 overflow-y-auto">
|
<div v-if="!loaded" class="flex-1 overflow-y-auto">
|
||||||
<p>Loading latest activity...</p>
|
<p>Loading latest activity...</p>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
v-else-if="feed"
|
|
||||||
class="flex-1 flex flex-col items-center overflow-y-auto overflow-x-hidden"
|
|
||||||
>
|
|
||||||
<h3>Last git activity</h3>
|
|
||||||
<img :src="feed.avatarUrl" alt="User avatar" class="avatar" loading="lazy" />
|
|
||||||
<Link :href="feed.repoUrl">
|
|
||||||
<h3>repo: {{ feed.repoName }}</h3>
|
|
||||||
</Link>
|
|
||||||
<p>Action: {{ feed.opType }}</p>
|
|
||||||
<p>Message: {{ feed.commitMessage }}</p>
|
|
||||||
<small> {{ new Date(feed.createdAt).toLocaleString() }}</small>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-else class="flex-1 overflow-y-auto">
|
|
||||||
<p>No activity found.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-else-if="feed"
|
||||||
|
class="flex-1 flex flex-col items-center overflow-y-auto overflow-x-hidden"
|
||||||
|
>
|
||||||
|
<h3>Last git activity</h3>
|
||||||
|
<img
|
||||||
|
:src="feed.avatarUrl"
|
||||||
|
alt="User avatar"
|
||||||
|
class="avatar"
|
||||||
|
loading="lazy"
|
||||||
|
/>
|
||||||
|
<Link :href="feed.repoUrl">
|
||||||
|
<h3>repo: {{ feed.repoName }}</h3>
|
||||||
|
</Link>
|
||||||
|
<p>Action: {{ feed.opType }}</p>
|
||||||
|
<p>Message: {{ feed.commitMessage }}</p>
|
||||||
|
<small> {{ new Date(feed.createdAt).toLocaleString() }}</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="flex-1 overflow-y-auto">
|
||||||
|
<p>No activity found.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.avatar {
|
.avatar {
|
||||||
max-width: 200px;
|
max-width: 200px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: auto;
|
height: auto;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -4,70 +4,66 @@ import Link from "@/components/text/Link.vue";
|
|||||||
import ToggleHeader from "@/components/text/ToggleHeader.vue";
|
import ToggleHeader from "@/components/text/ToggleHeader.vue";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
items: {
|
items: {
|
||||||
type: Array,
|
type: Array,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
variant: {
|
variant: {
|
||||||
type: String,
|
type: String,
|
||||||
default: "list",
|
default: "list",
|
||||||
},
|
},
|
||||||
title: {
|
title: {
|
||||||
type: String,
|
type: String,
|
||||||
default: "",
|
default: "",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const show = ref(false);
|
const show = ref(false);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div v-if="title" class="h-fit w-full">
|
<div v-if="title" class="h-fit w-full">
|
||||||
<ToggleHeader v-model="show" class="justify-between flex items-center">
|
<ToggleHeader v-model="show" class="justify-between flex items-center">
|
||||||
{{ title }}
|
{{ title }}
|
||||||
</ToggleHeader>
|
</ToggleHeader>
|
||||||
<template v-if="show">
|
<template v-if="show">
|
||||||
<Link
|
<Link
|
||||||
v-if="variant === 'list'"
|
v-if="variant === 'list'"
|
||||||
v-for="(item, i) in items"
|
v-for="(item, i) in items"
|
||||||
:key="i"
|
:key="i"
|
||||||
:href="item.link"
|
:href="item.link"
|
||||||
>
|
>
|
||||||
<p class="bdr-2 bg-bg_tertiary">{{ item.name }}</p>
|
<p class="bdr-2 bg-bg_tertiary">{{ item.name }}</p>
|
||||||
</Link>
|
</Link>
|
||||||
<table class="w-full" v-else>
|
<table class="w-full" v-else>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="item in items" :key="item.id">
|
<tr v-for="item in items" :key="item.id">
|
||||||
<th>{{ item.type }}</th>
|
<th>{{ item.type }}</th>
|
||||||
<td v-if="item.link">
|
<td v-if="item.link">
|
||||||
<Link :href="item.link">{{ item.name }}</Link>
|
<Link :href="item.link">{{ item.name }}</Link>
|
||||||
</td>
|
</td>
|
||||||
<td v-else>{{ item.name }}</td>
|
<td v-else>{{ item.name }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
<template v-else>
|
|
||||||
<template v-if="variant === 'list'">
|
|
||||||
<Link
|
|
||||||
v-for="(item, i) in items"
|
|
||||||
:key="i"
|
|
||||||
:href="item.link"
|
|
||||||
>
|
|
||||||
<p class="bdr-2 bg-bg_tertiary">{{ item.name }}</p>
|
|
||||||
</Link>
|
|
||||||
</template>
|
|
||||||
<table class="w-full" v-else>
|
|
||||||
<tbody>
|
|
||||||
<tr v-for="item in items" :key="item.id">
|
|
||||||
<th>{{ item.type }}</th>
|
|
||||||
<td v-if="item.link">
|
|
||||||
<Link :href="item.link">{{ item.name }}</Link>
|
|
||||||
</td>
|
|
||||||
<td v-else>{{ item.name }}</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</template>
|
</template>
|
||||||
|
</div>
|
||||||
|
<template v-else>
|
||||||
|
<template v-if="variant === 'list'">
|
||||||
|
<Link v-for="(item, i) in items" :key="i" :href="item.link">
|
||||||
|
<p class="bdr-2 bg-bg_tertiary">{{ item.name }}</p>
|
||||||
|
</Link>
|
||||||
|
</template>
|
||||||
|
<table class="w-full" v-else>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="item in items" :key="item.id">
|
||||||
|
<th>{{ item.type }}</th>
|
||||||
|
<td v-if="item.link">
|
||||||
|
<Link :href="item.link">{{ item.name }}</Link>
|
||||||
|
</td>
|
||||||
|
<td v-else>{{ item.name }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</template>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -6,19 +6,19 @@ import DOMPurify from "dompurify";
|
|||||||
const mdIt = MarkdownIt().use(katex);
|
const mdIt = MarkdownIt().use(katex);
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
source: String,
|
source: String,
|
||||||
});
|
});
|
||||||
|
|
||||||
function renderMarkdown(source) {
|
function renderMarkdown(source) {
|
||||||
return DOMPurify.sanitize(mdIt.render(source));
|
return DOMPurify.sanitize(mdIt.render(source));
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
v-html="renderMarkdown(props.source)"
|
v-html="renderMarkdown(props.source)"
|
||||||
class="flex flex-col items-center"
|
class="flex flex-col items-center"
|
||||||
></div>
|
></div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|||||||
@@ -1,93 +1,92 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import Button from "@/components/input/Button.vue";
|
import Button from "@/components/input/Button.vue";
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<audio/>
|
<audio />
|
||||||
<div class="musicPlayerGrid">
|
<div class="musicPlayerGrid">
|
||||||
<div class="album_cover">
|
<div class="album_cover">
|
||||||
<img src="/img/Untitled.png" alt=""></img>
|
<img src="/img/Untitled.png" alt="" />
|
||||||
</div>
|
|
||||||
<div class="controls">
|
|
||||||
<div class="sliders">
|
|
||||||
<div class="timeline"/>
|
|
||||||
<div class="volume"/>
|
|
||||||
</div>
|
|
||||||
<div class="buttons">
|
|
||||||
<div class="rewind"/>
|
|
||||||
<div class="playPause"/>
|
|
||||||
<div class="fastforward"/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="controls">
|
||||||
|
<div class="sliders">
|
||||||
|
<div class="timeline" />
|
||||||
|
<div class="volume" />
|
||||||
|
</div>
|
||||||
|
<div class="buttons">
|
||||||
|
<div class="rewind" />
|
||||||
|
<div class="playPause" />
|
||||||
|
<div class="fastforward" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.musicPlayerGrid {
|
.musicPlayerGrid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-gap: 5px;
|
grid-gap: 5px;
|
||||||
grid-template-rows: repeat(4, 1fr);
|
grid-template-rows: repeat(4, 1fr);
|
||||||
background-color: blue;
|
background-color: blue;
|
||||||
align-items: stretch; /* rows (block axis) */
|
align-items: stretch; /* rows (block axis) */
|
||||||
justify-items: stretch; /* columns (inline axis) */
|
justify-items: stretch; /* columns (inline axis) */
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
}
|
}
|
||||||
img {
|
img {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.album_cover {
|
.album_cover {
|
||||||
grid-row: 1 / span 3;
|
grid-row: 1 / span 3;
|
||||||
background-color: grey;
|
background-color: grey;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
.controls {
|
.controls {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
grid-row: 4 / span 1;
|
grid-row: 4 / span 1;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-rows: repeat(4, 1fr);
|
grid-template-rows: repeat(4, 1fr);
|
||||||
grid-gap: 5px;
|
grid-gap: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sliders {
|
.sliders {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(2, 1fr);
|
grid-template-columns: repeat(2, 1fr);
|
||||||
grid-gap: 5px;
|
grid-gap: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.timeline {
|
.timeline {
|
||||||
grid-column: 1;
|
grid-column: 1;
|
||||||
background-color: white;
|
background-color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.volume {
|
.volume {
|
||||||
grid-column: 2;
|
grid-column: 2;
|
||||||
background-color: white;
|
background-color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.buttons {
|
.buttons {
|
||||||
background-color: black;
|
background-color: black;
|
||||||
grid-row: 2 / -1;
|
grid-row: 2 / -1;
|
||||||
|
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(4, 1fr);
|
grid-template-columns: repeat(4, 1fr);
|
||||||
grid-gap: 5px;
|
grid-gap: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.rewind {
|
.rewind {
|
||||||
grid-column: 1;
|
grid-column: 1;
|
||||||
background-color: grey;
|
background-color: grey;
|
||||||
}
|
}
|
||||||
.fastforward {
|
.fastforward {
|
||||||
grid-column: 4;
|
grid-column: 4;
|
||||||
background-color: grey;
|
background-color: grey;
|
||||||
}
|
}
|
||||||
.playPause {
|
.playPause {
|
||||||
grid-column: 2/span 2;
|
grid-column: 2 / span 2;
|
||||||
background-color: grey;
|
background-color: grey;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -2,42 +2,42 @@
|
|||||||
import { computed } from "vue";
|
import { computed } from "vue";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
objArr: {
|
objArr: {
|
||||||
type: Array,
|
type: Array,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const resolvedColumns = computed(() => {
|
const resolvedColumns = computed(() => {
|
||||||
const keys = new Set();
|
const keys = new Set();
|
||||||
|
|
||||||
for (const obj of props.objArr) {
|
for (const obj of props.objArr) {
|
||||||
Object.keys(obj).forEach((key) => keys.add(key));
|
Object.keys(obj).forEach((key) => keys.add(key));
|
||||||
}
|
}
|
||||||
|
|
||||||
return Array.from(keys).map((key) => ({
|
return Array.from(keys).map((key) => ({
|
||||||
key,
|
key,
|
||||||
label: key,
|
label: key,
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th v-for="col in resolvedColumns" :key="col.key">
|
<th v-for="col in resolvedColumns" :key="col.key">
|
||||||
{{ col.label }}
|
{{ col.label }}
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="(row, rowIndex) in objArr" :key="rowIndex">
|
<tr v-for="(row, rowIndex) in objArr" :key="rowIndex">
|
||||||
<td v-for="col in resolvedColumns" :key="col.key">
|
<td v-for="col in resolvedColumns" :key="col.key">
|
||||||
{{ row[col.key] ?? "" }}
|
{{ row[col.key] ?? "" }}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="streamLive" class="overflow-auto">
|
<div v-if="streamLive" class="overflow-auto">
|
||||||
<Header>Radio</Header>
|
<Header>Radio</Header>
|
||||||
<img src="/img/tmpen31z3pe.PNG" alt="Radio" width="176" height="177" />
|
<img src="/img/tmpen31z3pe.PNG" alt="Radio" width="176" height="177" />
|
||||||
<audio controls :src="streamUrl" ref="audio"></audio>
|
<audio controls :src="streamUrl" ref="audio"></audio>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="overflow-auto">
|
<div v-else class="overflow-auto">
|
||||||
<Header>Radio</Header>
|
<Header>Radio</Header>
|
||||||
<img src="/img/tmpen31z3pe.PNG" alt="Radio" width="176" height="177" />
|
<img src="/img/tmpen31z3pe.PNG" alt="Radio" width="176" height="177" />
|
||||||
<div class="m-1 text-center">
|
<div class="m-1 text-center">
|
||||||
<p>Radio is offline. Message for info!</p>
|
<p>Radio is offline. Message for info!</p>
|
||||||
<Button class="w-full" @click="checkStream()">Check Stream</Button>
|
<Button class="w-full" @click="checkStream()">Check Stream</Button>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
@@ -25,32 +25,32 @@ const streamLive = ref(false);
|
|||||||
const audio = useTemplateRef("audio");
|
const audio = useTemplateRef("audio");
|
||||||
|
|
||||||
async function checkStream() {
|
async function checkStream() {
|
||||||
try {
|
try {
|
||||||
await axios.head("/radio/stream");
|
await axios.head("/radio/stream");
|
||||||
if (!streamLive.value) {
|
if (!streamLive.value) {
|
||||||
streamLive.value = true;
|
streamLive.value = true;
|
||||||
streamUrl.value = "/radio/stream";
|
streamUrl.value = "/radio/stream";
|
||||||
await nextTick();
|
await nextTick();
|
||||||
if (audio.value) {
|
if (audio.value) {
|
||||||
audio.value.load();
|
audio.value.load();
|
||||||
audio.value.volume = 0.2;
|
audio.value.volume = 0.2;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
streamLive.value = false;
|
|
||||||
}
|
}
|
||||||
|
} catch (err) {
|
||||||
|
streamLive.value = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
checkStream();
|
checkStream();
|
||||||
setInterval(checkStream, 120000);
|
setInterval(checkStream, 120000);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
img {
|
img {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-height: 150px;
|
max-height: 150px;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -2,22 +2,22 @@
|
|||||||
import { computed } from "vue";
|
import { computed } from "vue";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
linkArr: {
|
linkArr: {
|
||||||
type: Array,
|
type: Array,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const keys = ["name", "link"];
|
const keys = ["name", "link"];
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<RouterLink
|
<RouterLink
|
||||||
class="bdr-2 bg-bg_primary"
|
class="bdr-2 bg-bg_primary"
|
||||||
v-for="(row, rowIndex) in linkArr"
|
v-for="(row, rowIndex) in linkArr"
|
||||||
:key="rowIndex"
|
:key="rowIndex"
|
||||||
:to="row.link"
|
:to="row.link"
|
||||||
>
|
>
|
||||||
{{ row.name }}
|
{{ row.name }}
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -3,14 +3,14 @@ import { ref, computed, onMounted, onUnmounted } from "vue";
|
|||||||
import Header from "@/components/text/Header.vue";
|
import Header from "@/components/text/Header.vue";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
images: {
|
images: {
|
||||||
type: Array,
|
type: Array,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
interval: {
|
interval: {
|
||||||
type: Number,
|
type: Number,
|
||||||
default: 10000,
|
default: 10000,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const currentIndex = ref(0);
|
const currentIndex = ref(0);
|
||||||
@@ -20,58 +20,58 @@ const currentUrl = computed(() => props.images[currentIndex.value].url);
|
|||||||
let nextId;
|
let nextId;
|
||||||
|
|
||||||
function nextImage() {
|
function nextImage() {
|
||||||
clearTimeout(nextId);
|
clearTimeout(nextId);
|
||||||
currentIndex.value = (currentIndex.value + 1) % props.images.length;
|
currentIndex.value = (currentIndex.value + 1) % props.images.length;
|
||||||
nextId = setTimeout(nextImage, props.interval);
|
nextId = setTimeout(nextImage, props.interval);
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
nextId = setTimeout(nextImage, props.interval);
|
nextId = setTimeout(nextImage, props.interval);
|
||||||
});
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
clearTimeout(nextId);
|
clearTimeout(nextId);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="slideshow-wrapper">
|
<div class="slideshow-wrapper">
|
||||||
<Transition name="fade">
|
<Transition name="fade">
|
||||||
<div class="image-viewer" @click="nextImage" :key="currentIndex">
|
<div class="image-viewer" @click="nextImage" :key="currentIndex">
|
||||||
<Header v-if="currentComment">
|
<Header v-if="currentComment">
|
||||||
{{ currentComment }}
|
{{ currentComment }}
|
||||||
</Header>
|
</Header>
|
||||||
<img :src="currentUrl" alt="Image Viewer" fetchpriority="high" />
|
<img :src="currentUrl" alt="Image Viewer" fetchpriority="high" />
|
||||||
</div>
|
</div>
|
||||||
</Transition>
|
</Transition>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.slideshow-wrapper {
|
.slideshow-wrapper {
|
||||||
display: grid;
|
display: grid;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.image-viewer {
|
.image-viewer {
|
||||||
grid-area: 1 / 1;
|
grid-area: 1 / 1;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
img {
|
img {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fade-enter-active,
|
.fade-enter-active,
|
||||||
.fade-leave-active {
|
.fade-leave-active {
|
||||||
transition: opacity 0.5s ease;
|
transition: opacity 0.5s ease;
|
||||||
}
|
}
|
||||||
.fade-enter-from,
|
.fade-enter-from,
|
||||||
.fade-leave-to {
|
.fade-leave-to {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -8,14 +8,14 @@ const day = ref("");
|
|||||||
const month = ref("");
|
const month = ref("");
|
||||||
|
|
||||||
function updateDateTime() {
|
function updateDateTime() {
|
||||||
const date = new Date();
|
const date = new Date();
|
||||||
day.value = date.getDate();
|
day.value = date.getDate();
|
||||||
time.value = date.toLocaleTimeString("en-GB", {
|
time.value = date.toLocaleTimeString("en-GB", {
|
||||||
hour: "2-digit",
|
hour: "2-digit",
|
||||||
minute: "2-digit",
|
minute: "2-digit",
|
||||||
});
|
});
|
||||||
weekday.value = date.toLocaleDateString("en-GB", { weekday: "long" });
|
weekday.value = date.toLocaleDateString("en-GB", { weekday: "long" });
|
||||||
month.value = date.toLocaleDateString("en-GB", { month: "long" });
|
month.value = date.toLocaleDateString("en-GB", { month: "long" });
|
||||||
}
|
}
|
||||||
|
|
||||||
updateDateTime();
|
updateDateTime();
|
||||||
@@ -24,15 +24,15 @@ setInterval(updateDateTime, 60000);
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<Header>{{ weekday }} {{ day }}, {{ month }}</Header>
|
<Header>{{ weekday }} {{ day }}, {{ month }}</Header>
|
||||||
<h1>{{ time }}</h1>
|
<h1>{{ time }}</h1>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
div {
|
div {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 4px;
|
padding: 4px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -18,109 +18,109 @@ const seconds = ref(0);
|
|||||||
const audio = new Audio("/sound/auughhh.mp3");
|
const audio = new Audio("/sound/auughhh.mp3");
|
||||||
|
|
||||||
function tick() {
|
function tick() {
|
||||||
seconds.value++;
|
seconds.value++;
|
||||||
if (seconds.value === 60) {
|
if (seconds.value === 60) {
|
||||||
minutes.value++;
|
minutes.value++;
|
||||||
seconds.value = 0;
|
seconds.value = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (minutes.value >= minutesInput.value) {
|
if (minutes.value >= minutesInput.value) {
|
||||||
if (seconds.value >= secondsInput.value) {
|
if (seconds.value >= secondsInput.value) {
|
||||||
finished.value = true;
|
finished.value = true;
|
||||||
playFinishedSound();
|
playFinishedSound();
|
||||||
clearInterval(timer.value);
|
clearInterval(timer.value);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function startTimer() {
|
function startTimer() {
|
||||||
finished.value = false;
|
finished.value = false;
|
||||||
paused.value = false;
|
paused.value = false;
|
||||||
timer.value = setInterval(tick, 1000);
|
timer.value = setInterval(tick, 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
function pauseTimer() {
|
function pauseTimer() {
|
||||||
if (finished.value) return;
|
if (finished.value) return;
|
||||||
|
|
||||||
if (paused.value) {
|
if (paused.value) {
|
||||||
timer.value = setInterval(tick, 1000);
|
timer.value = setInterval(tick, 1000);
|
||||||
paused.value = false;
|
paused.value = false;
|
||||||
} else {
|
} else {
|
||||||
clearInterval(timer.value);
|
clearInterval(timer.value);
|
||||||
paused.value = true;
|
paused.value = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function resetTimer() {
|
function resetTimer() {
|
||||||
finished.value = true;
|
finished.value = true;
|
||||||
paused.value = true;
|
paused.value = true;
|
||||||
clearInterval(timer.value);
|
clearInterval(timer.value);
|
||||||
minutes.value = 0;
|
minutes.value = 0;
|
||||||
seconds.value = 0;
|
seconds.value = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
function playFinishedSound() {
|
function playFinishedSound() {
|
||||||
audio.play();
|
audio.play();
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="timer-root flex flex-col gap-1 p-1 items-center">
|
<div class="timer-root flex flex-col gap-1 p-1 items-center">
|
||||||
<Header>Timer</Header>
|
<Header>Timer</Header>
|
||||||
<div v-if="finished && paused" class="flex flex-col">
|
<div v-if="finished && paused" class="flex flex-col">
|
||||||
<div class="flex flex-row p-2 place-content-around">
|
<div class="flex flex-row p-2 place-content-around">
|
||||||
<input
|
<input
|
||||||
class="w-2/3"
|
class="w-2/3"
|
||||||
v-model="minutesInput"
|
v-model="minutesInput"
|
||||||
type="range"
|
type="range"
|
||||||
min="0"
|
min="0"
|
||||||
max="59"
|
max="59"
|
||||||
aria-label="Minutes"
|
aria-label="Minutes"
|
||||||
/>
|
/>
|
||||||
<p>{{ minutesInput }}m</p>
|
<p>{{ minutesInput }}m</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-row p-2 place-content-around">
|
<div class="flex flex-row p-2 place-content-around">
|
||||||
<input
|
<input
|
||||||
class="w-2/3"
|
class="w-2/3"
|
||||||
v-model="secondsInput"
|
v-model="secondsInput"
|
||||||
type="range"
|
type="range"
|
||||||
min="0"
|
min="0"
|
||||||
max="59"
|
max="59"
|
||||||
aria-label="Seconds"
|
aria-label="Seconds"
|
||||||
/>
|
/>
|
||||||
<p>{{ secondsInput }}s</p>
|
<p>{{ secondsInput }}s</p>
|
||||||
</div>
|
</div>
|
||||||
<Button @click="startTimer">Proceed</Button>
|
<Button @click="startTimer">Proceed</Button>
|
||||||
</div>
|
|
||||||
<div v-if="finished && !paused" class="flex flex-col">
|
|
||||||
<h1>Timer finished!</h1>
|
|
||||||
<Button @click="resetTimer">Reset</Button>
|
|
||||||
</div>
|
|
||||||
<div v-if="!finished && paused" class="flex flex-col">
|
|
||||||
<h1>Paused</h1>
|
|
||||||
<Button @click="resetTimer">Reset</Button>
|
|
||||||
</div>
|
|
||||||
<div v-if="!finished && !paused" class="flex flex-col">
|
|
||||||
<p>
|
|
||||||
{{ minutes.toString().padStart(2, "0") }}:{{
|
|
||||||
seconds.toString().padStart(2, "0")
|
|
||||||
}}
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
{{ minutesInput.toString().padStart(2, "0") }}:{{
|
|
||||||
secondsInput.toString().padStart(2, "0")
|
|
||||||
}}
|
|
||||||
</p>
|
|
||||||
<Button @click="pauseTimer">Pause</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="finished && !paused" class="flex flex-col">
|
||||||
|
<h1>Timer finished!</h1>
|
||||||
|
<Button @click="resetTimer">Reset</Button>
|
||||||
|
</div>
|
||||||
|
<div v-if="!finished && paused" class="flex flex-col">
|
||||||
|
<h1>Paused</h1>
|
||||||
|
<Button @click="resetTimer">Reset</Button>
|
||||||
|
</div>
|
||||||
|
<div v-if="!finished && !paused" class="flex flex-col">
|
||||||
|
<p>
|
||||||
|
{{ minutes.toString().padStart(2, "0") }}:{{
|
||||||
|
seconds.toString().padStart(2, "0")
|
||||||
|
}}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
{{ minutesInput.toString().padStart(2, "0") }}:{{
|
||||||
|
secondsInput.toString().padStart(2, "0")
|
||||||
|
}}
|
||||||
|
</p>
|
||||||
|
<Button @click="pauseTimer">Pause</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@media (max-width: 850px) {
|
@media (max-width: 850px) {
|
||||||
.timer-root {
|
.timer-root {
|
||||||
padding: 2px;
|
padding: 2px;
|
||||||
gap: 2px;
|
gap: 2px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
ref="container"
|
ref="container"
|
||||||
class="overflow-auto w-full h-full"
|
class="overflow-auto w-full h-full"
|
||||||
@mousedown="handleMouseDown"
|
@mousedown="handleMouseDown"
|
||||||
@mousemove="handleMouseMove"
|
@mousemove="handleMouseMove"
|
||||||
@mouseup="handleMouseUp"
|
@mouseup="handleMouseUp"
|
||||||
@mouseleave="handleMouseLeave"
|
@mouseleave="handleMouseLeave"
|
||||||
:style="{ cursor: isDragging ? 'grabbing' : 'grab' }"
|
:style="{ cursor: isDragging ? 'grabbing' : 'grab' }"
|
||||||
>
|
>
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
@@ -23,45 +23,45 @@ const scrollLeft = ref(0);
|
|||||||
const scrollTop = ref(0);
|
const scrollTop = ref(0);
|
||||||
|
|
||||||
const handleMouseDown = (e) => {
|
const handleMouseDown = (e) => {
|
||||||
isDragging.value = true;
|
isDragging.value = true;
|
||||||
startX.value = e.pageX - container.value.offsetLeft;
|
startX.value = e.pageX - container.value.offsetLeft;
|
||||||
startY.value = e.pageY - container.value.offsetTop;
|
startY.value = e.pageY - container.value.offsetTop;
|
||||||
scrollLeft.value = container.value.scrollLeft;
|
scrollLeft.value = container.value.scrollLeft;
|
||||||
scrollTop.value = container.value.scrollTop;
|
scrollTop.value = container.value.scrollTop;
|
||||||
|
|
||||||
// Prevent text selection while dragging
|
// Prevent text selection while dragging
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleMouseMove = (e) => {
|
const handleMouseMove = (e) => {
|
||||||
if (!isDragging.value) return;
|
if (!isDragging.value) return;
|
||||||
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
const x = e.pageX - container.value.offsetLeft;
|
const x = e.pageX - container.value.offsetLeft;
|
||||||
const y = e.pageY - container.value.offsetTop;
|
const y = e.pageY - container.value.offsetTop;
|
||||||
const walkX = (x - startX.value) * 1; // Multiply by scroll speed factor
|
const walkX = (x - startX.value) * 1; // Multiply by scroll speed factor
|
||||||
const walkY = (y - startY.value) * 1;
|
const walkY = (y - startY.value) * 1;
|
||||||
|
|
||||||
container.value.scrollLeft = scrollLeft.value - walkX;
|
container.value.scrollLeft = scrollLeft.value - walkX;
|
||||||
container.value.scrollTop = scrollTop.value - walkY;
|
container.value.scrollTop = scrollTop.value - walkY;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleMouseUp = () => {
|
const handleMouseUp = () => {
|
||||||
isDragging.value = false;
|
isDragging.value = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleMouseLeave = () => {
|
const handleMouseLeave = () => {
|
||||||
isDragging.value = false;
|
isDragging.value = false;
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
/* Prevent text selection while dragging */
|
/* Prevent text selection while dragging */
|
||||||
div[style*="cursor: grabbing"] {
|
div[style*="cursor: grabbing"] {
|
||||||
user-select: none;
|
user-select: none;
|
||||||
-webkit-user-select: none;
|
-webkit-user-select: none;
|
||||||
-moz-user-select: none;
|
-moz-user-select: none;
|
||||||
-ms-user-select: none;
|
-ms-user-select: none;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -2,29 +2,29 @@
|
|||||||
import { computed } from "vue";
|
import { computed } from "vue";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
sourceArr: {
|
sourceArr: {
|
||||||
type: Array,
|
type: Array,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
function sourceType(link) {
|
function sourceType(link) {
|
||||||
return "video/" + link.split(".").pop();
|
return "video/" + link.split(".").pop();
|
||||||
}
|
}
|
||||||
|
|
||||||
const keys = ["name", "link"];
|
const keys = ["name", "link"];
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<video
|
<video
|
||||||
v-for="(source, rowIndex) in sourceArr"
|
v-for="(source, rowIndex) in sourceArr"
|
||||||
:key="rowIndex"
|
:key="rowIndex"
|
||||||
class="bdr-1"
|
class="bdr-1"
|
||||||
width="300"
|
width="300"
|
||||||
height="400"
|
height="400"
|
||||||
controls
|
controls
|
||||||
preload="none"
|
preload="none"
|
||||||
>
|
>
|
||||||
<source :src="source.link" :type="sourceType(source.link)" />
|
<source :src="source.link" :type="sourceType(source.link)" />
|
||||||
</video>
|
</video>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="a4page-portrait bdr-1 flex flex-col relative overflow-scroll">
|
<div class="a4page-portrait bdr-1 flex flex-col relative overflow-scroll">
|
||||||
<RouterLink to="/" class="bdr-2">
|
<RouterLink to="/" class="bdr-2">
|
||||||
<img src="/img/memes/epic.jpeg" alt="" loading="lazy" />
|
<img src="/img/memes/epic.jpeg" alt="" loading="lazy" />
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
<h1>Click her, she will take you home</h1>
|
<h1>Click her, she will take you home</h1>
|
||||||
<span style="height: 100px"></span>
|
<span style="height: 100px"></span>
|
||||||
<h1>WIP</h1>
|
<h1>WIP</h1>
|
||||||
<h4>Sorry for taking you here</h4>
|
<h4>Sorry for taking you here</h4>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div ref="container" class="overflow-y-auto">
|
<div ref="container" class="overflow-y-auto">
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
@@ -13,14 +13,14 @@ const container = useTemplateRef("container");
|
|||||||
let scroller = null;
|
let scroller = null;
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (!container.value) return;
|
if (!container.value) return;
|
||||||
scroller = new AutoScroller(container.value);
|
scroller = new AutoScroller(container.value);
|
||||||
scroller.start();
|
scroller.start();
|
||||||
});
|
});
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
scroller?.destroy();
|
scroller?.destroy();
|
||||||
scroller?.free();
|
scroller?.free();
|
||||||
scroller = null;
|
scroller = null;
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import axios from "axios";
|
|||||||
|
|
||||||
export async function gql(query, variables = {}) {
|
export async function gql(query, variables = {}) {
|
||||||
const res = await axios.post("/api/graphql", { query, variables });
|
const res = await axios.post("/api/graphql", { query, variables });
|
||||||
if (res.data.errors && !res.data.data) throw new Error(res.data.errors[0].message);
|
if (res.data.errors && !res.data.data)
|
||||||
|
throw new Error(res.data.errors[0].message);
|
||||||
return res.data.data;
|
return res.data.data;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,41 +3,69 @@ import { RouterView } from "vue-router";
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="cv-layout">
|
<div class="cv-layout">
|
||||||
<RouterView />
|
<RouterView />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.cv-layout {
|
.cv-layout {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
background: white;
|
background: white;
|
||||||
color: #111;
|
color: #111;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.cv-layout h1, .cv-layout h2, .cv-layout h3, .cv-layout h4,
|
.cv-layout h1,
|
||||||
.cv-layout p, .cv-layout small, .cv-layout code, .cv-layout ul, .cv-layout li,
|
.cv-layout h2,
|
||||||
.cv-layout td, .cv-layout tr, .cv-layout table {
|
.cv-layout h3,
|
||||||
color: #111;
|
.cv-layout h4,
|
||||||
|
.cv-layout p,
|
||||||
|
.cv-layout small,
|
||||||
|
.cv-layout code,
|
||||||
|
.cv-layout ul,
|
||||||
|
.cv-layout li,
|
||||||
|
.cv-layout td,
|
||||||
|
.cv-layout tr,
|
||||||
|
.cv-layout table {
|
||||||
|
color: #111;
|
||||||
|
}
|
||||||
|
.cv-layout h1,
|
||||||
|
.cv-layout h2,
|
||||||
|
.cv-layout h3,
|
||||||
|
.cv-layout h4 {
|
||||||
|
margin: 0;
|
||||||
}
|
}
|
||||||
.cv-layout h1, .cv-layout h2, .cv-layout h3, .cv-layout h4 { margin: 0; }
|
|
||||||
.cv-layout a {
|
.cv-layout a {
|
||||||
color: #111;
|
color: #111;
|
||||||
background-color: transparent !important;
|
background-color: transparent !important;
|
||||||
letter-spacing: normal;
|
letter-spacing: normal;
|
||||||
}
|
}
|
||||||
.cv-layout input, .cv-layout textarea {
|
.cv-layout input,
|
||||||
color: #111;
|
.cv-layout textarea {
|
||||||
background-color: white;
|
color: #111;
|
||||||
border: 1px solid #ccc;
|
background-color: white;
|
||||||
padding: 0;
|
border: 1px solid #ccc;
|
||||||
width: auto;
|
padding: 0;
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
.cv-layout input::placeholder,
|
||||||
|
.cv-layout textarea::placeholder {
|
||||||
|
color: #999;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
.cv-layout table {
|
||||||
|
border: 0 solid transparent;
|
||||||
|
}
|
||||||
|
.cv-layout tr {
|
||||||
|
border-color: transparent;
|
||||||
|
}
|
||||||
|
.cv-layout th {
|
||||||
|
border: none;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
.cv-layout td {
|
||||||
|
padding: 0;
|
||||||
}
|
}
|
||||||
.cv-layout input::placeholder, .cv-layout textarea::placeholder { color: #999; opacity: 1; }
|
|
||||||
.cv-layout table { border: 0 solid transparent; }
|
|
||||||
.cv-layout tr { border-color: transparent; }
|
|
||||||
.cv-layout th { border: none; padding: 0; }
|
|
||||||
.cv-layout td { padding: 0; }
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -5,28 +5,28 @@ import Footer from "@/components/Footer.vue";
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="default-layout halftone">
|
<div class="default-layout halftone">
|
||||||
<Navbar class="no-print sticky top-0 z-50" />
|
<Navbar class="no-print sticky top-0 z-50" />
|
||||||
<main class="default-content">
|
<main class="default-content">
|
||||||
<RouterView v-slot="{ Component }">
|
<RouterView v-slot="{ Component }">
|
||||||
<Transition name="slide" mode="out-in">
|
<Transition name="slide" mode="out-in">
|
||||||
<component :is="Component" :key="$route.path" />
|
<component :is="Component" :key="$route.path" />
|
||||||
</Transition>
|
</Transition>
|
||||||
</RouterView>
|
</RouterView>
|
||||||
</main>
|
</main>
|
||||||
<Footer class="no-print sticky bottom-0 z-50" />
|
<Footer class="no-print sticky bottom-0 z-50" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.default-layout {
|
.default-layout {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
.default-content {
|
.default-content {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -7,96 +7,103 @@ import { useHomeDataStore } from "@/stores/homeData";
|
|||||||
import { useAuthStore } from "@/stores/auth";
|
import { useAuthStore } from "@/stores/auth";
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
history: createWebHistory(import.meta.env.BASE_URL),
|
history: createWebHistory(import.meta.env.BASE_URL),
|
||||||
routes: [
|
routes: [
|
||||||
|
{
|
||||||
|
path: "/",
|
||||||
|
component: DefaultLayout,
|
||||||
|
children: [
|
||||||
{
|
{
|
||||||
path: "/",
|
path: "",
|
||||||
component: DefaultLayout,
|
name: "landing",
|
||||||
children: [
|
component: Landing,
|
||||||
{
|
|
||||||
path: "",
|
|
||||||
name: "landing",
|
|
||||||
component: Landing,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "stp",
|
|
||||||
name: "home",
|
|
||||||
component: () => import("@/views/home/Home.vue"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "admin/login",
|
|
||||||
name: "admin-login",
|
|
||||||
component: () => import("@/views/admin/Login.vue"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "admin",
|
|
||||||
name: "admin",
|
|
||||||
component: () => import("@/views/admin/Admin.vue"),
|
|
||||||
meta: { requiresAdmin: true },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "shrines",
|
|
||||||
name: "shrine links",
|
|
||||||
component: () => import("@/views/home/shrines/Shrines.vue"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "shrines/gto",
|
|
||||||
name: "gto shrine",
|
|
||||||
component: () => import("@/views/home/shrines/GTO.vue"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "shrines/skipskipbenben",
|
|
||||||
name: "skipskipbenben shrine",
|
|
||||||
component: () => import("@/views/home/shrines/Skipskipbenben.vue"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "shrines/evangelion",
|
|
||||||
name: "evangelion shrine",
|
|
||||||
component: () => import("@/views/home/shrines/Evangelion.vue"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "shrines/demoman",
|
|
||||||
name: "demoman shrine",
|
|
||||||
component: () => import("@/views/home/shrines/Demoman.vue"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: ":pathMatch(.*)*",
|
|
||||||
name: "404",
|
|
||||||
component: () => import("@/views/404/404.vue"),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/cv",
|
path: "stp",
|
||||||
component: CVLayout,
|
name: "home",
|
||||||
children: [
|
component: () => import("@/views/home/Home.vue"),
|
||||||
{
|
|
||||||
path: "",
|
|
||||||
name: "cv",
|
|
||||||
component: () => import("@/views/CV/CV.vue"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "jobs",
|
|
||||||
name: "job-applications",
|
|
||||||
component: () => import("@/views/CV/JobApplications.vue"),
|
|
||||||
meta: { requiresAdmin: true },
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
],
|
{
|
||||||
|
path: "admin/login",
|
||||||
|
name: "admin-login",
|
||||||
|
component: () => import("@/views/admin/Login.vue"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "admin",
|
||||||
|
name: "admin",
|
||||||
|
component: () => import("@/views/admin/Admin.vue"),
|
||||||
|
meta: { requiresAdmin: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "shrines",
|
||||||
|
name: "shrine links",
|
||||||
|
component: () => import("@/views/home/shrines/Shrines.vue"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "shrines/gto",
|
||||||
|
name: "gto shrine",
|
||||||
|
component: () => import("@/views/home/shrines/GTO.vue"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "shrines/skipskipbenben",
|
||||||
|
name: "skipskipbenben shrine",
|
||||||
|
component: () => import("@/views/home/shrines/Skipskipbenben.vue"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "shrines/evangelion",
|
||||||
|
name: "evangelion shrine",
|
||||||
|
component: () => import("@/views/home/shrines/Evangelion.vue"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "shrines/demoman",
|
||||||
|
name: "demoman shrine",
|
||||||
|
component: () => import("@/views/home/shrines/Demoman.vue"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: ":pathMatch(.*)*",
|
||||||
|
name: "404",
|
||||||
|
component: () => import("@/views/404/404.vue"),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/cv",
|
||||||
|
component: CVLayout,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: "",
|
||||||
|
name: "cv",
|
||||||
|
component: () => import("@/views/CV/CV.vue"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "jobs",
|
||||||
|
name: "job-applications",
|
||||||
|
component: () => import("@/views/CV/JobApplications.vue"),
|
||||||
|
meta: { requiresAdmin: true },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
router.beforeEach(async (to) => {
|
router.beforeEach(async (to) => {
|
||||||
if (!to.meta.requiresAdmin) return;
|
if (!to.meta.requiresAdmin) return;
|
||||||
const homeData = useHomeDataStore();
|
const homeData = useHomeDataStore();
|
||||||
if (!homeData.loaded) {
|
if (!homeData.loaded) {
|
||||||
await new Promise((resolve) => {
|
await new Promise((resolve) => {
|
||||||
const stop = watch(() => homeData.loaded, (val) => {
|
const stop = watch(
|
||||||
if (val) { stop(); resolve(); }
|
() => homeData.loaded,
|
||||||
});
|
(val) => {
|
||||||
});
|
if (val) {
|
||||||
}
|
stop();
|
||||||
if (!useAuthStore().user.admin) return { path: "/admin/login", query: { redirect: to.fullPath } };
|
resolve();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (!useAuthStore().user.admin)
|
||||||
|
return { path: "/admin/login", query: { redirect: to.fullPath } };
|
||||||
});
|
});
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
<template>
|
<template>
|
||||||
<main class="flex flex-col items-center">
|
<main class="flex flex-col items-center">
|
||||||
<div
|
<div
|
||||||
class="a4page-portrait items-center bdr-1 flex flex-col relative overflow-scroll"
|
class="a4page-portrait items-center bdr-1 flex flex-col relative overflow-scroll"
|
||||||
>
|
>
|
||||||
<h1>404</h1>
|
<h1>404</h1>
|
||||||
<RouterLink to="/" class="bdr-2">
|
<RouterLink to="/" class="bdr-2">
|
||||||
<img src="/img/memes/epic.jpeg" loading="lazy" />
|
<img src="/img/memes/epic.jpeg" loading="lazy" />
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
<h1>Click her, she will take you home</h1>
|
<h1>Click her, she will take you home</h1>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,17 +1,17 @@
|
|||||||
<script setup></script>
|
<script setup></script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col sm:flex-row">
|
<div class="flex flex-col sm:flex-row">
|
||||||
<div class="sm:w-2/7 mt-auto mb-auto ml-0">
|
<div class="sm:w-2/7 mt-auto mb-auto ml-0">
|
||||||
<slot name="left" />
|
<slot name="left" />
|
||||||
</div>
|
|
||||||
<div class="w-full p-2">
|
|
||||||
<div
|
|
||||||
class="flex-row flex place-content-between m-auto place-items-center"
|
|
||||||
>
|
|
||||||
<slot name="top" />
|
|
||||||
</div>
|
|
||||||
<slot />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="w-full p-2">
|
||||||
|
<div
|
||||||
|
class="flex-row flex place-content-between m-auto place-items-center"
|
||||||
|
>
|
||||||
|
<slot name="top" />
|
||||||
|
</div>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -9,15 +9,15 @@ import ManageRadio from "./ManageRadio.vue";
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<main class="justify-center flex flex-row w-full h-full">
|
<main class="justify-center flex flex-row w-full h-full">
|
||||||
<div class="bdr-1 flex flex-col">
|
<div class="bdr-1 flex flex-col">
|
||||||
<CreateUser class="bdr-2 bg-bg_primary" />
|
<CreateUser class="bdr-2 bg-bg_primary" />
|
||||||
<CreatePost class="bdr-2 bg-bg_primary" />
|
<CreatePost class="bdr-2 bg-bg_primary" />
|
||||||
<CreateFavorite class="bdr-2 bg-bg_primary" />
|
<CreateFavorite class="bdr-2 bg-bg_primary" />
|
||||||
<CreateActivity class="bdr-2 bg-bg_primary" />
|
<CreateActivity class="bdr-2 bg-bg_primary" />
|
||||||
<CreateRowing class="bdr-2 bg-bg_primary" />
|
<CreateRowing class="bdr-2 bg-bg_primary" />
|
||||||
<ManageUsers class="bdr-2 bg-bg_primary" />
|
<ManageUsers class="bdr-2 bg-bg_primary" />
|
||||||
<ManageRadio class="bdr-2 bg-bg_primary" />
|
<ManageRadio class="bdr-2 bg-bg_primary" />
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -11,29 +11,35 @@ const name = ref("");
|
|||||||
const link = ref("");
|
const link = ref("");
|
||||||
|
|
||||||
async function post() {
|
async function post() {
|
||||||
try {
|
try {
|
||||||
const data = await gql(
|
const data = await gql(
|
||||||
`mutation CreateActivity($input: CreateActivityInput!) { createActivity(input: $input) { id } }`,
|
`mutation CreateActivity($input: CreateActivityInput!) { createActivity(input: $input) { id } }`,
|
||||||
{ input: { type: type.value, name: name.value, link: link.value || undefined } },
|
{
|
||||||
);
|
input: {
|
||||||
type.value = "";
|
type: type.value,
|
||||||
name.value = "";
|
name: name.value,
|
||||||
link.value = "";
|
link: link.value || undefined,
|
||||||
console.log(data.createActivity);
|
},
|
||||||
emit("done");
|
},
|
||||||
} catch (err) {
|
);
|
||||||
console.error(err);
|
type.value = "";
|
||||||
}
|
name.value = "";
|
||||||
|
link.value = "";
|
||||||
|
console.log(data.createActivity);
|
||||||
|
emit("done");
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<h1>Create Activity</h1>
|
<h1>Create Activity</h1>
|
||||||
<input type="text" v-model="type" placeholder="Type" @keyup.enter="post" />
|
<input type="text" v-model="type" placeholder="Type" @keyup.enter="post" />
|
||||||
<input type="text" v-model="name" placeholder="Name" @keyup.enter="post" />
|
<input type="text" v-model="name" placeholder="Name" @keyup.enter="post" />
|
||||||
<input type="text" v-model="link" placeholder="Link" @keyup.enter="post" />
|
<input type="text" v-model="link" placeholder="Link" @keyup.enter="post" />
|
||||||
<Button @click="post">Upload</Button>
|
<Button @click="post">Upload</Button>
|
||||||
<Button @click="emit('cancel')">Cancel</Button>
|
<Button @click="emit('cancel')">Cancel</Button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -10,28 +10,40 @@ const name = ref("");
|
|||||||
const link = ref("");
|
const link = ref("");
|
||||||
|
|
||||||
async function submit() {
|
async function submit() {
|
||||||
try {
|
try {
|
||||||
await gql(
|
await gql(
|
||||||
`mutation CreateBookmark($input: CreateBookmarkInput!) { createBookmark(input: $input) { id } }`,
|
`mutation CreateBookmark($input: CreateBookmarkInput!) { createBookmark(input: $input) { id } }`,
|
||||||
{ input: { category: category.value, name: name.value, link: link.value } },
|
{
|
||||||
);
|
input: { category: category.value, name: name.value, link: link.value },
|
||||||
category.value = "";
|
},
|
||||||
name.value = "";
|
);
|
||||||
link.value = "";
|
category.value = "";
|
||||||
emit("done");
|
name.value = "";
|
||||||
} catch (err) {
|
link.value = "";
|
||||||
console.error(err);
|
emit("done");
|
||||||
}
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<h1>Create Bookmark</h1>
|
<h1>Create Bookmark</h1>
|
||||||
<input type="text" v-model="category" placeholder="Category" />
|
<input type="text" v-model="category" placeholder="Category" />
|
||||||
<input type="text" v-model="name" placeholder="Name" @keyup.enter="submit" />
|
<input
|
||||||
<input type="text" v-model="link" placeholder="Link" @keyup.enter="submit" />
|
type="text"
|
||||||
<Button @click="submit">Upload</Button>
|
v-model="name"
|
||||||
<Button @click="emit('cancel')">Cancel</Button>
|
placeholder="Name"
|
||||||
</div>
|
@keyup.enter="submit"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
v-model="link"
|
||||||
|
placeholder="Link"
|
||||||
|
@keyup.enter="submit"
|
||||||
|
/>
|
||||||
|
<Button @click="submit">Upload</Button>
|
||||||
|
<Button @click="emit('cancel')">Cancel</Button>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -11,29 +11,35 @@ const name = ref("");
|
|||||||
const link = ref("");
|
const link = ref("");
|
||||||
|
|
||||||
async function post() {
|
async function post() {
|
||||||
try {
|
try {
|
||||||
const data = await gql(
|
const data = await gql(
|
||||||
`mutation CreateFavorite($input: CreateFavoriteInput!) { createFavorite(input: $input) { id } }`,
|
`mutation CreateFavorite($input: CreateFavoriteInput!) { createFavorite(input: $input) { id } }`,
|
||||||
{ input: { type: type.value, name: name.value, link: link.value || undefined } },
|
{
|
||||||
);
|
input: {
|
||||||
type.value = "";
|
type: type.value,
|
||||||
name.value = "";
|
name: name.value,
|
||||||
link.value = "";
|
link: link.value || undefined,
|
||||||
console.log(data.createFavorite);
|
},
|
||||||
emit("done");
|
},
|
||||||
} catch (err) {
|
);
|
||||||
console.error(err);
|
type.value = "";
|
||||||
}
|
name.value = "";
|
||||||
|
link.value = "";
|
||||||
|
console.log(data.createFavorite);
|
||||||
|
emit("done");
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<h1>Create Favorite</h1>
|
<h1>Create Favorite</h1>
|
||||||
<input type="text" v-model="type" placeholder="Type" @keyup.enter="post" />
|
<input type="text" v-model="type" placeholder="Type" @keyup.enter="post" />
|
||||||
<input type="text" v-model="name" placeholder="Name" @keyup.enter="post" />
|
<input type="text" v-model="name" placeholder="Name" @keyup.enter="post" />
|
||||||
<input type="text" v-model="link" placeholder="Link" @keyup.enter="post" />
|
<input type="text" v-model="link" placeholder="Link" @keyup.enter="post" />
|
||||||
<Button @click="post">Upload</Button>
|
<Button @click="post">Upload</Button>
|
||||||
<Button @click="emit('cancel')">Cancel</Button>
|
<Button @click="emit('cancel')">Cancel</Button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -9,31 +9,32 @@ const title = ref("");
|
|||||||
const content = ref("");
|
const content = ref("");
|
||||||
|
|
||||||
async function post() {
|
async function post() {
|
||||||
try {
|
try {
|
||||||
const data = await gql(
|
const data = await gql(
|
||||||
`mutation CreatePost($input: CreatePostInput!) { createPost(input: $input) { id } }`,
|
`mutation CreatePost($input: CreatePostInput!) { createPost(input: $input) { id } }`,
|
||||||
{ input: { title: title.value, content: content.value } },
|
{ input: { title: title.value, content: content.value } },
|
||||||
);
|
);
|
||||||
title.value = "";
|
title.value = "";
|
||||||
content.value = "";
|
content.value = "";
|
||||||
console.log(data.createPost);
|
console.log(data.createPost);
|
||||||
emit("done");
|
emit("done");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<h1>Create Post</h1>
|
<h1>Create Post</h1>
|
||||||
<input type="text" v-model="title" placeholder="Title" @keyup.enter="post" />
|
<input
|
||||||
<textarea
|
type="text"
|
||||||
class="h-50"
|
v-model="title"
|
||||||
v-model="content"
|
placeholder="Title"
|
||||||
placeholder="Content"
|
@keyup.enter="post"
|
||||||
></textarea>
|
/>
|
||||||
<Button @click="post">Upload</Button>
|
<textarea class="h-50" v-model="content" placeholder="Content"></textarea>
|
||||||
<Button @click="emit('cancel')">Cancel</Button>
|
<Button @click="post">Upload</Button>
|
||||||
</div>
|
<Button @click="emit('cancel')">Cancel</Button>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -9,47 +9,60 @@ const images = ref([]);
|
|||||||
const results = ref([]);
|
const results = ref([]);
|
||||||
|
|
||||||
function onFileChange(e) {
|
function onFileChange(e) {
|
||||||
images.value = Array.from(e.target.files);
|
images.value = Array.from(e.target.files);
|
||||||
results.value = [];
|
results.value = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
async function submit() {
|
async function submit() {
|
||||||
if (!images.value.length) return;
|
if (!images.value.length) return;
|
||||||
results.value = images.value.map((f) => ({ name: f.name, status: "Uploading..." }));
|
results.value = images.value.map((f) => ({
|
||||||
|
name: f.name,
|
||||||
|
status: "Uploading...",
|
||||||
|
}));
|
||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
images.value.map(async (file, i) => {
|
images.value.map(async (file, i) => {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append("image", file);
|
formData.append("image", file);
|
||||||
try {
|
try {
|
||||||
const res = await axios.post("/api/rowing", formData, {
|
const res = await axios.post("/api/rowing", formData, {
|
||||||
headers: { "Content-Type": "multipart/form-data" },
|
headers: { "Content-Type": "multipart/form-data" },
|
||||||
});
|
});
|
||||||
const mins = Math.floor(res.data.Time / 1e9 / 60);
|
const mins = Math.floor(res.data.Time / 1e9 / 60);
|
||||||
const secs = String(Math.floor((res.data.Time / 1e9) % 60)).padStart(2, "0");
|
const secs = String(Math.floor((res.data.Time / 1e9) % 60)).padStart(
|
||||||
results.value[i].status = `${res.data.Distance}m in ${mins}:${secs}`;
|
2,
|
||||||
results.value[i].ok = true;
|
"0",
|
||||||
} catch (err) {
|
);
|
||||||
results.value[i].status = err.response?.data?.error || "Upload failed";
|
results.value[i].status = `${res.data.Distance}m in ${mins}:${secs}`;
|
||||||
results.value[i].ok = false;
|
results.value[i].ok = true;
|
||||||
}
|
} catch (err) {
|
||||||
})
|
results.value[i].status = err.response?.data?.error || "Upload failed";
|
||||||
);
|
results.value[i].ok = false;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
images.value = [];
|
images.value = [];
|
||||||
emit("done");
|
emit("done");
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<h1>Create Rowing</h1>
|
<h1>Create Rowing</h1>
|
||||||
<input type="file" accept="image/jpeg,image/png,image/gif,image/webp" multiple @change="onFileChange" />
|
<input
|
||||||
<Button @click="submit">Upload</Button>
|
type="file"
|
||||||
<Button @click="emit('cancel')">Cancel</Button>
|
accept="image/jpeg,image/png,image/gif,image/webp"
|
||||||
<div v-for="r in results" :key="r.name">
|
multiple
|
||||||
<span class="text-primary">{{ r.name }}: </span>
|
@change="onFileChange"
|
||||||
<span :class="r.ok ? 'text-secondary' : 'text-red-500'">{{ r.status }}</span>
|
/>
|
||||||
</div>
|
<Button @click="submit">Upload</Button>
|
||||||
|
<Button @click="emit('cancel')">Cancel</Button>
|
||||||
|
<div v-for="r in results" :key="r.name">
|
||||||
|
<span class="text-primary">{{ r.name }}: </span>
|
||||||
|
<span :class="r.ok ? 'text-secondary' : 'text-red-500'">{{
|
||||||
|
r.status
|
||||||
|
}}</span>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -11,35 +11,45 @@ const message = ref("");
|
|||||||
const error = ref("");
|
const error = ref("");
|
||||||
|
|
||||||
async function handleCreate() {
|
async function handleCreate() {
|
||||||
message.value = "";
|
message.value = "";
|
||||||
error.value = "";
|
error.value = "";
|
||||||
try {
|
try {
|
||||||
const data = await gql(
|
const data = await gql(
|
||||||
`mutation CreateUser($input: CreateUserInput!) { createUser(input: $input) { id username } }`,
|
`mutation CreateUser($input: CreateUserInput!) { createUser(input: $input) { id username } }`,
|
||||||
{ input: { username: username.value, password: password.value } },
|
{ input: { username: username.value, password: password.value } },
|
||||||
);
|
);
|
||||||
message.value = `User "${data.createUser.username}" created successfully.`;
|
message.value = `User "${data.createUser.username}" created successfully.`;
|
||||||
username.value = "";
|
username.value = "";
|
||||||
password.value = "";
|
password.value = "";
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error.value = err.message || "Failed to create user.";
|
error.value = err.message || "Failed to create user.";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div v-if="auth.loggedIn && auth.user.admin" class="flex flex-col">
|
<div v-if="auth.loggedIn && auth.user.admin" class="flex flex-col">
|
||||||
<h1>Create User</h1>
|
<h1>Create User</h1>
|
||||||
<p v-if="message" class="text-green-500">{{ message }}</p>
|
<p v-if="message" class="text-green-500">{{ message }}</p>
|
||||||
<p v-if="error" class="text-red-500">{{ error }}</p>
|
<p v-if="error" class="text-red-500">{{ error }}</p>
|
||||||
<input type="text" v-model="username" placeholder="Username" @keyup.enter="handleCreate" />
|
<input
|
||||||
<input type="password" v-model="password" placeholder="Password" @keyup.enter="handleCreate" />
|
type="text"
|
||||||
<Button @click="handleCreate">Create Account</Button>
|
v-model="username"
|
||||||
</div>
|
placeholder="Username"
|
||||||
<div v-else-if="auth.loggedIn" class="flex flex-col">
|
@keyup.enter="handleCreate"
|
||||||
<p>You do not have permission to create users.</p>
|
/>
|
||||||
</div>
|
<input
|
||||||
<div v-else class="flex flex-col">
|
type="password"
|
||||||
<p>You must be logged in as an admin to create users.</p>
|
v-model="password"
|
||||||
</div>
|
placeholder="Password"
|
||||||
|
@keyup.enter="handleCreate"
|
||||||
|
/>
|
||||||
|
<Button @click="handleCreate">Create Account</Button>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="auth.loggedIn" class="flex flex-col">
|
||||||
|
<p>You do not have permission to create users.</p>
|
||||||
|
</div>
|
||||||
|
<div v-else class="flex flex-col">
|
||||||
|
<p>You must be logged in as an admin to create users.</p>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -12,29 +12,39 @@ const username = ref("");
|
|||||||
const password = ref("");
|
const password = ref("");
|
||||||
|
|
||||||
async function handleLogin() {
|
async function handleLogin() {
|
||||||
await auth.logIn(username.value, password.value);
|
await auth.logIn(username.value, password.value);
|
||||||
if (auth.loggedIn && route.query.redirect) {
|
if (auth.loggedIn && route.query.redirect) {
|
||||||
router.push(route.query.redirect);
|
router.push(route.query.redirect);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleLogout() {
|
function handleLogout() {
|
||||||
auth.logOut();
|
auth.logOut();
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div v-if="auth.loggedIn" class="flex flex-col">
|
<div v-if="auth.loggedIn" class="flex flex-col">
|
||||||
<h1>Logged in</h1>
|
<h1>Logged in</h1>
|
||||||
<p>{{ auth.user.id }}</p>
|
<p>{{ auth.user.id }}</p>
|
||||||
<p>{{ auth.user.username }}</p>
|
<p>{{ auth.user.username }}</p>
|
||||||
<p>{{ auth.user.admin }}</p>
|
<p>{{ auth.user.admin }}</p>
|
||||||
<Button @click="handleLogout">Log Out</Button>
|
<Button @click="handleLogout">Log Out</Button>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="flex flex-col">
|
<div v-else class="flex flex-col">
|
||||||
<h1>Login</h1>
|
<h1>Login</h1>
|
||||||
<input type="text" v-model="username" placeholder="Username" @keyup.enter="handleLogin" />
|
<input
|
||||||
<input type="password" v-model="password" placeholder="Password" @keyup.enter="handleLogin" />
|
type="text"
|
||||||
<Button @click="handleLogin">Log In</Button>
|
v-model="username"
|
||||||
</div>
|
placeholder="Username"
|
||||||
|
@keyup.enter="handleLogin"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
v-model="password"
|
||||||
|
placeholder="Password"
|
||||||
|
@keyup.enter="handleLogin"
|
||||||
|
/>
|
||||||
|
<Button @click="handleLogin">Log In</Button>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -9,94 +9,108 @@ const results = ref([]);
|
|||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
|
|
||||||
async function fetchSongs() {
|
async function fetchSongs() {
|
||||||
try {
|
try {
|
||||||
const res = await axios.get("/api/radio/songs");
|
const res = await axios.get("/api/radio/songs");
|
||||||
songs.value = res.data.songs;
|
songs.value = res.data.songs;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onFileChange(e) {
|
function onFileChange(e) {
|
||||||
files.value = Array.from(e.target.files);
|
files.value = Array.from(e.target.files);
|
||||||
results.value = [];
|
results.value = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
async function upload() {
|
async function upload() {
|
||||||
if (!files.value.length) return;
|
if (!files.value.length) return;
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
results.value = files.value.map((f) => ({ name: f.name, status: "Uploading..." }));
|
results.value = files.value.map((f) => ({
|
||||||
|
name: f.name,
|
||||||
|
status: "Uploading...",
|
||||||
|
}));
|
||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
files.value.map(async (file, i) => {
|
files.value.map(async (file, i) => {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append("file", file);
|
formData.append("file", file);
|
||||||
try {
|
try {
|
||||||
await axios.post("/api/radio/upload", formData, {
|
await axios.post("/api/radio/upload", formData, {
|
||||||
headers: { "Content-Type": "multipart/form-data" },
|
headers: { "Content-Type": "multipart/form-data" },
|
||||||
});
|
});
|
||||||
results.value[i].status = "Uploaded";
|
results.value[i].status = "Uploaded";
|
||||||
results.value[i].ok = true;
|
results.value[i].ok = true;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
results.value[i].status = err.response?.data?.error || "Upload failed";
|
results.value[i].status = err.response?.data?.error || "Upload failed";
|
||||||
results.value[i].ok = false;
|
results.value[i].ok = false;
|
||||||
}
|
}
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
files.value = [];
|
files.value = [];
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
await fetchSongs();
|
await fetchSongs();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deleteSong(name) {
|
async function deleteSong(name) {
|
||||||
try {
|
try {
|
||||||
await axios.delete(`/api/radio/songs/${encodeURIComponent(name)}`);
|
await axios.delete(`/api/radio/songs/${encodeURIComponent(name)}`);
|
||||||
await fetchSongs();
|
await fetchSongs();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function toggleSong(song) {
|
async function toggleSong(song) {
|
||||||
const action = song.disabled ? "enable" : "disable";
|
const action = song.disabled ? "enable" : "disable";
|
||||||
try {
|
try {
|
||||||
await axios.patch(`/api/radio/songs/${encodeURIComponent(song.name)}/${action}`);
|
await axios.patch(
|
||||||
await fetchSongs();
|
`/api/radio/songs/${encodeURIComponent(song.name)}/${action}`,
|
||||||
} catch (err) {
|
);
|
||||||
console.error(err);
|
await fetchSongs();
|
||||||
}
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatSize(bytes) {
|
function formatSize(bytes) {
|
||||||
if (bytes < 1024) return bytes + " B";
|
if (bytes < 1024) return bytes + " B";
|
||||||
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + " KB";
|
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + " KB";
|
||||||
return (bytes / (1024 * 1024)).toFixed(1) + " MB";
|
return (bytes / (1024 * 1024)).toFixed(1) + " MB";
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(fetchSongs);
|
onMounted(fetchSongs);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<h1>Manage Radio</h1>
|
<h1>Manage Radio</h1>
|
||||||
<input type="file" accept=".mp3,.ogg,.flac,.wav,.m4a,.opus" multiple @change="onFileChange" />
|
<input
|
||||||
<Button @click="upload" :disabled="loading">Upload</Button>
|
type="file"
|
||||||
<div v-for="r in results" :key="r.name">
|
accept=".mp3,.ogg,.flac,.wav,.m4a,.opus"
|
||||||
<span class="text-primary">{{ r.name }}: </span>
|
multiple
|
||||||
<span :class="r.ok ? 'text-secondary' : 'text-red-500'">{{ r.status }}</span>
|
@change="onFileChange"
|
||||||
</div>
|
/>
|
||||||
<div
|
<Button @click="upload" :disabled="loading">Upload</Button>
|
||||||
v-for="song in songs"
|
<div v-for="r in results" :key="r.name">
|
||||||
:key="song.name"
|
<span class="text-primary">{{ r.name }}: </span>
|
||||||
class="flex flex-row items-center gap-2"
|
<span :class="r.ok ? 'text-secondary' : 'text-red-500'">{{
|
||||||
:class="{ 'opacity-50': song.disabled }"
|
r.status
|
||||||
>
|
}}</span>
|
||||||
<span :class="{ 'line-through': song.disabled }">{{ song.name }}</span>
|
|
||||||
<span class="text-secondary text-sm">{{ formatSize(song.size) }}</span>
|
|
||||||
<span v-if="song.disabled" class="text-red-400 text-xs">disabled</span>
|
|
||||||
<Button @click="toggleSong(song)">{{ song.disabled ? "Enable" : "Disable" }}</Button>
|
|
||||||
<Button @click="deleteSong(song.name)">Delete</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
v-for="song in songs"
|
||||||
|
:key="song.name"
|
||||||
|
class="flex flex-row items-center gap-2"
|
||||||
|
:class="{ 'opacity-50': song.disabled }"
|
||||||
|
>
|
||||||
|
<span :class="{ 'line-through': song.disabled }">{{ song.name }}</span>
|
||||||
|
<span class="text-secondary text-sm">{{ formatSize(song.size) }}</span>
|
||||||
|
<span v-if="song.disabled" class="text-red-400 text-xs">disabled</span>
|
||||||
|
<Button @click="toggleSong(song)">{{
|
||||||
|
song.disabled ? "Enable" : "Disable"
|
||||||
|
}}</Button>
|
||||||
|
<Button @click="deleteSong(song.name)">Delete</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -8,38 +8,39 @@ const auth = useAuthStore();
|
|||||||
const users = ref([]);
|
const users = ref([]);
|
||||||
|
|
||||||
async function fetchUsers() {
|
async function fetchUsers() {
|
||||||
try {
|
try {
|
||||||
const data = await gql(`query { users { id username admin } }`);
|
const data = await gql(`query { users { id username admin } }`);
|
||||||
users.value = data.users;
|
users.value = data.users;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function toggleAdmin(user) {
|
async function toggleAdmin(user) {
|
||||||
try {
|
try {
|
||||||
const data = await auth.setUserAdmin(user.id, !user.admin);
|
const data = await auth.setUserAdmin(user.id, !user.admin);
|
||||||
user.admin = data.admin;
|
user.admin = data.admin;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(fetchUsers);
|
onMounted(fetchUsers);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<h1>Manage Users</h1>
|
<h1>Manage Users</h1>
|
||||||
<div v-for="user in users" :key="user.id" class="flex flex-row items-center gap-2">
|
<div
|
||||||
<span>{{ user.username }}</span>
|
v-for="user in users"
|
||||||
<span v-if="user.admin">(admin)</span>
|
:key="user.id"
|
||||||
<Button
|
class="flex flex-row items-center gap-2"
|
||||||
v-if="user.id !== auth.user.id"
|
>
|
||||||
@click="toggleAdmin(user)"
|
<span>{{ user.username }}</span>
|
||||||
>
|
<span v-if="user.admin">(admin)</span>
|
||||||
{{ user.admin ? "Demote" : "Promote" }}
|
<Button v-if="user.id !== auth.user.id" @click="toggleAdmin(user)">
|
||||||
</Button>
|
{{ user.admin ? "Demote" : "Promote" }}
|
||||||
</div>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useTemplateRef, ref, onMounted, onUnmounted } from 'vue';
|
import { useTemplateRef, ref, onMounted, onUnmounted } from "vue";
|
||||||
|
|
||||||
const display = useTemplateRef('display')
|
const display = useTemplateRef("display");
|
||||||
const displayText = ref("");
|
const displayText = ref("");
|
||||||
|
|
||||||
const charHeight: number = 14;
|
const charHeight: number = 14;
|
||||||
@@ -10,33 +10,37 @@ let n: number;
|
|||||||
let m: number;
|
let m: number;
|
||||||
|
|
||||||
function setup() {
|
function setup() {
|
||||||
display.value.style.fontSize = `${charHeight}px`;
|
display.value.style.fontSize = `${charHeight}px`;
|
||||||
display.value.style.lineHeight = `${charHeight}px`;
|
display.value.style.lineHeight = `${charHeight}px`;
|
||||||
fillDisplay()
|
fillDisplay();
|
||||||
}
|
}
|
||||||
|
|
||||||
function fillDisplay() {
|
function fillDisplay() {
|
||||||
// M rows N columns
|
// M rows N columns
|
||||||
m = Math.floor(display.value.offsetHeight / charHeight);
|
m = Math.floor(display.value.offsetHeight / charHeight);
|
||||||
n = Math.floor(display.value.offsetWidth / charWidth);
|
n = Math.floor(display.value.offsetWidth / charWidth);
|
||||||
const row = ' '.repeat(n);
|
const row = " ".repeat(n);
|
||||||
displayText.value = (row + '\n').repeat(m - 1) + row
|
displayText.value = (row + "\n").repeat(m - 1) + row;
|
||||||
}
|
}
|
||||||
|
|
||||||
function close() {
|
function close() {
|
||||||
displayText.value = ""
|
displayText.value = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
setup()
|
setup();
|
||||||
})
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
close()
|
close();
|
||||||
})
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<pre class="overflow-scroll w-full h-full bg-black text-white m-0 p-0" id="container" ref="display">{{ displayText
|
<pre
|
||||||
}}</pre>
|
class="overflow-scroll w-full h-full bg-black text-white m-0 p-0"
|
||||||
|
id="container"
|
||||||
|
ref="display"
|
||||||
|
>{{ displayText }}</pre
|
||||||
|
>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -5,7 +5,9 @@ import Header from "@/components/text/Header.vue";
|
|||||||
import { useHomeDataStore } from "@/stores/homeData";
|
import { useHomeDataStore } from "@/stores/homeData";
|
||||||
import { useAuthStore } from "@/stores/auth";
|
import { useAuthStore } from "@/stores/auth";
|
||||||
|
|
||||||
const CreateBookmark = defineAsyncComponent(() => import("@/views/admin/CreateBookmark.vue"));
|
const CreateBookmark = defineAsyncComponent(
|
||||||
|
() => import("@/views/admin/CreateBookmark.vue"),
|
||||||
|
);
|
||||||
|
|
||||||
const homeData = useHomeDataStore();
|
const homeData = useHomeDataStore();
|
||||||
const authStore = useAuthStore();
|
const authStore = useAuthStore();
|
||||||
@@ -13,48 +15,57 @@ const authStore = useAuthStore();
|
|||||||
const showCreate = ref(false);
|
const showCreate = ref(false);
|
||||||
|
|
||||||
const groupedBookmarks = computed(() => {
|
const groupedBookmarks = computed(() => {
|
||||||
const groups = {};
|
const groups = {};
|
||||||
for (const b of homeData.bookmarks) {
|
for (const b of homeData.bookmarks) {
|
||||||
if (!groups[b.category]) groups[b.category] = [];
|
if (!groups[b.category]) groups[b.category] = [];
|
||||||
groups[b.category].push(b);
|
groups[b.category].push(b);
|
||||||
}
|
}
|
||||||
return Object.entries(groups);
|
return Object.entries(groups);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="bookmarks-wrapper">
|
<div class="bookmarks-wrapper">
|
||||||
<Header class="text-left">
|
<Header class="text-left">
|
||||||
<span class="flex items-center justify-between w-full">
|
<span class="flex items-center justify-between w-full">
|
||||||
{{ showCreate ? "Create Bookmark" : "Bookmarks" }}
|
{{ showCreate ? "Create Bookmark" : "Bookmarks" }}
|
||||||
<button v-if="authStore.user.admin" class="text-sm px-1" @click="showCreate = !showCreate">
|
<button
|
||||||
{{ showCreate ? "x" : "+" }}
|
v-if="authStore.user.admin"
|
||||||
</button>
|
class="text-sm px-1"
|
||||||
</span>
|
@click="showCreate = !showCreate"
|
||||||
</Header>
|
>
|
||||||
<CreateBookmark v-if="showCreate" class="flex-1 min-h-0 p-1" @done="showCreate = false" @cancel="showCreate = false" />
|
{{ showCreate ? "x" : "+" }}
|
||||||
<div v-if="!showCreate" class="bookmarks-scroll">
|
</button>
|
||||||
<LinkTable
|
</span>
|
||||||
v-for="group in groupedBookmarks"
|
</Header>
|
||||||
:key="group[0]"
|
<CreateBookmark
|
||||||
:title="group[0]"
|
v-if="showCreate"
|
||||||
:items="group[1]"
|
class="flex-1 min-h-0 p-1"
|
||||||
/>
|
@done="showCreate = false"
|
||||||
</div>
|
@cancel="showCreate = false"
|
||||||
|
/>
|
||||||
|
<div v-if="!showCreate" class="bookmarks-scroll">
|
||||||
|
<LinkTable
|
||||||
|
v-for="group in groupedBookmarks"
|
||||||
|
:key="group[0]"
|
||||||
|
:title="group[0]"
|
||||||
|
:items="group[1]"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.bookmarks-wrapper {
|
.bookmarks-wrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bookmarks-scroll {
|
.bookmarks-scroll {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -2,14 +2,14 @@
|
|||||||
import Slideshow from "@/components/util/Slideshow.vue";
|
import Slideshow from "@/components/util/Slideshow.vue";
|
||||||
|
|
||||||
const images = [
|
const images = [
|
||||||
{ url: "/img/memes/pidgeon.gif", comment: "鸟" },
|
{ url: "/img/memes/pidgeon.gif", comment: "鸟" },
|
||||||
// { url: "/img/memes/no_slip.png" },
|
// { url: "/img/memes/no_slip.png" },
|
||||||
// { url: "/img/memes/epic.jpeg" },
|
// { url: "/img/memes/epic.jpeg" },
|
||||||
// { url: "/img/bedroom/img2.png", comment: "办公桌" },
|
// { url: "/img/bedroom/img2.png", comment: "办公桌" },
|
||||||
// { url: "/img/bedroom/img1.png", comment: "床" },
|
// { url: "/img/bedroom/img1.png", comment: "床" },
|
||||||
];
|
];
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Slideshow :images="images" />
|
<Slideshow :images="images" />
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -7,7 +7,9 @@ import { ref, defineAsyncComponent } from "vue";
|
|||||||
import { useActivityStore } from "@/stores/activity";
|
import { useActivityStore } from "@/stores/activity";
|
||||||
import { useAuthStore } from "@/stores/auth";
|
import { useAuthStore } from "@/stores/auth";
|
||||||
|
|
||||||
const CreateActivity = defineAsyncComponent(() => import("@/views/admin/CreateActivity.vue"));
|
const CreateActivity = defineAsyncComponent(
|
||||||
|
() => import("@/views/admin/CreateActivity.vue"),
|
||||||
|
);
|
||||||
|
|
||||||
const activityStore = useActivityStore();
|
const activityStore = useActivityStore();
|
||||||
const authStore = useAuthStore();
|
const authStore = useAuthStore();
|
||||||
@@ -15,18 +17,31 @@ const showCreate = ref(false);
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col items-center">
|
<div class="flex flex-col items-center">
|
||||||
<Header>
|
<Header>
|
||||||
<span class="flex items-center justify-between w-full">
|
<span class="flex items-center justify-between w-full">
|
||||||
{{ showCreate ? "Create Activity" : "Consumption" }}
|
{{ showCreate ? "Create Activity" : "Consumption" }}
|
||||||
<button v-if="authStore.user.admin" class="text-sm px-1" @click="showCreate = !showCreate">
|
<button
|
||||||
{{ showCreate ? "x" : "+" }}
|
v-if="authStore.user.admin"
|
||||||
</button>
|
class="text-sm px-1"
|
||||||
</span>
|
@click="showCreate = !showCreate"
|
||||||
</Header>
|
>
|
||||||
<CreateActivity v-if="showCreate" class="flex-1 w-full p-1" @done="showCreate = false" @cancel="showCreate = false" />
|
{{ showCreate ? "x" : "+" }}
|
||||||
<AutoScroll v-if="!showCreate" class="flex-1 w-full">
|
</button>
|
||||||
<LinkTable variant="table" class="w-full" :items="activityStore.activity" />
|
</span>
|
||||||
</AutoScroll>
|
</Header>
|
||||||
</div>
|
<CreateActivity
|
||||||
|
v-if="showCreate"
|
||||||
|
class="flex-1 w-full p-1"
|
||||||
|
@done="showCreate = false"
|
||||||
|
@cancel="showCreate = false"
|
||||||
|
/>
|
||||||
|
<AutoScroll v-if="!showCreate" class="flex-1 w-full">
|
||||||
|
<LinkTable
|
||||||
|
variant="table"
|
||||||
|
class="w-full"
|
||||||
|
:items="activityStore.activity"
|
||||||
|
/>
|
||||||
|
</AutoScroll>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -7,7 +7,9 @@ import { ref, defineAsyncComponent } from "vue";
|
|||||||
import { useFavoritesStore } from "@/stores/favorites";
|
import { useFavoritesStore } from "@/stores/favorites";
|
||||||
import { useAuthStore } from "@/stores/auth";
|
import { useAuthStore } from "@/stores/auth";
|
||||||
|
|
||||||
const CreateFavorite = defineAsyncComponent(() => import("@/views/admin/CreateFavorite.vue"));
|
const CreateFavorite = defineAsyncComponent(
|
||||||
|
() => import("@/views/admin/CreateFavorite.vue"),
|
||||||
|
);
|
||||||
|
|
||||||
const favoritesStore = useFavoritesStore();
|
const favoritesStore = useFavoritesStore();
|
||||||
const authStore = useAuthStore();
|
const authStore = useAuthStore();
|
||||||
@@ -15,22 +17,31 @@ const showCreate = ref(false);
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col items-center">
|
<div class="flex flex-col items-center">
|
||||||
<Header>
|
<Header>
|
||||||
<span class="flex items-center justify-between w-full">
|
<span class="flex items-center justify-between w-full">
|
||||||
{{ showCreate ? "Create Favorite" : "favs" }}
|
{{ showCreate ? "Create Favorite" : "favs" }}
|
||||||
<button v-if="authStore.user.admin" class="text-sm px-1" @click="showCreate = !showCreate">
|
<button
|
||||||
{{ showCreate ? "x" : "+" }}
|
v-if="authStore.user.admin"
|
||||||
</button>
|
class="text-sm px-1"
|
||||||
</span>
|
@click="showCreate = !showCreate"
|
||||||
</Header>
|
>
|
||||||
<CreateFavorite v-if="showCreate" class="w-full flex-1 p-1" @done="showCreate = false" @cancel="showCreate = false" />
|
{{ showCreate ? "x" : "+" }}
|
||||||
<AutoScroll v-if="!showCreate" class="w-full flex-1">
|
</button>
|
||||||
<LinkTable
|
</span>
|
||||||
variant="table"
|
</Header>
|
||||||
class="w-full"
|
<CreateFavorite
|
||||||
:items="favoritesStore.favorites"
|
v-if="showCreate"
|
||||||
/>
|
class="w-full flex-1 p-1"
|
||||||
</AutoScroll>
|
@done="showCreate = false"
|
||||||
</div>
|
@cancel="showCreate = false"
|
||||||
|
/>
|
||||||
|
<AutoScroll v-if="!showCreate" class="w-full flex-1">
|
||||||
|
<LinkTable
|
||||||
|
variant="table"
|
||||||
|
class="w-full"
|
||||||
|
:items="favoritesStore.favorites"
|
||||||
|
/>
|
||||||
|
</AutoScroll>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -7,7 +7,9 @@ import { ref, computed, defineAsyncComponent } from "vue";
|
|||||||
import { useAuthStore } from "@/stores/auth";
|
import { useAuthStore } from "@/stores/auth";
|
||||||
import { usePostsStore } from "@/stores/posts";
|
import { usePostsStore } from "@/stores/posts";
|
||||||
|
|
||||||
const CreatePost = defineAsyncComponent(() => import("@/views/admin/CreatePost.vue"));
|
const CreatePost = defineAsyncComponent(
|
||||||
|
() => import("@/views/admin/CreatePost.vue"),
|
||||||
|
);
|
||||||
|
|
||||||
const authStore = useAuthStore();
|
const authStore = useAuthStore();
|
||||||
const postsStore = usePostsStore();
|
const postsStore = usePostsStore();
|
||||||
@@ -20,75 +22,70 @@ const rightCap = computed(() => idx.value === postsStore.postsCount - 1);
|
|||||||
|
|
||||||
const post = computed(() => postsStore.posts[idx.value]);
|
const post = computed(() => postsStore.posts[idx.value]);
|
||||||
const userOwnsPost = computed(
|
const userOwnsPost = computed(
|
||||||
() => post.value.author.username == authStore.user.username,
|
() => post.value.author.username == authStore.user.username,
|
||||||
);
|
);
|
||||||
|
|
||||||
function nextPost() {
|
function nextPost() {
|
||||||
if (idx.value < postsStore.postsCount - 1) {
|
if (idx.value < postsStore.postsCount - 1) {
|
||||||
idx.value++;
|
idx.value++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function prevPost() {
|
function prevPost() {
|
||||||
if (idx.value > 0) {
|
if (idx.value > 0) {
|
||||||
idx.value--;
|
idx.value--;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function deletePost() {
|
function deletePost() {
|
||||||
postsStore.deletePost(post.value);
|
postsStore.deletePost(post.value);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col flex-1 min-h-0">
|
<div class="flex flex-col flex-1 min-h-0">
|
||||||
<Header>
|
<Header>
|
||||||
<span class="flex items-center justify-between w-full">
|
<span class="flex items-center justify-between w-full">
|
||||||
{{ showCreate ? "Create Post" : post.title }}
|
{{ showCreate ? "Create Post" : post.title }}
|
||||||
<button v-if="authStore.user.admin" class="text-sm px-1" @click="showCreate = !showCreate">
|
<button
|
||||||
{{ showCreate ? "x" : "+" }}
|
v-if="authStore.user.admin"
|
||||||
</button>
|
class="text-sm px-1"
|
||||||
</span>
|
@click="showCreate = !showCreate"
|
||||||
</Header>
|
|
||||||
<CreatePost v-if="showCreate" class="flex-1 min-h-0 p-1" @done="showCreate = false" @cancel="showCreate = false" />
|
|
||||||
<div
|
|
||||||
v-if="!showCreate"
|
|
||||||
class="flex flex-col flex-1 min-h-0 p-1 overflow-auto text-left items-start justify-start"
|
|
||||||
>
|
>
|
||||||
<small
|
{{ showCreate ? "x" : "+" }}
|
||||||
>Created at:
|
</button>
|
||||||
{{ new Date(post.createdAt).toLocaleString() }}</small
|
</span>
|
||||||
>
|
</Header>
|
||||||
<small>By: {{ post.author.username }}</small>
|
<CreatePost
|
||||||
<Markdown
|
v-if="showCreate"
|
||||||
class="flex-1 border-box text-wrap"
|
class="flex-1 min-h-0 p-1"
|
||||||
:source="post.content"
|
@done="showCreate = false"
|
||||||
/>
|
@cancel="showCreate = false"
|
||||||
<div class="flex flex-row w-full">
|
/>
|
||||||
<Button
|
<div
|
||||||
class="flex-1 border-box"
|
v-if="!showCreate"
|
||||||
v-if="!leftCap"
|
class="flex flex-col flex-1 min-h-0 p-1 overflow-auto text-left items-start justify-start"
|
||||||
@click="prevPost"
|
>
|
||||||
>
|
<small>Created at: {{ new Date(post.createdAt).toLocaleString() }}</small>
|
||||||
Prev
|
<small>By: {{ post.author.username }}</small>
|
||||||
</Button>
|
<Markdown class="flex-1 border-box text-wrap" :source="post.content" />
|
||||||
<Button
|
<div class="flex flex-row w-full">
|
||||||
class="flex-1 border-box"
|
<Button class="flex-1 border-box" v-if="!leftCap" @click="prevPost">
|
||||||
v-if="!rightCap"
|
Prev
|
||||||
@click="nextPost"
|
</Button>
|
||||||
>
|
<Button class="flex-1 border-box" v-if="!rightCap" @click="nextPost">
|
||||||
Next
|
Next
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<Button class="w-full" v-if="userOwnsPost" @click="deletePost"
|
<Button class="w-full" v-if="userOwnsPost" @click="deletePost"
|
||||||
>Delete</Button
|
>Delete</Button
|
||||||
>
|
>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
img {
|
img {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -2,25 +2,25 @@
|
|||||||
import Header from "@/components/text/Header.vue";
|
import Header from "@/components/text/Header.vue";
|
||||||
import LinkTable from "@/components/util/LinkTable.vue";
|
import LinkTable from "@/components/util/LinkTable.vue";
|
||||||
const gym = [
|
const gym = [
|
||||||
{ name: "Row", type: "30 min" },
|
{ name: "Row", type: "30 min" },
|
||||||
{ name: "Run", type: "5k" },
|
{ name: "Run", type: "5k" },
|
||||||
{ name: "Pushup & Squat", type: "50" },
|
{ name: "Pushup & Squat", type: "50" },
|
||||||
];
|
];
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col place-content-between items-center">
|
<div class="flex flex-col place-content-between items-center">
|
||||||
<Header>Gym</Header>
|
<Header>Gym</Header>
|
||||||
<p>I'm not a gym geek</p>
|
<p>I'm not a gym geek</p>
|
||||||
<p>4/7 days I do:</p>
|
<p>4/7 days I do:</p>
|
||||||
<div class="overflow-scroll w-full border-box">
|
<div class="overflow-scroll w-full border-box">
|
||||||
<LinkTable variant="table" class="w-full" :items="gym" />
|
<LinkTable variant="table" class="w-full" :items="gym" />
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
img {
|
img {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -5,7 +5,9 @@ import { useHomeDataStore } from "@/stores/homeData";
|
|||||||
import { useAuthStore } from "@/stores/auth";
|
import { useAuthStore } from "@/stores/auth";
|
||||||
import { storeToRefs } from "pinia";
|
import { storeToRefs } from "pinia";
|
||||||
|
|
||||||
const CreateRowing = defineAsyncComponent(() => import("@/views/admin/CreateRowing.vue"));
|
const CreateRowing = defineAsyncComponent(
|
||||||
|
() => import("@/views/admin/CreateRowing.vue"),
|
||||||
|
);
|
||||||
|
|
||||||
const store = useHomeDataStore();
|
const store = useHomeDataStore();
|
||||||
const authStore = useAuthStore();
|
const authStore = useAuthStore();
|
||||||
@@ -117,13 +119,22 @@ function formatValue(key, val) {
|
|||||||
<Header>
|
<Header>
|
||||||
<span class="flex items-center justify-between w-full">
|
<span class="flex items-center justify-between w-full">
|
||||||
{{ showCreate ? "Upload Rowing" : "Rowing" }}
|
{{ showCreate ? "Upload Rowing" : "Rowing" }}
|
||||||
<button v-if="authStore.user.admin" class="text-sm px-1" @click="showCreate = !showCreate">
|
<button
|
||||||
|
v-if="authStore.user.admin"
|
||||||
|
class="text-sm px-1"
|
||||||
|
@click="showCreate = !showCreate"
|
||||||
|
>
|
||||||
{{ showCreate ? "x" : "+" }}
|
{{ showCreate ? "x" : "+" }}
|
||||||
</button>
|
</button>
|
||||||
</span>
|
</span>
|
||||||
</Header>
|
</Header>
|
||||||
|
|
||||||
<CreateRowing v-if="showCreate" class="flex-1 p-1" @done="showCreate = false" @cancel="showCreate = false" />
|
<CreateRowing
|
||||||
|
v-if="showCreate"
|
||||||
|
class="flex-1 p-1"
|
||||||
|
@done="showCreate = false"
|
||||||
|
@cancel="showCreate = false"
|
||||||
|
/>
|
||||||
<div v-else-if="loading" class="flex-1 flex items-center justify-center">
|
<div v-else-if="loading" class="flex-1 flex items-center justify-center">
|
||||||
<p>Loading...</p>
|
<p>Loading...</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -25,223 +25,219 @@ import Bookmarks from "./Bookmarks.vue";
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<main class="justify-center flex flex-row w-full h-full overflow-x-hidden">
|
<main class="justify-center flex flex-row w-full h-full overflow-x-hidden">
|
||||||
<div class="outerWrap flex flex-row">
|
<div class="outerWrap flex flex-row">
|
||||||
<div class="sidebar">
|
<div class="sidebar">
|
||||||
<Time class="time-sidebar sidebar-cell" />
|
<Time class="time-sidebar sidebar-cell" />
|
||||||
<Timer class="timer-sidebar sidebar-cell" />
|
<Timer class="timer-sidebar sidebar-cell" />
|
||||||
<Radio class="radio-sidebar sidebar-cell" />
|
<Radio class="radio-sidebar sidebar-cell" />
|
||||||
<CommitHistory class="commits-sidebar flex-1 sidebar-cell" />
|
<CommitHistory class="commits-sidebar flex-1 sidebar-cell" />
|
||||||
|
|
||||||
<!-- <Elle class="flex-1" /> -->
|
<!-- <Elle class="flex-1" /> -->
|
||||||
<!-- <MusicPlayer /> -->
|
<!-- <MusicPlayer /> -->
|
||||||
<img
|
<img
|
||||||
src="/img/memes/fire-woman.gif"
|
src="/img/memes/fire-woman.gif"
|
||||||
alt=""
|
alt=""
|
||||||
width="178"
|
width="178"
|
||||||
height="178"
|
height="178"
|
||||||
class="border-tertiary border-2 sidebar-image box-border w-full bg-tertiary"
|
class="border-tertiary border-2 sidebar-image box-border w-full bg-tertiary"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div class="a4page-portrait homeGrid relative bdr-1 border-quaternary">
|
||||||
class="a4page-portrait homeGrid relative bdr-1 border-quaternary"
|
<!-- <Intro class="intro" /> -->
|
||||||
>
|
<Intro2 class="intro grid-cell" />
|
||||||
<!-- <Intro class="intro" /> -->
|
<!-- <BadApple class="intro" /> -->
|
||||||
<Intro2 class="intro grid-cell" />
|
<Listening class="listening grid-cell" />
|
||||||
<!-- <BadApple class="intro" /> -->
|
<Stamps class="stamps grid-cell" />
|
||||||
<Listening class="listening grid-cell" />
|
<Feed class="feed grid-cell" />
|
||||||
<Stamps class="stamps grid-cell" />
|
<Links class="links grid-cell" />
|
||||||
<Feed class="feed grid-cell" />
|
<Collage class="collage grid-cell" />
|
||||||
<Links class="links grid-cell" />
|
<Consumption class="consumption grid-cell" />
|
||||||
<Collage class="collage grid-cell" />
|
<Favorites class="favorites grid-cell" />
|
||||||
<Consumption class="consumption grid-cell" />
|
<!-- <Gym class="gym" /> -->
|
||||||
<Favorites class="favorites grid-cell" />
|
<Gym2 class="gym grid-cell" />
|
||||||
<!-- <Gym class="gym" /> -->
|
</div>
|
||||||
<Gym2 class="gym grid-cell" />
|
<div class="sidebar">
|
||||||
</div>
|
<Steam class="steam-sidebar sidebar-cell" />
|
||||||
<div class="sidebar">
|
<Bookmarks class="bookmarks-sidebar sidebar-cell" />
|
||||||
<Steam class="steam-sidebar sidebar-cell" />
|
<Chat class="chat-sidebar flex-1 min-h-0 chat-home sidebar-cell" />
|
||||||
<Bookmarks class="bookmarks-sidebar sidebar-cell" />
|
<Miku
|
||||||
<Chat
|
class="sidebar-image miku-image box-border border-tertiary border-2 bg-bg_primary"
|
||||||
class="chat-sidebar flex-1 min-h-0 chat-home sidebar-cell"
|
/>
|
||||||
/>
|
</div>
|
||||||
<Miku
|
</div>
|
||||||
class="sidebar-image miku-image box-border border-tertiary border-2 bg-bg_primary"
|
</main>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.grid-cell {
|
.grid-cell {
|
||||||
background-color: var(--bg_primary);
|
background-color: var(--bg_primary);
|
||||||
border-width: 2px;
|
border-width: 2px;
|
||||||
border-style: solid;
|
border-style: solid;
|
||||||
border-color: var(--quaternary);
|
border-color: var(--quaternary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.intro {
|
.intro {
|
||||||
grid-area: intro;
|
grid-area: intro;
|
||||||
}
|
}
|
||||||
.listening {
|
.listening {
|
||||||
grid-area: listening;
|
grid-area: listening;
|
||||||
}
|
}
|
||||||
.stamps {
|
.stamps {
|
||||||
grid-area: stamps;
|
grid-area: stamps;
|
||||||
}
|
}
|
||||||
.feed {
|
.feed {
|
||||||
grid-area: feed;
|
grid-area: feed;
|
||||||
}
|
}
|
||||||
.links {
|
.links {
|
||||||
grid-area: links;
|
grid-area: links;
|
||||||
}
|
}
|
||||||
.collage {
|
.collage {
|
||||||
grid-area: collage;
|
grid-area: collage;
|
||||||
}
|
}
|
||||||
.consumption {
|
.consumption {
|
||||||
grid-area: consumption;
|
grid-area: consumption;
|
||||||
}
|
}
|
||||||
.gym {
|
.gym {
|
||||||
grid-area: gym;
|
grid-area: gym;
|
||||||
}
|
}
|
||||||
.favorites {
|
.favorites {
|
||||||
grid-area: favorites;
|
grid-area: favorites;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-cell {
|
.sidebar-cell {
|
||||||
background-color: var(--bg_primary);
|
background-color: var(--bg_primary);
|
||||||
border-width: 2px;
|
border-width: 2px;
|
||||||
border-style: solid;
|
border-style: solid;
|
||||||
border-color: var(--quaternary);
|
border-color: var(--quaternary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar {
|
.sidebar {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
width: 15rem;
|
width: 15rem;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
gap: 5px;
|
gap: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.outerWrap {
|
.outerWrap {
|
||||||
height: 310mm;
|
height: 310mm;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.homeGrid {
|
.homeGrid {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 5px;
|
gap: 5px;
|
||||||
grid-template-columns: repeat(10, 1fr);
|
grid-template-columns: repeat(10, 1fr);
|
||||||
grid-template-rows: repeat(10, 1fr);
|
grid-template-rows: repeat(10, 1fr);
|
||||||
grid-template-areas:
|
grid-template-areas:
|
||||||
"intro intro intro intro intro intro listening listening listening listening"
|
"intro intro intro intro intro intro listening listening listening listening"
|
||||||
"intro intro intro intro intro intro listening listening listening listening"
|
"intro intro intro intro intro intro listening listening listening listening"
|
||||||
"intro intro intro intro intro intro listening listening listening listening"
|
"intro intro intro intro intro intro listening listening listening listening"
|
||||||
"intro intro intro intro intro intro stamps stamps stamps stamps"
|
"intro intro intro intro intro intro stamps stamps stamps stamps"
|
||||||
"feed feed feed links links collage collage collage collage collage"
|
"feed feed feed links links collage collage collage collage collage"
|
||||||
"feed feed feed links links collage collage collage collage collage"
|
"feed feed feed links links collage collage collage collage collage"
|
||||||
"feed feed feed links links collage collage collage collage collage"
|
"feed feed feed links links collage collage collage collage collage"
|
||||||
"feed feed feed links links collage collage collage collage collage"
|
"feed feed feed links links collage collage collage collage collage"
|
||||||
"consumption consumption consumption consumption gym gym gym favorites favorites favorites"
|
"consumption consumption consumption consumption gym gym gym favorites favorites favorites"
|
||||||
"consumption consumption consumption consumption gym gym gym favorites favorites favorites";
|
"consumption consumption consumption consumption gym gym gym favorites favorites favorites";
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-home {
|
.chat-home {
|
||||||
max-height: 800px;
|
max-height: 800px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.miku-image {
|
.miku-image {
|
||||||
height: 15rem;
|
height: 15rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 1360px) {
|
@media (max-width: 1360px) {
|
||||||
.outerWrap {
|
.outerWrap {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
height: auto;
|
height: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.homeGrid {
|
.homeGrid {
|
||||||
order: -1;
|
order: -1;
|
||||||
width: 95vw;
|
width: 95vw;
|
||||||
height: 297mm;
|
height: 297mm;
|
||||||
margin-inline: 0;
|
margin-inline: 0;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar {
|
.sidebar {
|
||||||
width: 95vw;
|
width: 95vw;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
flex: unset;
|
flex: unset;
|
||||||
justify-content: space-around;
|
justify-content: space-around;
|
||||||
}
|
}
|
||||||
|
|
||||||
.commits-sidebar,
|
.commits-sidebar,
|
||||||
.steam-sidebar,
|
.steam-sidebar,
|
||||||
.bookmarks-sidebar {
|
.bookmarks-sidebar {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-height: 300px;
|
max-height: 300px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-sidebar {
|
.chat-sidebar {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-height: 400px;
|
min-height: 400px;
|
||||||
max-height: 800px;
|
max-height: 800px;
|
||||||
height: 25vh;
|
height: 25vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
.time-sidebar,
|
.time-sidebar,
|
||||||
.sidebar-image,
|
.sidebar-image,
|
||||||
.radio-sidebar,
|
.radio-sidebar,
|
||||||
.timer-sidebar {
|
.timer-sidebar {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* .sidebar-image { */
|
/* .sidebar-image { */
|
||||||
/* max-height: 150px; */
|
/* max-height: 150px; */
|
||||||
/* max-width: 150px; */
|
/* max-width: 150px; */
|
||||||
/* object-fit: contain; */
|
/* object-fit: contain; */
|
||||||
/* } */
|
/* } */
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 700px) {
|
@media (max-width: 700px) {
|
||||||
.homeGrid {
|
.homeGrid {
|
||||||
border-image: none;
|
border-image: none;
|
||||||
border-width: 0;
|
border-width: 0;
|
||||||
grid-template-columns: 1fr 1fr 1fr;
|
grid-template-columns: 1fr 1fr 1fr;
|
||||||
grid-template-rows: repeat(11, 1fr);
|
grid-template-rows: repeat(11, 1fr);
|
||||||
height: 150vh;
|
height: 150vh;
|
||||||
grid-template-areas:
|
grid-template-areas:
|
||||||
"intro intro intro"
|
"intro intro intro"
|
||||||
"intro intro intro"
|
"intro intro intro"
|
||||||
"listening stamps stamps"
|
"listening stamps stamps"
|
||||||
"listening feed feed"
|
"listening feed feed"
|
||||||
"links feed feed"
|
"links feed feed"
|
||||||
"links feed feed"
|
"links feed feed"
|
||||||
"links feed feed"
|
"links feed feed"
|
||||||
"favorites feed feed"
|
"favorites feed feed"
|
||||||
"favorites consumption consumption"
|
"favorites consumption consumption"
|
||||||
"favorites consumption consumption"
|
"favorites consumption consumption"
|
||||||
"favorites consumption consumption";
|
"favorites consumption consumption";
|
||||||
}
|
}
|
||||||
|
|
||||||
.collage {
|
.collage {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.gym {
|
.gym {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.bg-random {
|
.bg-random {
|
||||||
background-color: var(--bg_primary);
|
background-color: var(--bg_primary);
|
||||||
background-image: url("/img/miku/miku2.gif");
|
background-image: url("/img/miku/miku2.gif");
|
||||||
background-size: 10px 10px;
|
background-size: 10px 10px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -4,27 +4,29 @@ import Paragraph from "@/components/text/Paragraph.vue";
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex-1 border-box flex flex-col p-1 text-left items-start justify-start">
|
<div
|
||||||
<Header>Yo</Header>
|
class="flex-1 border-box flex flex-col p-1 text-left items-start justify-start"
|
||||||
<!-- <Header>Intro</Header> -->
|
>
|
||||||
<!-- <Paragraph> -->
|
<Header>Yo</Header>
|
||||||
<!-- Hi, I'm Adam, thank you for visiting my website. -->
|
<!-- <Header>Intro</Header> -->
|
||||||
<!-- </Paragraph> -->
|
<!-- <Paragraph> -->
|
||||||
<!-- <Header>Getting around</Header> -->
|
<!-- Hi, I'm Adam, thank you for visiting my website. -->
|
||||||
<!-- <Paragraph> -->
|
<!-- </Paragraph> -->
|
||||||
<!-- Pages available can be traversed through links below. I am hoping to -->
|
<!-- <Header>Getting around</Header> -->
|
||||||
<!-- add some shrines, code-walkthoughs, live chat and page transitions -->
|
<!-- <Paragraph> -->
|
||||||
<!-- at a later date. -->
|
<!-- Pages available can be traversed through links below. I am hoping to -->
|
||||||
<!-- </Paragraph> -->
|
<!-- add some shrines, code-walkthoughs, live chat and page transitions -->
|
||||||
<!-- <Header>Contact</Header> -->
|
<!-- at a later date. -->
|
||||||
<!-- <Paragraph> -->
|
<!-- </Paragraph> -->
|
||||||
<!-- Please email me <a href="mailto:adam.a.french@outlook.com">here</a>, -->
|
<!-- <Header>Contact</Header> -->
|
||||||
<!-- or contact me though any of the social medias linked. -->
|
<!-- <Paragraph> -->
|
||||||
<!-- </Paragraph> -->
|
<!-- Please email me <a href="mailto:adam.a.french@outlook.com">here</a>, -->
|
||||||
<!-- <Header>A Quote</Header> -->
|
<!-- or contact me though any of the social medias linked. -->
|
||||||
<!-- <Paragraph> -->
|
<!-- </Paragraph> -->
|
||||||
<!-- One crossed wire, one wayward pinch of potassium chlorate, one -->
|
<!-- <Header>A Quote</Header> -->
|
||||||
<!-- errant twitch, and KA-BLOOIE! -->
|
<!-- <Paragraph> -->
|
||||||
<!-- </Paragraph> -->
|
<!-- One crossed wire, one wayward pinch of potassium chlorate, one -->
|
||||||
</div>
|
<!-- errant twitch, and KA-BLOOIE! -->
|
||||||
|
<!-- </Paragraph> -->
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -3,46 +3,46 @@ import { rand } from "@vueuse/core";
|
|||||||
import { ref, onMounted, onUnmounted, nextTick } from "vue";
|
import { ref, onMounted, onUnmounted, nextTick } from "vue";
|
||||||
|
|
||||||
interface Item {
|
interface Item {
|
||||||
x: number;
|
x: number;
|
||||||
y: number;
|
y: number;
|
||||||
dx: number;
|
dx: number;
|
||||||
dy: number;
|
dy: number;
|
||||||
content: string;
|
content: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const container = ref<HTMLDivElement | null>(null);
|
const container = ref<HTMLDivElement | null>(null);
|
||||||
const itemEls = ref<HTMLDivElement[]>([]);
|
const itemEls = ref<HTMLDivElement[]>([]);
|
||||||
|
|
||||||
const phrases = [
|
const phrases = [
|
||||||
"Welcome to my website",
|
"Welcome to my website",
|
||||||
"I'm looking to do a big revamp",
|
"I'm looking to do a big revamp",
|
||||||
"A4 sheets of paper are what I'm used to",
|
"A4 sheets of paper are what I'm used to",
|
||||||
"I'd love to know your recommendations",
|
"I'd love to know your recommendations",
|
||||||
"for very short books",
|
"for very short books",
|
||||||
"Message me by any means necessary",
|
"Message me by any means necessary",
|
||||||
"I like anime, all kinds of music and sci fic",
|
"I like anime, all kinds of music and sci fic",
|
||||||
];
|
];
|
||||||
|
|
||||||
// Non-reactive animation state to avoid triggering Vue re-renders every frame
|
// Non-reactive animation state to avoid triggering Vue re-renders every frame
|
||||||
const animState = phrases.map((text, i) => ({
|
const animState = phrases.map((text, i) => ({
|
||||||
x: 0,
|
x: 0,
|
||||||
y: i * 20,
|
y: i * 20,
|
||||||
dx: rand(0, 60) / 100,
|
dx: rand(0, 60) / 100,
|
||||||
dy: 1.0,
|
dy: 1.0,
|
||||||
content: text,
|
content: text,
|
||||||
cachedW: 0,
|
cachedW: 0,
|
||||||
cachedH: 0,
|
cachedH: 0,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Reactive items only for initial render
|
// Reactive items only for initial render
|
||||||
const items = ref<Item[]>(
|
const items = ref<Item[]>(
|
||||||
animState.map((s) => ({
|
animState.map((s) => ({
|
||||||
x: s.x,
|
x: s.x,
|
||||||
y: s.y,
|
y: s.y,
|
||||||
dx: s.dx,
|
dx: s.dx,
|
||||||
dy: s.dy,
|
dy: s.dy,
|
||||||
content: s.content,
|
content: s.content,
|
||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
|
|
||||||
let rafId = 0;
|
let rafId = 0;
|
||||||
@@ -52,79 +52,76 @@ let lastFrameTime = 0;
|
|||||||
const FRAME_INTERVAL = 1000 / 30;
|
const FRAME_INTERVAL = 1000 / 30;
|
||||||
|
|
||||||
function measureSizes() {
|
function measureSizes() {
|
||||||
const c = container.value;
|
const c = container.value;
|
||||||
if (c) {
|
if (c) {
|
||||||
cachedCW = c.clientWidth;
|
cachedCW = c.clientWidth;
|
||||||
cachedCH = c.clientHeight;
|
cachedCH = c.clientHeight;
|
||||||
|
}
|
||||||
|
itemEls.value.forEach((el, i) => {
|
||||||
|
if (el && animState[i]) {
|
||||||
|
animState[i].cachedW = el.offsetWidth;
|
||||||
|
animState[i].cachedH = el.offsetHeight;
|
||||||
}
|
}
|
||||||
itemEls.value.forEach((el, i) => {
|
});
|
||||||
if (el && animState[i]) {
|
|
||||||
animState[i].cachedW = el.offsetWidth;
|
|
||||||
animState[i].cachedH = el.offsetHeight;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function animate(timestamp: number) {
|
function animate(timestamp: number) {
|
||||||
if (!cachedCW || !cachedCH) {
|
if (!cachedCW || !cachedCH) {
|
||||||
rafId = requestAnimationFrame(animate);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (timestamp - lastFrameTime < FRAME_INTERVAL) {
|
|
||||||
rafId = requestAnimationFrame(animate);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
lastFrameTime = timestamp;
|
|
||||||
|
|
||||||
for (let i = 0; i < animState.length; i++) {
|
|
||||||
const s = animState[i];
|
|
||||||
const el = itemEls.value[i];
|
|
||||||
if (!el) continue;
|
|
||||||
|
|
||||||
s.x += s.dx;
|
|
||||||
s.y += s.dy;
|
|
||||||
|
|
||||||
if (s.x < 0 || s.x > cachedCW - s.cachedW) s.dx *= -1;
|
|
||||||
if (s.y < 0 || s.y > cachedCH - s.cachedH) s.dy *= -1;
|
|
||||||
|
|
||||||
el.style.transform = `translate(${s.x}px, ${s.y}px)`;
|
|
||||||
}
|
|
||||||
|
|
||||||
rafId = requestAnimationFrame(animate);
|
rafId = requestAnimationFrame(animate);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (timestamp - lastFrameTime < FRAME_INTERVAL) {
|
||||||
|
rafId = requestAnimationFrame(animate);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
lastFrameTime = timestamp;
|
||||||
|
|
||||||
|
for (let i = 0; i < animState.length; i++) {
|
||||||
|
const s = animState[i];
|
||||||
|
const el = itemEls.value[i];
|
||||||
|
if (!el) continue;
|
||||||
|
|
||||||
|
s.x += s.dx;
|
||||||
|
s.y += s.dy;
|
||||||
|
|
||||||
|
if (s.x < 0 || s.x > cachedCW - s.cachedW) s.dx *= -1;
|
||||||
|
if (s.y < 0 || s.y > cachedCH - s.cachedH) s.dy *= -1;
|
||||||
|
|
||||||
|
el.style.transform = `translate(${s.x}px, ${s.y}px)`;
|
||||||
|
}
|
||||||
|
|
||||||
|
rafId = requestAnimationFrame(animate);
|
||||||
}
|
}
|
||||||
|
|
||||||
let resizeObserver: ResizeObserver;
|
let resizeObserver: ResizeObserver;
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await nextTick();
|
await nextTick();
|
||||||
measureSizes();
|
measureSizes();
|
||||||
rafId = requestAnimationFrame(animate);
|
rafId = requestAnimationFrame(animate);
|
||||||
|
|
||||||
resizeObserver = new ResizeObserver(measureSizes);
|
resizeObserver = new ResizeObserver(measureSizes);
|
||||||
resizeObserver.observe(container.value!);
|
resizeObserver.observe(container.value!);
|
||||||
});
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
cancelAnimationFrame(rafId);
|
cancelAnimationFrame(rafId);
|
||||||
resizeObserver?.disconnect();
|
resizeObserver?.disconnect();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
<div ref="container" class="w-full h-full relative overflow-hidden">
|
||||||
<div
|
<div
|
||||||
ref="container"
|
v-for="(item, i) in items"
|
||||||
class="w-full h-full relative overflow-hidden"
|
:key="i"
|
||||||
|
ref="itemEls"
|
||||||
|
class="absolute w-fit h-fit"
|
||||||
>
|
>
|
||||||
<div
|
<h1>
|
||||||
v-for="(item, i) in items"
|
{{ item.content }}
|
||||||
:key="i"
|
</h1>
|
||||||
ref="itemEls"
|
|
||||||
class="absolute w-fit h-fit"
|
|
||||||
>
|
|
||||||
<h1>
|
|
||||||
{{ item.content }}
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -4,31 +4,31 @@ import LinkTable from "@/components/util/LinkTable.vue";
|
|||||||
import Header from "@/components/text/Header.vue";
|
import Header from "@/components/text/Header.vue";
|
||||||
|
|
||||||
const site_links = [
|
const site_links = [
|
||||||
{ name: "CV", link: "/cv" },
|
{ name: "CV", link: "/cv" },
|
||||||
{ name: "Bookmarks", link: "/bookmarks" },
|
{ name: "Bookmarks", link: "/bookmarks" },
|
||||||
{ name: "Shrines", link: "/shrines" },
|
{ name: "Shrines", link: "/shrines" },
|
||||||
{ name: "Admin", link: "/admin" },
|
{ name: "Admin", link: "/admin" },
|
||||||
];
|
];
|
||||||
|
|
||||||
const social_links = [
|
const social_links = [
|
||||||
{ name: "Gitea", link: "/gitea/explore/repos" },
|
{ name: "Gitea", link: "/gitea/explore/repos" },
|
||||||
{ name: "Steam", link: "https://steamcommunity.com/id/SteveThePug" },
|
{ name: "Steam", link: "https://steamcommunity.com/id/SteveThePug" },
|
||||||
{ name: "Github", link: "https://github.com/SteveThePug" },
|
{ name: "Github", link: "https://github.com/SteveThePug" },
|
||||||
{ name: "Spotify", link: "https://open.spotify.com/user/stevethepug" },
|
{ name: "Spotify", link: "https://open.spotify.com/user/stevethepug" },
|
||||||
{ name: "Notes", link: "/notes/" },
|
{ name: "Notes", link: "/notes/" },
|
||||||
];
|
];
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col overflow-auto">
|
<div class="flex flex-col overflow-auto">
|
||||||
<Header>Links</Header>
|
<Header>Links</Header>
|
||||||
<div class="flex flex-col justify-between flex-1">
|
<div class="flex flex-col justify-between flex-1">
|
||||||
<div class="flex flex-col gap-1">
|
<div class="flex flex-col gap-1">
|
||||||
<RouterTable :linkArr="site_links" />
|
<RouterTable :linkArr="site_links" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col gap-1">
|
<div class="flex flex-col gap-1">
|
||||||
<LinkTable :items="social_links" />
|
<LinkTable :items="social_links" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -11,75 +11,76 @@ let nextId = null;
|
|||||||
let refreshId = null;
|
let refreshId = null;
|
||||||
|
|
||||||
function nextSong() {
|
function nextSong() {
|
||||||
clearTimeout(nextId);
|
clearTimeout(nextId);
|
||||||
nextId = setTimeout(nextSong, 5000);
|
nextId = setTimeout(nextSong, 5000);
|
||||||
idx.value = (idx.value + 1) % songsStore.songsCount;
|
idx.value = (idx.value + 1) % songsStore.songsCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
songsStore.fetchSongs();
|
songsStore.fetchSongs();
|
||||||
nextId = setTimeout(nextSong, 5000);
|
nextId = setTimeout(nextSong, 5000);
|
||||||
refreshId = setInterval(songsStore.fetchSongs, 120000);
|
refreshId = setInterval(songsStore.fetchSongs, 120000);
|
||||||
});
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
clearTimeout(nextId);
|
clearTimeout(nextId);
|
||||||
clearInterval(refreshId);
|
clearInterval(refreshId);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="listening-wrapper">
|
<div class="listening-wrapper">
|
||||||
<Transition name="fade">
|
<Transition name="fade">
|
||||||
<div
|
<div
|
||||||
@click="nextSong"
|
@click="nextSong"
|
||||||
:key="song.track.name"
|
:key="song.track.name"
|
||||||
class="flex flex-col items-center pt-2"
|
class="flex flex-col items-center pt-2"
|
||||||
>
|
>
|
||||||
<Header>Listening To</Header>
|
<Header>Listening To</Header>
|
||||||
<img :src="song.track.album.images[0].url" :alt="song.track.album.name + ' album art'" />
|
<img
|
||||||
<p class="text-center">
|
:src="song.track.album.images[0].url"
|
||||||
<strong>Song:</strong> {{ song.track.name }}
|
:alt="song.track.album.name + ' album art'"
|
||||||
</p>
|
/>
|
||||||
<p class="text-center">
|
<p class="text-center"><strong>Song:</strong> {{ song.track.name }}</p>
|
||||||
<strong>Artist:</strong> {{ song.track.artists[0].name }}
|
<p class="text-center">
|
||||||
</p>
|
<strong>Artist:</strong> {{ song.track.artists[0].name }}
|
||||||
</div>
|
</p>
|
||||||
|
</div>
|
||||||
</Transition>
|
</Transition>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.listening-wrapper {
|
.listening-wrapper {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
img {
|
img {
|
||||||
width: 70%;
|
width: 70%;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
height: auto;
|
height: auto;
|
||||||
}
|
}
|
||||||
p {
|
p {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fade-enter-active,
|
.fade-enter-active,
|
||||||
.fade-leave-active {
|
.fade-leave-active {
|
||||||
transition: opacity 0.5s ease;
|
transition: opacity 0.5s ease;
|
||||||
}
|
}
|
||||||
.fade-leave-active {
|
.fade-leave-active {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
}
|
}
|
||||||
.fade-enter-from,
|
.fade-enter-from,
|
||||||
.fade-leave-to {
|
.fade-leave-to {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -2,12 +2,12 @@
|
|||||||
import Slideshow from "@/components/util/Slideshow.vue";
|
import Slideshow from "@/components/util/Slideshow.vue";
|
||||||
|
|
||||||
const images = [
|
const images = [
|
||||||
{ url: "/img/miku/miku1.gif" },
|
{ url: "/img/miku/miku1.gif" },
|
||||||
{ url: "/img/miku/miku2.gif" },
|
{ url: "/img/miku/miku2.gif" },
|
||||||
// { url: "/img/miku/miku2.png" },
|
// { url: "/img/miku/miku2.png" },
|
||||||
];
|
];
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Slideshow class="p-5" :images="images" :interval="10000" />
|
<Slideshow class="p-5" :images="images" :interval="10000" />
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -6,20 +6,20 @@ import Link from "@/components/text/Link.vue";
|
|||||||
import { shuffleArray } from "@/js/utils.js";
|
import { shuffleArray } from "@/js/utils.js";
|
||||||
|
|
||||||
let srcs = [
|
let srcs = [
|
||||||
"/img/stamps/portal.gif",
|
"/img/stamps/portal.gif",
|
||||||
"/img/stamps/miku.gif",
|
"/img/stamps/miku.gif",
|
||||||
"/img/stamps/utau.gif",
|
"/img/stamps/utau.gif",
|
||||||
"/img/stamps/teto.webp",
|
"/img/stamps/teto.webp",
|
||||||
"/img/stamps/3ds.jpg",
|
"/img/stamps/3ds.jpg",
|
||||||
"/img/stamps/fry.png",
|
"/img/stamps/fry.png",
|
||||||
"/img/stamps/ai.png",
|
"/img/stamps/ai.png",
|
||||||
"/img/stamps/rei.png",
|
"/img/stamps/rei.png",
|
||||||
"/img/stamps/tetris.gif",
|
"/img/stamps/tetris.gif",
|
||||||
"/img/stamps/tf2.gif",
|
"/img/stamps/tf2.gif",
|
||||||
"/img/stamps/demo.gif",
|
"/img/stamps/demo.gif",
|
||||||
"/img/stamps/demo.gif",
|
"/img/stamps/demo.gif",
|
||||||
"/img/stamps/demo.gif",
|
"/img/stamps/demo.gif",
|
||||||
"/img/stamps/demo.gif",
|
"/img/stamps/demo.gif",
|
||||||
];
|
];
|
||||||
shuffleArray(srcs);
|
shuffleArray(srcs);
|
||||||
|
|
||||||
@@ -31,71 +31,76 @@ let dx = 0.2;
|
|||||||
let dy = 0.12;
|
let dy = 0.12;
|
||||||
|
|
||||||
function bounce() {
|
function bounce() {
|
||||||
const el = touchscreen.value?.$el;
|
const el = touchscreen.value?.$el;
|
||||||
if (!el) return;
|
if (!el) return;
|
||||||
|
|
||||||
const maxX = el.scrollWidth - el.clientWidth;
|
const maxX = el.scrollWidth - el.clientWidth;
|
||||||
const maxY = el.scrollHeight - el.clientHeight;
|
const maxY = el.scrollHeight - el.clientHeight;
|
||||||
|
|
||||||
if (maxX > 0) {
|
if (maxX > 0) {
|
||||||
posX += dx;
|
posX += dx;
|
||||||
if (posX <= 0) {
|
if (posX <= 0) {
|
||||||
posX = 0;
|
posX = 0;
|
||||||
dx = -dx;
|
dx = -dx;
|
||||||
} else if (posX >= maxX) {
|
} else if (posX >= maxX) {
|
||||||
posX = maxX;
|
posX = maxX;
|
||||||
dx = -dx;
|
dx = -dx;
|
||||||
}
|
|
||||||
el.scrollLeft = posX;
|
|
||||||
}
|
}
|
||||||
if (maxY > 0) {
|
el.scrollLeft = posX;
|
||||||
posY += dy;
|
}
|
||||||
if (posY <= 0) {
|
if (maxY > 0) {
|
||||||
posY = 0;
|
posY += dy;
|
||||||
dy = -dy;
|
if (posY <= 0) {
|
||||||
} else if (posY >= maxY) {
|
posY = 0;
|
||||||
posY = maxY;
|
dy = -dy;
|
||||||
dy = -dy;
|
} else if (posY >= maxY) {
|
||||||
}
|
posY = maxY;
|
||||||
el.scrollTop = posY;
|
dy = -dy;
|
||||||
}
|
}
|
||||||
|
el.scrollTop = posY;
|
||||||
|
}
|
||||||
|
|
||||||
animId = requestAnimationFrame(bounce);
|
animId = requestAnimationFrame(bounce);
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
animId = requestAnimationFrame(bounce);
|
animId = requestAnimationFrame(bounce);
|
||||||
});
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
if (animId) cancelAnimationFrame(animId);
|
if (animId) cancelAnimationFrame(animId);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Touchscreen ref="touchscreen">
|
<Touchscreen ref="touchscreen">
|
||||||
<div class="flex flex-wrap tst">
|
<div class="flex flex-wrap tst">
|
||||||
<Link bare href="https://www.adam-french.co.uk">
|
<Link bare href="https://www.adam-french.co.uk">
|
||||||
<img src="https://www.adam-french.co.uk/img/stamps/mine.gif" alt="adam-french.co.uk" />
|
<img
|
||||||
</Link>
|
src="https://www.adam-french.co.uk/img/stamps/mine.gif"
|
||||||
<Link bare href="https://jacobbarron.xyz">
|
alt="adam-french.co.uk"
|
||||||
<img
|
/>
|
||||||
src="https://jacobbarron.xyz/Banneh.gif"
|
</Link>
|
||||||
alt="jacobbarron.xyz"
|
<Link bare href="https://jacobbarron.xyz">
|
||||||
/>
|
<img src="https://jacobbarron.xyz/Banneh.gif" alt="jacobbarron.xyz" />
|
||||||
</Link>
|
</Link>
|
||||||
<img v-for="src in srcs" :src="src" :alt="src.split('/').pop().split('.')[0]" loading="lazy" />
|
<img
|
||||||
</div>
|
v-for="src in srcs"
|
||||||
</Touchscreen>
|
:src="src"
|
||||||
|
:alt="src.split('/').pop().split('.')[0]"
|
||||||
|
loading="lazy"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Touchscreen>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
img {
|
img {
|
||||||
width: 89px;
|
width: 89px;
|
||||||
height: 59px;
|
height: 59px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tst {
|
.tst {
|
||||||
min-width: calc(89px * 4);
|
min-width: calc(89px * 4);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -17,116 +17,116 @@ let nextId = null;
|
|||||||
let refreshId = null;
|
let refreshId = null;
|
||||||
|
|
||||||
function nextGame() {
|
function nextGame() {
|
||||||
clearTimeout(nextId);
|
clearTimeout(nextId);
|
||||||
nextId = setTimeout(nextGame, 5000);
|
nextId = setTimeout(nextGame, 5000);
|
||||||
if (steamStatus.value.recentGames.length) {
|
if (steamStatus.value.recentGames.length) {
|
||||||
idx.value = (idx.value + 1) % steamStatus.value.recentGames.length;
|
idx.value = (idx.value + 1) % steamStatus.value.recentGames.length;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
nextId = setTimeout(nextGame, 5000);
|
nextId = setTimeout(nextGame, 5000);
|
||||||
refreshId = setInterval(() => steamStore.fetchSteam(), 5 * 60 * 1000);
|
refreshId = setInterval(() => steamStore.fetchSteam(), 5 * 60 * 1000);
|
||||||
});
|
});
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
clearTimeout(nextId);
|
clearTimeout(nextId);
|
||||||
clearInterval(refreshId);
|
clearInterval(refreshId);
|
||||||
});
|
});
|
||||||
|
|
||||||
function formatHours(minutes) {
|
function formatHours(minutes) {
|
||||||
const hrs = (minutes / 60).toFixed(1);
|
const hrs = (minutes / 60).toFixed(1);
|
||||||
return `${hrs}h`;
|
return `${hrs}h`;
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="steam-wrapper">
|
<div class="steam-wrapper">
|
||||||
<Header class="text-left">
|
<Header class="text-left">
|
||||||
<span class="flex items-center gap-2">
|
<span class="flex items-center gap-2">
|
||||||
Steam
|
Steam
|
||||||
<span
|
<span
|
||||||
class="inline-block w-2 h-2 rounded-full"
|
class="inline-block w-2 h-2 rounded-full"
|
||||||
:class="steamStatus.online ? 'bg-green-500' : 'bg-gray-400'"
|
:class="steamStatus.online ? 'bg-green-500' : 'bg-gray-400'"
|
||||||
:title="steamStatus.online ? 'Online' : 'Offline'"
|
:title="steamStatus.online ? 'Online' : 'Offline'"
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
</Header>
|
</Header>
|
||||||
|
|
||||||
<div v-if="!loaded" class="p-2 text-sm">Loading...</div>
|
<div v-if="!loaded" class="p-2 text-sm">Loading...</div>
|
||||||
|
|
||||||
<div v-else-if="game" class="game-container">
|
<div v-else-if="game" class="game-container">
|
||||||
<Transition name="fade">
|
<Transition name="fade">
|
||||||
<div
|
<div
|
||||||
@click="nextGame"
|
@click="nextGame"
|
||||||
:key="game.appId"
|
:key="game.appId"
|
||||||
class="flex flex-col items-center pt-2"
|
class="flex flex-col items-center pt-2"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
:src="game.headerImageUrl"
|
:src="game.headerImageUrl"
|
||||||
:alt="game.name"
|
:alt="game.name"
|
||||||
width="145"
|
width="145"
|
||||||
height="68"
|
height="68"
|
||||||
class="game-img"
|
class="game-img"
|
||||||
/>
|
/>
|
||||||
<p class="text-center">
|
<p class="text-center">
|
||||||
<strong>{{ game.name }}</strong>
|
<strong>{{ game.name }}</strong>
|
||||||
</p>
|
</p>
|
||||||
<p class="text-center text-tertiary text-xs">
|
<p class="text-center text-tertiary text-xs">
|
||||||
{{ formatHours(game.playtime2Weeks) }} last 2 weeks
|
{{ formatHours(game.playtime2Weeks) }} last 2 weeks
|
||||||
</p>
|
</p>
|
||||||
</div>
|
|
||||||
</Transition>
|
|
||||||
</div>
|
</div>
|
||||||
|
</Transition>
|
||||||
<div v-else class="p-2 text-sm">No recent games.</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="p-2 text-sm">No recent games.</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.steam-wrapper {
|
.steam-wrapper {
|
||||||
position: relative;
|
position: relative;
|
||||||
height: 54mm;
|
height: 54mm;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.game-container {
|
.game-container {
|
||||||
position: relative;
|
position: relative;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
}
|
}
|
||||||
|
|
||||||
.game-img {
|
.game-img {
|
||||||
width: 90%;
|
width: 90%;
|
||||||
max-width: 300px;
|
max-width: 300px;
|
||||||
height: auto;
|
height: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
p {
|
p {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fade-enter-active,
|
.fade-enter-active,
|
||||||
.fade-leave-active {
|
.fade-leave-active {
|
||||||
transition: opacity 0.5s ease;
|
transition: opacity 0.5s ease;
|
||||||
}
|
}
|
||||||
.fade-leave-active {
|
.fade-leave-active {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
}
|
}
|
||||||
.fade-enter-from,
|
.fade-enter-from,
|
||||||
.fade-leave-to {
|
.fade-leave-to {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 850px) {
|
@media (max-width: 850px) {
|
||||||
.steam-wrapper {
|
.steam-wrapper {
|
||||||
height: auto;
|
height: auto;
|
||||||
min-height: 120px;
|
min-height: 120px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -3,27 +3,25 @@ import VideoTable from "@/components/util/VideoTable.vue";
|
|||||||
import Link from "@/components/text/Link.vue";
|
import Link from "@/components/text/Link.vue";
|
||||||
|
|
||||||
const videoSources = [
|
const videoSources = [
|
||||||
{ name: "demoman", link: "/img/demoman/1760582395316219.webm" },
|
{ name: "demoman", link: "/img/demoman/1760582395316219.webm" },
|
||||||
{ name: "demoman", link: "/img/demoman/1761052136609718.webm" },
|
{ name: "demoman", link: "/img/demoman/1761052136609718.webm" },
|
||||||
{ name: "demoman", link: "/img/demoman/1761088452011210.mp4" },
|
{ name: "demoman", link: "/img/demoman/1761088452011210.mp4" },
|
||||||
{ name: "demoman", link: "/img/demoman/1761570214170465.webm" },
|
{ name: "demoman", link: "/img/demoman/1761570214170465.webm" },
|
||||||
{ name: "demoman", link: "/img/demoman/1761828457509465.webm" },
|
{ name: "demoman", link: "/img/demoman/1761828457509465.webm" },
|
||||||
];
|
];
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<main class="items-center flex flex-col">
|
<main class="items-center flex flex-col">
|
||||||
<div
|
<div
|
||||||
class="a4page-portrait bdr-1 flex flex-row relative overflow-scroll items-center"
|
class="a4page-portrait bdr-1 flex flex-row relative overflow-scroll items-center"
|
||||||
>
|
>
|
||||||
<p>
|
<p>
|
||||||
<Link href="https://wiki.teamfortress.com/wiki/Demoman"
|
<Link href="https://wiki.teamfortress.com/wiki/Demoman">The goat</Link>
|
||||||
>The goat</Link
|
</p>
|
||||||
>
|
<div>
|
||||||
</p>
|
<VideoTable :sourceArr="videoSources" />
|
||||||
<div>
|
</div>
|
||||||
<VideoTable :sourceArr="videoSources" />
|
</div>
|
||||||
</div>
|
</main>
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -3,11 +3,11 @@ import Wip from "@/components/util/Wip.vue";
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<main class="items-center flex flex-col">
|
<main class="items-center flex flex-col">
|
||||||
<div
|
<div
|
||||||
class="a4page-portrait items-center bdr-1 flex flex-col relative overflow-scroll"
|
class="a4page-portrait items-center bdr-1 flex flex-col relative overflow-scroll"
|
||||||
>
|
>
|
||||||
<Wip />
|
<Wip />
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -4,7 +4,9 @@ import Wip from "@/components/util/Wip.vue";
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<main class="items-center flex flex-col">
|
<main class="items-center flex flex-col">
|
||||||
<div class="a4page-portrait items-center bdr-1 flex flex-col relative overflow-scroll">
|
<div
|
||||||
|
class="a4page-portrait items-center bdr-1 flex flex-col relative overflow-scroll"
|
||||||
|
>
|
||||||
<Wip />
|
<Wip />
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -1,20 +1,20 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import RouterTable from "@/components/util/RouterTable.vue";
|
import RouterTable from "@/components/util/RouterTable.vue";
|
||||||
const shrine_links = [
|
const shrine_links = [
|
||||||
{ name: "Demoman", link: "/shrines/demoman" },
|
{ name: "Demoman", link: "/shrines/demoman" },
|
||||||
{ name: "Evangelion", link: "/shrines/evangelion" },
|
{ name: "Evangelion", link: "/shrines/evangelion" },
|
||||||
{ name: "GTO", link: "/shrines/gto" },
|
{ name: "GTO", link: "/shrines/gto" },
|
||||||
{ name: "Skipskipbenben", link: "/shrines/skipskipbenben" },
|
{ name: "Skipskipbenben", link: "/shrines/skipskipbenben" },
|
||||||
];
|
];
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<main class="items-center flex flex-col">
|
<main class="items-center flex flex-col">
|
||||||
<div class="background" />
|
<div class="background" />
|
||||||
<div
|
<div
|
||||||
class="a4page-portrait bdr-1 flex flex-col relative overflow-scroll gap-1"
|
class="a4page-portrait bdr-1 flex flex-col relative overflow-scroll gap-1"
|
||||||
>
|
>
|
||||||
<RouterTable :linkArr="shrine_links" />
|
<RouterTable :linkArr="shrine_links" />
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -3,11 +3,11 @@ import Wip from "@/components/util/Wip.vue";
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<main class="items-center flex flex-col">
|
<main class="items-center flex flex-col">
|
||||||
<div
|
<div
|
||||||
class="a4page-portrait items-center bdr-1 flex flex-col relative overflow-scroll"
|
class="a4page-portrait items-center bdr-1 flex flex-col relative overflow-scroll"
|
||||||
>
|
>
|
||||||
<Wip />
|
<Wip />
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -5,52 +5,51 @@ import Header from "@/components/text/Header.vue";
|
|||||||
import Paragraph from "@/components/text/Paragraph.vue";
|
import Paragraph from "@/components/text/Paragraph.vue";
|
||||||
|
|
||||||
const links = [
|
const links = [
|
||||||
{ name: "GitHub", href: "https://github.com/SteveThePug" },
|
{ name: "GitHub", href: "https://github.com/SteveThePug" },
|
||||||
{ name: "Gitea", href: "/gitea/explore/repos" },
|
{ name: "Gitea", href: "/gitea/explore/repos" },
|
||||||
{ name: "Spotify", href: "https://open.spotify.com/user/stevethepug" },
|
{ name: "Spotify", href: "https://open.spotify.com/user/stevethepug" },
|
||||||
];
|
];
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<main class="flex justify-center px-4 py-16">
|
<main class="flex justify-center px-4 py-16">
|
||||||
<div class="max-w-xl w-full flex flex-col gap-12">
|
<div class="max-w-xl w-full flex flex-col gap-12">
|
||||||
<section>
|
<section>
|
||||||
<Header>Adam French</Header>
|
<Header>Adam French</Header>
|
||||||
<Paragraph>
|
<Paragraph>
|
||||||
Junior software engineer focused on full-stack development,
|
Junior software engineer focused on full-stack development, systems
|
||||||
systems programming, and infrastructure. First Class Honours
|
programming, and infrastructure. First Class Honours in Computer
|
||||||
in Computer Science with Mathematics from Leeds and
|
Science with Mathematics from Leeds and Waterloo.
|
||||||
Waterloo.
|
</Paragraph>
|
||||||
</Paragraph>
|
</section>
|
||||||
</section>
|
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<Header>About</Header>
|
<Header>About</Header>
|
||||||
<Paragraph>
|
<Paragraph>
|
||||||
This website is self-hosted and has a lot more on it than it
|
This website is self-hosted and has a lot more on it than it needs to.
|
||||||
needs to. Please have a look at my
|
Please have a look at my
|
||||||
<InlineLink to="/cv">CV</InlineLink> for a full breakdown of
|
<InlineLink to="/cv">CV</InlineLink> for a full breakdown of my
|
||||||
my experience, projects, and skills. Please visit
|
experience, projects, and skills. Please visit
|
||||||
<InlineLink to="/stp">STP</InlineLink> for the prefered but
|
<InlineLink to="/stp">STP</InlineLink> for the prefered but less
|
||||||
less professional experience.
|
professional experience.
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<nav class="navRow flex flex-row flex-wrap gap-4 justify-around">
|
<nav class="navRow flex flex-row flex-wrap gap-4 justify-around">
|
||||||
<Link to="/cv"> CV </Link>
|
<Link to="/cv"> CV </Link>
|
||||||
<Link to="/stp"> STP </Link>
|
<Link to="/stp"> STP </Link>
|
||||||
<Link href="mailto:adam.a.french@outlook.com"> Email </Link>
|
<Link href="mailto:adam.a.french@outlook.com"> Email </Link>
|
||||||
<Link v-for="link in links" :key="link.name" :href="link.href">
|
<Link v-for="link in links" :key="link.name" :href="link.href">
|
||||||
{{ link.name }}
|
{{ link.name }}
|
||||||
</Link>
|
</Link>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.navRow > a {
|
.navRow > a {
|
||||||
padding: 0.5rem 1rem;
|
padding: 0.5rem 1rem;
|
||||||
border: 1px solid currentColor;
|
border: 1px solid currentColor;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,72 +1,70 @@
|
|||||||
<template>
|
<template>
|
||||||
<main>
|
<main>
|
||||||
<table id="cover-nav" class="cover-nav no-print">
|
<table id="cover-nav" class="cover-nav no-print">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Companies</th>
|
<th>Companies</th>
|
||||||
<th>Completed</th>
|
<th>Completed</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td><a href="#LloydsBank">Lloyds</a></td>
|
<td><a href="#LloydsBank">Lloyds</a></td>
|
||||||
<td>YES</td>
|
<td>YES</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<div class="no-print m-1 w-full text-center"></div>
|
<div class="no-print m-1 w-full text-center"></div>
|
||||||
<div id="LloydsBank" class="a5page">
|
<div id="LloydsBank" class="a5page">
|
||||||
<div class="contact">
|
<div class="contact">
|
||||||
<h1>Adam French</h1>
|
<h1>Adam French</h1>
|
||||||
<!-- <a href="index.html"><img width=25 height=50 src="img/rune.png"></a> -->
|
<!-- <a href="index.html"><img width=25 height=50 src="img/rune.png"></a> -->
|
||||||
<div class="contact-details">
|
<div class="contact-details">
|
||||||
<p>+447563266931</p>
|
<p>+447563266931</p>
|
||||||
<p>adam.a.french@outlook.com</p>
|
<p>adam.a.french@outlook.com</p>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<h2>BAE graduate digital intelligence software engineer</h2>
|
|
||||||
<p>
|
|
||||||
I am writing to express my interest in your software engineering
|
|
||||||
position. BAE Systems has hosted multiple stools at the
|
|
||||||
University of Leeds and have always exhibited their development
|
|
||||||
of leading-edge software and technology. This is where the
|
|
||||||
origin of my interest in BAE systems emerged and I'm hopeful
|
|
||||||
that this interest shall continue.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
I'm confidient im a strong fit for this role. My technical
|
|
||||||
background includes extensive experience with frontend
|
|
||||||
frameworks such as React. My devotion however lies more in
|
|
||||||
backend development as has more potential to graple problems
|
|
||||||
related to optimisation and designing coherent interfaces.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<em> The C# Programming Yellow Book </em> was my first
|
|
||||||
introduction to C# during A-Level, Java was our vessel for
|
|
||||||
teaching object-orientated programming at university. I am
|
|
||||||
confident I have the relevant experience to grasp the languages
|
|
||||||
stated for the role I am applying for.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
My academic background in Computer Science and Mathematics has
|
|
||||||
honed my ability to translate abstract concepts into structured,
|
|
||||||
logical solutions. Just as I have transformed theoretical
|
|
||||||
hypotheses into formal proofs, I aim to transform business
|
|
||||||
requirements into robust, maintainable software systems through
|
|
||||||
collaboration and rigorous reasoning.
|
|
||||||
</p>
|
|
||||||
<p>Thank you for reading - Adam F</p>
|
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</div>
|
||||||
|
<h2>BAE graduate digital intelligence software engineer</h2>
|
||||||
|
<p>
|
||||||
|
I am writing to express my interest in your software engineering
|
||||||
|
position. BAE Systems has hosted multiple stools at the University of
|
||||||
|
Leeds and have always exhibited their development of leading-edge
|
||||||
|
software and technology. This is where the origin of my interest in BAE
|
||||||
|
systems emerged and I'm hopeful that this interest shall continue.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
I'm confidient im a strong fit for this role. My technical background
|
||||||
|
includes extensive experience with frontend frameworks such as React. My
|
||||||
|
devotion however lies more in backend development as has more potential
|
||||||
|
to graple problems related to optimisation and designing coherent
|
||||||
|
interfaces.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<em> The C# Programming Yellow Book </em> was my first introduction to
|
||||||
|
C# during A-Level, Java was our vessel for teaching object-orientated
|
||||||
|
programming at university. I am confident I have the relevant experience
|
||||||
|
to grasp the languages stated for the role I am applying for.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
My academic background in Computer Science and Mathematics has honed my
|
||||||
|
ability to translate abstract concepts into structured, logical
|
||||||
|
solutions. Just as I have transformed theoretical hypotheses into formal
|
||||||
|
proofs, I aim to transform business requirements into robust,
|
||||||
|
maintainable software systems through collaboration and rigorous
|
||||||
|
reasoning.
|
||||||
|
</p>
|
||||||
|
<p>Thank you for reading - Adam F</p>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@import "@/assets/css/cv_styles.css";
|
@import "@/assets/css/cv_styles.css";
|
||||||
@media print {
|
@media print {
|
||||||
@page {
|
@page {
|
||||||
size: A5 landscape;
|
size: A5 landscape;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -16,60 +16,60 @@ const path = Array.isArray(pathArray) ? pathArray.join("/") : pathArray;
|
|||||||
const url = `/api/notes/${path}`;
|
const url = `/api/notes/${path}`;
|
||||||
|
|
||||||
function getFilename(headers) {
|
function getFilename(headers) {
|
||||||
const disposition = headers["content-disposition"];
|
const disposition = headers["content-disposition"];
|
||||||
if (!disposition) return null;
|
if (!disposition) return null;
|
||||||
|
|
||||||
const match = disposition.match(/filename="?([^"]+)"?/);
|
const match = disposition.match(/filename="?([^"]+)"?/);
|
||||||
return match ? match[1] : null;
|
return match ? match[1] : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchFile() {
|
async function fetchFile() {
|
||||||
const response = await axios.get(url, { responseType: "blob" });
|
const response = await axios.get(url, { responseType: "blob" });
|
||||||
filename.value = getFilename(response.headers);
|
filename.value = getFilename(response.headers);
|
||||||
|
|
||||||
const lastModified = response.headers["last-modified"];
|
const lastModified = response.headers["last-modified"];
|
||||||
last_edited.value = lastModified ? new Date(lastModified) : null;
|
last_edited.value = lastModified ? new Date(lastModified) : null;
|
||||||
|
|
||||||
if (filename.value.toLowerCase().endsWith(".md")) {
|
if (filename.value.toLowerCase().endsWith(".md")) {
|
||||||
const text = await response.data.text();
|
const text = await response.data.text();
|
||||||
file.value = fixLinks(text);
|
file.value = fixLinks(text);
|
||||||
} else {
|
} else {
|
||||||
file.value = response.data;
|
file.value = response.data;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function fixLinks(filedata) {
|
function fixLinks(filedata) {
|
||||||
return filedata.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (match, text, url) => {
|
return filedata.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (match, text, url) => {
|
||||||
if (
|
if (
|
||||||
url.startsWith("http://") ||
|
url.startsWith("http://") ||
|
||||||
url.startsWith("https://") ||
|
url.startsWith("https://") ||
|
||||||
url.startsWith("#") ||
|
url.startsWith("#") ||
|
||||||
url.startsWith("./") ||
|
url.startsWith("./") ||
|
||||||
url.startsWith("../") ||
|
url.startsWith("../") ||
|
||||||
url.startsWith("//")
|
url.startsWith("//")
|
||||||
) {
|
) {
|
||||||
return match;
|
return match;
|
||||||
}
|
}
|
||||||
|
|
||||||
return `[${text}](/notes/${url})`;
|
return `[${text}](/notes/${url})`;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(fetchFile);
|
onMounted(fetchFile);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<main class="items-center flex flex-col">
|
<main class="items-center flex flex-col">
|
||||||
<div class="background" />
|
<div class="background" />
|
||||||
<div
|
<div
|
||||||
v-if="file"
|
v-if="file"
|
||||||
class="a4page-portrait border-primary-1 flex flex-col relative overflow-scroll gap-1 bg-bg_primary"
|
class="a4page-portrait border-primary-1 flex flex-col relative overflow-scroll gap-1 bg-bg_primary"
|
||||||
>
|
>
|
||||||
<h1>{{ filename }}</h1>
|
<h1>{{ filename }}</h1>
|
||||||
<small>{{ last_edited }}</small>
|
<small>{{ last_edited }}</small>
|
||||||
<Markdown class="flex-1 border-box text-wrap" :source="file" />
|
<Markdown class="flex-1 border-box text-wrap" :source="file" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else>Loading…</div>
|
<div v-else>Loading…</div>
|
||||||
</main>
|
</main>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -9,25 +9,25 @@ import topLevelAwait from "vite-plugin-top-level-await";
|
|||||||
|
|
||||||
// https://vite.dev/config/
|
// https://vite.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [
|
plugins: [
|
||||||
vue(),
|
vue(),
|
||||||
...(process.env.NODE_ENV !== "production" ? [vueDevTools()] : []),
|
...(process.env.NODE_ENV !== "production" ? [vueDevTools()] : []),
|
||||||
tailwindcss(),
|
tailwindcss(),
|
||||||
wasm(),
|
wasm(),
|
||||||
topLevelAwait(),
|
topLevelAwait(),
|
||||||
],
|
],
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
"@": fileURLToPath(new URL("./src", import.meta.url)),
|
"@": fileURLToPath(new URL("./src", import.meta.url)),
|
||||||
},
|
|
||||||
},
|
},
|
||||||
server: {
|
},
|
||||||
host: "0.0.0.0",
|
server: {
|
||||||
proxy: {
|
host: "0.0.0.0",
|
||||||
"/api": "http://localhost:8080",
|
proxy: {
|
||||||
"/gitea": "http://localhost:3000",
|
"/api": "http://localhost:8080",
|
||||||
"/radio": "http://localhost:8000",
|
"/gitea": "http://localhost:3000",
|
||||||
"/searxng": "http://localhost:8080",
|
"/radio": "http://localhost:8000",
|
||||||
},
|
"/searxng": "http://localhost:8080",
|
||||||
},
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user