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
|
||||
|
||||
volumes:
|
||||
# Postgres database
|
||||
dbdata:
|
||||
# File upload
|
||||
uploads:
|
||||
# Vue build
|
||||
vue_dist:
|
||||
|
||||
# Searxng data
|
||||
searxng_data:
|
||||
|
||||
services:
|
||||
|
||||
@@ -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_dev.conf.template \
|
||||
>/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.template \
|
||||
>/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_setup.conf.template >/etc/nginx/nginx.conf
|
||||
fi
|
||||
|
||||
|
||||
@@ -1,31 +1,31 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="description" content="Adam French's personal website">
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="description" content="Adam French's personal website" />
|
||||
<title>AF</title>
|
||||
<link rel="preconnect" href="https://i.scdn.co" crossorigin>
|
||||
<link rel="preconnect" href="https://i.scdn.co" crossorigin />
|
||||
<link
|
||||
rel="preconnect"
|
||||
href="https://cdn.akamai.steamstatic.com"
|
||||
crossorigin
|
||||
>
|
||||
<link rel="icon" type="/img/x-icon" href="/img/favicon.ico">
|
||||
/>
|
||||
<link rel="icon" type="/img/x-icon" href="/img/favicon.ico" />
|
||||
<link
|
||||
rel="preload"
|
||||
href="/fonts/big_noodle_titling.woff2"
|
||||
as="font"
|
||||
type="font/woff2"
|
||||
crossorigin
|
||||
>
|
||||
/>
|
||||
<link
|
||||
rel="preload"
|
||||
href="/fonts/CreatoDisplay-Bold.woff2"
|
||||
as="font"
|
||||
type="font/woff2"
|
||||
crossorigin
|
||||
>
|
||||
/>
|
||||
</head>
|
||||
<body id="app">
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
|
||||
@@ -19,7 +19,13 @@ const computedRel = computed(() => {
|
||||
<RouterLink v-if="to" :to="to" class="inline-link">
|
||||
<slot />
|
||||
</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 />
|
||||
</a>
|
||||
</template>
|
||||
|
||||
@@ -20,7 +20,13 @@ const computedRel = computed(() => {
|
||||
<RouterLink v-if="to" :to="to" :class="{ link: !bare }">
|
||||
<slot />
|
||||
</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 />
|
||||
</a>
|
||||
</template>
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
<template>
|
||||
<div ref="container" @mouseenter="onMouseEnter" @mouseleave="onMouseLeave" class="overflow-y-auto">
|
||||
<div
|
||||
ref="container"
|
||||
@mouseenter="onMouseEnter"
|
||||
@mouseleave="onMouseLeave"
|
||||
class="overflow-y-auto"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -20,8 +20,7 @@ let resizeObserver = null;
|
||||
|
||||
function scrollToBottom() {
|
||||
if (messagesContainer.value) {
|
||||
messagesContainer.value.scrollTop =
|
||||
messagesContainer.value.scrollHeight;
|
||||
messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,9 +145,7 @@ onUnmounted(() => {
|
||||
>
|
||||
<span class="text-tertiary">{{ message.authorId }}:</span>
|
||||
<template
|
||||
v-for="(part, i) in parseMessageParts(
|
||||
message.text || '',
|
||||
)"
|
||||
v-for="(part, i) in parseMessageParts(message.text || '')"
|
||||
:key="i"
|
||||
>
|
||||
<Link
|
||||
@@ -161,9 +158,7 @@ onUnmounted(() => {
|
||||
>
|
||||
<span v-else>{{ part.value }}</span>
|
||||
</template>
|
||||
<template
|
||||
v-if="message.fileUrl && isSafeFileUrl(message.fileUrl)"
|
||||
>
|
||||
<template v-if="message.fileUrl && isSafeFileUrl(message.fileUrl)">
|
||||
<img
|
||||
v-if="isImageUrl(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"
|
||||
>
|
||||
<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">
|
||||
<h3>repo: {{ feed.repoName }}</h3>
|
||||
</Link>
|
||||
|
||||
@@ -50,11 +50,7 @@ const show = ref(false);
|
||||
</div>
|
||||
<template v-else>
|
||||
<template v-if="variant === 'list'">
|
||||
<Link
|
||||
v-for="(item, i) in items"
|
||||
:key="i"
|
||||
:href="item.link"
|
||||
>
|
||||
<Link v-for="(item, i) in items" :key="i" :href="item.link">
|
||||
<p class="bdr-2 bg-bg_tertiary">{{ item.name }}</p>
|
||||
</Link>
|
||||
</template>
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
<script setup>
|
||||
import Button from "@/components/input/Button.vue";
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<audio />
|
||||
<div class="musicPlayerGrid">
|
||||
<div class="album_cover">
|
||||
<img src="/img/Untitled.png" alt=""></img>
|
||||
<img src="/img/Untitled.png" alt="" />
|
||||
</div>
|
||||
<div class="controls">
|
||||
<div class="sliders">
|
||||
|
||||
@@ -2,6 +2,7 @@ import axios from "axios";
|
||||
|
||||
export async function gql(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;
|
||||
}
|
||||
|
||||
@@ -17,27 +17,55 @@ import { RouterView } from "vue-router";
|
||||
</style>
|
||||
|
||||
<style>
|
||||
.cv-layout h1, .cv-layout h2, .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 {
|
||||
.cv-layout h1,
|
||||
.cv-layout h2,
|
||||
.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;
|
||||
}
|
||||
.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 {
|
||||
color: #111;
|
||||
background-color: transparent !important;
|
||||
letter-spacing: normal;
|
||||
}
|
||||
.cv-layout input, .cv-layout textarea {
|
||||
.cv-layout input,
|
||||
.cv-layout textarea {
|
||||
color: #111;
|
||||
background-color: white;
|
||||
border: 1px solid #ccc;
|
||||
padding: 0;
|
||||
width: auto;
|
||||
}
|
||||
.cv-layout input::placeholder, .cv-layout textarea::placeholder { color: #999; opacity: 1; }
|
||||
.cv-layout table { border: 0 solid transparent; }
|
||||
.cv-layout tr { border-color: transparent; }
|
||||
.cv-layout th { border: none; padding: 0; }
|
||||
.cv-layout td { padding: 0; }
|
||||
.cv-layout input::placeholder,
|
||||
.cv-layout textarea::placeholder {
|
||||
color: #999;
|
||||
opacity: 1;
|
||||
}
|
||||
.cv-layout table {
|
||||
border: 0 solid transparent;
|
||||
}
|
||||
.cv-layout tr {
|
||||
border-color: transparent;
|
||||
}
|
||||
.cv-layout th {
|
||||
border: none;
|
||||
padding: 0;
|
||||
}
|
||||
.cv-layout td {
|
||||
padding: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -91,12 +91,19 @@ router.beforeEach(async (to) => {
|
||||
const homeData = useHomeDataStore();
|
||||
if (!homeData.loaded) {
|
||||
await new Promise((resolve) => {
|
||||
const stop = watch(() => homeData.loaded, (val) => {
|
||||
if (val) { stop(); resolve(); }
|
||||
});
|
||||
const stop = watch(
|
||||
() => 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;
|
||||
|
||||
@@ -23,7 +23,14 @@ const editRefForm = ref({});
|
||||
const REF_CATEGORIES = ["profile", "experience"];
|
||||
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`;
|
||||
|
||||
@@ -46,7 +53,9 @@ async function createApplication() {
|
||||
location: form.value.location || undefined,
|
||||
url: form.value.url || 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(
|
||||
`mutation CreateJobApplication($input: CreateJobApplicationInput!) {
|
||||
@@ -55,7 +64,15 @@ async function createApplication() {
|
||||
{ input },
|
||||
);
|
||||
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) {
|
||||
console.error(err);
|
||||
}
|
||||
@@ -88,7 +105,9 @@ async function saveEdit(id) {
|
||||
location: editForm.value.location || undefined,
|
||||
url: editForm.value.url || 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(
|
||||
`mutation UpdateJobApplication($id: ID!, $input: UpdateJobApplicationInput!) {
|
||||
@@ -117,7 +136,16 @@ async function deleteApplication(id) {
|
||||
}
|
||||
|
||||
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) => [
|
||||
a.jobTitle,
|
||||
a.company,
|
||||
@@ -171,7 +199,11 @@ async function createReference() {
|
||||
|
||||
function startRefEdit(ref) {
|
||||
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() {
|
||||
@@ -245,58 +277,140 @@ onMounted(() => {
|
||||
<RouterLink to="/cv" class="ja-back">← CV</RouterLink>
|
||||
<h2 class="ja-heading">Job Applications</h2>
|
||||
</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 class="ja-ref-section">
|
||||
<h3 class="ja-ref-heading">Quick Reference</h3>
|
||||
<div v-for="cat in REF_CATEGORIES" :key="cat" class="ja-ref-category">
|
||||
<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">
|
||||
<span class="ja-ref-label">{{ ref.label }}</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 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>
|
||||
<button
|
||||
class="ja-btn ja-btn-sm"
|
||||
@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 v-else>
|
||||
<select 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
|
||||
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>
|
||||
<input v-model="editRefForm.label" class="ja-input ja-input-sm" 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>
|
||||
<input
|
||||
v-model="editRefForm.label"
|
||||
class="ja-input ja-input-sm"
|
||||
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>
|
||||
</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>
|
||||
<form class="ja-ref-form" @submit.prevent="createReference">
|
||||
<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>
|
||||
<input v-model="refForm.label" class="ja-input" placeholder="Label *" required />
|
||||
<input v-model="refForm.value" class="ja-input" placeholder="Value *" required />
|
||||
<input
|
||||
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>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<form class="ja-form" @submit.prevent="createApplication">
|
||||
<div class="ja-form-row">
|
||||
<input v-model="form.jobTitle" class="ja-input" placeholder="Job title *" required />
|
||||
<input v-model="form.company" class="ja-input" placeholder="Company *" required />
|
||||
<input
|
||||
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">
|
||||
<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>
|
||||
</div>
|
||||
<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.appliedAt" class="ja-input" type="date" title="Applied date" />
|
||||
<input
|
||||
v-model="form.appliedAt"
|
||||
class="ja-input"
|
||||
type="date"
|
||||
title="Applied date"
|
||||
/>
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
</form>
|
||||
@@ -317,43 +431,93 @@ onMounted(() => {
|
||||
<template v-for="app in applications" :key="app.id">
|
||||
<tr v-if="editingId !== app.id">
|
||||
<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>
|
||||
</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.appliedAt ? app.appliedAt.substring(0, 10) : "—" }}</td>
|
||||
<td class="ja-notes-cell">{{ app.notes ?? "" }}</td>
|
||||
<td class="ja-actions">
|
||||
<button class="ja-btn ja-btn-sm" @click="startEdit(app)">Edit</button>
|
||||
<button class="ja-btn ja-btn-sm ja-btn-danger" @click="deleteApplication(app.id)">Delete</button>
|
||||
<button class="ja-btn ja-btn-sm" @click="startEdit(app)">
|
||||
Edit
|
||||
</button>
|
||||
<button
|
||||
class="ja-btn ja-btn-sm ja-btn-danger"
|
||||
@click="deleteApplication(app.id)"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-else class="ja-edit-row">
|
||||
<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>
|
||||
<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>
|
||||
<select 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
|
||||
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>
|
||||
</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>
|
||||
<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>
|
||||
<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 class="ja-actions">
|
||||
<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>
|
||||
<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>
|
||||
</tr>
|
||||
</template>
|
||||
@@ -453,7 +617,9 @@ onMounted(() => {
|
||||
cursor: pointer;
|
||||
font-size: 0.85rem;
|
||||
white-space: nowrap;
|
||||
transition: background 0.15s, color 0.15s;
|
||||
transition:
|
||||
background 0.15s,
|
||||
color 0.15s;
|
||||
}
|
||||
|
||||
.ja-btn:hover {
|
||||
@@ -543,12 +709,30 @@ onMounted(() => {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.status-applied { background: #dbeafe; color: #1e40af; }
|
||||
.status-screening { 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; }
|
||||
.status-applied {
|
||||
background: #dbeafe;
|
||||
color: #1e40af;
|
||||
}
|
||||
.status-screening {
|
||||
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 {
|
||||
color: #888;
|
||||
|
||||
@@ -14,7 +14,13 @@ async function post() {
|
||||
try {
|
||||
const data = await gql(
|
||||
`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 = "";
|
||||
name.value = "";
|
||||
|
||||
@@ -13,7 +13,9 @@ async function submit() {
|
||||
try {
|
||||
await gql(
|
||||
`mutation CreateBookmark($input: CreateBookmarkInput!) { createBookmark(input: $input) { id } }`,
|
||||
{ input: { category: category.value, name: name.value, link: link.value } },
|
||||
{
|
||||
input: { category: category.value, name: name.value, link: link.value },
|
||||
},
|
||||
);
|
||||
category.value = "";
|
||||
name.value = "";
|
||||
@@ -29,8 +31,18 @@ async function submit() {
|
||||
<div class="flex flex-col">
|
||||
<h1>Create Bookmark</h1>
|
||||
<input type="text" v-model="category" placeholder="Category" />
|
||||
<input type="text" v-model="name" placeholder="Name" @keyup.enter="submit" />
|
||||
<input type="text" v-model="link" placeholder="Link" @keyup.enter="submit" />
|
||||
<input
|
||||
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="emit('cancel')">Cancel</Button>
|
||||
</div>
|
||||
|
||||
@@ -14,7 +14,13 @@ async function post() {
|
||||
try {
|
||||
const data = await gql(
|
||||
`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 = "";
|
||||
name.value = "";
|
||||
|
||||
@@ -27,12 +27,13 @@ async function post() {
|
||||
<template>
|
||||
<div class="flex flex-col">
|
||||
<h1>Create Post</h1>
|
||||
<input type="text" v-model="title" placeholder="Title" @keyup.enter="post" />
|
||||
<textarea
|
||||
class="h-50"
|
||||
v-model="content"
|
||||
placeholder="Content"
|
||||
></textarea>
|
||||
<input
|
||||
type="text"
|
||||
v-model="title"
|
||||
placeholder="Title"
|
||||
@keyup.enter="post"
|
||||
/>
|
||||
<textarea class="h-50" v-model="content" placeholder="Content"></textarea>
|
||||
<Button @click="post">Upload</Button>
|
||||
<Button @click="emit('cancel')">Cancel</Button>
|
||||
</div>
|
||||
|
||||
@@ -15,7 +15,10 @@ function onFileChange(e) {
|
||||
|
||||
async function submit() {
|
||||
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(
|
||||
images.value.map(async (file, i) => {
|
||||
@@ -26,14 +29,17 @@ async function submit() {
|
||||
headers: { "Content-Type": "multipart/form-data" },
|
||||
});
|
||||
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].ok = true;
|
||||
} catch (err) {
|
||||
results.value[i].status = err.response?.data?.error || "Upload failed";
|
||||
results.value[i].ok = false;
|
||||
}
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
images.value = [];
|
||||
@@ -44,12 +50,19 @@ async function submit() {
|
||||
<template>
|
||||
<div class="flex flex-col gap-2">
|
||||
<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="emit('cancel')">Cancel</Button>
|
||||
<div v-for="r in results" :key="r.name">
|
||||
<span class="text-primary">{{ r.name }}: </span>
|
||||
<span :class="r.ok ? 'text-secondary' : 'text-red-500'">{{ r.status }}</span>
|
||||
<span :class="r.ok ? 'text-secondary' : 'text-red-500'">{{
|
||||
r.status
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -32,8 +32,18 @@ async function handleCreate() {
|
||||
<h1>Create User</h1>
|
||||
<p v-if="message" class="text-green-500">{{ message }}</p>
|
||||
<p v-if="error" class="text-red-500">{{ error }}</p>
|
||||
<input type="text" v-model="username" placeholder="Username" @keyup.enter="handleCreate" />
|
||||
<input type="password" v-model="password" placeholder="Password" @keyup.enter="handleCreate" />
|
||||
<input
|
||||
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>
|
||||
</div>
|
||||
<div v-else-if="auth.loggedIn" class="flex flex-col">
|
||||
|
||||
@@ -33,8 +33,18 @@ function handleLogout() {
|
||||
</div>
|
||||
<div v-else class="flex flex-col">
|
||||
<h1>Login</h1>
|
||||
<input type="text" v-model="username" placeholder="Username" @keyup.enter="handleLogin" />
|
||||
<input type="password" v-model="password" placeholder="Password" @keyup.enter="handleLogin" />
|
||||
<input
|
||||
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>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -25,7 +25,10 @@ function onFileChange(e) {
|
||||
async function upload() {
|
||||
if (!files.value.length) return;
|
||||
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(
|
||||
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].ok = false;
|
||||
}
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
files.value = [];
|
||||
@@ -61,7 +64,9 @@ async function deleteSong(name) {
|
||||
async function toggleSong(song) {
|
||||
const action = song.disabled ? "enable" : "disable";
|
||||
try {
|
||||
await axios.patch(`/api/radio/songs/${encodeURIComponent(song.name)}/${action}`);
|
||||
await axios.patch(
|
||||
`/api/radio/songs/${encodeURIComponent(song.name)}/${action}`,
|
||||
);
|
||||
await fetchSongs();
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
@@ -80,11 +85,18 @@ onMounted(fetchSongs);
|
||||
<template>
|
||||
<div class="flex flex-col gap-2">
|
||||
<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>
|
||||
<div v-for="r in results" :key="r.name">
|
||||
<span class="text-primary">{{ r.name }}: </span>
|
||||
<span :class="r.ok ? 'text-secondary' : 'text-red-500'">{{ r.status }}</span>
|
||||
<span :class="r.ok ? 'text-secondary' : 'text-red-500'">{{
|
||||
r.status
|
||||
}}</span>
|
||||
</div>
|
||||
<div
|
||||
v-for="song in songs"
|
||||
@@ -95,7 +107,9 @@ onMounted(fetchSongs);
|
||||
<span :class="{ 'line-through': song.disabled }">{{ song.name }}</span>
|
||||
<span class="text-secondary text-sm">{{ formatSize(song.size) }}</span>
|
||||
<span v-if="song.disabled" class="text-red-400 text-xs">disabled</span>
|
||||
<Button @click="toggleSong(song)">{{ song.disabled ? "Enable" : "Disable" }}</Button>
|
||||
<Button @click="toggleSong(song)">{{
|
||||
song.disabled ? "Enable" : "Disable"
|
||||
}}</Button>
|
||||
<Button @click="deleteSong(song.name)">Delete</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -31,13 +31,14 @@ onMounted(fetchUsers);
|
||||
<template>
|
||||
<div class="flex flex-col">
|
||||
<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 v-if="user.admin">(admin)</span>
|
||||
<Button
|
||||
v-if="user.id !== auth.user.id"
|
||||
@click="toggleAdmin(user)"
|
||||
>
|
||||
<Button v-if="user.id !== auth.user.id" @click="toggleAdmin(user)">
|
||||
{{ user.admin ? "Demote" : "Promote" }}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<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 charHeight: number = 14;
|
||||
@@ -12,31 +12,35 @@ let m: number;
|
||||
function setup() {
|
||||
display.value.style.fontSize = `${charHeight}px`;
|
||||
display.value.style.lineHeight = `${charHeight}px`;
|
||||
fillDisplay()
|
||||
fillDisplay();
|
||||
}
|
||||
|
||||
function fillDisplay() {
|
||||
// M rows N columns
|
||||
m = Math.floor(display.value.offsetHeight / charHeight);
|
||||
n = Math.floor(display.value.offsetWidth / charWidth);
|
||||
const row = ' '.repeat(n);
|
||||
displayText.value = (row + '\n').repeat(m - 1) + row
|
||||
const row = " ".repeat(n);
|
||||
displayText.value = (row + "\n").repeat(m - 1) + row;
|
||||
}
|
||||
|
||||
function close() {
|
||||
displayText.value = ""
|
||||
displayText.value = "";
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
setup()
|
||||
})
|
||||
setup();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
close()
|
||||
})
|
||||
close();
|
||||
});
|
||||
</script>
|
||||
|
||||
<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>
|
||||
|
||||
@@ -5,7 +5,9 @@ import Header from "@/components/text/Header.vue";
|
||||
import { useHomeDataStore } from "@/stores/homeData";
|
||||
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 authStore = useAuthStore();
|
||||
@@ -27,12 +29,21 @@ const groupedBookmarks = computed(() => {
|
||||
<Header class="text-left">
|
||||
<span class="flex items-center justify-between w-full">
|
||||
{{ 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" : "+" }}
|
||||
</button>
|
||||
</span>
|
||||
</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">
|
||||
<LinkTable
|
||||
v-for="group in groupedBookmarks"
|
||||
|
||||
@@ -7,7 +7,9 @@ import { ref, defineAsyncComponent } from "vue";
|
||||
import { useActivityStore } from "@/stores/activity";
|
||||
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 authStore = useAuthStore();
|
||||
@@ -19,14 +21,27 @@ const showCreate = ref(false);
|
||||
<Header>
|
||||
<span class="flex items-center justify-between w-full">
|
||||
{{ 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" : "+" }}
|
||||
</button>
|
||||
</span>
|
||||
</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">
|
||||
<LinkTable variant="table" class="w-full" :items="activityStore.activity" />
|
||||
<LinkTable
|
||||
variant="table"
|
||||
class="w-full"
|
||||
:items="activityStore.activity"
|
||||
/>
|
||||
</AutoScroll>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -7,7 +7,9 @@ import { ref, defineAsyncComponent } from "vue";
|
||||
import { useFavoritesStore } from "@/stores/favorites";
|
||||
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 authStore = useAuthStore();
|
||||
@@ -19,12 +21,21 @@ const showCreate = ref(false);
|
||||
<Header>
|
||||
<span class="flex items-center justify-between w-full">
|
||||
{{ 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" : "+" }}
|
||||
</button>
|
||||
</span>
|
||||
</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">
|
||||
<LinkTable
|
||||
variant="table"
|
||||
|
||||
@@ -7,7 +7,9 @@ import { ref, computed, defineAsyncComponent } from "vue";
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
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 postsStore = usePostsStore();
|
||||
@@ -45,38 +47,33 @@ function deletePost() {
|
||||
<Header>
|
||||
<span class="flex items-center justify-between w-full">
|
||||
{{ 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" : "+" }}
|
||||
</button>
|
||||
</span>
|
||||
</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
|
||||
v-if="!showCreate"
|
||||
class="flex flex-col flex-1 min-h-0 p-1 overflow-auto text-left items-start justify-start"
|
||||
>
|
||||
<small
|
||||
>Created at:
|
||||
{{ new Date(post.createdAt).toLocaleString() }}</small
|
||||
>
|
||||
<small>Created at: {{ new Date(post.createdAt).toLocaleString() }}</small>
|
||||
<small>By: {{ post.author.username }}</small>
|
||||
<Markdown
|
||||
class="flex-1 border-box text-wrap"
|
||||
:source="post.content"
|
||||
/>
|
||||
<Markdown class="flex-1 border-box text-wrap" :source="post.content" />
|
||||
<div class="flex flex-row w-full">
|
||||
<Button
|
||||
class="flex-1 border-box"
|
||||
v-if="!leftCap"
|
||||
@click="prevPost"
|
||||
>
|
||||
<Button class="flex-1 border-box" v-if="!leftCap" @click="prevPost">
|
||||
Prev
|
||||
</Button>
|
||||
<Button
|
||||
class="flex-1 border-box"
|
||||
v-if="!rightCap"
|
||||
@click="nextPost"
|
||||
>
|
||||
<Button class="flex-1 border-box" v-if="!rightCap" @click="nextPost">
|
||||
Next
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -5,7 +5,9 @@ import { useHomeDataStore } from "@/stores/homeData";
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
import { storeToRefs } from "pinia";
|
||||
|
||||
const CreateRowing = defineAsyncComponent(() => import("@/views/admin/CreateRowing.vue"));
|
||||
const CreateRowing = defineAsyncComponent(
|
||||
() => import("@/views/admin/CreateRowing.vue"),
|
||||
);
|
||||
|
||||
const store = useHomeDataStore();
|
||||
const authStore = useAuthStore();
|
||||
@@ -117,13 +119,22 @@ function formatValue(key, val) {
|
||||
<Header>
|
||||
<span class="flex items-center justify-between w-full">
|
||||
{{ 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" : "+" }}
|
||||
</button>
|
||||
</span>
|
||||
</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">
|
||||
<p>Loading...</p>
|
||||
</div>
|
||||
|
||||
@@ -44,9 +44,7 @@ import Bookmarks from "./Bookmarks.vue";
|
||||
loading="lazy"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="a4page-portrait homeGrid relative bdr-1 border-quaternary"
|
||||
>
|
||||
<div class="a4page-portrait homeGrid relative bdr-1 border-quaternary">
|
||||
<!-- <Intro class="intro" /> -->
|
||||
<Intro2 class="intro grid-cell" />
|
||||
<!-- <BadApple class="intro" /> -->
|
||||
@@ -63,9 +61,7 @@ import Bookmarks from "./Bookmarks.vue";
|
||||
<div class="sidebar">
|
||||
<Steam class="steam-sidebar sidebar-cell" />
|
||||
<Bookmarks class="bookmarks-sidebar sidebar-cell" />
|
||||
<Chat
|
||||
class="chat-sidebar flex-1 min-h-0 chat-home sidebar-cell"
|
||||
/>
|
||||
<Chat class="chat-sidebar flex-1 min-h-0 chat-home sidebar-cell" />
|
||||
<Miku
|
||||
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>
|
||||
|
||||
<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>Intro</Header> -->
|
||||
<!-- <Paragraph> -->
|
||||
|
||||
@@ -112,10 +112,7 @@ onUnmounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
ref="container"
|
||||
class="w-full h-full relative overflow-hidden"
|
||||
>
|
||||
<div ref="container" class="w-full h-full relative overflow-hidden">
|
||||
<div
|
||||
v-for="(item, i) in items"
|
||||
:key="i"
|
||||
|
||||
@@ -37,10 +37,11 @@ onUnmounted(() => {
|
||||
class="flex flex-col items-center pt-2"
|
||||
>
|
||||
<Header>Listening To</Header>
|
||||
<img :src="song.track.album.images[0].url" :alt="song.track.album.name + ' album art'" />
|
||||
<p class="text-center">
|
||||
<strong>Song:</strong> {{ song.track.name }}
|
||||
</p>
|
||||
<img
|
||||
:src="song.track.album.images[0].url"
|
||||
:alt="song.track.album.name + ' album art'"
|
||||
/>
|
||||
<p class="text-center"><strong>Song:</strong> {{ song.track.name }}</p>
|
||||
<p class="text-center">
|
||||
<strong>Artist:</strong> {{ song.track.artists[0].name }}
|
||||
</p>
|
||||
|
||||
@@ -76,15 +76,20 @@ onUnmounted(() => {
|
||||
<Touchscreen ref="touchscreen">
|
||||
<div class="flex flex-wrap tst">
|
||||
<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
|
||||
src="https://jacobbarron.xyz/Banneh.gif"
|
||||
alt="jacobbarron.xyz"
|
||||
src="https://www.adam-french.co.uk/img/stamps/mine.gif"
|
||||
alt="adam-french.co.uk"
|
||||
/>
|
||||
</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>
|
||||
</Touchscreen>
|
||||
</template>
|
||||
|
||||
@@ -17,9 +17,7 @@ const videoSources = [
|
||||
class="a4page-portrait bdr-1 flex flex-row relative overflow-scroll items-center"
|
||||
>
|
||||
<p>
|
||||
<Link href="https://wiki.teamfortress.com/wiki/Demoman"
|
||||
>The goat</Link
|
||||
>
|
||||
<Link href="https://wiki.teamfortress.com/wiki/Demoman">The goat</Link>
|
||||
</p>
|
||||
<div>
|
||||
<VideoTable :sourceArr="videoSources" />
|
||||
|
||||
@@ -4,7 +4,9 @@ import Wip from "@/components/util/Wip.vue";
|
||||
|
||||
<template>
|
||||
<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 />
|
||||
</div>
|
||||
</main>
|
||||
|
||||
@@ -17,22 +17,21 @@ const links = [
|
||||
<section>
|
||||
<Header>Adam French</Header>
|
||||
<Paragraph>
|
||||
Junior software engineer focused on full-stack development,
|
||||
systems programming, and infrastructure. First Class Honours
|
||||
in Computer Science with Mathematics from Leeds and
|
||||
Waterloo.
|
||||
Junior software engineer focused on full-stack development, systems
|
||||
programming, and infrastructure. First Class Honours in Computer
|
||||
Science with Mathematics from Leeds and Waterloo.
|
||||
</Paragraph>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<Header>About</Header>
|
||||
<Paragraph>
|
||||
This website is self-hosted and has a lot more on it than it
|
||||
needs to. Please have a look at my
|
||||
<InlineLink to="/cv">CV</InlineLink> for a full breakdown of
|
||||
my experience, projects, and skills. Please visit
|
||||
<InlineLink to="/stp">STP</InlineLink> for the prefered but
|
||||
less professional experience.
|
||||
This website is self-hosted and has a lot more on it than it needs to.
|
||||
Please have a look at my
|
||||
<InlineLink to="/cv">CV</InlineLink> for a full breakdown of my
|
||||
experience, projects, and skills. Please visit
|
||||
<InlineLink to="/stp">STP</InlineLink> for the prefered but less
|
||||
professional experience.
|
||||
</Paragraph>
|
||||
</section>
|
||||
|
||||
|
||||
@@ -28,33 +28,31 @@
|
||||
<h2>BAE graduate digital intelligence software engineer</h2>
|
||||
<p>
|
||||
I am writing to express my interest in your software engineering
|
||||
position. BAE Systems has hosted multiple stools at the
|
||||
University of Leeds and have always exhibited their development
|
||||
of leading-edge software and technology. This is where the
|
||||
origin of my interest in BAE systems emerged and I'm hopeful
|
||||
that this interest shall continue.
|
||||
position. BAE Systems has hosted multiple stools at the University of
|
||||
Leeds and have always exhibited their development of leading-edge
|
||||
software and technology. This is where the origin of my interest in BAE
|
||||
systems emerged and I'm hopeful that this interest shall continue.
|
||||
</p>
|
||||
<p>
|
||||
I'm confidient im a strong fit for this role. My technical
|
||||
background includes extensive experience with frontend
|
||||
frameworks such as React. My devotion however lies more in
|
||||
backend development as has more potential to graple problems
|
||||
related to optimisation and designing coherent interfaces.
|
||||
I'm confidient im a strong fit for this role. My technical background
|
||||
includes extensive experience with frontend frameworks such as React. My
|
||||
devotion however lies more in backend development as has more potential
|
||||
to graple problems related to optimisation and designing coherent
|
||||
interfaces.
|
||||
</p>
|
||||
<p>
|
||||
<em> The C# Programming Yellow Book </em> was my first
|
||||
introduction to C# during A-Level, Java was our vessel for
|
||||
teaching object-orientated programming at university. I am
|
||||
confident I have the relevant experience to grasp the languages
|
||||
stated for the role I am applying for.
|
||||
<em> The C# Programming Yellow Book </em> was my first introduction to
|
||||
C# during A-Level, Java was our vessel for teaching object-orientated
|
||||
programming at university. I am confident I have the relevant experience
|
||||
to grasp the languages stated for the role I am applying for.
|
||||
</p>
|
||||
<p>
|
||||
My academic background in Computer Science and Mathematics has
|
||||
honed my ability to translate abstract concepts into structured,
|
||||
logical solutions. Just as I have transformed theoretical
|
||||
hypotheses into formal proofs, I aim to transform business
|
||||
requirements into robust, maintainable software systems through
|
||||
collaboration and rigorous reasoning.
|
||||
My academic background in Computer Science and Mathematics has honed my
|
||||
ability to translate abstract concepts into structured, logical
|
||||
solutions. Just as I have transformed theoretical hypotheses into formal
|
||||
proofs, I aim to transform business requirements into robust,
|
||||
maintainable software systems through collaboration and rigorous
|
||||
reasoning.
|
||||
</p>
|
||||
<p>Thank you for reading - Adam F</p>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user