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:
@@ -6,10 +6,13 @@ networks:
|
|||||||
- subnet: 172.28.0.0/16
|
- subnet: 172.28.0.0/16
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
|
# Postgres database
|
||||||
dbdata:
|
dbdata:
|
||||||
|
# File upload
|
||||||
uploads:
|
uploads:
|
||||||
|
# Vue build
|
||||||
vue_dist:
|
vue_dist:
|
||||||
|
# Searxng data
|
||||||
searxng_data:
|
searxng_data:
|
||||||
|
|
||||||
services:
|
services:
|
||||||
|
|||||||
@@ -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,31 +1,31 @@
|
|||||||
<!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>
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ const faces_string = faces.join(" ");
|
|||||||
|
|
||||||
<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">
|
||||||
|
|||||||
@@ -19,7 +19,13 @@ const computedRel = computed(() => {
|
|||||||
<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
|
||||||
|
v-else
|
||||||
|
:href="href"
|
||||||
|
:target="target"
|
||||||
|
:rel="computedRel"
|
||||||
|
class="inline-link"
|
||||||
|
>
|
||||||
<slot />
|
<slot />
|
||||||
</a>
|
</a>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -20,7 +20,13 @@ const computedRel = computed(() => {
|
|||||||
<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
|
||||||
|
v-else
|
||||||
|
:href="href"
|
||||||
|
:target="target"
|
||||||
|
:rel="computedRel"
|
||||||
|
:class="{ link: !bare }"
|
||||||
|
>
|
||||||
<slot />
|
<slot />
|
||||||
</a>
|
</a>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<div ref="container" @mouseenter="onMouseEnter" @mouseleave="onMouseLeave" class="overflow-y-auto">
|
<div
|
||||||
|
ref="container"
|
||||||
|
@mouseenter="onMouseEnter"
|
||||||
|
@mouseleave="onMouseLeave"
|
||||||
|
class="overflow-y-auto"
|
||||||
|
>
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -20,8 +20,7 @@ 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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,9 +145,7 @@ onUnmounted(() => {
|
|||||||
>
|
>
|
||||||
<span class="text-tertiary">{{ message.authorId }}:</span>
|
<span class="text-tertiary">{{ message.authorId }}:</span>
|
||||||
<template
|
<template
|
||||||
v-for="(part, i) in parseMessageParts(
|
v-for="(part, i) in parseMessageParts(message.text || '')"
|
||||||
message.text || '',
|
|
||||||
)"
|
|
||||||
:key="i"
|
:key="i"
|
||||||
>
|
>
|
||||||
<Link
|
<Link
|
||||||
@@ -161,9 +158,7 @@ onUnmounted(() => {
|
|||||||
>
|
>
|
||||||
<span v-else>{{ part.value }}</span>
|
<span v-else>{{ part.value }}</span>
|
||||||
</template>
|
</template>
|
||||||
<template
|
<template v-if="message.fileUrl && isSafeFileUrl(message.fileUrl)">
|
||||||
v-if="message.fileUrl && isSafeFileUrl(message.fileUrl)"
|
|
||||||
>
|
|
||||||
<img
|
<img
|
||||||
v-if="isImageUrl(message.fileUrl)"
|
v-if="isImageUrl(message.fileUrl)"
|
||||||
:src="message.fileUrl"
|
:src="message.fileUrl"
|
||||||
|
|||||||
@@ -21,7 +21,12 @@ const { gitFeed: feed, loaded } = storeToRefs(homeData);
|
|||||||
class="flex-1 flex flex-col items-center overflow-y-auto overflow-x-hidden"
|
class="flex-1 flex flex-col items-center overflow-y-auto overflow-x-hidden"
|
||||||
>
|
>
|
||||||
<h3>Last git activity</h3>
|
<h3>Last git activity</h3>
|
||||||
<img :src="feed.avatarUrl" alt="User avatar" class="avatar" loading="lazy" />
|
<img
|
||||||
|
:src="feed.avatarUrl"
|
||||||
|
alt="User avatar"
|
||||||
|
class="avatar"
|
||||||
|
loading="lazy"
|
||||||
|
/>
|
||||||
<Link :href="feed.repoUrl">
|
<Link :href="feed.repoUrl">
|
||||||
<h3>repo: {{ feed.repoName }}</h3>
|
<h3>repo: {{ feed.repoName }}</h3>
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -50,11 +50,7 @@ const show = ref(false);
|
|||||||
</div>
|
</div>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<template v-if="variant === 'list'">
|
<template v-if="variant === 'list'">
|
||||||
<Link
|
<Link v-for="(item, i) in items" :key="i" :href="item.link">
|
||||||
v-for="(item, i) in items"
|
|
||||||
:key="i"
|
|
||||||
: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>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,23 +1,22 @@
|
|||||||
<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>
|
||||||
<div class="controls">
|
<div class="controls">
|
||||||
<div class="sliders">
|
<div class="sliders">
|
||||||
<div class="timeline"/>
|
<div class="timeline" />
|
||||||
<div class="volume"/>
|
<div class="volume" />
|
||||||
</div>
|
</div>
|
||||||
<div class="buttons">
|
<div class="buttons">
|
||||||
<div class="rewind"/>
|
<div class="rewind" />
|
||||||
<div class="playPause"/>
|
<div class="playPause" />
|
||||||
<div class="fastforward"/>
|
<div class="fastforward" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -87,7 +86,7 @@ img {
|
|||||||
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,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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,27 +17,55 @@ import { RouterView } from "vue-router";
|
|||||||
</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,
|
||||||
|
.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;
|
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,
|
||||||
|
.cv-layout textarea {
|
||||||
color: #111;
|
color: #111;
|
||||||
background-color: white;
|
background-color: white;
|
||||||
border: 1px solid #ccc;
|
border: 1px solid #ccc;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
width: auto;
|
width: auto;
|
||||||
}
|
}
|
||||||
.cv-layout input::placeholder, .cv-layout textarea::placeholder { color: #999; opacity: 1; }
|
.cv-layout input::placeholder,
|
||||||
.cv-layout table { border: 0 solid transparent; }
|
.cv-layout textarea::placeholder {
|
||||||
.cv-layout tr { border-color: transparent; }
|
color: #999;
|
||||||
.cv-layout th { border: none; padding: 0; }
|
opacity: 1;
|
||||||
.cv-layout td { padding: 0; }
|
}
|
||||||
|
.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>
|
||||||
|
|||||||
@@ -91,12 +91,19 @@ router.beforeEach(async (to) => {
|
|||||||
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();
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (!useAuthStore().user.admin) return { path: "/admin/login", query: { redirect: to.fullPath } };
|
if (!useAuthStore().user.admin)
|
||||||
|
return { path: "/admin/login", query: { redirect: to.fullPath } };
|
||||||
});
|
});
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|||||||
@@ -23,7 +23,14 @@ const editRefForm = ref({});
|
|||||||
const REF_CATEGORIES = ["profile", "experience"];
|
const REF_CATEGORIES = ["profile", "experience"];
|
||||||
const REF_FIELDS = `id category label value sortOrder createdAt`;
|
const REF_FIELDS = `id category label value sortOrder createdAt`;
|
||||||
|
|
||||||
const STATUS_OPTIONS = ["Applied", "Screening", "Interview", "Offer", "Rejected", "Withdrawn"];
|
const STATUS_OPTIONS = [
|
||||||
|
"Applied",
|
||||||
|
"Screening",
|
||||||
|
"Interview",
|
||||||
|
"Offer",
|
||||||
|
"Rejected",
|
||||||
|
"Withdrawn",
|
||||||
|
];
|
||||||
|
|
||||||
const APP_FIELDS = `id jobTitle company location url status notes appliedAt createdAt`;
|
const APP_FIELDS = `id jobTitle company location url status notes appliedAt createdAt`;
|
||||||
|
|
||||||
@@ -46,7 +53,9 @@ async function createApplication() {
|
|||||||
location: form.value.location || undefined,
|
location: form.value.location || undefined,
|
||||||
url: form.value.url || undefined,
|
url: form.value.url || undefined,
|
||||||
notes: form.value.notes || undefined,
|
notes: form.value.notes || undefined,
|
||||||
appliedAt: form.value.appliedAt ? new Date(form.value.appliedAt).toISOString() : undefined,
|
appliedAt: form.value.appliedAt
|
||||||
|
? new Date(form.value.appliedAt).toISOString()
|
||||||
|
: undefined,
|
||||||
};
|
};
|
||||||
const data = await gql(
|
const data = await gql(
|
||||||
`mutation CreateJobApplication($input: CreateJobApplicationInput!) {
|
`mutation CreateJobApplication($input: CreateJobApplicationInput!) {
|
||||||
@@ -55,7 +64,15 @@ async function createApplication() {
|
|||||||
{ input },
|
{ input },
|
||||||
);
|
);
|
||||||
applications.value.unshift(data.createJobApplication);
|
applications.value.unshift(data.createJobApplication);
|
||||||
form.value = { jobTitle: "", company: "", location: "", url: "", status: "Applied", notes: "", appliedAt: "" };
|
form.value = {
|
||||||
|
jobTitle: "",
|
||||||
|
company: "",
|
||||||
|
location: "",
|
||||||
|
url: "",
|
||||||
|
status: "Applied",
|
||||||
|
notes: "",
|
||||||
|
appliedAt: "",
|
||||||
|
};
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
}
|
}
|
||||||
@@ -88,7 +105,9 @@ async function saveEdit(id) {
|
|||||||
location: editForm.value.location || undefined,
|
location: editForm.value.location || undefined,
|
||||||
url: editForm.value.url || undefined,
|
url: editForm.value.url || undefined,
|
||||||
notes: editForm.value.notes || undefined,
|
notes: editForm.value.notes || undefined,
|
||||||
appliedAt: editForm.value.appliedAt ? new Date(editForm.value.appliedAt).toISOString() : undefined,
|
appliedAt: editForm.value.appliedAt
|
||||||
|
? new Date(editForm.value.appliedAt).toISOString()
|
||||||
|
: undefined,
|
||||||
};
|
};
|
||||||
const data = await gql(
|
const data = await gql(
|
||||||
`mutation UpdateJobApplication($id: ID!, $input: UpdateJobApplicationInput!) {
|
`mutation UpdateJobApplication($id: ID!, $input: UpdateJobApplicationInput!) {
|
||||||
@@ -117,7 +136,16 @@ async function deleteApplication(id) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function exportCsv() {
|
function exportCsv() {
|
||||||
const headers = ["Job Title", "Company", "Status", "Location", "URL", "Applied", "Notes", "Created"];
|
const headers = [
|
||||||
|
"Job Title",
|
||||||
|
"Company",
|
||||||
|
"Status",
|
||||||
|
"Location",
|
||||||
|
"URL",
|
||||||
|
"Applied",
|
||||||
|
"Notes",
|
||||||
|
"Created",
|
||||||
|
];
|
||||||
const rows = applications.value.map((a) => [
|
const rows = applications.value.map((a) => [
|
||||||
a.jobTitle,
|
a.jobTitle,
|
||||||
a.company,
|
a.company,
|
||||||
@@ -171,7 +199,11 @@ async function createReference() {
|
|||||||
|
|
||||||
function startRefEdit(ref) {
|
function startRefEdit(ref) {
|
||||||
editingRefId.value = ref.id;
|
editingRefId.value = ref.id;
|
||||||
editRefForm.value = { category: ref.category, label: ref.label, value: ref.value };
|
editRefForm.value = {
|
||||||
|
category: ref.category,
|
||||||
|
label: ref.label,
|
||||||
|
value: ref.value,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function cancelRefEdit() {
|
function cancelRefEdit() {
|
||||||
@@ -245,58 +277,140 @@ onMounted(() => {
|
|||||||
<RouterLink to="/cv" class="ja-back">← CV</RouterLink>
|
<RouterLink to="/cv" class="ja-back">← CV</RouterLink>
|
||||||
<h2 class="ja-heading">Job Applications</h2>
|
<h2 class="ja-heading">Job Applications</h2>
|
||||||
</div>
|
</div>
|
||||||
<button class="ja-btn" @click="exportCsv" :disabled="!applications.length">Export CSV</button>
|
<button
|
||||||
|
class="ja-btn"
|
||||||
|
@click="exportCsv"
|
||||||
|
:disabled="!applications.length"
|
||||||
|
>
|
||||||
|
Export CSV
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="ja-ref-section">
|
<div class="ja-ref-section">
|
||||||
<h3 class="ja-ref-heading">Quick Reference</h3>
|
<h3 class="ja-ref-heading">Quick Reference</h3>
|
||||||
<div v-for="cat in REF_CATEGORIES" :key="cat" class="ja-ref-category">
|
<div v-for="cat in REF_CATEGORIES" :key="cat" class="ja-ref-category">
|
||||||
<h4 class="ja-ref-cat-label">{{ cat }}</h4>
|
<h4 class="ja-ref-cat-label">{{ cat }}</h4>
|
||||||
<div v-for="ref in refsByCategory(cat)" :key="ref.id" class="ja-ref-item">
|
<div
|
||||||
|
v-for="ref in refsByCategory(cat)"
|
||||||
|
:key="ref.id"
|
||||||
|
class="ja-ref-item"
|
||||||
|
>
|
||||||
<template v-if="editingRefId !== ref.id">
|
<template v-if="editingRefId !== ref.id">
|
||||||
<span class="ja-ref-label">{{ ref.label }}</span>
|
<span class="ja-ref-label">{{ ref.label }}</span>
|
||||||
<span class="ja-ref-value" :title="ref.value">{{ ref.value }}</span>
|
<span class="ja-ref-value" :title="ref.value">{{ ref.value }}</span>
|
||||||
<button class="ja-btn ja-btn-sm" @click="copyToClipboard(ref.value)" title="Copy">Copy</button>
|
<button
|
||||||
<button class="ja-btn ja-btn-sm" @click="startRefEdit(ref)">Edit</button>
|
class="ja-btn ja-btn-sm"
|
||||||
<button class="ja-btn ja-btn-sm ja-btn-danger" @click="deleteReference(ref.id)">Delete</button>
|
@click="copyToClipboard(ref.value)"
|
||||||
|
title="Copy"
|
||||||
|
>
|
||||||
|
Copy
|
||||||
|
</button>
|
||||||
|
<button class="ja-btn ja-btn-sm" @click="startRefEdit(ref)">
|
||||||
|
Edit
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="ja-btn ja-btn-sm ja-btn-danger"
|
||||||
|
@click="deleteReference(ref.id)"
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<select v-model="editRefForm.category" class="ja-input ja-input-sm ja-select">
|
<select
|
||||||
<option v-for="c in REF_CATEGORIES" :key="c" :value="c">{{ c }}</option>
|
v-model="editRefForm.category"
|
||||||
|
class="ja-input ja-input-sm ja-select"
|
||||||
|
>
|
||||||
|
<option v-for="c in REF_CATEGORIES" :key="c" :value="c">
|
||||||
|
{{ c }}
|
||||||
|
</option>
|
||||||
</select>
|
</select>
|
||||||
<input v-model="editRefForm.label" class="ja-input ja-input-sm" placeholder="Label" />
|
<input
|
||||||
<input v-model="editRefForm.value" class="ja-input ja-input-sm" placeholder="Value" />
|
v-model="editRefForm.label"
|
||||||
<button class="ja-btn ja-btn-sm ja-btn-primary" @click="saveRefEdit(ref.id)">Save</button>
|
class="ja-input ja-input-sm"
|
||||||
<button class="ja-btn ja-btn-sm" @click="cancelRefEdit">Cancel</button>
|
placeholder="Label"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
v-model="editRefForm.value"
|
||||||
|
class="ja-input ja-input-sm"
|
||||||
|
placeholder="Value"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
class="ja-btn ja-btn-sm ja-btn-primary"
|
||||||
|
@click="saveRefEdit(ref.id)"
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
<button class="ja-btn ja-btn-sm" @click="cancelRefEdit">
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<p v-if="!refsByCategory(cat).length" class="ja-ref-empty">No {{ cat }} items yet.</p>
|
<p v-if="!refsByCategory(cat).length" class="ja-ref-empty">
|
||||||
|
No {{ cat }} items yet.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<form class="ja-ref-form" @submit.prevent="createReference">
|
<form class="ja-ref-form" @submit.prevent="createReference">
|
||||||
<select v-model="refForm.category" class="ja-input ja-select">
|
<select v-model="refForm.category" class="ja-input ja-select">
|
||||||
<option v-for="c in REF_CATEGORIES" :key="c" :value="c">{{ c }}</option>
|
<option v-for="c in REF_CATEGORIES" :key="c" :value="c">
|
||||||
|
{{ c }}
|
||||||
|
</option>
|
||||||
</select>
|
</select>
|
||||||
<input v-model="refForm.label" class="ja-input" placeholder="Label *" required />
|
<input
|
||||||
<input v-model="refForm.value" class="ja-input" placeholder="Value *" required />
|
v-model="refForm.label"
|
||||||
|
class="ja-input"
|
||||||
|
placeholder="Label *"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
v-model="refForm.value"
|
||||||
|
class="ja-input"
|
||||||
|
placeholder="Value *"
|
||||||
|
required
|
||||||
|
/>
|
||||||
<button type="submit" class="ja-btn ja-btn-primary">Add</button>
|
<button type="submit" class="ja-btn ja-btn-primary">Add</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form class="ja-form" @submit.prevent="createApplication">
|
<form class="ja-form" @submit.prevent="createApplication">
|
||||||
<div class="ja-form-row">
|
<div class="ja-form-row">
|
||||||
<input v-model="form.jobTitle" class="ja-input" placeholder="Job title *" required />
|
<input
|
||||||
<input v-model="form.company" class="ja-input" placeholder="Company *" required />
|
v-model="form.jobTitle"
|
||||||
|
class="ja-input"
|
||||||
|
placeholder="Job title *"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
v-model="form.company"
|
||||||
|
class="ja-input"
|
||||||
|
placeholder="Company *"
|
||||||
|
required
|
||||||
|
/>
|
||||||
<select v-model="form.status" class="ja-input ja-select">
|
<select v-model="form.status" class="ja-input ja-select">
|
||||||
<option v-for="s in STATUS_OPTIONS" :key="s" :value="s">{{ s }}</option>
|
<option v-for="s in STATUS_OPTIONS" :key="s" :value="s">
|
||||||
|
{{ s }}
|
||||||
|
</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="ja-form-row">
|
<div class="ja-form-row">
|
||||||
<input v-model="form.location" class="ja-input" placeholder="Location" />
|
<input
|
||||||
|
v-model="form.location"
|
||||||
|
class="ja-input"
|
||||||
|
placeholder="Location"
|
||||||
|
/>
|
||||||
<input v-model="form.url" class="ja-input" placeholder="URL" />
|
<input v-model="form.url" class="ja-input" placeholder="URL" />
|
||||||
<input v-model="form.appliedAt" class="ja-input" type="date" title="Applied date" />
|
<input
|
||||||
|
v-model="form.appliedAt"
|
||||||
|
class="ja-input"
|
||||||
|
type="date"
|
||||||
|
title="Applied date"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="ja-form-row">
|
<div class="ja-form-row">
|
||||||
<textarea v-model="form.notes" class="ja-input ja-textarea" placeholder="Notes" />
|
<textarea
|
||||||
|
v-model="form.notes"
|
||||||
|
class="ja-input ja-textarea"
|
||||||
|
placeholder="Notes"
|
||||||
|
/>
|
||||||
<button type="submit" class="ja-btn ja-btn-primary">Add</button>
|
<button type="submit" class="ja-btn ja-btn-primary">Add</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
@@ -317,43 +431,93 @@ onMounted(() => {
|
|||||||
<template v-for="app in applications" :key="app.id">
|
<template v-for="app in applications" :key="app.id">
|
||||||
<tr v-if="editingId !== app.id">
|
<tr v-if="editingId !== app.id">
|
||||||
<td>
|
<td>
|
||||||
<a v-if="app.url" :href="app.url" target="_blank" rel="noopener" class="ja-link">{{ app.jobTitle }}</a>
|
<a
|
||||||
|
v-if="app.url"
|
||||||
|
:href="app.url"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener"
|
||||||
|
class="ja-link"
|
||||||
|
>{{ app.jobTitle }}</a
|
||||||
|
>
|
||||||
<span v-else>{{ app.jobTitle }}</span>
|
<span v-else>{{ app.jobTitle }}</span>
|
||||||
</td>
|
</td>
|
||||||
<td>{{ app.company }}</td>
|
<td>{{ app.company }}</td>
|
||||||
<td><span :class="['ja-badge', statusClass(app.status)]">{{ app.status }}</span></td>
|
<td>
|
||||||
|
<span :class="['ja-badge', statusClass(app.status)]">{{
|
||||||
|
app.status
|
||||||
|
}}</span>
|
||||||
|
</td>
|
||||||
<td>{{ app.location ?? "—" }}</td>
|
<td>{{ app.location ?? "—" }}</td>
|
||||||
<td>{{ app.appliedAt ? app.appliedAt.substring(0, 10) : "—" }}</td>
|
<td>{{ app.appliedAt ? app.appliedAt.substring(0, 10) : "—" }}</td>
|
||||||
<td class="ja-notes-cell">{{ app.notes ?? "" }}</td>
|
<td class="ja-notes-cell">{{ app.notes ?? "" }}</td>
|
||||||
<td class="ja-actions">
|
<td class="ja-actions">
|
||||||
<button class="ja-btn ja-btn-sm" @click="startEdit(app)">Edit</button>
|
<button class="ja-btn ja-btn-sm" @click="startEdit(app)">
|
||||||
<button class="ja-btn ja-btn-sm ja-btn-danger" @click="deleteApplication(app.id)">Delete</button>
|
Edit
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="ja-btn ja-btn-sm ja-btn-danger"
|
||||||
|
@click="deleteApplication(app.id)"
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr v-else class="ja-edit-row">
|
<tr v-else class="ja-edit-row">
|
||||||
<td>
|
<td>
|
||||||
<input v-model="editForm.jobTitle" class="ja-input ja-input-sm" placeholder="Job title" />
|
<input
|
||||||
|
v-model="editForm.jobTitle"
|
||||||
|
class="ja-input ja-input-sm"
|
||||||
|
placeholder="Job title"
|
||||||
|
/>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<input v-model="editForm.company" class="ja-input ja-input-sm" placeholder="Company" />
|
<input
|
||||||
|
v-model="editForm.company"
|
||||||
|
class="ja-input ja-input-sm"
|
||||||
|
placeholder="Company"
|
||||||
|
/>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<select v-model="editForm.status" class="ja-input ja-input-sm ja-select">
|
<select
|
||||||
<option v-for="s in STATUS_OPTIONS" :key="s" :value="s">{{ s }}</option>
|
v-model="editForm.status"
|
||||||
|
class="ja-input ja-input-sm ja-select"
|
||||||
|
>
|
||||||
|
<option v-for="s in STATUS_OPTIONS" :key="s" :value="s">
|
||||||
|
{{ s }}
|
||||||
|
</option>
|
||||||
</select>
|
</select>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<input v-model="editForm.location" class="ja-input ja-input-sm" placeholder="Location" />
|
<input
|
||||||
|
v-model="editForm.location"
|
||||||
|
class="ja-input ja-input-sm"
|
||||||
|
placeholder="Location"
|
||||||
|
/>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<input v-model="editForm.appliedAt" class="ja-input ja-input-sm" type="date" />
|
<input
|
||||||
|
v-model="editForm.appliedAt"
|
||||||
|
class="ja-input ja-input-sm"
|
||||||
|
type="date"
|
||||||
|
/>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<input v-model="editForm.notes" class="ja-input ja-input-sm" placeholder="Notes" />
|
<input
|
||||||
|
v-model="editForm.notes"
|
||||||
|
class="ja-input ja-input-sm"
|
||||||
|
placeholder="Notes"
|
||||||
|
/>
|
||||||
</td>
|
</td>
|
||||||
<td class="ja-actions">
|
<td class="ja-actions">
|
||||||
<button class="ja-btn ja-btn-sm ja-btn-primary" @click="saveEdit(app.id)">Save</button>
|
<button
|
||||||
<button class="ja-btn ja-btn-sm" @click="cancelEdit">Cancel</button>
|
class="ja-btn ja-btn-sm ja-btn-primary"
|
||||||
|
@click="saveEdit(app.id)"
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
<button class="ja-btn ja-btn-sm" @click="cancelEdit">
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</template>
|
</template>
|
||||||
@@ -453,7 +617,9 @@ onMounted(() => {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
transition: background 0.15s, color 0.15s;
|
transition:
|
||||||
|
background 0.15s,
|
||||||
|
color 0.15s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ja-btn:hover {
|
.ja-btn:hover {
|
||||||
@@ -543,12 +709,30 @@ onMounted(() => {
|
|||||||
color: #333;
|
color: #333;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-applied { background: #dbeafe; color: #1e40af; }
|
.status-applied {
|
||||||
.status-screening { background: #fef9c3; color: #854d0e; }
|
background: #dbeafe;
|
||||||
.status-interview { background: #ede9fe; color: #5b21b6; }
|
color: #1e40af;
|
||||||
.status-offer { background: #dcfce7; color: #166534; }
|
}
|
||||||
.status-rejected { background: #fee2e2; color: #991b1b; }
|
.status-screening {
|
||||||
.status-withdrawn { background: #f3f4f6; color: #6b7280; }
|
background: #fef9c3;
|
||||||
|
color: #854d0e;
|
||||||
|
}
|
||||||
|
.status-interview {
|
||||||
|
background: #ede9fe;
|
||||||
|
color: #5b21b6;
|
||||||
|
}
|
||||||
|
.status-offer {
|
||||||
|
background: #dcfce7;
|
||||||
|
color: #166534;
|
||||||
|
}
|
||||||
|
.status-rejected {
|
||||||
|
background: #fee2e2;
|
||||||
|
color: #991b1b;
|
||||||
|
}
|
||||||
|
.status-withdrawn {
|
||||||
|
background: #f3f4f6;
|
||||||
|
color: #6b7280;
|
||||||
|
}
|
||||||
|
|
||||||
.ja-empty {
|
.ja-empty {
|
||||||
color: #888;
|
color: #888;
|
||||||
|
|||||||
@@ -14,7 +14,13 @@ 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: type.value,
|
||||||
|
name: name.value,
|
||||||
|
link: link.value || undefined,
|
||||||
|
},
|
||||||
|
},
|
||||||
);
|
);
|
||||||
type.value = "";
|
type.value = "";
|
||||||
name.value = "";
|
name.value = "";
|
||||||
|
|||||||
@@ -13,7 +13,9 @@ 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 = "";
|
category.value = "";
|
||||||
name.value = "";
|
name.value = "";
|
||||||
@@ -29,8 +31,18 @@ async function submit() {
|
|||||||
<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"
|
||||||
|
v-model="name"
|
||||||
|
placeholder="Name"
|
||||||
|
@keyup.enter="submit"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
v-model="link"
|
||||||
|
placeholder="Link"
|
||||||
|
@keyup.enter="submit"
|
||||||
|
/>
|
||||||
<Button @click="submit">Upload</Button>
|
<Button @click="submit">Upload</Button>
|
||||||
<Button @click="emit('cancel')">Cancel</Button>
|
<Button @click="emit('cancel')">Cancel</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -14,7 +14,13 @@ 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: type.value,
|
||||||
|
name: name.value,
|
||||||
|
link: link.value || undefined,
|
||||||
|
},
|
||||||
|
},
|
||||||
);
|
);
|
||||||
type.value = "";
|
type.value = "";
|
||||||
name.value = "";
|
name.value = "";
|
||||||
|
|||||||
@@ -27,12 +27,13 @@ async function post() {
|
|||||||
<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>
|
/>
|
||||||
|
<textarea class="h-50" v-model="content" placeholder="Content"></textarea>
|
||||||
<Button @click="post">Upload</Button>
|
<Button @click="post">Upload</Button>
|
||||||
<Button @click="emit('cancel')">Cancel</Button>
|
<Button @click="emit('cancel')">Cancel</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -15,7 +15,10 @@ function onFileChange(e) {
|
|||||||
|
|
||||||
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) => {
|
||||||
@@ -26,14 +29,17 @@ async function submit() {
|
|||||||
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(
|
||||||
|
2,
|
||||||
|
"0",
|
||||||
|
);
|
||||||
results.value[i].status = `${res.data.Distance}m in ${mins}:${secs}`;
|
results.value[i].status = `${res.data.Distance}m in ${mins}:${secs}`;
|
||||||
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;
|
||||||
}
|
}
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
images.value = [];
|
images.value = [];
|
||||||
@@ -44,12 +50,19 @@ async function submit() {
|
|||||||
<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
|
||||||
|
type="file"
|
||||||
|
accept="image/jpeg,image/png,image/gif,image/webp"
|
||||||
|
multiple
|
||||||
|
@change="onFileChange"
|
||||||
|
/>
|
||||||
<Button @click="submit">Upload</Button>
|
<Button @click="submit">Upload</Button>
|
||||||
<Button @click="emit('cancel')">Cancel</Button>
|
<Button @click="emit('cancel')">Cancel</Button>
|
||||||
<div v-for="r in results" :key="r.name">
|
<div v-for="r in results" :key="r.name">
|
||||||
<span class="text-primary">{{ r.name }}: </span>
|
<span class="text-primary">{{ r.name }}: </span>
|
||||||
<span :class="r.ok ? 'text-secondary' : 'text-red-500'">{{ r.status }}</span>
|
<span :class="r.ok ? 'text-secondary' : 'text-red-500'">{{
|
||||||
|
r.status
|
||||||
|
}}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -32,8 +32,18 @@ async function handleCreate() {
|
|||||||
<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"
|
||||||
|
v-model="username"
|
||||||
|
placeholder="Username"
|
||||||
|
@keyup.enter="handleCreate"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
v-model="password"
|
||||||
|
placeholder="Password"
|
||||||
|
@keyup.enter="handleCreate"
|
||||||
|
/>
|
||||||
<Button @click="handleCreate">Create Account</Button>
|
<Button @click="handleCreate">Create Account</Button>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="auth.loggedIn" class="flex flex-col">
|
<div v-else-if="auth.loggedIn" class="flex flex-col">
|
||||||
|
|||||||
@@ -33,8 +33,18 @@ function handleLogout() {
|
|||||||
</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"
|
||||||
|
v-model="username"
|
||||||
|
placeholder="Username"
|
||||||
|
@keyup.enter="handleLogin"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
v-model="password"
|
||||||
|
placeholder="Password"
|
||||||
|
@keyup.enter="handleLogin"
|
||||||
|
/>
|
||||||
<Button @click="handleLogin">Log In</Button>
|
<Button @click="handleLogin">Log In</Button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -25,7 +25,10 @@ function onFileChange(e) {
|
|||||||
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) => {
|
||||||
@@ -41,7 +44,7 @@ async function upload() {
|
|||||||
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 = [];
|
||||||
@@ -61,7 +64,9 @@ async function deleteSong(name) {
|
|||||||
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(
|
||||||
|
`/api/radio/songs/${encodeURIComponent(song.name)}/${action}`,
|
||||||
|
);
|
||||||
await fetchSongs();
|
await fetchSongs();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
@@ -80,11 +85,18 @@ onMounted(fetchSongs);
|
|||||||
<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
|
||||||
|
type="file"
|
||||||
|
accept=".mp3,.ogg,.flac,.wav,.m4a,.opus"
|
||||||
|
multiple
|
||||||
|
@change="onFileChange"
|
||||||
|
/>
|
||||||
<Button @click="upload" :disabled="loading">Upload</Button>
|
<Button @click="upload" :disabled="loading">Upload</Button>
|
||||||
<div v-for="r in results" :key="r.name">
|
<div v-for="r in results" :key="r.name">
|
||||||
<span class="text-primary">{{ r.name }}: </span>
|
<span class="text-primary">{{ r.name }}: </span>
|
||||||
<span :class="r.ok ? 'text-secondary' : 'text-red-500'">{{ r.status }}</span>
|
<span :class="r.ok ? 'text-secondary' : 'text-red-500'">{{
|
||||||
|
r.status
|
||||||
|
}}</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-for="song in songs"
|
v-for="song in songs"
|
||||||
@@ -95,7 +107,9 @@ onMounted(fetchSongs);
|
|||||||
<span :class="{ 'line-through': song.disabled }">{{ song.name }}</span>
|
<span :class="{ 'line-through': song.disabled }">{{ song.name }}</span>
|
||||||
<span class="text-secondary text-sm">{{ formatSize(song.size) }}</span>
|
<span class="text-secondary text-sm">{{ formatSize(song.size) }}</span>
|
||||||
<span v-if="song.disabled" class="text-red-400 text-xs">disabled</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="toggleSong(song)">{{
|
||||||
|
song.disabled ? "Enable" : "Disable"
|
||||||
|
}}</Button>
|
||||||
<Button @click="deleteSong(song.name)">Delete</Button>
|
<Button @click="deleteSong(song.name)">Delete</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -31,13 +31,14 @@ onMounted(fetchUsers);
|
|||||||
<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
|
||||||
|
v-for="user in users"
|
||||||
|
:key="user.id"
|
||||||
|
class="flex flex-row items-center gap-2"
|
||||||
|
>
|
||||||
<span>{{ user.username }}</span>
|
<span>{{ user.username }}</span>
|
||||||
<span v-if="user.admin">(admin)</span>
|
<span v-if="user.admin">(admin)</span>
|
||||||
<Button
|
<Button v-if="user.id !== auth.user.id" @click="toggleAdmin(user)">
|
||||||
v-if="user.id !== auth.user.id"
|
|
||||||
@click="toggleAdmin(user)"
|
|
||||||
>
|
|
||||||
{{ user.admin ? "Demote" : "Promote" }}
|
{{ user.admin ? "Demote" : "Promote" }}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -12,31 +12,35 @@ 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();
|
||||||
@@ -27,12 +29,21 @@ const groupedBookmarks = computed(() => {
|
|||||||
<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
|
||||||
|
v-if="authStore.user.admin"
|
||||||
|
class="text-sm px-1"
|
||||||
|
@click="showCreate = !showCreate"
|
||||||
|
>
|
||||||
{{ showCreate ? "x" : "+" }}
|
{{ showCreate ? "x" : "+" }}
|
||||||
</button>
|
</button>
|
||||||
</span>
|
</span>
|
||||||
</Header>
|
</Header>
|
||||||
<CreateBookmark v-if="showCreate" class="flex-1 min-h-0 p-1" @done="showCreate = false" @cancel="showCreate = false" />
|
<CreateBookmark
|
||||||
|
v-if="showCreate"
|
||||||
|
class="flex-1 min-h-0 p-1"
|
||||||
|
@done="showCreate = false"
|
||||||
|
@cancel="showCreate = false"
|
||||||
|
/>
|
||||||
<div v-if="!showCreate" class="bookmarks-scroll">
|
<div v-if="!showCreate" class="bookmarks-scroll">
|
||||||
<LinkTable
|
<LinkTable
|
||||||
v-for="group in groupedBookmarks"
|
v-for="group in groupedBookmarks"
|
||||||
|
|||||||
@@ -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();
|
||||||
@@ -19,14 +21,27 @@ const showCreate = ref(false);
|
|||||||
<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
|
||||||
|
v-if="authStore.user.admin"
|
||||||
|
class="text-sm px-1"
|
||||||
|
@click="showCreate = !showCreate"
|
||||||
|
>
|
||||||
{{ showCreate ? "x" : "+" }}
|
{{ showCreate ? "x" : "+" }}
|
||||||
</button>
|
</button>
|
||||||
</span>
|
</span>
|
||||||
</Header>
|
</Header>
|
||||||
<CreateActivity v-if="showCreate" class="flex-1 w-full p-1" @done="showCreate = false" @cancel="showCreate = false" />
|
<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">
|
<AutoScroll v-if="!showCreate" class="flex-1 w-full">
|
||||||
<LinkTable variant="table" class="w-full" :items="activityStore.activity" />
|
<LinkTable
|
||||||
|
variant="table"
|
||||||
|
class="w-full"
|
||||||
|
:items="activityStore.activity"
|
||||||
|
/>
|
||||||
</AutoScroll>
|
</AutoScroll>
|
||||||
</div>
|
</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();
|
||||||
@@ -19,12 +21,21 @@ const showCreate = ref(false);
|
|||||||
<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
|
||||||
|
v-if="authStore.user.admin"
|
||||||
|
class="text-sm px-1"
|
||||||
|
@click="showCreate = !showCreate"
|
||||||
|
>
|
||||||
{{ showCreate ? "x" : "+" }}
|
{{ showCreate ? "x" : "+" }}
|
||||||
</button>
|
</button>
|
||||||
</span>
|
</span>
|
||||||
</Header>
|
</Header>
|
||||||
<CreateFavorite v-if="showCreate" class="w-full flex-1 p-1" @done="showCreate = false" @cancel="showCreate = false" />
|
<CreateFavorite
|
||||||
|
v-if="showCreate"
|
||||||
|
class="w-full flex-1 p-1"
|
||||||
|
@done="showCreate = false"
|
||||||
|
@cancel="showCreate = false"
|
||||||
|
/>
|
||||||
<AutoScroll v-if="!showCreate" class="w-full flex-1">
|
<AutoScroll v-if="!showCreate" class="w-full flex-1">
|
||||||
<LinkTable
|
<LinkTable
|
||||||
variant="table"
|
variant="table"
|
||||||
|
|||||||
@@ -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();
|
||||||
@@ -45,38 +47,33 @@ function deletePost() {
|
|||||||
<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
|
||||||
|
v-if="authStore.user.admin"
|
||||||
|
class="text-sm px-1"
|
||||||
|
@click="showCreate = !showCreate"
|
||||||
|
>
|
||||||
{{ showCreate ? "x" : "+" }}
|
{{ showCreate ? "x" : "+" }}
|
||||||
</button>
|
</button>
|
||||||
</span>
|
</span>
|
||||||
</Header>
|
</Header>
|
||||||
<CreatePost v-if="showCreate" class="flex-1 min-h-0 p-1" @done="showCreate = false" @cancel="showCreate = false" />
|
<CreatePost
|
||||||
|
v-if="showCreate"
|
||||||
|
class="flex-1 min-h-0 p-1"
|
||||||
|
@done="showCreate = false"
|
||||||
|
@cancel="showCreate = false"
|
||||||
|
/>
|
||||||
<div
|
<div
|
||||||
v-if="!showCreate"
|
v-if="!showCreate"
|
||||||
class="flex flex-col flex-1 min-h-0 p-1 overflow-auto text-left items-start justify-start"
|
class="flex flex-col flex-1 min-h-0 p-1 overflow-auto text-left items-start justify-start"
|
||||||
>
|
>
|
||||||
<small
|
<small>Created at: {{ new Date(post.createdAt).toLocaleString() }}</small>
|
||||||
>Created at:
|
|
||||||
{{ new Date(post.createdAt).toLocaleString() }}</small
|
|
||||||
>
|
|
||||||
<small>By: {{ post.author.username }}</small>
|
<small>By: {{ post.author.username }}</small>
|
||||||
<Markdown
|
<Markdown class="flex-1 border-box text-wrap" :source="post.content" />
|
||||||
class="flex-1 border-box text-wrap"
|
|
||||||
:source="post.content"
|
|
||||||
/>
|
|
||||||
<div class="flex flex-row w-full">
|
<div class="flex flex-row w-full">
|
||||||
<Button
|
<Button class="flex-1 border-box" v-if="!leftCap" @click="prevPost">
|
||||||
class="flex-1 border-box"
|
|
||||||
v-if="!leftCap"
|
|
||||||
@click="prevPost"
|
|
||||||
>
|
|
||||||
Prev
|
Prev
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button class="flex-1 border-box" v-if="!rightCap" @click="nextPost">
|
||||||
class="flex-1 border-box"
|
|
||||||
v-if="!rightCap"
|
|
||||||
@click="nextPost"
|
|
||||||
>
|
|
||||||
Next
|
Next
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -44,9 +44,7 @@ import Bookmarks from "./Bookmarks.vue";
|
|||||||
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" /> -->
|
<!-- <Intro class="intro" /> -->
|
||||||
<Intro2 class="intro grid-cell" />
|
<Intro2 class="intro grid-cell" />
|
||||||
<!-- <BadApple class="intro" /> -->
|
<!-- <BadApple class="intro" /> -->
|
||||||
@@ -63,9 +61,7 @@ import Bookmarks from "./Bookmarks.vue";
|
|||||||
<div class="sidebar">
|
<div class="sidebar">
|
||||||
<Steam class="steam-sidebar sidebar-cell" />
|
<Steam class="steam-sidebar sidebar-cell" />
|
||||||
<Bookmarks class="bookmarks-sidebar sidebar-cell" />
|
<Bookmarks class="bookmarks-sidebar sidebar-cell" />
|
||||||
<Chat
|
<Chat class="chat-sidebar flex-1 min-h-0 chat-home sidebar-cell" />
|
||||||
class="chat-sidebar flex-1 min-h-0 chat-home sidebar-cell"
|
|
||||||
/>
|
|
||||||
<Miku
|
<Miku
|
||||||
class="sidebar-image miku-image box-border border-tertiary border-2 bg-bg_primary"
|
class="sidebar-image miku-image box-border border-tertiary border-2 bg-bg_primary"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -4,7 +4,9 @@ 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
|
||||||
|
class="flex-1 border-box flex flex-col p-1 text-left items-start justify-start"
|
||||||
|
>
|
||||||
<Header>Yo</Header>
|
<Header>Yo</Header>
|
||||||
<!-- <Header>Intro</Header> -->
|
<!-- <Header>Intro</Header> -->
|
||||||
<!-- <Paragraph> -->
|
<!-- <Paragraph> -->
|
||||||
|
|||||||
@@ -112,10 +112,7 @@ onUnmounted(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div ref="container" class="w-full h-full relative overflow-hidden">
|
||||||
ref="container"
|
|
||||||
class="w-full h-full relative overflow-hidden"
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
v-for="(item, i) in items"
|
v-for="(item, i) in items"
|
||||||
:key="i"
|
:key="i"
|
||||||
|
|||||||
@@ -37,10 +37,11 @@ onUnmounted(() => {
|
|||||||
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"><strong>Song:</strong> {{ song.track.name }}</p>
|
||||||
<p class="text-center">
|
<p class="text-center">
|
||||||
<strong>Artist:</strong> {{ song.track.artists[0].name }}
|
<strong>Artist:</strong> {{ song.track.artists[0].name }}
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -76,15 +76,20 @@ onUnmounted(() => {
|
|||||||
<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" />
|
|
||||||
</Link>
|
|
||||||
<Link bare href="https://jacobbarron.xyz">
|
|
||||||
<img
|
<img
|
||||||
src="https://jacobbarron.xyz/Banneh.gif"
|
src="https://www.adam-french.co.uk/img/stamps/mine.gif"
|
||||||
alt="jacobbarron.xyz"
|
alt="adam-french.co.uk"
|
||||||
/>
|
/>
|
||||||
</Link>
|
</Link>
|
||||||
<img v-for="src in srcs" :src="src" :alt="src.split('/').pop().split('.')[0]" loading="lazy" />
|
<Link bare href="https://jacobbarron.xyz">
|
||||||
|
<img src="https://jacobbarron.xyz/Banneh.gif" alt="jacobbarron.xyz" />
|
||||||
|
</Link>
|
||||||
|
<img
|
||||||
|
v-for="src in srcs"
|
||||||
|
:src="src"
|
||||||
|
:alt="src.split('/').pop().split('.')[0]"
|
||||||
|
loading="lazy"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Touchscreen>
|
</Touchscreen>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -17,9 +17,7 @@ const videoSources = [
|
|||||||
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>
|
</p>
|
||||||
<div>
|
<div>
|
||||||
<VideoTable :sourceArr="videoSources" />
|
<VideoTable :sourceArr="videoSources" />
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -17,22 +17,21 @@ const links = [
|
|||||||
<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>
|
||||||
|
|
||||||
|
|||||||
@@ -28,33 +28,31 @@
|
|||||||
<h2>BAE graduate digital intelligence software engineer</h2>
|
<h2>BAE graduate digital intelligence software engineer</h2>
|
||||||
<p>
|
<p>
|
||||||
I am writing to express my interest in your software engineering
|
I am writing to express my interest in your software engineering
|
||||||
position. BAE Systems has hosted multiple stools at the
|
position. BAE Systems has hosted multiple stools at the University of
|
||||||
University of Leeds and have always exhibited their development
|
Leeds and have always exhibited their development of leading-edge
|
||||||
of leading-edge software and technology. This is where the
|
software and technology. This is where the origin of my interest in BAE
|
||||||
origin of my interest in BAE systems emerged and I'm hopeful
|
systems emerged and I'm hopeful that this interest shall continue.
|
||||||
that this interest shall continue.
|
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
I'm confidient im a strong fit for this role. My technical
|
I'm confidient im a strong fit for this role. My technical background
|
||||||
background includes extensive experience with frontend
|
includes extensive experience with frontend frameworks such as React. My
|
||||||
frameworks such as React. My devotion however lies more in
|
devotion however lies more in backend development as has more potential
|
||||||
backend development as has more potential to graple problems
|
to graple problems related to optimisation and designing coherent
|
||||||
related to optimisation and designing coherent interfaces.
|
interfaces.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<em> The C# Programming Yellow Book </em> was my first
|
<em> The C# Programming Yellow Book </em> was my first introduction to
|
||||||
introduction to C# during A-Level, Java was our vessel for
|
C# during A-Level, Java was our vessel for teaching object-orientated
|
||||||
teaching object-orientated programming at university. I am
|
programming at university. I am confident I have the relevant experience
|
||||||
confident I have the relevant experience to grasp the languages
|
to grasp the languages stated for the role I am applying for.
|
||||||
stated for the role I am applying for.
|
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
My academic background in Computer Science and Mathematics has
|
My academic background in Computer Science and Mathematics has honed my
|
||||||
honed my ability to translate abstract concepts into structured,
|
ability to translate abstract concepts into structured, logical
|
||||||
logical solutions. Just as I have transformed theoretical
|
solutions. Just as I have transformed theoretical hypotheses into formal
|
||||||
hypotheses into formal proofs, I aim to transform business
|
proofs, I aim to transform business requirements into robust,
|
||||||
requirements into robust, maintainable software systems through
|
maintainable software systems through collaboration and rigorous
|
||||||
collaboration and rigorous reasoning.
|
reasoning.
|
||||||
</p>
|
</p>
|
||||||
<p>Thank you for reading - Adam F</p>
|
<p>Thank you for reading - Adam F</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user