diff --git a/backend/services/email_sync.go b/backend/services/email_sync.go index a539511..1d3189a 100644 --- a/backend/services/email_sync.go +++ b/backend/services/email_sync.go @@ -51,12 +51,12 @@ type graphMessagesResponse struct { } type graphMessage struct { - ID string `json:"id"` - Subject string `json:"subject"` - ReceivedDateTime string `json:"receivedDateTime"` - From graphFrom `json:"from"` - Body graphBody `json:"body"` - BodyPreview string `json:"bodyPreview"` + ID string `json:"id"` + Subject string `json:"subject"` + ReceivedDateTime string `json:"receivedDateTime"` + From graphFrom `json:"from"` + Body graphBody `json:"body"` + BodyPreview string `json:"bodyPreview"` } type graphFrom struct { diff --git a/backend/services/websocket.go b/backend/services/websocket.go index 0f779f0..4963a63 100644 --- a/backend/services/websocket.go +++ b/backend/services/websocket.go @@ -41,8 +41,8 @@ var ( ) const ( - rateLimitWindow = time.Second - rateLimitMaxMsgs = 10 + rateLimitWindow = time.Second + rateLimitMaxMsgs = 10 ) func InitWebSocket(database *gorm.DB, domain string) { diff --git a/docker-compose.yml b/docker-compose.yml index 3834d61..991f90a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,198 +1,201 @@ networks: - app-network: - driver: bridge - ipam: - config: - - subnet: 172.28.0.0/16 + app-network: + driver: bridge + ipam: + config: + - subnet: 172.28.0.0/16 volumes: - dbdata: - uploads: - vue_dist: - - searxng_data: + # Postgres database + dbdata: + # File upload + uploads: + # Vue build + vue_dist: + # Searxng data + searxng_data: services: - vue: - build: - context: ./vue - dockerfile: Dockerfile - container_name: vue - volumes: - - vue_dist:/output - networks: - - app-network + vue: + build: + context: ./vue + dockerfile: Dockerfile + container_name: vue + volumes: + - vue_dist:/output + networks: + - app-network - nginx: - build: - context: ./nginx - dockerfile: Dockerfile - container_name: nginx - env_file: ./.env - restart: always - depends_on: - - vue - - backend - - icecast2 - - gitea - - hasura - - quartz - - searxng - networks: - - app-network - ports: - - 80:80 - - 443:443 - volumes: - - ./certbot/conf:/etc/letsencrypt - - ./certbot/www:/var/www/certbot - - uploads:/uploads - - vue_dist:/etc/nginx/html + nginx: + build: + context: ./nginx + dockerfile: Dockerfile + container_name: nginx + env_file: ./.env + restart: always + depends_on: + - vue + - backend + - icecast2 + - gitea + - hasura + - quartz + - searxng + networks: + - app-network + ports: + - 80:80 + - 443:443 + volumes: + - ./certbot/conf:/etc/letsencrypt + - ./certbot/www:/var/www/certbot + - uploads:/uploads + - vue_dist:/etc/nginx/html - certbot: - image: certbot/certbot:v3.1.0 - container_name: certbot - volumes: - - ./certbot/entrypoint.sh:/entrypoint.sh - - ./certbot/conf:/etc/letsencrypt - - ./certbot/www:/var/www/certbot - entrypoint: ["/entrypoint.sh"] - env_file: - - .env - networks: - - app-network + certbot: + image: certbot/certbot:v3.1.0 + container_name: certbot + volumes: + - ./certbot/entrypoint.sh:/entrypoint.sh + - ./certbot/conf:/etc/letsencrypt + - ./certbot/www:/var/www/certbot + entrypoint: ["/entrypoint.sh"] + env_file: + - .env + networks: + - app-network - backend: - build: - context: ./backend - dockerfile: Dockerfile - container_name: "${BACKEND_HOST}" - restart: always - depends_on: - - db - networks: - - app-network - env_file: - - ./.env - volumes: - - ./backend/token/:/backend/token - - ${OBSIDIAN_DIR}:/backend/notes - - ./logs:/backend/logs - - uploads:/backend/uploads - - ./icecast2/fallback_music:/backend/fallback_music - healthcheck: - test: ["CMD", "wget", "-qO-", "http://localhost:8080/"] - interval: 30s - timeout: 5s - retries: 3 - start_period: 10s + backend: + build: + context: ./backend + dockerfile: Dockerfile + container_name: "${BACKEND_HOST}" + restart: always + depends_on: + - db + networks: + - app-network + env_file: + - ./.env + volumes: + - ./backend/token/:/backend/token + - ${OBSIDIAN_DIR}:/backend/notes + - ./logs:/backend/logs + - uploads:/backend/uploads + - ./icecast2/fallback_music:/backend/fallback_music + healthcheck: + test: ["CMD", "wget", "-qO-", "http://localhost:8080/"] + interval: 30s + timeout: 5s + retries: 3 + start_period: 10s - autoheal: - image: willfarrell/autoheal:latest - container_name: autoheal - restart: always - environment: - - AUTOHEAL_CONTAINER_LABEL=all - - AUTOHEAL_INTERVAL=30 - - AUTOHEAL_START_PERIOD=60 - volumes: - - /var/run/docker.sock:/var/run/docker.sock + autoheal: + image: willfarrell/autoheal:latest + container_name: autoheal + restart: always + environment: + - AUTOHEAL_CONTAINER_LABEL=all + - AUTOHEAL_INTERVAL=30 + - AUTOHEAL_START_PERIOD=60 + volumes: + - /var/run/docker.sock:/var/run/docker.sock - db: - image: postgres:16 - container_name: "${POSTGRES_HOST}" - restart: always - env_file: - - ./.env - networks: - - app-network - volumes: - - dbdata:/var/lib/postgresql/data + db: + image: postgres:16 + container_name: "${POSTGRES_HOST}" + restart: always + env_file: + - ./.env + networks: + - app-network + volumes: + - dbdata:/var/lib/postgresql/data - hasura: - image: hasura/graphql-engine:v2.44.0 - container_name: "${HASURA_HOST}" - restart: always - depends_on: - - db - networks: - - app-network - environment: - 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_ENABLE_CONSOLE: "false" - HASURA_GRAPHQL_DEV_MODE: "false" - HASURA_GRAPHQL_ENABLED_LOG_TYPES: "startup, http-log, webhook-log, websocket-log, query-log" + hasura: + image: hasura/graphql-engine:v2.44.0 + container_name: "${HASURA_HOST}" + restart: always + depends_on: + - db + networks: + - app-network + environment: + 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_ENABLE_CONSOLE: "false" + HASURA_GRAPHQL_DEV_MODE: "false" + HASURA_GRAPHQL_ENABLED_LOG_TYPES: "startup, http-log, webhook-log, websocket-log, query-log" - icecast2: - build: - context: ./icecast2 - dockerfile: Dockerfile - container_name: "${ICECAST_HOST}" - restart: always - networks: - - app-network - env_file: - - ./.env - volumes: - - ./icecast2/fallback_music:/music:ro - ports: - - "${LIQUIDSOAP_HARBOR_PORT:-8001}:${LIQUIDSOAP_HARBOR_PORT:-8001}" + icecast2: + build: + context: ./icecast2 + dockerfile: Dockerfile + container_name: "${ICECAST_HOST}" + restart: always + networks: + - app-network + env_file: + - ./.env + volumes: + - ./icecast2/fallback_music:/music:ro + ports: + - "${LIQUIDSOAP_HARBOR_PORT:-8001}:${LIQUIDSOAP_HARBOR_PORT:-8001}" - quartz: - build: - context: ./quartz - dockerfile: Dockerfile - container_name: "${QUARTZ_HOST}" - restart: always - networks: - - app-network - env_file: - - ./.env - volumes: - - ${OBSIDIAN_DIR}:/quartz/content:ro + quartz: + build: + context: ./quartz + dockerfile: Dockerfile + container_name: "${QUARTZ_HOST}" + restart: always + networks: + - app-network + env_file: + - ./.env + volumes: + - ${OBSIDIAN_DIR}:/quartz/content:ro - searxng: - build: - context: ./searxng - dockerfile: Dockerfile - container_name: "${SEARXNG_HOST}" - restart: unless-stopped - networks: - - app-network - environment: - - BASE_URL=https://www.${DOMAIN}/searxng/ - - INSTANCE_NAME=searxng - - SEARXNG_SECRET_KEY=${SEARXNG_SECRET_KEY} - volumes: - - searxng_data:/etc/searxng + searxng: + build: + context: ./searxng + dockerfile: Dockerfile + container_name: "${SEARXNG_HOST}" + restart: unless-stopped + networks: + - app-network + environment: + - BASE_URL=https://www.${DOMAIN}/searxng/ + - INSTANCE_NAME=searxng + - SEARXNG_SECRET_KEY=${SEARXNG_SECRET_KEY} + volumes: + - searxng_data:/etc/searxng - gitea: - image: docker.gitea.com/gitea:1.25.4-rootless - container_name: "${GITEA_HOST}" - entrypoint: ["/usr/bin/dumb-init", "--", "/etc/gitea/entrypoint.sh"] - networks: - - app-network - environment: - - GITEA__database__DB_TYPE=postgres - - GITEA__database__HOST=${POSTGRES_HOST} - - GITEA__database__NAME=${POSTGRES_GITEA_DB} - - GITEA__database__USER=${POSTGRES_USER} - - GITEA__database__PASSWD=${POSTGRES_PASSWORD} - - GITEA__server__LFS_JWT_SECRET=${GITEA_LFS_JWT_SECRET} - - GITEA__security__INTERNAL_TOKEN=${GITEA_INTERNAL_TOKEN} - - GITEA__oauth2__JWT_SECRET=${GITEA_OAUTH2_JWT_SECRET} - - USER_UID=1000 - - USER_GID=1000 - restart: always - volumes: - - ./gitea/data:/var/lib/gitea - - ./gitea/config:/etc/gitea - - ./gitea/entrypoint.sh:/etc/gitea/entrypoint.sh:ro - - /etc/timezone:/etc/timezone:ro - - /etc/localtime:/etc/localtime:ro - ports: - - "2222:2222" - - "3000:3000" - depends_on: - - db + gitea: + image: docker.gitea.com/gitea:1.25.4-rootless + container_name: "${GITEA_HOST}" + entrypoint: ["/usr/bin/dumb-init", "--", "/etc/gitea/entrypoint.sh"] + networks: + - app-network + environment: + - GITEA__database__DB_TYPE=postgres + - GITEA__database__HOST=${POSTGRES_HOST} + - GITEA__database__NAME=${POSTGRES_GITEA_DB} + - GITEA__database__USER=${POSTGRES_USER} + - GITEA__database__PASSWD=${POSTGRES_PASSWORD} + - GITEA__server__LFS_JWT_SECRET=${GITEA_LFS_JWT_SECRET} + - GITEA__security__INTERNAL_TOKEN=${GITEA_INTERNAL_TOKEN} + - GITEA__oauth2__JWT_SECRET=${GITEA_OAUTH2_JWT_SECRET} + - USER_UID=1000 + - USER_GID=1000 + restart: always + volumes: + - ./gitea/data:/var/lib/gitea + - ./gitea/config:/etc/gitea + - ./gitea/entrypoint.sh:/etc/gitea/entrypoint.sh:ro + - /etc/timezone:/etc/timezone:ro + - /etc/localtime:/etc/localtime:ro + ports: + - "2222:2222" + - "3000:3000" + depends_on: + - db diff --git a/nginx/entrypoint.sh b/nginx/entrypoint.sh index fb1aeca..64bf332 100755 --- a/nginx/entrypoint.sh +++ b/nginx/entrypoint.sh @@ -1,7 +1,7 @@ #!/bin/sh set -e -# Check if dev mode, certificate exists, or setup mode +# Check if DEV_MODE if [ "$DEV_MODE" = "true" ]; then echo "Dev mode. Generating self-signed certificate for HTTPS." CERT_DIR="/etc/letsencrypt/live/localhost" @@ -12,16 +12,19 @@ if [ "$DEV_MODE" = "true" ]; then -out "$CERT_DIR/fullchain.pem" \ -subj "/CN=localhost" 2>/dev/null 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}' \ /etc/nginx/nginx.conf elif [ -f "/etc/letsencrypt/live/$DOMAIN/fullchain.pem" ] && [ -f "/etc/letsencrypt/live/$DOMAIN/privkey.pem" ]; then 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}' \ /etc/nginx/nginx.conf else 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.conf fi diff --git a/vue/index.html b/vue/index.html index 48e593e..b55612f 100644 --- a/vue/index.html +++ b/vue/index.html @@ -1,33 +1,33 @@ - - - - - AF - - - - - - - - - + + + + + AF + + + + + + + + + diff --git a/vue/src/App.vue b/vue/src/App.vue index 536574d..dcd008a 100644 --- a/vue/src/App.vue +++ b/vue/src/App.vue @@ -3,5 +3,5 @@ import { RouterView } from "vue-router"; diff --git a/vue/src/components/Footer.vue b/vue/src/components/Footer.vue index 1d96aff..36614e9 100644 --- a/vue/src/components/Footer.vue +++ b/vue/src/components/Footer.vue @@ -5,128 +5,128 @@ const clock = ref(""); let timer; function updateClock() { - const now = new Date(); - clock.value = now.toLocaleDateString("en-US", { - weekday: "short", - month: "short", - day: "numeric", - hour: "2-digit", - minute: "2-digit", - }); + const now = new Date(); + clock.value = now.toLocaleDateString("en-US", { + weekday: "short", + month: "short", + day: "numeric", + hour: "2-digit", + minute: "2-digit", + }); } onMounted(() => { - updateClock(); - timer = setInterval(updateClock, 1000); + updateClock(); + timer = setInterval(updateClock, 1000); }); onUnmounted(() => { - clearInterval(timer); + clearInterval(timer); }); const user = "visitor"; diff --git a/vue/src/components/Navbar.vue b/vue/src/components/Navbar.vue index bc00b1a..2c9a296 100644 --- a/vue/src/components/Navbar.vue +++ b/vue/src/components/Navbar.vue @@ -6,55 +6,55 @@ import { useRoute } from "vue-router"; const route = useRoute(); const parentPath = computed(() => { - const segments = route.path.split("/").filter(Boolean); - if (segments.length == 1) { - return "/"; - } else { - segments.pop(); - return segments.length ? "/" + segments.join("/") : null; - } + const segments = route.path.split("/").filter(Boolean); + if (segments.length == 1) { + return "/"; + } else { + segments.pop(); + return segments.length ? "/" + segments.join("/") : null; + } }); const faces = [ - "^_^", - "¯\\_(ツ)_/¯", - "(◕‿◕✿)", - "ಠ_ಠ", - "ʘ‿ʘ", - "^̮^", - ">_>", - "¬_¬", - "˙ ͜ʟ˙", - "( ͡° ͜ʖ ͡°)", - "[̲̅$̲̅(̲̅5̲̅)̲̅$̲̅]", - "(ง'̀-'́)ง", - "\ (•◡•) /", - "( ͡ᵔ ͜ʖ ͡ᵔ )", - "ᕙ(⇀‸↼‶)ᕗ", - "⚆ _ ⚆", - "(。◕‿◕。)", - "(╯°□°)╯︵ ʞooqǝɔɐɟ", - "̿ ̿ ̿'̿'\̵͇̿̿\з=(•_•)=ε/̵͇̿̿/'̿'̿ ̿", - "(☞゚ヮ゚)☞ ☜(゚ヮ゚☜)", + "^_^", + "¯\\_(ツ)_/¯", + "(◕‿◕✿)", + "ಠ_ಠ", + "ʘ‿ʘ", + "^̮^", + ">_>", + "¬_¬", + "˙ ͜ʟ˙", + "( ͡° ͜ʖ ͡°)", + "[̲̅$̲̅(̲̅5̲̅)̲̅$̲̅]", + "(ง'̀-'́)ง", + "\ (•◡•) /", + "( ͡ᵔ ͜ʖ ͡ᵔ )", + "ᕙ(⇀‸↼‶)ᕗ", + "⚆ _ ⚆", + "(。◕‿◕。)", + "(╯°□°)╯︵ ʞooqǝɔɐɟ", + "̿ ̿ ̿'̿'\̵͇̿̿\з=(•_•)=ε/̵͇̿̿/'̿'̿ ̿", + "(☞゚ヮ゚)☞ ☜(゚ヮ゚☜)", ]; const faces_string = faces.join(" "); diff --git a/vue/src/components/borders/UtenaFrame.vue b/vue/src/components/borders/UtenaFrame.vue index 7854a40..51ee1ee 100644 --- a/vue/src/components/borders/UtenaFrame.vue +++ b/vue/src/components/borders/UtenaFrame.vue @@ -1,57 +1,57 @@ diff --git a/vue/src/components/elle/Elle.vue b/vue/src/components/elle/Elle.vue index a22a642..1dae9bb 100644 --- a/vue/src/components/elle/Elle.vue +++ b/vue/src/components/elle/Elle.vue @@ -6,11 +6,11 @@ const container = ref(null); // List of (offset, width) function generateOffsets(width = 100, step = 10, n = 20) { - return Array.from({ length: n }, (_, i) => ({ - width, - offset: step * i, - color: getRandomColor(), - })); + return Array.from({ length: n }, (_, i) => ({ + width, + offset: step * i, + color: getRandomColor(), + })); } const offsets = ref(generateOffsets((150, 15, 10))); let rafId; @@ -18,71 +18,71 @@ let rafId; const speed = 0.5; // pixels per frame function animate() { - const ctnr = container.value; - for (const item of offsets.value) { - const width = Math.max(ctnr.offsetWidth, item.width); + const ctnr = container.value; + for (const item of offsets.value) { + const width = Math.max(ctnr.offsetWidth, item.width); - console.log(ctnr.offsetWidth); + console.log(ctnr.offsetWidth); - item.offset -= speed; - if (item.offset <= -width) { - item.color = getRandomColor(); - item.offset = 0; - } + item.offset -= speed; + if (item.offset <= -width) { + item.color = getRandomColor(); + item.offset = 0; } - rafId = requestAnimationFrame(animate); + } + rafId = requestAnimationFrame(animate); } onMounted(() => { - rafId = requestAnimationFrame(animate); + rafId = requestAnimationFrame(animate); }); onUnmounted(() => { - cancelAnimationFrame(rafId); + cancelAnimationFrame(rafId); }); diff --git a/vue/src/components/input/Button.vue b/vue/src/components/input/Button.vue index 9efe93a..a7caa23 100644 --- a/vue/src/components/input/Button.vue +++ b/vue/src/components/input/Button.vue @@ -1,11 +1,11 @@ diff --git a/vue/src/components/input/ToggleButton.vue b/vue/src/components/input/ToggleButton.vue index 492a9b3..9d1a6f1 100644 --- a/vue/src/components/input/ToggleButton.vue +++ b/vue/src/components/input/ToggleButton.vue @@ -2,30 +2,30 @@ import { ref } from "vue"; const props = defineProps({ - modelValue: { - type: Boolean, - default: false, - }, + modelValue: { + type: Boolean, + default: false, + }, }); const emit = defineEmits(["update:modelValue"]); function toggle() { - emit("update:modelValue", !props.modelValue); + emit("update:modelValue", !props.modelValue); } diff --git a/vue/src/components/text/Header.vue b/vue/src/components/text/Header.vue index 3f2bffc..7214254 100644 --- a/vue/src/components/text/Header.vue +++ b/vue/src/components/text/Header.vue @@ -1,7 +1,7 @@ diff --git a/vue/src/components/text/Headline.vue b/vue/src/components/text/Headline.vue index 1325378..9047790 100644 --- a/vue/src/components/text/Headline.vue +++ b/vue/src/components/text/Headline.vue @@ -12,74 +12,74 @@ let rafId; const speed = 0.5; // pixels per frame function measureWidth() { - const ctnr = container.value; - const it1 = item1.value; - if (ctnr && it1) { - cachedWidth = Math.max(ctnr.offsetWidth, it1.scrollWidth); - } + const ctnr = container.value; + const it1 = item1.value; + if (ctnr && it1) { + cachedWidth = Math.max(ctnr.offsetWidth, it1.scrollWidth); + } } function animate() { - const ctnr = container.value; - if (!ctnr || cachedWidth === 0) { - rafId = requestAnimationFrame(animate); - return; - } - - offset -= speed; - - if (offset <= -cachedWidth) { - offset += cachedWidth; - } - - ctnr.style.transform = `translateX(${offset}px)`; - + const ctnr = container.value; + if (!ctnr || cachedWidth === 0) { rafId = requestAnimationFrame(animate); + return; + } + + offset -= speed; + + if (offset <= -cachedWidth) { + offset += cachedWidth; + } + + ctnr.style.transform = `translateX(${offset}px)`; + + rafId = requestAnimationFrame(animate); } let resizeObserver; onMounted(() => { - measureWidth(); - rafId = requestAnimationFrame(animate); + measureWidth(); + rafId = requestAnimationFrame(animate); - resizeObserver = new ResizeObserver(measureWidth); - resizeObserver.observe(container.value); + resizeObserver = new ResizeObserver(measureWidth); + resizeObserver.observe(container.value); }); onUnmounted(() => { - cancelAnimationFrame(rafId); - resizeObserver?.disconnect(); + cancelAnimationFrame(rafId); + resizeObserver?.disconnect(); }); diff --git a/vue/src/components/text/InlineLink.vue b/vue/src/components/text/InlineLink.vue index e9dfc12..ec47ea8 100644 --- a/vue/src/components/text/InlineLink.vue +++ b/vue/src/components/text/InlineLink.vue @@ -2,38 +2,44 @@ import { computed } from "vue"; const props = defineProps({ - href: { type: String, default: "" }, - to: { type: String, default: "" }, - target: { type: String, default: undefined }, - rel: { type: String, default: undefined }, + href: { type: String, default: "" }, + to: { type: String, default: "" }, + target: { type: String, default: undefined }, + rel: { type: String, default: undefined }, }); const computedRel = computed(() => { - if (props.rel !== undefined) return props.rel; - if (props.target === "_blank") return "noopener noreferrer"; - return undefined; + if (props.rel !== undefined) return props.rel; + if (props.target === "_blank") return "noopener noreferrer"; + return undefined; }); diff --git a/vue/src/components/text/Link.vue b/vue/src/components/text/Link.vue index b9d8fc5..38808a9 100644 --- a/vue/src/components/text/Link.vue +++ b/vue/src/components/text/Link.vue @@ -2,37 +2,43 @@ import { computed } from "vue"; const props = defineProps({ - href: { type: String, default: "" }, - to: { type: String, default: "" }, - target: { type: String, default: undefined }, - rel: { type: String, default: undefined }, - bare: { type: Boolean, default: false }, + href: { type: String, default: "" }, + to: { type: String, default: "" }, + target: { type: String, default: undefined }, + rel: { type: String, default: undefined }, + bare: { type: Boolean, default: false }, }); const computedRel = computed(() => { - if (props.rel !== undefined) return props.rel; - if (props.target === "_blank") return "noopener noreferrer"; - return undefined; + if (props.rel !== undefined) return props.rel; + if (props.target === "_blank") return "noopener noreferrer"; + return undefined; }); diff --git a/vue/src/components/text/Paragraph.vue b/vue/src/components/text/Paragraph.vue index f99f3db..5ad31d2 100644 --- a/vue/src/components/text/Paragraph.vue +++ b/vue/src/components/text/Paragraph.vue @@ -1,5 +1,5 @@ diff --git a/vue/src/components/text/ToggleHeader.vue b/vue/src/components/text/ToggleHeader.vue index 79b1dd8..bea5130 100644 --- a/vue/src/components/text/ToggleHeader.vue +++ b/vue/src/components/text/ToggleHeader.vue @@ -3,36 +3,36 @@ import { ref } from "vue"; import ToggleButton from "@/components/input/ToggleButton.vue"; const props = defineProps({ - modelValue: { - type: Boolean, - default: false, - }, + modelValue: { + type: Boolean, + default: false, + }, }); const emit = defineEmits(["update:modelValue"]); const toggleButtonRef = ref(null); const updateValue = (newValue) => { - emit("update:modelValue", newValue); + emit("update:modelValue", newValue); }; const handleClick = () => { - toggleButtonRef.value?.$el?.click(); + toggleButtonRef.value?.$el?.click(); }; diff --git a/vue/src/components/util/AutoScroll.vue b/vue/src/components/util/AutoScroll.vue index bfceea4..d08eca5 100644 --- a/vue/src/components/util/AutoScroll.vue +++ b/vue/src/components/util/AutoScroll.vue @@ -1,7 +1,12 @@ diff --git a/vue/src/components/util/Chat.vue b/vue/src/components/util/Chat.vue index 3a2f618..fe7f048 100644 --- a/vue/src/components/util/Chat.vue +++ b/vue/src/components/util/Chat.vue @@ -19,210 +19,205 @@ const SCROLL_THRESHOLD = 100; let resizeObserver = null; function scrollToBottom() { - if (messagesContainer.value) { - messagesContainer.value.scrollTop = - messagesContainer.value.scrollHeight; - } + if (messagesContainer.value) { + messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight; + } } function scrollToBottomIfNear() { - if (isNearBottom.value) { - scrollToBottom(); - } + if (isNearBottom.value) { + scrollToBottom(); + } } function onScroll() { - if (!messagesContainer.value) return; - const { scrollHeight, scrollTop, clientHeight } = messagesContainer.value; - isNearBottom.value = - scrollHeight - scrollTop - clientHeight < SCROLL_THRESHOLD; + if (!messagesContainer.value) return; + const { scrollHeight, scrollTop, clientHeight } = messagesContainer.value; + isNearBottom.value = + scrollHeight - scrollTop - clientHeight < SCROLL_THRESHOLD; } function goToBottom() { - isNearBottom.value = true; - scrollToBottom(); + isNearBottom.value = true; + scrollToBottom(); } watch( - () => messages.value.length, - () => { - nextTick(scrollToBottomIfNear); - }, + () => messages.value.length, + () => { + nextTick(scrollToBottomIfNear); + }, ); function sendMessage() { - const text = messageInput.value.trim(); - if (!text) return; - isNearBottom.value = true; - messagesStore.sendMessage(text); - messageInput.value = ""; + const text = messageInput.value.trim(); + if (!text) return; + isNearBottom.value = true; + messagesStore.sendMessage(text); + messageInput.value = ""; } async function onFileSelected(e) { - const file = e.target.files[0]; - if (!file) return; - isNearBottom.value = true; - await messagesStore.uploadAndSendFile(file); - fileInput.value.value = ""; + const file = e.target.files[0]; + if (!file) return; + isNearBottom.value = true; + await messagesStore.uploadAndSendFile(file); + fileInput.value.value = ""; } function isImageUrl(url) { - return /\.(jpg|jpeg|png|gif|webp)$/i.test(url); + return /\.(jpg|jpeg|png|gif|webp)$/i.test(url); } function isVideoUrl(url) { - return /\.(mp4|webm|ogg|mov)$/i.test(url); + return /\.(mp4|webm|ogg|mov)$/i.test(url); } function isSafeFileUrl(url) { - return typeof url === "string" && url.startsWith("/uploads/"); + return typeof url === "string" && url.startsWith("/uploads/"); } const urlRegex = /(https?:\/\/[^\s<]+)/g; function parseMessageParts(text) { - const parts = []; - let lastIndex = 0; - let match; - while ((match = urlRegex.exec(text)) !== null) { - if (match.index > lastIndex) { - parts.push({ - type: "text", - value: text.slice(lastIndex, match.index), - }); - } - parts.push({ type: "link", value: match[1] }); - lastIndex = urlRegex.lastIndex; + const parts = []; + let lastIndex = 0; + let match; + while ((match = urlRegex.exec(text)) !== null) { + if (match.index > lastIndex) { + parts.push({ + type: "text", + value: text.slice(lastIndex, match.index), + }); } - if (lastIndex < text.length) { - parts.push({ type: "text", value: text.slice(lastIndex) }); - } - return parts; + parts.push({ type: "link", value: match[1] }); + lastIndex = urlRegex.lastIndex; + } + if (lastIndex < text.length) { + parts.push({ type: "text", value: text.slice(lastIndex) }); + } + return parts; } onMounted(() => { - messagesStore.connect(); + messagesStore.connect(); - if (messagesContainer.value) { - messagesContainer.value.addEventListener("scroll", onScroll, { - passive: true, - }); - } + if (messagesContainer.value) { + messagesContainer.value.addEventListener("scroll", onScroll, { + passive: true, + }); + } - if (messagesInner.value) { - resizeObserver = new ResizeObserver(scrollToBottomIfNear); - resizeObserver.observe(messagesInner.value); - } + if (messagesInner.value) { + resizeObserver = new ResizeObserver(scrollToBottomIfNear); + resizeObserver.observe(messagesInner.value); + } - scrollToBottom(); + scrollToBottom(); }); onUnmounted(() => { - messagesStore.disconnect(); + messagesStore.disconnect(); - if (messagesContainer.value) { - messagesContainer.value.removeEventListener("scroll", onScroll); - } + if (messagesContainer.value) { + messagesContainer.value.removeEventListener("scroll", onScroll); + } - if (resizeObserver) { - resizeObserver.disconnect(); - resizeObserver = null; - } + if (resizeObserver) { + resizeObserver.disconnect(); + resizeObserver = null; + } }); diff --git a/vue/src/components/util/CommitHistory.vue b/vue/src/components/util/CommitHistory.vue index f826b22..4b8af8b 100644 --- a/vue/src/components/util/CommitHistory.vue +++ b/vue/src/components/util/CommitHistory.vue @@ -9,37 +9,42 @@ const { gitFeed: feed, loaded } = storeToRefs(homeData); diff --git a/vue/src/components/util/LinkTable.vue b/vue/src/components/util/LinkTable.vue index bac38e6..e389af7 100644 --- a/vue/src/components/util/LinkTable.vue +++ b/vue/src/components/util/LinkTable.vue @@ -4,70 +4,66 @@ import Link from "@/components/text/Link.vue"; import ToggleHeader from "@/components/text/ToggleHeader.vue"; const props = defineProps({ - items: { - type: Array, - required: true, - }, - variant: { - type: String, - default: "list", - }, - title: { - type: String, - default: "", - }, + items: { + type: Array, + required: true, + }, + variant: { + type: String, + default: "list", + }, + title: { + type: String, + default: "", + }, }); const show = ref(false);