add TOC add better blog navigation and a new blog post
This commit is contained in:
@@ -8,6 +8,24 @@
|
|||||||
html,
|
html,
|
||||||
body {
|
body {
|
||||||
@apply bg-soft-lavender dark:bg-midnight text-deep-indigo dark:text-white;
|
@apply bg-soft-lavender dark:bg-midnight text-deep-indigo dark:text-white;
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* make scroll bars dark in dark mode */
|
||||||
|
html {
|
||||||
|
color-scheme: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.dark {
|
||||||
|
color-scheme: dark;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
@apply hover:dark:bg-[#1E1F1E] hover:bg-[#E5E6E5]/80 flex items-center h-fit p-[2px] rounded;
|
||||||
|
}
|
||||||
|
|
||||||
|
hr {
|
||||||
|
@apply text-deep-indigo dark:text-white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.font-jetbrains {
|
.font-jetbrains {
|
||||||
|
|||||||
21
components/MiniBlogCard.vue
Normal file
21
components/MiniBlogCard.vue
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
const props = defineProps({
|
||||||
|
to: String,
|
||||||
|
title: String,
|
||||||
|
description: String,
|
||||||
|
rightAlign: Boolean,
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<NuxtLink :to="props.to"
|
||||||
|
class="py-8 px-6 border-[#ECE6E7] dark:border-[#232326] border rounded-lg hover:dark:bg-obsidian-night hover:bg-[hsl(270,68%,95.71%)] hover:transition-colors select-none"
|
||||||
|
:class="props.rightAlign ? 'text-right' : ''">
|
||||||
|
<div
|
||||||
|
class="p-1.5 inline-flex bg-[#ECE6E7] dark:bg-[#1A1A1D] rounded-full mb-4 border border-gray-300/80 dark:border-neutral-800/70">
|
||||||
|
<Icon :name="props.rightAlign ? 'tabler:arrow-right' : 'tabler:arrow-left'" size="24" />
|
||||||
|
</div>
|
||||||
|
<p class="font-semibold">{{ props.title }}</p>
|
||||||
|
<p class="text-sm">{{ props.description }}</p>
|
||||||
|
</NuxtLink>
|
||||||
|
</template>
|
||||||
@@ -2,96 +2,76 @@
|
|||||||
let colorMode = useColorMode();
|
let colorMode = useColorMode();
|
||||||
|
|
||||||
const changeTheme = () => {
|
const changeTheme = () => {
|
||||||
if (colorMode.preference === "dark") {
|
if (colorMode.preference === "dark") {
|
||||||
// from dark => light
|
// from dark => light
|
||||||
colorMode.preference = "light"
|
colorMode.preference = "light"
|
||||||
document.documentElement.classList.remove("dark");
|
} else if (colorMode.preference === "light") {
|
||||||
} else if (colorMode.preference === "light") {
|
// from light => system
|
||||||
// from light => system
|
colorMode.preference = "system";
|
||||||
colorMode.preference = "system";
|
} else {
|
||||||
if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
// from system => dark
|
||||||
document.documentElement.classList.add("dark");
|
colorMode.preference = "dark";
|
||||||
} else {
|
}
|
||||||
document.documentElement.classList.remove("dark");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// from system => dark
|
|
||||||
colorMode.preference = "dark";
|
|
||||||
document.documentElement.classList.add("dark");
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<nav class="h-16 z-10 w-full">
|
<nav class="h-16 z-10 w-full">
|
||||||
<div class="px-6 max-w-7xl grid gap-2 grid-cols-12 ms-auto me-auto h-full justify-evenly">
|
<div class="px-6 max-w-7xl grid gap-2 grid-cols-12 ms-auto me-auto h-full justify-evenly">
|
||||||
<div class="ml-0 col-span-6 sm:col-span-4 flex items-center">
|
<div class="ml-0 col-span-6 sm:col-span-4 flex items-center">
|
||||||
<ul class="flex gap-x-8">
|
<ul class="flex gap-x-8">
|
||||||
<li
|
<li
|
||||||
class="absolute w-fit -translate-x-full top-0 px-2 py-4 bg-soft-lavender dark:bg-midnight text-deep-indigo dark:text-white opacity-0 focus-within:translate-x-0 focus-within:opacity-100">
|
class="absolute w-fit -translate-x-full top-0 px-2 py-4 bg-soft-lavender dark:bg-midnight text-deep-indigo dark:text-white opacity-0 focus-within:translate-x-0 focus-within:opacity-100">
|
||||||
<a href="#main">Skip to content</a>
|
<a href="#main">Skip to content</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<NuxtLink to="/">
|
<NuxtLink to="/">
|
||||||
Home
|
Home
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<NuxtLink to="/blog">
|
<NuxtLink to="/blog">
|
||||||
Blog
|
Blog
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div class="mr-0 col-span-4 items-center hidden sm:flex"> </div>
|
<div class="mr-0 col-span-4 items-center hidden sm:flex"> </div>
|
||||||
<div class="mr-0 col-span-6 sm:col-span-4 flex items-center justify-end">
|
<div class="mr-0 col-span-6 sm:col-span-4 flex items-center justify-end">
|
||||||
<ul class="flex gap-x-4">
|
<ul class="flex gap-x-4">
|
||||||
<li>
|
<li>
|
||||||
<a href="https://www.github.com/juls0730">
|
<a href="https://www.github.com/juls0730">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
<button>
|
||||||
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
|
<Icon name="tabler:brand-github" size="22" />
|
||||||
stroke-width="2"
|
</button>
|
||||||
d="M9 19c-4.3 1.4-4.3-2.5-6-3m12 5v-3.5c0-1 .1-1.4-.5-2c2.8-.3 5.5-1.4 5.5-6a4.6 4.6 0 0 0-1.3-3.2a4.2 4.2 0 0 0-.1-3.2s-1.1-.3-3.5 1.3a12.3 12.3 0 0 0-6.2 0C6.5 2.8 5.4 3.1 5.4 3.1a4.2 4.2 0 0 0-.1 3.2A4.6 4.6 0 0 0 4 9.5c0 4.6 2.7 5.7 5.5 6c-.6.6-.6 1.2-.5 2V21" />
|
</a>
|
||||||
</svg>
|
</li>
|
||||||
</a>
|
<li>
|
||||||
</li>
|
<a href="https://x.com/julie4055_">
|
||||||
<li>
|
<button>
|
||||||
<a href="https://x.com/julie4055_">
|
<Icon name="tabler:brand-x" size="22" />
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
</button>
|
||||||
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
|
</a>
|
||||||
stroke-width="2" d="m4 4l11.733 16H20L8.267 4zm0 16l6.768-6.768m2.46-2.46L20 4" />
|
</li>
|
||||||
</svg>
|
<li>
|
||||||
</a>
|
<button @click="changeTheme">
|
||||||
</li>
|
<Icon v-if="$colorMode.preference === 'dark'" name="tabler:moon" size="22" />
|
||||||
<li>
|
<Icon v-else-if="$colorMode.preference === 'light'" name="tabler:sun-high" size="22" />
|
||||||
<button @click="changeTheme">
|
<Icon v-else name="ph:monitor-bold" size="22" />
|
||||||
<div v-if="$colorMode.preference === 'dark'">
|
</button>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
</li>
|
||||||
<path fill="none" stroke="currentColor" stroke-linecap="round"
|
</ul>
|
||||||
stroke-linejoin="round" stroke-width="2"
|
</div>
|
||||||
d="M12 3h.393a7.5 7.5 0 0 0 7.92 12.446A9 9 0 1 1 12 2.992z" />
|
</div>
|
||||||
</svg>
|
</nav>
|
||||||
</div>
|
|
||||||
<div v-else-if="$colorMode.preference === 'light'">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
|
||||||
<path fill="none" stroke="currentColor" stroke-linecap="round"
|
|
||||||
stroke-linejoin="round" stroke-width="2"
|
|
||||||
d="M14.828 14.828a4 4 0 1 0-5.656-5.656a4 4 0 0 0 5.656 5.656m-8.485 2.829l-1.414 1.414M6.343 6.343L4.929 4.929m12.728 1.414l1.414-1.414m-1.414 12.728l1.414 1.414M4 12H2m10-8V2m8 10h2m-10 8v2" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div v-else>
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 256 256">
|
|
||||||
<path fill="currentColor"
|
|
||||||
d="M208 36H48a28 28 0 0 0-28 28v112a28 28 0 0 0 28 28h160a28 28 0 0 0 28-28V64a28 28 0 0 0-28-28Zm4 140a4 4 0 0 1-4 4H48a4 4 0 0 1-4-4V64a4 4 0 0 1 4-4h160a4 4 0 0 1 4 4Zm-40 52a12 12 0 0 1-12 12H96a12 12 0 0 1 0-24h64a12 12 0 0 1 12 12Z" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
button {
|
||||||
|
padding: 0.25rem
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -6,7 +6,7 @@ export default {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="dark:bg-dark-slate bg-touched-lavender relative border p-6 col-span-12 sm:col-span-10 sm:col-start-2 md:col-start-auto md:col-span-6 xl:!col-span-4 h-[60vw] max-h-[425px] min-h-[375px] border-soft-lilac dark:border-midnight-slate/30 shadow-md rounded-lg">
|
class="dark:bg-dark-slate bg-touched-lavender relative border p-6 h-[60vw] max-h-[425px] min-h-[375px] border-soft-lilac dark:border-midnight-slate/30 shadow-md rounded-lg">
|
||||||
<div class="flex mb-4 items-center" v-if="headerIcon">
|
<div class="flex mb-4 items-center" v-if="headerIcon">
|
||||||
<Icon size="64" class="text-sea-green" :name="headerIcon" />
|
<Icon size="64" class="text-sea-green" :name="headerIcon" />
|
||||||
<div class="ml-auto flex">
|
<div class="ml-auto flex">
|
||||||
|
|||||||
75
components/TableOfContents.vue
Normal file
75
components/TableOfContents.vue
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { watchDebounced } from '@vueuse/core'
|
||||||
|
import type { Ref } from 'vue';
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<{ doc: any, activeTocId: string | null }>(), {})
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const sliderHeight = useState('sliderHeight', () => 0)
|
||||||
|
const sliderTop = useState('sliderTop', () => 0)
|
||||||
|
const tocLinksH2: Ref<Array<HTMLElement>> = ref([])
|
||||||
|
const tocLinksH3: Ref<Array<HTMLElement>> = ref([])
|
||||||
|
|
||||||
|
const tocLinks = computed(() => props.doc?.body?.toc?.links ?? [])
|
||||||
|
|
||||||
|
const onClick = (id: string) => {
|
||||||
|
const el = document.getElementById(id)
|
||||||
|
if (el) {
|
||||||
|
router.push({ hash: `#${id}` })
|
||||||
|
el.scrollIntoView({ behavior: 'smooth', block: 'center' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const tocHeader = ref();
|
||||||
|
const tocIsClosed = ref(false);
|
||||||
|
|
||||||
|
watchDebounced(
|
||||||
|
() => props.activeTocId,
|
||||||
|
(newActiveTocId) => {
|
||||||
|
const h2Link = tocLinksH2.value.find((el: HTMLElement) => el.id === `toc-${newActiveTocId}`)
|
||||||
|
const h3Link = tocLinksH3.value.find((el: HTMLElement) => el.id === `toc-${newActiveTocId}`)
|
||||||
|
|
||||||
|
// TODO: dont hard code these offsets
|
||||||
|
if (h2Link) {
|
||||||
|
sliderHeight.value = h2Link.offsetHeight
|
||||||
|
sliderTop.value = h2Link.offsetTop - 24
|
||||||
|
} else if (h3Link) {
|
||||||
|
sliderHeight.value = h3Link.offsetHeight
|
||||||
|
sliderTop.value = h3Link.offsetTop - 24
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ debounce: 0, immediate: true }
|
||||||
|
)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="border-[#ECE6E7] dark:border-[#232326] pb-3 border-b border-dashed lg:border-b-0 overflow-hidden"
|
||||||
|
v-if="tocLinks.length > 0">
|
||||||
|
<span ref="tocHeader" @click="tocIsClosed = !tocIsClosed"
|
||||||
|
class="cursor-pointer lg:cursor-auto flex justify-between">
|
||||||
|
<h4 class="font-bold lg:mb-0">Table of Contents</h4>
|
||||||
|
<Icon name="tabler:chevron-down" class="lg:!hidden transition-transform duration-200"
|
||||||
|
:class="tocIsClosed ? 'rotate-180' : ''" />
|
||||||
|
</span>
|
||||||
|
<nav class="flex space-y-3 overflow-ellipsis">
|
||||||
|
<div class="relative w-0.5 rounded hidden lg:block">
|
||||||
|
<div class="absolute left-0 w-full transition-all duration-200 rounded bg-fuschia"
|
||||||
|
:style="{ height: `${sliderHeight}px`, top: `${sliderTop}px` }"></div>
|
||||||
|
</div>
|
||||||
|
<ul class="lg:pl-4 lg:block" :class="tocIsClosed ? 'hidden' : ''">
|
||||||
|
<li role="link" v-for="{ id, text, children } in tocLinks" :id="`toc-${id}`" :key="id" ref="tocLinksH2"
|
||||||
|
class="cursor-pointer lg:text-sm ml-0 mb-2 last:mb-0"
|
||||||
|
:class="{ 'font-semibold': id === activeTocId }" @click="onClick(id)">
|
||||||
|
{{ text }}
|
||||||
|
<ul v-if="children" class="ml-3 my-2">
|
||||||
|
<li role="link" v-for=" { id: childId, text: childText } in children" :id="`toc-${childId}`"
|
||||||
|
:key="childId" ref="tocLinksH3" class="cursor-pointer lg:text-xs ml-0 mb-2 last:mb-0"
|
||||||
|
:class="{ 'font-semibold': childId === activeTocId }" @click.stop="onClick(childId)">
|
||||||
|
{{ childText }}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -1,121 +1,136 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="container my-2 dark:bg-[#1d1b1d] bg-[hsl(270,26.89%,94.47%)]">
|
<div id="code-container" class="pt-3 overflow-hidden rounded-md my-2 dark:bg-[#1d1b1d] bg-[#f1edf5] shadow-sm">
|
||||||
<span v-if="filename" class="filename-text text-xs leading-none tracking-tight text-gray-400 font-jetbrains">
|
<div class="flex justify-between mx-2 mb-1">
|
||||||
{{ filename }}
|
<span v-if="language"
|
||||||
</span>
|
class="px-2 py-1 text-xs leading-none tracking-tight text-gray-400 font-jetbrains capitalize">
|
||||||
<slot />
|
{{ language }}
|
||||||
<div class="bottom-container">
|
</span>
|
||||||
<div class="copy-container">
|
<span v-if="filename" class="px-2 py-1 text-xs leading-none tracking-tight text-gray-400 font-jetbrains">
|
||||||
<button
|
{{ filename }}
|
||||||
class="rounded hover:bg-zinc-300/70 dark:hover:bg-zinc-800/60 transition-colors duration-200 flex p-1"
|
</span>
|
||||||
@click="copy(code)" @keypress.space="copy(code)">
|
</div>
|
||||||
<div class="h-6" v-if="copied">
|
<div ref="codeElm">
|
||||||
<Icon size="24" name="tabler:check" />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
<div class="h-6" v-else>
|
<div class="bottom-container">
|
||||||
<Icon size="24" name="tabler:copy" />
|
<div class="copy-container">
|
||||||
</div>
|
<button class="p-1 hover:bg-zinc-300/70 dark:hover:bg-zinc-700/40" @click="copyCode()"
|
||||||
</button>
|
@keypress.space="copyCode()">
|
||||||
</div>
|
<div class="h-6" v-if="copied">
|
||||||
</div>
|
<Icon size="24" name="tabler:check" />
|
||||||
</div>
|
</div>
|
||||||
|
<div class="h-6" v-else>
|
||||||
|
<Icon size="24" name="tabler:copy" />
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useClipboard } from '@vueuse/core';
|
import { useClipboard } from '@vueuse/core';
|
||||||
const { copy, copied } = useClipboard();
|
const { copy, copied } = useClipboard();
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
code?: string;
|
code?: string;
|
||||||
language?: string | null;
|
language?: string | null;
|
||||||
filename?: string | null;
|
filename?: string | null;
|
||||||
highlights?: Array<any>;
|
highlights?: Array<number>;
|
||||||
}>(),
|
}>(),
|
||||||
{ code: '', language: null, filename: null, highlights: undefined }
|
{ code: '', language: null, filename: null, highlights: [] }
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const codeElm = ref();
|
||||||
|
|
||||||
|
const copyCode = () => {
|
||||||
|
if (!codeElm.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let str = "";
|
||||||
|
let lines = codeElm.value.getElementsByClassName("line");
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
let line = lines[i]
|
||||||
|
str += line.textContent
|
||||||
|
if (!str.endsWith("\n") && i != lines.length - 1) {
|
||||||
|
str += "\n"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
copy(str);
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.icon {
|
.icon {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.container {
|
#code-container:is(:hover, :focus-within) .bottom-container {
|
||||||
position: relative;
|
opacity: 1;
|
||||||
padding-top: 0.5em;
|
pointer-events: all;
|
||||||
overflow: hidden;
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container:is(:hover, :focus-within) .bottom-container {
|
|
||||||
opacity: 1;
|
|
||||||
pointer-events: all;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.bottom-container {
|
.bottom-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
transition-property: opacity;
|
transition-property: opacity;
|
||||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
transition-duration: 250ms;
|
transition-duration: 250ms;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
position: relative;
|
position: relative;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
.copy-container {
|
.copy-container {
|
||||||
height: fit-content;
|
height: fit-content;
|
||||||
display: flex;
|
display: flex;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 15px;
|
bottom: 15px;
|
||||||
right: 15px;
|
right: 15px;
|
||||||
}
|
|
||||||
|
|
||||||
.filename-text {
|
|
||||||
position: absolute;
|
|
||||||
top: 0.25rem;
|
|
||||||
right: 0.25rem;
|
|
||||||
padding: 0.25em 0.5em;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
:slotted(pre) {
|
:slotted(pre) {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: 1 1 0%;
|
flex: 1 1 0%;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
padding: 1rem;
|
padding-bottom: 1rem;
|
||||||
line-height: 1.625;
|
line-height: 1.625;
|
||||||
counter-reset: lines;
|
counter-reset: lines;
|
||||||
}
|
}
|
||||||
|
|
||||||
:slotted(pre code) {
|
:slotted(pre code) {
|
||||||
width: 100%;
|
display: flex;
|
||||||
display: flex;
|
width: 100%;
|
||||||
flex-direction: column;
|
min-width: max-content;
|
||||||
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
:slotted(pre code .line) {
|
:slotted(pre code .line) {
|
||||||
min-height: 1em;
|
min-width: 100%;
|
||||||
|
min-height: 1em;
|
||||||
|
padding-left: 1rem;
|
||||||
|
padding-right: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
:slotted(pre code .line::before) {
|
:slotted(pre code .line::before) {
|
||||||
counter-increment: lines;
|
counter-increment: lines;
|
||||||
content: counter(lines);
|
content: counter(lines);
|
||||||
width: 1em;
|
width: 1em;
|
||||||
margin-right: 1.5rem;
|
margin-right: 1.5rem;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
color: rgba(115, 138, 148, 0.4);
|
color: rgba(115, 138, 148, 0.4);
|
||||||
}
|
}
|
||||||
|
|
||||||
:slotted(pre code .highlight) {
|
:slotted(pre code .highlighted) {
|
||||||
background-color: #363b46;
|
background-color: #e4e1ee;
|
||||||
display: block;
|
}
|
||||||
margin-right: -1em;
|
|
||||||
margin-left: -1em;
|
.dark :slotted(pre code .highlighted) {
|
||||||
padding-right: 1em;
|
background-color: #2e2b2e;
|
||||||
padding-left: 0.75em;
|
|
||||||
border-left: 0.25em solid red;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -1,41 +1,35 @@
|
|||||||
<template>
|
<template>
|
||||||
<div :id="id"
|
<div class="group flex mt-2">
|
||||||
class="group flex mt-2">
|
<h2 :id="id" class="text-2xl">
|
||||||
<div class="text-2xl">
|
<slot />
|
||||||
<slot />
|
</h2>
|
||||||
</div>
|
<button @click="copy(location.origin + location.pathname + '#' + id)"
|
||||||
<button @click="copy(location.origin + location.pathname + '#' + id)"
|
class="dark:text-white ml-2 group-hover:opacity-100 opacity-0 transition-all">
|
||||||
class="group-hover:opacity-100 ml-2 hover:bg-zinc-800 flex items-center h-fit p-[2px] transition-opacity duration-200 rounded opacity-0">
|
<div class="h-5" v-if="copied">
|
||||||
<div class="h-4"
|
<Icon size="20" name="tabler:check" />
|
||||||
v-if="copied">
|
</div>
|
||||||
<Icon size="16"
|
<div class="h-5" v-else>
|
||||||
name="tabler:check" />
|
<Icon size="20" name="tabler:link" />
|
||||||
</div>
|
</div>
|
||||||
<div class="h-4"
|
</button>
|
||||||
v-else>
|
</div>
|
||||||
<Icon size="16"
|
|
||||||
name="tabler:link" />
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {
|
import { useBrowserLocation, useClipboard } from '@vueuse/core';
|
||||||
useClipboard, useBrowserLocation
|
const { copy, copied } = useClipboard();
|
||||||
} from '@vueuse/core';
|
withDefaults(
|
||||||
const { copy, copied, text } = useClipboard();
|
defineProps<{
|
||||||
const props = withDefaults(
|
id?: string;
|
||||||
defineProps<{
|
}>(),
|
||||||
id?: string;
|
{ id: '' }
|
||||||
}>(),
|
|
||||||
{ id: '' }
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const location = useBrowserLocation()
|
const location = useBrowserLocation()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.icon {
|
.icon {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
35
components/content/ProseH3.vue
Executable file
35
components/content/ProseH3.vue
Executable file
@@ -0,0 +1,35 @@
|
|||||||
|
<template>
|
||||||
|
<div class="group flex mt-2">
|
||||||
|
<h3 :id="id" class="text-xl">
|
||||||
|
<slot />
|
||||||
|
</h3>
|
||||||
|
<button @click="copy(location.origin + location.pathname + '#' + id)"
|
||||||
|
class="dark:text-white ml-2 group-hover:opacity-100 opacity-0 transition-all">
|
||||||
|
<div class="h-5" v-if="copied">
|
||||||
|
<Icon size="20" name="tabler:check" />
|
||||||
|
</div>
|
||||||
|
<div class="h-5" v-else>
|
||||||
|
<Icon size="20" name="tabler:link" />
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useBrowserLocation, useClipboard } from '@vueuse/core';
|
||||||
|
const { copy, copied } = useClipboard();
|
||||||
|
withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
id?: string;
|
||||||
|
}>(),
|
||||||
|
{ id: '' }
|
||||||
|
);
|
||||||
|
|
||||||
|
const location = useBrowserLocation()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.icon {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,29 +1,20 @@
|
|||||||
---
|
---
|
||||||
title: My 100DaysOfCode Challenge
|
title: My 100DaysOfCode Challenge
|
||||||
description: A review of my 100DaysOfCode challenge
|
description: A review of my 100DaysOfCode challenge
|
||||||
image:
|
image:
|
||||||
src: '/images/100DaysOfCode-overview.webp'
|
src: "/images/100DaysOfCode-overview.webp"
|
||||||
alt: 'How I made my site fast'
|
alt: "How I made my site fast"
|
||||||
head:
|
date: January 01, 2023
|
||||||
meta:
|
|
||||||
- name: 'keywords'
|
|
||||||
content: 'web dev, nodejs'
|
|
||||||
- name: 'robots'
|
|
||||||
content: 'index, follow'
|
|
||||||
- name: 'author'
|
|
||||||
content: 'juls07'
|
|
||||||
- name: 'copyright'
|
|
||||||
content: '© 2022 juls07'
|
|
||||||
date: 2023-01-01
|
|
||||||
# _draft: true
|
|
||||||
tags:
|
tags:
|
||||||
- web dev
|
- web dev
|
||||||
- nodejs
|
- nodejs
|
||||||
---
|
---
|
||||||
|
|
||||||
For my #100DaysOfCode challenge on [twitter](https://twitter.com/julie4055_) I decided to make a UI framework similar to vue.js (terrible idea). For day 1 it started simple with learning about reactivity, a short read of this [great blog post](https://lihautan.com/reactivity-in-web-frameworks-the-when/) by Tan Li Hau I had a basic trackable object function in the script.js file. For day 2 I had covered a few more things in the UI framework world, I had a templating system, a basic virtualDOM-like implementation, a [different reactive prop function](https://dev.to/siddharthshyniben/implementing-reactivity-from-scratch-51op) from siddharth, and we had a basic click directive working, great work so far.
|
For my #100DaysOfCode challenge on [twitter](https://twitter.com/julie4055_) I decided to make a UI framework similar to vue.js (terrible idea). For day 1 it started simple with learning about reactivity, a short read of this [great blog post](https://lihautan.com/reactivity-in-web-frameworks-the-when/) by Tan Li Hau I had a basic trackable object function in the script.js file. For day 2 I had covered a few more things in the UI framework world, I had a templating system, a basic virtualDOM-like implementation, a [different reactive prop function](https://dev.to/siddharthshyniben/implementing-reactivity-from-scratch-51op) from siddharth, and we had a basic click directive working, great work so far.
|
||||||
<!--more-->
|
|
||||||
Day 3 and 4 are kinda lame, on day 3 I added a model directive similar to vue.js' v-model and on day 4 I changed the way templates were handled so I could have text before the template and I started trying to be like JSX (lmao), kinda lame and basic IMO. Now day 5 I learned about SPA routing, very simple routing that just changed the page link and rendered the template from the new page. Day 6-9 is more boring shit, simple things like a once attribute, an html attribute, etc etc. But on day 10 I learned about conditional rendering, this was an amazing learning experience for me, it taught me loads about recursive functions and how recursive functions should be used. skipping over day 11 because I added a few more directives on day 12 I made a mounted function similar to vue.js' mounted function except for the fact that it was made by me so it was terrible XD. On day 13 crazy thing happened, I decided to stop pretending to be JSX and made template files using SSR, SSR was very simple to get working, basically I just pre-rendered the page and sent the page to the client, pretty simple, but SSR would give me many more opportunities in the future of this project. skipping over day 14-20 (can you tell I was loosing motivation fast or what?) on day 21 we actually got some code gen on the server side, very simple but still pretty cool in my opinion, here's the code I shared on day 21
|
<!--more-->
|
||||||
|
|
||||||
|
Day 3 and 4 are kinda lame, on day 3 I added a model directive similar to vue.js' v-model and on day 4 I changed the way templates were handled so I could have text before the template and I started trying to be like JSX (lmao), kinda lame and basic IMO. Now day 5 I learned about SPA routing, very simple routing that just changed the page link and rendered the template from the new page. Day 6-9 is more boring shit, simple things like a once attribute, an html attribute, etc etc. But on day 10 I learned about conditional rendering, this was an amazing learning experience for me, it taught me loads about recursive functions and how recursive functions should be used. skipping over day 11 because I added a few more directives on day 12 I made a mounted function similar to vue.js' mounted function except for the fact that it was made by me so it was terrible XD. On day 13 crazy thing happened, I decided to stop pretending to be JSX and made template files using SSR, SSR was very simple to get working, basically I just pre-rendered the page and sent the page to the client, pretty simple, but SSR would give me many more opportunities in the future of this project. skipping over day 14-20 (can you tell I was loosing motivation fast or what?) on day 21 we actually got some code gen on the server side, very simple but still pretty cool in my opinion, here's the code I shared on day 21
|
||||||
|
|
||||||
```ts[index.ts]
|
```ts[index.ts]
|
||||||
template = template.replace('<script async src="/src/entry-client.ts" type="module"></script>', '');
|
template = template.replace('<script async src="/src/entry-client.ts" type="module"></script>', '');
|
||||||
@@ -197,9 +188,13 @@ export function hydrateElements(appState: Reactive) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
On day 95 I completely consolidated the hydration function to reduce the amount of loops I call to hydrate the elements on the page. On day 98 I learned about real Virtual DOMs and the advantage of virtual DOMs, this is where in the project I was telling myself "fuck it, I should completely restart the whole project later and fix all of the dirty nasty code I have written.", but I continued the project as normal knowing that completely restarting the whole project would be much more than a 1 day process. And that's it, a brief overview of my 100DaysOfCode challenge.
|
On day 95 I completely consolidated the hydration function to reduce the amount of loops I call to hydrate the elements on the page. On day 98 I learned about real Virtual DOMs and the advantage of virtual DOMs, this is where in the project I was telling myself "fuck it, I should completely restart the whole project later and fix all of the dirty nasty code I have written.", but I continued the project as normal knowing that completely restarting the whole project would be much more than a 1 day process. And that's it, a brief overview of my 100DaysOfCode challenge.
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
In the end of this project I feel more motivated to learn more about frameworks and everything related to that, a reflection coming soon to my [twitter](https://twitter.com/julie4055_) so keep an eye out if you're interested in that, after all that I hope you enjoyed reading.
|
In the end of this project I feel more motivated to learn more about frameworks and everything related to that, a reflection coming soon to my [twitter](https://twitter.com/julie4055_) so keep an eye out if you're interested in that, after all that I hope you enjoyed reading.
|
||||||
<br/>
|
<br/>
|
||||||
<p class="text-xl font-extrabold"><a href="https://twitch.tv/julie4055_">To be continued...?</a></p>
|
|
||||||
|
<p class="text-xl font-extrabold"><a href="https://twitch.tv/julie4055_">To be continued...?</a></p>
|
||||||
|
|||||||
@@ -1,21 +1,10 @@
|
|||||||
---
|
---
|
||||||
title: How I made my site fast
|
title: How I made my site fast
|
||||||
description: How I made my social media site fast
|
description: How I made my social media site fast
|
||||||
image:
|
image:
|
||||||
src: '/images/how-i-made-my-site-fast.webp'
|
src: "/images/how-i-made-my-site-fast.webp"
|
||||||
alt: 'How I made my site fast'
|
alt: "How I made my site fast"
|
||||||
head:
|
date: May 22, 2022
|
||||||
meta:
|
|
||||||
- name: 'keywords'
|
|
||||||
content: 'web dev, fullstack development, angular, nodejs, redis'
|
|
||||||
- name: 'robots'
|
|
||||||
content: 'index, follow'
|
|
||||||
- name: 'author'
|
|
||||||
content: 'juls07'
|
|
||||||
- name: 'copyright'
|
|
||||||
content: '© 2022 juls07'
|
|
||||||
date: 2022-05-22
|
|
||||||
# _draft: true
|
|
||||||
tags:
|
tags:
|
||||||
- web dev
|
- web dev
|
||||||
- fullstack development
|
- fullstack development
|
||||||
@@ -23,8 +12,11 @@ tags:
|
|||||||
- nodejs
|
- nodejs
|
||||||
- redis
|
- redis
|
||||||
---
|
---
|
||||||
|
|
||||||
So yesterday I talked about my social media site that I'm working on. Today I implemented redis, a memory store that I used to reduce requests at most by 48ms. First when I tried to use redis I just used the `redis` package off of npmjs because it only makes sense but when I tried to get a key with the name of `user-cache-[userId]` It failed for some reason, I still dont know why it failed but using the `ioredis` package and everything started working, not user data processing, what I used redis to cache since it's a terrible for loop that executes database requests for every post, but now it's all stored in memory.
|
So yesterday I talked about my social media site that I'm working on. Today I implemented redis, a memory store that I used to reduce requests at most by 48ms. First when I tried to use redis I just used the `redis` package off of npmjs because it only makes sense but when I tried to get a key with the name of `user-cache-[userId]` It failed for some reason, I still dont know why it failed but using the `ioredis` package and everything started working, not user data processing, what I used redis to cache since it's a terrible for loop that executes database requests for every post, but now it's all stored in memory.
|
||||||
|
|
||||||
<!--more-->
|
<!--more-->
|
||||||
|
|
||||||
I still want to add TTL since my VPS that I intend on deploying my site on for production only has 1GB of RAM but I'm not sure how to do that. If you saw my blog post from [yesterday](/blog/what-ive-been-doing) then you'll know what my user code used to look like, now it looks like this.
|
I still want to add TTL since my VPS that I intend on deploying my site on for production only has 1GB of RAM but I'm not sure how to do that. If you saw my blog post from [yesterday](/blog/what-ive-been-doing) then you'll know what my user code used to look like, now it looks like this.
|
||||||
|
|
||||||
```js[post.js]
|
```js[post.js]
|
||||||
@@ -53,4 +45,4 @@ for (let i = 0; i < replies.length; i++) {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
With this you can see first it checks for the user data in cache if there is nonde then it will grab the data from mongodb and then write it to the redis cache. Next time, I want to cache replies and posts but I'm still worried about memory constraint on my VPS. After all of the redis caching I still want to speed up my site more, I want to replace JWT because I dont like it that much and I still want to find a workaround to Opengraph tags. After everything If you want to hear about it more often follow me on [@julie4055_](https://twitter.com/julie4055_), I'm also thinking about writing a blog daily-ish rather than just posting every other month or so, and that'll be it have a great rest of your day!
|
With this you can see first it checks for the user data in cache if there is nonde then it will grab the data from mongodb and then write it to the redis cache. Next time, I want to cache replies and posts but I'm still worried about memory constraint on my VPS. After all of the redis caching I still want to speed up my site more, I want to replace JWT because I dont like it that much and I still want to find a workaround to Opengraph tags. After everything If you want to hear about it more often follow me on [@julie4055\_](https://twitter.com/julie4055_), I'm also thinking about writing a blog daily-ish rather than just posting every other month or so, and that'll be it have a great rest of your day!
|
||||||
|
|||||||
449
content/blog/magic-of-docker-compose.md
Normal file
449
content/blog/magic-of-docker-compose.md
Normal file
@@ -0,0 +1,449 @@
|
|||||||
|
---
|
||||||
|
title: "The Magic of Docker Compose: Transforming Project Setups"
|
||||||
|
description: Simplify your life with Docker Compose
|
||||||
|
image:
|
||||||
|
src: "/images/100DaysOfCode-overview.webp"
|
||||||
|
alt: "How I made my site fast"
|
||||||
|
date: April 22, 2024
|
||||||
|
tags:
|
||||||
|
- web dev
|
||||||
|
- docker
|
||||||
|
---
|
||||||
|
|
||||||
|
In some of my recent full-stack projects, I've been using Docker Compose to simplify my project setup. Instead of downloading PostgreSQL, aligning it with the project's authentication scheme, tagging it to prevent conflicts with other projects, setting up the cache layer or other services, and building backend services, I run just one simple command:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose up
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
With this one command, you, your maintainers, contributors, and users can bypass the hassle of managing backend service requirements, databases, and other aspects, allowing everyone to focus on what truly matters — the project itself. When I open a project, whether to use it, fix a bug, or for any other reason, I don't want to run multiple commands to get it working; I just want to use the project.
|
||||||
|
|
||||||
|
## The Why
|
||||||
|
|
||||||
|
Many of my full-stack projects rely on various services. These can range from a Redis DB (rip lol) to a MySQL DB, a PostgreSQL DB, a compiled backend, or numerous other possibilities. However, setting them up can be tedious, resolving conflicts with other databases, managing missing dependencies, and ensuring the services are running. I simply want to focus on the project. While using a project, I shouldn't have to concern myself with the maintainers' choice of database, their decision to use a compiled backend or any other choices, and neither should anyone else. I find that the largest roadblock to me working on projects is dependencies, whether it’s an electron app, which is constantly in a state of dependency hell, an open-source web app, or API, I always find myself wasting time on things like dependencies, and sometimes I even lose interest in contributing to the project because of how hard it is to get the project working at all.
|
||||||
|
|
||||||
|
Docker Compose also hands itself to developing microservices and multi-environment production, docker containers can be easily spun up nearly on demand, and you can make guarantees about the environment that the service is running on even if you or other members of your team use Windows, thanks to Docker building on top of the Linux kernel. You and your contributors will benefit greatly once you get past the hurdle of implementing Docker Compose into your project. want to test a clean install of your app? Just delete the docker image and make a new one. Working from another computer for the time being? That’s fine, clone the repo and start the docker container. Once you use Docker Compose to manage projects, you won't be able to go back. Have you ever been in a scenario where you have an old project and you want to look back at it or work on it again, but it's poorly documented and you don't know exactly what you used and how you had it set up? Trust me, I’ve been there, and it sucks, but by using Docker Compose, I find myself going into old projects, starting up Docker Compose, and it being exactly like the day I left it.
|
||||||
|
|
||||||
|
## The How
|
||||||
|
|
||||||
|
Implementing Docker Compose can be a bit of a struggle if you have never done it before, but it’s quite simple once you know what you are doing. I’m going to show you how you might implement Docker Compose in a Vue project that uses KeyDB as the main database, and a Go backend in a mono-repo with Nginx as a reverse proxy between those services with docker-compose managing everything, I’m not going to go over why I chose these specific tools because it’s not exactly important for this blog post.
|
||||||
|
|
||||||
|
### Frontend
|
||||||
|
|
||||||
|
First, we can create a Vue project, make sure you use single-page routing in your configuration. You can start a Vue project with the Vue CLI with these commands:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun create vue@latest
|
||||||
|
cd <project-name>
|
||||||
|
bun i
|
||||||
|
```
|
||||||
|
|
||||||
|
Then, we can create a Docker file so that we can eventually run our Vue web app in a Docker container. First, let's make the file in the root of the project and name it `Dockerfile`, and on the first line, pull the bunjs docker image, like so:
|
||||||
|
|
||||||
|
```docker
|
||||||
|
# Dockerfile
|
||||||
|
FROM oven/bun:alpine
|
||||||
|
```
|
||||||
|
|
||||||
|
We use the `oven/bun` image from the Alpine branch specifically because the Alpine images use a stripped-down version of Linux that takes up less space and is more efficient. Usually, when picking docker images to pull from, I pick alpine-based images and would recommend you do too. Then, we prepare the docker container so that it uses our project code, like this:
|
||||||
|
|
||||||
|
```docker
|
||||||
|
RUN mkdir /app
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY . /app
|
||||||
|
```
|
||||||
|
|
||||||
|
Here, we make the app directory in the docker container’s root directory, and set the “working directory”, which makes it so that any command executed from that point forward is executed in that directory, in this case, the app directory, and copy all the files from the current directory to the /app directory, but don’t worry, this doesn't actually move the project to somewhere else, and hot reloading will still work as expected. Finally, we run the commands to ensure our project has up-to-date dependencies, and then start our project, like so:
|
||||||
|
|
||||||
|
```docker
|
||||||
|
RUN bun install
|
||||||
|
|
||||||
|
# Run the vite dev server with the bun runtime
|
||||||
|
CMD ["bun", "--bun", "dev", "-- --host=0.0.0.0"]
|
||||||
|
```
|
||||||
|
|
||||||
|
We use the Bun runtime by using the `--bun` argument because if we don't, Bun will default to the slower Node.js runtime. Using the `-- --host=0.0.0.0` argument indicates that we are passing an argument to the underlying program, in this case, Vue, and that we are passing an argument, in this case, `host` because we want to expose our Vue project so that we can access it from the network later. You can run your Vue project without exposing it to the network, but it will make it so you cant access your project from the nginx reverse proxy we are going to setup later in our project.
|
||||||
|
|
||||||
|
Next, we will build the actual frontend for our full-stack project. I will be making a visitor counter since it’s simple to start with but is a little more complex than a Hello World application.
|
||||||
|
|
||||||
|
First, start off by removing the default layout in `src/App.vue` so that our App.vue looks like this:
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { RouterView } from "vue-router";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<RouterView />
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
All this does is render the view from the SPA router and nothing more, later on in this project, you could add a navbar component to this page, or something similar, but for now, we are going to leave it with just the router view. Next, create a `VisitCount.vue` component in the components directory, this component will render the visit count after it fetches the data from the API. The component should look like this:
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<script lang="ts" setup>
|
||||||
|
const count = await fetch("/api/count")
|
||||||
|
.then((res) => res.json())
|
||||||
|
.then((data) => data.count);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<p>This page has been visited {{ count }} times!</p>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
We are doing a couple of things for a few reasons:
|
||||||
|
|
||||||
|
- We split this into its own component so we can use the Suspense component when we wish to display this component since it’s an async component
|
||||||
|
- We use the .then method to process the data before we access it so that we don't have to define multiple variables to simplify the process of getting the count data
|
||||||
|
|
||||||
|
Next, we can modify `src/HomeView.vue` so that it displays our VisitCounter.vue component that we just made so that HomeView.vue looks like this:
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { defineAsyncComponent } from "vue";
|
||||||
|
|
||||||
|
const VisitCount = defineAsyncComponent(
|
||||||
|
() => import("../components/VisitCount.vue")
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Suspense>
|
||||||
|
<VisitCount />
|
||||||
|
|
||||||
|
<template #fallback> Loading... </template>
|
||||||
|
</Suspense>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
In this file, we’re doing some interesting things related to Vue. First, we are using defineAsyncComponent to dynamically import our async component that we just made. We use the Suspense component so that we can load our async component and control the loading state, async Vue components **will not work** without Suspense. Here, we use a simple “Loading…”, but you can use any loading state you desire, or you could go without a loading state entirely if you don’t mind the component showing nothing while it waits for the component to load.
|
||||||
|
|
||||||
|
### Backend
|
||||||
|
|
||||||
|
For our backend, we are going to be using a Go backend with a KeyDB database as the backend since it’s simple and fast. First, let’s initialize a go project in our project:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir server
|
||||||
|
cd server
|
||||||
|
go mod init api
|
||||||
|
```
|
||||||
|
|
||||||
|
After that, make a `main.go` file in the server directory we just created. We are going to use GIN for our web framework since it’s simple yet powerful. We can start a simple GIN web server with the following code:
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
r := gin.Default()
|
||||||
|
|
||||||
|
r.GET("/count", func(c *gin.Context) {
|
||||||
|
c.String(http.StatusOK, "Foo")
|
||||||
|
})
|
||||||
|
|
||||||
|
r.Run(":8080")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Don’t forget to add gin as a dependency:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go get github.com/gin-gonic/gin
|
||||||
|
```
|
||||||
|
|
||||||
|
The code above is a very simple GIN server, all it does is listen on 0.0.0.0:8080 and assigns a route for /count to return a 200 with a text response of `Foo`. Next, we should add functionality to actually get and increment the visit count from the KeyDB database. We can modify the code from above like so:
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context" // [!code hl:2]
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/redis/go-redis/v9" // [!code hl]
|
||||||
|
)
|
||||||
|
|
||||||
|
var ctx = context.Background() // [!code hl]
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
client := redis.NewClient(&redis.Options{ // [!code hl:5]
|
||||||
|
Addr: "keydb:6379",
|
||||||
|
Password: "",
|
||||||
|
DB: 0,
|
||||||
|
})
|
||||||
|
|
||||||
|
r := gin.Default()
|
||||||
|
|
||||||
|
// This route returns a count from the database and increments it for every request
|
||||||
|
r.GET("/count", func(c *gin.Context) {
|
||||||
|
count, err := client.Incr(ctx, "count").Result() // [!code hl:8]
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Error incrementing count in Redis:", err)
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to increment count"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{"count": count})
|
||||||
|
})
|
||||||
|
|
||||||
|
r.Run(":8080")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Once again, don’t forget to add go-redis as a dependency:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go get github.com/redis/go-redis/v9
|
||||||
|
```
|
||||||
|
|
||||||
|
The final code for the backend Go server now connects to our KeyDB database on line 15. The server also uses a backend server from go-redis called `Incr` which increments a value in the database and returns the result, and even create the table if it didn't exists previously. If the server, for whatever reason, can't set the count, we log an error to the console using the `log` library that we added, and return an error in JSON to the client. If the request succeeds, however, we send the client a 200 status code with a JSON payload that contains the visitor count in the `count` field.
|
||||||
|
|
||||||
|
Finally, for the backend, we can create a Dockerfile. First, we pull in the golang image:
|
||||||
|
|
||||||
|
```docker
|
||||||
|
# NOTE: The hot reloading won't actually work by itself, it *needs* docker-compose to work
|
||||||
|
|
||||||
|
FROM golang:alpine
|
||||||
|
```
|
||||||
|
|
||||||
|
We once again, use the alpine image since it’s smaller and more efficient. We then set up the proper environment variables and directory to make the app build properly and to make sure that hot reloading works:
|
||||||
|
|
||||||
|
```docker
|
||||||
|
ENV PROJECT_DIR=/app
|
||||||
|
|
||||||
|
RUN mkdir /app
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY . /app
|
||||||
|
```
|
||||||
|
|
||||||
|
Finally, we pull CompileDaemon, which is what we will be using to auto-reload the server when we make changes to the server so that we don't have to restart the server every time we do any change, and tell it to build and run our code, like so:
|
||||||
|
|
||||||
|
```docker
|
||||||
|
RUN go get github.com/githubnemo/CompileDaemon
|
||||||
|
RUN go install github.com/githubnemo/CompileDaemon
|
||||||
|
|
||||||
|
ENTRYPOINT CompileDaemon -build="go build -o api" -command="./api"
|
||||||
|
```
|
||||||
|
|
||||||
|
From this point, you can be done, if you change the URL that the frontend fetches data from, and make sure that the Docker Compose file exposes both the backend and frontend, you can stop here. However, I wont be doing that, I prefer that my apps are accessible from one URL so that they are easy to expose and use later without having to worry about CORS or exposing multiple ports, etc. so, we can move on to the second to last part of my example
|
||||||
|
|
||||||
|
### Nginx
|
||||||
|
|
||||||
|
We are using Nginx as a reverse proxy. Basically, we can have some services running on the network and nginx can take web requests to its port and redirect them to where they belong. To get started with nginx, make an `nginx.conf` file in the root directory of your project (where the index.html file is located). Put this into the nginx.conf file you just created:
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
events {
|
||||||
|
worker_connections 1024;
|
||||||
|
}
|
||||||
|
|
||||||
|
http {
|
||||||
|
server {
|
||||||
|
# This is the port Nginx will listen on
|
||||||
|
listen 3000;
|
||||||
|
|
||||||
|
# reduce spam in the terminal
|
||||||
|
error_log /var/log/nginx/error.log warn;
|
||||||
|
access_log off;
|
||||||
|
|
||||||
|
# Server name
|
||||||
|
server_name localhost;
|
||||||
|
|
||||||
|
# Proxy / to web:5173
|
||||||
|
location / {
|
||||||
|
proxy_pass http://web:5173/;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
|
||||||
|
# WebSocket support (for HMR)
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection "upgrade";
|
||||||
|
}
|
||||||
|
|
||||||
|
# Proxy /api to api:8080
|
||||||
|
location /api/ {
|
||||||
|
proxy_pass http://api:8080/;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Here’s a breakdown of what any of this does:
|
||||||
|
|
||||||
|
- The `worker_connections` field in the events structure specifies the maximum amount of concurrent connections, assuming your computer allows for that many open files.
|
||||||
|
- the `proxy_pass` field in either of the location structures tells nginx to take requests from, for example, /about and passes it to the Vue web server at the hostname `web` and port `5173`, which will be explained more in the Docker Compose section. On the other hand, requests that start with `/api/` will be forwarded to the backend server.
|
||||||
|
- In either of the location structures, we set a various number of headers, all of which mainly give the corresponding web server information about the actual client as opposed to information about the proxy, which we are likely to not care about.
|
||||||
|
- In the `location /` structure, at the end, we specify some fields to allow for connections to be “upgraded” to a WebSocket connection, which allows for HMR
|
||||||
|
|
||||||
|
Now, we are finally able to move on to the cherry on top.
|
||||||
|
|
||||||
|
### Docker Compose
|
||||||
|
|
||||||
|
This is the most important part of this whole project, sure, we can use whatever stack you wish to use, but knowing how to correctly use Docker Compose is the most important part. First, create a `compose.yml` file in the root directory of your project (the same place where you just made the nginx.conf file). Start that file with the name of your project, I’m going to go with `docker-compose-example`, but you can use whatever you want:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
name: docker-compose-example
|
||||||
|
```
|
||||||
|
|
||||||
|
Then, we have to define our services, in our circumstance, we have 4 services, a Vue frontend, a Go backend, a KeyDB database, and an Nginx reverse proxy. We already have Dockerfiles for both our web service and our API service, so let’s add these service to out compose.yml like so:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
web:
|
||||||
|
build: .
|
||||||
|
volumes:
|
||||||
|
- ./:/app
|
||||||
|
depends_on:
|
||||||
|
- api
|
||||||
|
|
||||||
|
api:
|
||||||
|
build: ./server
|
||||||
|
volumes:
|
||||||
|
- ./server:/app
|
||||||
|
```
|
||||||
|
|
||||||
|
We must specify the correct volumes that we set in the Dockerfile to the corresponding project directories so that we can still use HMR and auto-rebuild. These new lines tell Docker Compose to build our images based on Dockerfiles in either the ./ or ./server directory for our web and API service respectively, so it will automatically build our Docker containers for us. Next, we have to deal with our services, like our KeyDB database, and our Nginx reverse proxy. We can set these services up very easily in our Docker Compose file like this:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
|
||||||
|
keydb:
|
||||||
|
image: eqalpha/keydb:alpine
|
||||||
|
restart: always
|
||||||
|
ports:
|
||||||
|
- '6379:6379'
|
||||||
|
volumes:
|
||||||
|
- keydb:/data
|
||||||
|
|
||||||
|
nginx:
|
||||||
|
image: nginx:alpine
|
||||||
|
ports:
|
||||||
|
- '3000:3000'
|
||||||
|
volumes:
|
||||||
|
- ./nginx.conf:/etc/nginx/nginx.conf:ro
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
keydb:
|
||||||
|
driver: local
|
||||||
|
```
|
||||||
|
|
||||||
|
There are a couple things to unpack here, so let’s break it down:
|
||||||
|
|
||||||
|
Both our Nginx and KeyDB service use an image that is on DockerHub, so we don't need to make the Dockerfile ourselves, instead, Docker Compose will pull the image automatically and have everything preconfigured for us, like every other image I have used in this article, I opted for the alpine images. Our KeyDB service uses an external volume defined in the volumes section at the bottom of the file so that we can have persistent data, which is important when we might have data we want to keep in databases. Furthermore, the KeyDB section has a `ports` key, which defines the ports that that container uses, and which port it should be mapped to on the host system. it’s not 100% necessary to have this port be exposed, but I like having it exposed so that I can look at the data in an external DB viewer without having to do complicated hacks to access the database in the container. Next, the KeyDB service has a key that tells it to restart always, so if it encounters a crash, or stops unexpectedly it will be restarted by docker automatically. Finally, in our Nginx service, we have a volume that points to a local file and also has `ro` at the end of it, so what gives? The local path mounts the nginx.conf file that we made previously in the article to the path that Nginx looks for the nginx config so that we can use our modified config, and the `ro` at the end of the file tells Docker to mount it as a read-only file in the container.
|
||||||
|
|
||||||
|
We’re not quite done yet though, because if you start up the containers you might notice that it might not work properly right away. Our `compose.yml` file should look like this so far:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
name: docker-compose-example
|
||||||
|
services:
|
||||||
|
web:
|
||||||
|
build: .
|
||||||
|
volumes:
|
||||||
|
- ./:/app
|
||||||
|
depends_on:
|
||||||
|
- api
|
||||||
|
|
||||||
|
api:
|
||||||
|
build: ./server
|
||||||
|
volumes:
|
||||||
|
- ./server:/app
|
||||||
|
|
||||||
|
keydb:
|
||||||
|
image: eqalpha/keydb:alpine
|
||||||
|
restart: always
|
||||||
|
ports:
|
||||||
|
- "6379:6379"
|
||||||
|
volumes:
|
||||||
|
- keydb:/data
|
||||||
|
|
||||||
|
nginx:
|
||||||
|
image: nginx:alpine
|
||||||
|
ports:
|
||||||
|
- "3000:3000"
|
||||||
|
volumes:
|
||||||
|
- ./nginx.conf:/etc/nginx/nginx.conf:ro
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
keydb:
|
||||||
|
driver: local
|
||||||
|
```
|
||||||
|
|
||||||
|
We need to make sure that all the docker containers startup in the correct order. For example, you wouldn't want your backend, which depends on the database to start before the database. To solve this, we can tell Docker Compose that containers depend on other containers using the “depends_on” key:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
name: docker-compose-example
|
||||||
|
services:
|
||||||
|
web:
|
||||||
|
build: .
|
||||||
|
volumes:
|
||||||
|
- ./:/app
|
||||||
|
depends_on: # [!code hl:2]
|
||||||
|
- api
|
||||||
|
|
||||||
|
api:
|
||||||
|
build: ./server
|
||||||
|
volumes:
|
||||||
|
- ./server:/app
|
||||||
|
depends_on: # [!code hl:2]
|
||||||
|
- keydb
|
||||||
|
|
||||||
|
keydb:
|
||||||
|
image: eqalpha/keydb:alpine
|
||||||
|
restart: always
|
||||||
|
ports:
|
||||||
|
- "6379:6379"
|
||||||
|
volumes:
|
||||||
|
- keydb:/data
|
||||||
|
|
||||||
|
nginx:
|
||||||
|
image: nginx:alpine
|
||||||
|
ports:
|
||||||
|
- "3000:3000"
|
||||||
|
volumes:
|
||||||
|
- ./nginx.conf:/etc/nginx/nginx.conf:ro
|
||||||
|
depends_on: # [!code hl:3]
|
||||||
|
- web
|
||||||
|
- api
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
keydb:
|
||||||
|
driver: local
|
||||||
|
```
|
||||||
|
|
||||||
|
Finally, you can now run this in the root directory of your project and it will automatically start up a db, the frontend and backend, and nginx to proxy the frontend and backend to one IP:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose up --build
|
||||||
|
```
|
||||||
|
|
||||||
|
and for subsequent runs of your project, assuming you don't change either of the Dockerfiles, you can lose the `--build` argument for much faster startup times. Now, if you open your browser to http://localhost:3000/ you can see the visit count and if you refresh the page, you can see it slowly tick up. If you want to spam the API with a ton of requests with Apache Bench, for example you can do so with this:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ab -c 100 -n 10000 localhost:3000/api/count
|
||||||
|
```
|
||||||
|
|
||||||
|
You will see that the counter ticks up exactly 10000 requests, and it will complete reasonably fast.
|
||||||
|
|
||||||
|
## Conclusion
|
||||||
|
|
||||||
|
In summary, Docker Compose is an amazing tool for managing full-stack projects efficiently. It abstracts away many of the complexities involved in the various services and dependencies, allowing you to focus on what really matters: building great projects. Once you get the hang of using Docker Compose, you will find yourself incorporating it into many of your projects, both new and old.
|
||||||
|
|
||||||
|
If you want to dive deeper into the example project I described in this post, feel free to check out the [Github repo](https://github.com/juls07/docker-compose-vue-example) for the full source code. Experimenting with Docker Compose in your own projects will help you appreciate its power and simplicity.
|
||||||
|
|
||||||
|
Thanks for reading, and I hope this article has sparked interest in Docker Compose. Whether you’re an experienced dev, or just starting out, give Docker Compose a try, and you may find it becomes an essential part of your development workflow!
|
||||||
@@ -1,20 +1,10 @@
|
|||||||
---
|
---
|
||||||
title: My nuxtjs + trpc fullstack web app
|
title: My nuxtjs + trpc fullstack web app
|
||||||
description: My experiences with nuxtjs 3 and tRPC
|
description: My experiences with nuxtjs 3 and tRPC
|
||||||
image:
|
image:
|
||||||
src: '/images/archlinux.webp'
|
src: "/images/archlinux.webp"
|
||||||
alt: 'Why I chose archlinux'
|
alt: "Why I chose archlinux"
|
||||||
head:
|
date: September 20, 2020
|
||||||
meta:
|
|
||||||
- name: 'keywords'
|
|
||||||
content: 'web dev, fullstack development, nuxtjs, tRPC, redis'
|
|
||||||
- name: 'robots'
|
|
||||||
content: 'index, follow'
|
|
||||||
- name: 'author'
|
|
||||||
content: 'juls07'
|
|
||||||
- name: 'copyright'
|
|
||||||
content: '© 2022 juls07'
|
|
||||||
date: 2022-09-20
|
|
||||||
_draft: true
|
_draft: true
|
||||||
tags:
|
tags:
|
||||||
- web dev
|
- web dev
|
||||||
|
|||||||
@@ -1,28 +1,19 @@
|
|||||||
---
|
---
|
||||||
title: Rebuilding my personal website!
|
title: Rebuilding my personal website!
|
||||||
description: My experiences with nuxtjs 3 and tRPC
|
description: My experiences with nuxtjs 3 and tRPC
|
||||||
image:
|
image:
|
||||||
src: '/images/website-rebuild.webp'
|
src: "/images/website-rebuild.webp"
|
||||||
alt: 'placeholder'
|
alt: "placeholder"
|
||||||
head:
|
date: January 01, 2023
|
||||||
meta:
|
|
||||||
- name: 'keywords'
|
|
||||||
content: 'web dev, fullstack development, nuxtjs, tRPC, redis'
|
|
||||||
- name: 'robots'
|
|
||||||
content: 'index, follow'
|
|
||||||
- name: 'author'
|
|
||||||
content: 'juls07'
|
|
||||||
- name: 'copyright'
|
|
||||||
content: '© 2022 juls07'
|
|
||||||
date: 2023-01-01
|
|
||||||
# _draft: true
|
|
||||||
tags:
|
tags:
|
||||||
- web dev
|
- web dev
|
||||||
- nuxtjs 3
|
- nuxtjs 3
|
||||||
---
|
---
|
||||||
|
|
||||||
For the past month I've been quietly rebuilding my portfolio site, designing and building a new site. My previous site was bland and dull, this one is like a whole new world. The old site was just a two colors that don't even go well together, it was clear to me that was no way to present myself. So about halfway though november I started fresh, with Nuxt3 and a whole lot of insomnia in my possession I went to figma to design my new site.
|
For the past month I've been quietly rebuilding my portfolio site, designing and building a new site. My previous site was bland and dull, this one is like a whole new world. The old site was just a two colors that don't even go well together, it was clear to me that was no way to present myself. So about halfway though november I started fresh, with Nuxt3 and a whole lot of insomnia in my possession I went to figma to design my new site.
|
||||||
<!--more-->
|
|
||||||
After a couple days of making a design in figma I setup a new nuxt3 project and went to work, after just a few weeks I had almost everything completed, but the last few things I had to do were to make the blogs page and finish the project cards. After a few weeks of designing and getting feedback from the [web dev and design]( https://discord.gg/web) discord server, mainly feedback from aqil#7927 an absolute machine in the visual design help channel. However, rebuilding the site still wasn't done, so about halfway through december I decided to change a few things, the typing animation on the home page was added that day and a few accessibility things here and there.
|
<!--more-->
|
||||||
|
|
||||||
|
After a couple days of making a design in figma I setup a new nuxt3 project and went to work, after just a few weeks I had almost everything completed, but the last few things I had to do were to make the blogs page and finish the project cards. After a few weeks of designing and getting feedback from the [web dev and design](https://discord.gg/web) discord server, mainly feedback from aqil#7927 an absolute machine in the visual design help channel. However, rebuilding the site still wasn't done, so about halfway through december I decided to change a few things, the typing animation on the home page was added that day and a few accessibility things here and there.
|
||||||
<br />
|
<br />
|
||||||
Once 2023 hit I started working on this much more, with my [100DaysOfCode Challenge](/blog/100daysofcode-challenge) completed I had much more time to work on this site. Mostly I just had to finish writing a few blog posts, and I would be able to finally publish the new site. So I got to writing this blog post and here we are right now. This post is just an update with the new site, here's the sites [github](https://github.com/juls0730/juls07.dev-v2) and that's all, checkout my [twitter](https://twitter.com/julie4055_) if you want to hear more about what I do, have a great 2023!
|
Once 2023 hit I started working on this much more, with my [100DaysOfCode Challenge](/blog/100daysofcode-challenge) completed I had much more time to work on this site. Mostly I just had to finish writing a few blog posts, and I would be able to finally publish the new site. So I got to writing this blog post and here we are right now. This post is just an update with the new site, here's the sites [github](https://github.com/juls0730/juls07.dev-v2) and that's all, checkout my [twitter](https://twitter.com/julie4055_) if you want to hear more about what I do, have a great 2023!
|
||||||
|
|||||||
@@ -1,21 +1,10 @@
|
|||||||
---
|
---
|
||||||
title: Running a qbittorrent webserver!
|
title: Running a qbittorrent webserver!
|
||||||
description: My experience with a qbittorrent webserver
|
description: My experience with a qbittorrent webserver
|
||||||
image:
|
image:
|
||||||
src: '/images/qbittorrent-web.webp'
|
src: "/images/qbittorrent-web.webp"
|
||||||
alt: 'Qbittorrent web server'
|
alt: "Qbittorrent web server"
|
||||||
head:
|
date: March 07, 2022
|
||||||
meta:
|
|
||||||
- name: 'keywords'
|
|
||||||
content: 'linux, bittorrent, docker, web server'
|
|
||||||
- name: 'robots'
|
|
||||||
content: 'index, follow'
|
|
||||||
- name: 'author'
|
|
||||||
content: 'juls07'
|
|
||||||
- name: 'copyright'
|
|
||||||
content: '© 2022 juls07'
|
|
||||||
date: 2022-03-07
|
|
||||||
# _draft: true
|
|
||||||
tags:
|
tags:
|
||||||
- Linux
|
- Linux
|
||||||
- Bittorrent
|
- Bittorrent
|
||||||
@@ -24,9 +13,11 @@ tags:
|
|||||||
---
|
---
|
||||||
|
|
||||||
Recently, I started running a qbittorrent webserver on my raspberry pi 4B+ using [this docker image](https://hotio.dev/containers/qbittorrent/), It has been quite a journey. First, I started looking for a way to run a qbittorrent in a container, this is how I run all the applications on my raspberry pi and I inevibly asked in the [Arch linux discord server](https://discord.gg/3m6dbPR) and I got a reply from @runsamok on discord and they reccomend the docker image I mentioned earlier.
|
Recently, I started running a qbittorrent webserver on my raspberry pi 4B+ using [this docker image](https://hotio.dev/containers/qbittorrent/), It has been quite a journey. First, I started looking for a way to run a qbittorrent in a container, this is how I run all the applications on my raspberry pi and I inevibly asked in the [Arch linux discord server](https://discord.gg/3m6dbPR) and I got a reply from @runsamok on discord and they reccomend the docker image I mentioned earlier.
|
||||||
<!--more-->
|
|
||||||
|
<!--more-->
|
||||||
|
|
||||||
Then I started seeding linux distros (obviously) and I started with a batch of about 7 give or take, Ubuntu 18.04, Ubuntu 20.04, Ubuntu 21.04, Arch Linux (duh), Rocky Linux, and tails linux. I shotly realized that Rocky and tails are baren wastelands when it comes to people wanting to torrent them so I eventually Scrapped them.
|
Then I started seeding linux distros (obviously) and I started with a batch of about 7 give or take, Ubuntu 18.04, Ubuntu 20.04, Ubuntu 21.04, Arch Linux (duh), Rocky Linux, and tails linux. I shotly realized that Rocky and tails are baren wastelands when it comes to people wanting to torrent them so I eventually Scrapped them.
|
||||||
<br />
|
<br />
|
||||||
So after about 10 or so days I wanted to automatically download Arch linux and ubuntu updates using RSS, the qbittorrent webserver has thought of that so you can easily so so in the RSS tab. However, When downloading the latest Arch Linux version it worked fine but I added an ubuntu feed that I accidentally downloaded the 10 most recent torrents, but I deleted them and moved on. Later down the road I was trying to download the march patch of the Arch Linux torrent but I could not, after a while of wondering why I realized my storage was full, my raspberry pi doesnt have a lot of storage but not a little, it turns out that the torrents I deleted from the Ubuntu RSS were still on the raspberry pi, this isnt the fault of the developers because I didnt check the "delete on disk" button but there was not "not enough space warning" message when I tried to download the Arch Linux torrent.
|
So after about 10 or so days I wanted to automatically download Arch linux and ubuntu updates using RSS, the qbittorrent webserver has thought of that so you can easily so so in the RSS tab. However, When downloading the latest Arch Linux version it worked fine but I added an ubuntu feed that I accidentally downloaded the 10 most recent torrents, but I deleted them and moved on. Later down the road I was trying to download the march patch of the Arch Linux torrent but I could not, after a while of wondering why I realized my storage was full, my raspberry pi doesnt have a lot of storage but not a little, it turns out that the torrents I deleted from the Ubuntu RSS were still on the raspberry pi, this isnt the fault of the developers because I didnt check the "delete on disk" button but there was not "not enough space warning" message when I tried to download the Arch Linux torrent.
|
||||||
<br />
|
<br />
|
||||||
Finally, after everything you're probably asking yourself if you should also host a bittorrent server, you should just as long as you have the free bandwidth and some extra computing power available. The Docker image I choose only takes about 30% cpu max so if you have a raspberry pi laying around it might be the perfect time to setup a bittorrent server. Thanks for reading if you havent already go ahead and follow my twitter [@julie4055_](https://twitter.com/julie4055_) and that'll be it have a great rest of your day!
|
Finally, after everything you're probably asking yourself if you should also host a bittorrent server, you should just as long as you have the free bandwidth and some extra computing power available. The Docker image I choose only takes about 30% cpu max so if you have a raspberry pi laying around it might be the perfect time to setup a bittorrent server. Thanks for reading if you havent already go ahead and follow my twitter [@julie4055\_](https://twitter.com/julie4055_) and that'll be it have a great rest of your day!
|
||||||
|
|||||||
@@ -1,21 +1,10 @@
|
|||||||
---
|
---
|
||||||
title: What I've been doing
|
title: What I've been doing
|
||||||
description: Stuff I've been doing recently
|
description: Stuff I've been doing recently
|
||||||
image:
|
image:
|
||||||
src: '/images/what-ive-been-doing.webp'
|
src: "/images/what-ive-been-doing.webp"
|
||||||
alt: "What I've been doing recently"
|
alt: "What I've been doing recently"
|
||||||
head:
|
date: May 21, 2022
|
||||||
meta:
|
|
||||||
- name: 'keywords'
|
|
||||||
content: 'web dev, fullstack development, angular, nodejs, mongodb'
|
|
||||||
- name: 'robots'
|
|
||||||
content: 'index, follow'
|
|
||||||
- name: 'author'
|
|
||||||
content: 'juls07'
|
|
||||||
- name: 'copyright'
|
|
||||||
content: '© 2022 juls07'
|
|
||||||
date: 2022-05-21
|
|
||||||
# _draft: true
|
|
||||||
tags:
|
tags:
|
||||||
- web dev
|
- web dev
|
||||||
- fullstack development
|
- fullstack development
|
||||||
@@ -25,27 +14,35 @@ tags:
|
|||||||
---
|
---
|
||||||
|
|
||||||
So I have been working on a social media site I am currently calling snowballsocial, so far I have users, posts, replies, and likes. Expressjs not having HTTP/2 is slightly annoying and I have kind of thought migrating all of my code to hapi but I'm too lazy to migrate all my code right now, especially since I just "asyncified" it, remaking my code rather than making all new code would probably be easier but I'll do that later.
|
So I have been working on a social media site I am currently calling snowballsocial, so far I have users, posts, replies, and likes. Expressjs not having HTTP/2 is slightly annoying and I have kind of thought migrating all of my code to hapi but I'm too lazy to migrate all my code right now, especially since I just "asyncified" it, remaking my code rather than making all new code would probably be easier but I'll do that later.
|
||||||
<!--more-->
|
|
||||||
|
<!--more-->
|
||||||
|
|
||||||
This project I again used mongoDB, I fist used mongodb in my [vuefullstack](https://github.com/juls0730/vuefullstack) app, and I loved it, it's much easier for the kind of projects I use than mySQL. Actually targeting a production site rather than just having fun like with my vuefullstack app or my rails-forum, etc. make me worry about alot more thigs like SEO, I tried to add OG tags to the post-show tag just showing post data but it doesnt work because the tags are added after the data loads.
|
This project I again used mongoDB, I fist used mongodb in my [vuefullstack](https://github.com/juls0730/vuefullstack) app, and I loved it, it's much easier for the kind of projects I use than mySQL. Actually targeting a production site rather than just having fun like with my vuefullstack app or my rails-forum, etc. make me worry about alot more thigs like SEO, I tried to add OG tags to the post-show tag just showing post data but it doesnt work because the tags are added after the data loads.
|
||||||
<br />
|
<br />
|
||||||
I've gotten a VPS from [linode](https://linode.com) so I could expose my non-static only page to the internet without exposing my home IP address, making me vulnerable to being doxxed and DDoS attacks. Using nignx I have reverse-proxied my api from port 3001 to 2087, and added ssl certs, and I have used it to server my static site on port 80, I have used nginx before and it's pretty simple to use. Optimizing has been the bain of development so far, making api requests faster, and reducing bundle size, I'm going to focus on api optimizations since its the harder one. My main worry for more than a couple requests is getting posts, since I do a for loop to change userId's to usernames
|
I've gotten a VPS from [linode](https://linode.com) so I could expose my non-static only page to the internet without exposing my home IP address, making me vulnerable to being doxxed and DDoS attacks. Using nignx I have reverse-proxied my api from port 3001 to 2087, and added ssl certs, and I have used it to server my static site on port 80, I have used nginx before and it's pretty simple to use. Optimizing has been the bain of development so far, making api requests faster, and reducing bundle size, I'm going to focus on api optimizations since its the harder one. My main worry for more than a couple requests is getting posts, since I do a for loop to change userId's to usernames
|
||||||
|
|
||||||
```js
|
```js
|
||||||
for (let i = 0; i < posts.length; i++) {
|
for (let i = 0; i < posts.length; i++) {
|
||||||
usermodel.findById(posts[i].creator, '-password -__v -followers -following -email', async function (err, user) {
|
usermodel.findById(
|
||||||
if (err) {
|
posts[i].creator,
|
||||||
return res.status(500).json({
|
"-password -__v -followers -following -email",
|
||||||
message: "Fetching posts failed"
|
async function (err, user) {
|
||||||
});
|
if (err) {
|
||||||
}
|
return res.status(500).json({
|
||||||
posts[i].creator = user;
|
message: "Fetching posts failed",
|
||||||
if (i === posts.length - 1) {
|
});
|
||||||
return res.status(200).json({
|
}
|
||||||
message: "Posts fetched successfully",
|
posts[i].creator = user;
|
||||||
posts: await posts,
|
if (i === posts.length - 1) {
|
||||||
maxPosts: await maxPosts
|
return res.status(200).json({
|
||||||
});
|
message: "Posts fetched successfully",
|
||||||
}
|
posts: await posts,
|
||||||
})
|
maxPosts: await maxPosts,
|
||||||
}
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
```
|
```
|
||||||
seen here is the code to change the userId's in get `/api/posts` so making at most 15 database requests per request, maybe a caching solution like redis might be applicable but I have never used something like that but I think I will definitely deploy something like that for caching. After everything If you want to hear about it more often follow me on [@julie4055_](https://twitter.com/julie4055_) and that'll be it have a great rest of your day!
|
|
||||||
|
seen here is the code to change the userId's in get `/api/posts` so making at most 15 database requests per request, maybe a caching solution like redis might be applicable but I have never used something like that but I think I will definitely deploy something like that for caching. After everything If you want to hear about it more often follow me on [@julie4055\_](https://twitter.com/julie4055_) and that'll be it have a great rest of your day!
|
||||||
|
|||||||
@@ -1,31 +1,22 @@
|
|||||||
---
|
---
|
||||||
title: Why I chose archlinux
|
title: Why I chose archlinux
|
||||||
description: All of my reasons for choosing archlinux
|
description: All of my reasons for choosing archlinux
|
||||||
image:
|
image:
|
||||||
src: /images/archlinux.webp
|
src: /images/archlinux.webp
|
||||||
alt: "Why I chose archlinux"
|
alt: "Why I chose archlinux"
|
||||||
head:
|
date: March 07, 2022
|
||||||
meta:
|
|
||||||
- name: 'keywords'
|
|
||||||
content: 'linux, archlinux'
|
|
||||||
- name: 'robots'
|
|
||||||
content: 'index, follow'
|
|
||||||
- name: 'author'
|
|
||||||
content: 'juls07'
|
|
||||||
- name: 'copyright'
|
|
||||||
content: '© 2022 juls07'
|
|
||||||
date: 2022-03-07
|
|
||||||
# _draft: true
|
|
||||||
tags:
|
tags:
|
||||||
- Linux
|
- Linux
|
||||||
- archlinux
|
- archlinux
|
||||||
---
|
---
|
||||||
|
|
||||||
I started using linux about 3 years ago, I started with Pop!_OS but after that I switched to fedora, then Zorin which is still the most beautiful out of the box linux distro I have ever used. After that howerver, I switched to arch and I've been on my misadventure's with arch for about 2 years now. I have a very minimalist approach to linux, trying to save as much space as possible save as much computer power as possible, arch being an extremely minimal install of linux was great for me.
|
I started using linux about 3 years ago, I started with Pop!\_OS but after that I switched to fedora, then Zorin which is still the most beautiful out of the box linux distro I have ever used. After that howerver, I switched to arch and I've been on my misadventure's with arch for about 2 years now. I have a very minimalist approach to linux, trying to save as much space as possible save as much computer power as possible, arch being an extremely minimal install of linux was great for me.
|
||||||
|
|
||||||
<!--more-->
|
<!--more-->
|
||||||
|
|
||||||
Next, being able to customize the install of arch is great, dont like systemd because of the company behind it, try runit or openrc, want btrfs so you can compress your files, go ahead, dont want grub, try systemdboot, etc. It's just the way arch lets you do whatever you want. Then, its the support from the [community discord](https://discord.gg/3m6dbPR) or the [arch wiki](https://wiki.archlinux.org/title/Main_page) if someone has had that problem theres a good chance you can get it fixed.
|
Next, being able to customize the install of arch is great, dont like systemd because of the company behind it, try runit or openrc, want btrfs so you can compress your files, go ahead, dont want grub, try systemdboot, etc. It's just the way arch lets you do whatever you want. Then, its the support from the [community discord](https://discord.gg/3m6dbPR) or the [arch wiki](https://wiki.archlinux.org/title/Main_page) if someone has had that problem theres a good chance you can get it fixed.
|
||||||
<br />
|
<br />
|
||||||
Overall, I know that arch isnt good for industrial applications since it doesnt scale well, its definitely for enthusiasts who have the time to work on their computer and not for linux novices. I love arch for its simplicity and flexibility and I think you should try it if your used to linux and want something new to play with. Using arch over the years has taught me so much about linux in general and if your a seasoned linux user give it a change, maybe you'll find the same love for it I did all the years ago.
|
Overall, I know that arch isnt good for industrial applications since it doesnt scale well, its definitely for enthusiasts who have the time to work on their computer and not for linux novices. I love arch for its simplicity and flexibility and I think you should try it if your used to linux and want something new to play with. Using arch over the years has taught me so much about linux in general and if your a seasoned linux user give it a change, maybe you'll find the same love for it I did all the years ago.
|
||||||
<br />
|
<br />
|
||||||
Thanks for reading, if you liked it go ahead and follow me on twitter [@julie4055_](https://twitter.com/julie4055_) or maybe just tell me how bad I am at
|
Thanks for reading, if you liked it go ahead and follow me on twitter [@julie4055\_](https://twitter.com/julie4055_) or maybe just tell me how bad I am at
|
||||||
making banners for my blogs, peace.
|
making banners for my blogs, peace.
|
||||||
|
|||||||
119
nuxt.config.ts
119
nuxt.config.ts
@@ -1,79 +1,72 @@
|
|||||||
import { defineNuxtConfig } from 'nuxt/config';
|
import { defineNuxtConfig } from 'nuxt/config';
|
||||||
// https://nuxt.com/docs/api/configuration/nuxt-config
|
// https://nuxt.com/docs/api/configuration/nuxt-config
|
||||||
export default defineNuxtConfig({
|
export default defineNuxtConfig({
|
||||||
app: {
|
app: {
|
||||||
head: {
|
head: {
|
||||||
title: 'Juls07',
|
title: 'Juls07',
|
||||||
|
|
||||||
meta: [
|
meta: [
|
||||||
{ charset: 'utf-8' },
|
{ charset: 'utf-8' },
|
||||||
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
|
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
|
||||||
|
|
||||||
// hid is used as unique identifier. Do not use `vmid` for it as it will not work
|
// hid is used as unique identifier. Do not use `vmid` for it as it will not work
|
||||||
{ hid: 'description', name: 'description', content: 'Juls07 is a fullstack web developer' }
|
{ hid: 'description', name: 'description', content: 'Juls07 is a fullstack web developer' }
|
||||||
],
|
],
|
||||||
link: [
|
link: [
|
||||||
{ rel: 'icon', href: '/favicon.png' }
|
{ rel: 'icon', href: '/favicon.png' }
|
||||||
],
|
],
|
||||||
htmlAttrs: {
|
htmlAttrs: {
|
||||||
lang: 'en'
|
lang: 'en'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
colorMode: {
|
colorMode: {
|
||||||
classSuffix: ''
|
classSuffix: ''
|
||||||
},
|
},
|
||||||
|
|
||||||
content: {
|
content: {
|
||||||
highlight: {
|
highlight: {
|
||||||
theme: {
|
theme: {
|
||||||
dark: 'min-dark',
|
dark: 'min-dark',
|
||||||
default: 'min-light'
|
default: 'min-light'
|
||||||
},
|
},
|
||||||
preload: ['json', 'js', 'ts', 'html', 'css', 'vue', 'svelte', 'diff', 'shell', 'markdown', 'yaml', 'bash', 'ini'],
|
preload: ['json', 'js', 'ts', 'html', 'css', 'vue', 'yaml', 'bash', 'go', 'docker', 'nginx'],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
css: [
|
css: [
|
||||||
'@/assets/css/main.css',
|
'@/assets/css/main.css',
|
||||||
],
|
],
|
||||||
|
|
||||||
|
|
||||||
devtools: {
|
devtools: {
|
||||||
enabled: true
|
enabled: true
|
||||||
},
|
},
|
||||||
|
|
||||||
experimental: {
|
experimental: {
|
||||||
payloadExtraction: true
|
payloadExtraction: true
|
||||||
},
|
},
|
||||||
|
|
||||||
image: {
|
image: {
|
||||||
provider: 'ipx'
|
provider: 'ipx'
|
||||||
},
|
},
|
||||||
|
|
||||||
modules: ['nuxt-icon', '@nuxt/content', '@nuxtjs/color-mode', '@nuxt/image'],
|
modules: ['nuxt-icon', '@nuxt/content', '@nuxtjs/color-mode', '@nuxt/image'],
|
||||||
|
|
||||||
nitro: {
|
nitro: {
|
||||||
preset: 'static',
|
plugins: ['plugins/read-time.ts'],
|
||||||
prerender: {
|
preset: 'static',
|
||||||
crawlLinks: true,
|
prerender: {
|
||||||
routes: ['/sitemap.xml', '/rss.xml']
|
crawlLinks: true,
|
||||||
},
|
routes: ['/sitemap.xml', '/rss.xml']
|
||||||
},
|
},
|
||||||
|
},
|
||||||
|
|
||||||
postcss: {
|
postcss: {
|
||||||
plugins: {
|
plugins: {
|
||||||
tailwindcss: {},
|
tailwindcss: {},
|
||||||
autoprefixer: {},
|
autoprefixer: {},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
router: {
|
|
||||||
options: {
|
|
||||||
strict: false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
sourcemap: false,
|
|
||||||
})
|
})
|
||||||
|
|||||||
11144
package-lock.json
generated
11144
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -11,7 +11,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@nuxt/devtools": "^1.1.5",
|
"@nuxt/devtools": "^1.1.5",
|
||||||
"@nuxt/image": "^1.5.0",
|
"@nuxt/image": "^1.5.0",
|
||||||
"@nuxtjs/color-mode": "^3.3.3",
|
"@nuxtjs/color-mode": "^3.4.0",
|
||||||
"@types/rss": "^0.0.32",
|
"@types/rss": "^0.0.32",
|
||||||
"autoprefixer": "^10.4.15",
|
"autoprefixer": "^10.4.15",
|
||||||
"nuxt": "^3.7.0",
|
"nuxt": "^3.7.0",
|
||||||
@@ -24,6 +24,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nuxt/content": "^2.12.1",
|
"@nuxt/content": "^2.12.1",
|
||||||
"@vueuse/core": "^10.9.0",
|
"@vueuse/core": "^10.9.0",
|
||||||
|
"sharp": "^0.33.3",
|
||||||
"xml": "^1.0.1"
|
"xml": "^1.0.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,46 +1,162 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { withoutTrailingSlash } from 'ufo'
|
||||||
|
import { useBrowserLocation, useClipboard } from '@vueuse/core';
|
||||||
|
const { copy, copied } = useClipboard();
|
||||||
|
const location = useBrowserLocation()
|
||||||
|
|
||||||
let year = new Date().getFullYear();
|
let year = new Date().getFullYear();
|
||||||
|
|
||||||
|
let route = useRoute();
|
||||||
|
const { data: doc } = await useAsyncData('blog', () => queryContent(route.path).findOne())
|
||||||
|
if (!doc.value) {
|
||||||
|
throw createError({ statusCode: 404, statusMessage: 'Article not found', fatal: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data: surround } = await useAsyncData(`${route.path}-surround`, () => queryContent('/blog')
|
||||||
|
.where({ _extension: 'md' })
|
||||||
|
.only(['title', 'description', '_path'])
|
||||||
|
.sort({ date: 1 })
|
||||||
|
.where({ _draft: false })
|
||||||
|
.findSurround(withoutTrailingSlash(route.path))
|
||||||
|
)
|
||||||
|
|
||||||
|
const activeTocId: Ref<string | null> = ref(null)
|
||||||
|
const nuxtContent = ref(null)
|
||||||
|
|
||||||
|
const updateActiveHeading = () => {
|
||||||
|
const headings = document.querySelectorAll('#main h2[id], #main h3[id]')
|
||||||
|
const windowHeight = window.innerHeight
|
||||||
|
const windowMidpoint = windowHeight / 2
|
||||||
|
|
||||||
|
headings.forEach((heading) => {
|
||||||
|
const headingRect = heading.getBoundingClientRect()
|
||||||
|
const headingBottom = headingRect.bottom
|
||||||
|
|
||||||
|
if (headingBottom <= windowMidpoint) {
|
||||||
|
activeTocId.value = heading.id
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
window.addEventListener('scroll', updateActiveHeading)
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
window.removeEventListener('scroll', updateActiveHeading)
|
||||||
|
})
|
||||||
|
|
||||||
|
useSeoMeta({
|
||||||
|
title: doc.value?.title,
|
||||||
|
description: doc.value?.description,
|
||||||
|
ogTitle: doc.value?.title,
|
||||||
|
ogDescription: doc.value?.description,
|
||||||
|
ogImage: doc.value?.image.src,
|
||||||
|
ogUrl: 'https://juls07.dev',
|
||||||
|
twitterTitle: doc.value?.title,
|
||||||
|
twitterDescription: doc.value?.description,
|
||||||
|
twitterImage: doc.value?.image.src,
|
||||||
|
twitterCard: 'summary_large_image',
|
||||||
|
})
|
||||||
|
|
||||||
|
useHead({
|
||||||
|
htmlAttrs: {
|
||||||
|
lang: 'en'
|
||||||
|
},
|
||||||
|
meta: [
|
||||||
|
{
|
||||||
|
name: "copyright",
|
||||||
|
content: "© ${year} juls07"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "robots",
|
||||||
|
content: "index, follow"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "keywords",
|
||||||
|
content: doc.value?.tags.join(", ")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "author",
|
||||||
|
content: "juls07",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
link: [
|
||||||
|
{
|
||||||
|
rel: 'icon',
|
||||||
|
type: 'image/png',
|
||||||
|
href: '/favicon.png'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="min-h-screen">
|
<div class="min-h-screen">
|
||||||
<Nav />
|
<Nav />
|
||||||
<div class="grid grid-cols-12 gap-5 justify-center">
|
<div class="grid grid-cols-12 gap-x-5 justify-center pt-6 mb-4 relative">
|
||||||
<div class="pt-6 mb-4 !col-start-2 md:!col-start-3 lg:!col-start-4 lg:col-span-6 md:col-span-8 col-span-10">
|
<div class="!col-start-2 md:!col-start-3 lg:!col-start-4 lg:col-span-6 md:col-span-8 col-span-10 order-1">
|
||||||
<ContentDoc v-slot="{ doc }">
|
<NuxtImg :src="doc.image.src" class="mb-2 rounded-md drop-shadow w-full" quality="80" />
|
||||||
<img :src="doc.image.src" class="mb-2 rounded-md drop-shadow" />
|
<h1 class="text-3xl dark:text-gray-100 md:text-4xl font-semibold mb-2">{{ doc.title }}</h1>
|
||||||
<h1 class="text-3xl dark:text-gray-100 md:text-4xl font-semibold mb-2">{{ doc.title }}</h1>
|
<p class="mb-1 dark:text-zinc-400 text-zinc-600">
|
||||||
<p class="mb-1 dark:text-zinc-400 text-zinc-600">
|
{{ doc.description }}
|
||||||
{{ doc.description }}
|
</p>
|
||||||
</p>
|
<p class="mb-2 text-zinc-500">
|
||||||
<p class="mb-2 text-zinc-500">
|
{{ new Date(doc.date).toDateString().split(' ').slice(1).join(' ') }} |
|
||||||
{{ new Date(doc.date).toDateString().split(' ').slice(1).join(' ') }}
|
{{ doc.readTime }} minute read
|
||||||
</p>
|
</p>
|
||||||
<div class="flex flex-wrap w-full gap-2 justify-start mb-3">
|
<div class="flex flex-wrap w-full gap-2 justify-start mb-3">
|
||||||
<IconTag v-for="tag in doc.tags" :name="tag" :iconName='tag' isTag="true" />
|
<IconTag v-for="tag in doc.tags" :name="tag" :iconName='tag' isTag="true" />
|
||||||
</div>
|
</div>
|
||||||
<main id="main" class="leading-relaxed">
|
<hr class="mb-4 border-[#ECE6E7] dark:border-[#232326] border-t" />
|
||||||
<ContentRenderer class="dark:text-gray-200 text-gray-800" :value="doc" />
|
</div>
|
||||||
</main>
|
<div class="!col-start-2 md:!col-start-3 lg:!col-start-4 lg:col-span-6 md:col-span-8 col-span-10 order-3">
|
||||||
</ContentDoc>
|
<main id="main" class="leading-relaxed">
|
||||||
</div>
|
<ContentRenderer ref="nuxtContent" class="dark:text-gray-200 text-gray-800" :value="doc" />
|
||||||
</div>
|
</main>
|
||||||
<footer class="grid grid-cols-12 gap-5 justify-center">
|
</div>
|
||||||
<div class="py-2 mb-4 !col-start-3 md:!col-start-4 xl:!col-start-5 xl:col-span-4 md:col-span-6 col-span-8">
|
<nav
|
||||||
<!-- <NewsletterSignup class="mb-2" /> -->
|
class="lg:ml-2 mb-3 lg:mb-0 col-start-2 md:col-start-3 lg:block lg:col-start-10 lg:col-span-2 md:col-span-8 col-span-10 lg:sticky lg:top-8 h-fit order-2 lg:order-4">
|
||||||
© {{ year }} Juls07 - GPL v3.0 License
|
<TableOfContents :doc="doc" :activeTocId="activeTocId" />
|
||||||
</div>
|
</nav>
|
||||||
</footer>
|
<div class="!col-start-2 md:!col-start-3 lg:!col-start-4 lg:col-span-6 md:col-span-8 col-span-10 order-5">
|
||||||
</div>
|
<div class="flex justify-between mt-10">
|
||||||
|
<NuxtLink class="flex items-center text-fuschia hover:underline visited:bg-rose-700" to="/blog">
|
||||||
|
← Back to Blog
|
||||||
|
</NuxtLink>
|
||||||
|
<button class="flex items-center px-2 py-1" @click="copy(location.origin + location.pathname)">
|
||||||
|
<Icon v-if="copied" name="tabler:check" class="mr-1.5" size="20" />
|
||||||
|
<Icon v-else name="tabler:link" class="mr-1.5" size="20" />
|
||||||
|
Copy Link
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div v-if="surround">
|
||||||
|
<hr class="my-6 border-[#ECE6E7] dark:border-[#232326] border-t" />
|
||||||
|
<div class="grid gap-8 grid-cols-1 sm:grid-cols-2">
|
||||||
|
<MiniBlogCard v-if="surround[0]" :to="surround[0]._path" :title="surround[0].title"
|
||||||
|
:description="surround[0].description" />
|
||||||
|
<MiniBlogCard class="col-start-2" v-if="surround[1]" :to="surround[1]._path"
|
||||||
|
:title="surround[1].title" :right-align="true" :description="surround[1].description" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<footer class="grid grid-cols-12 gap-5 justify-center">
|
||||||
|
<div
|
||||||
|
class="pt-6 mb-4 !col-start-2 md:!col-start-3 lg:!col-start-4 lg:col-span-6 md:col-span-8 col-span-10 text-center">
|
||||||
|
© {{ year }} Juls07 - GPL v3.0 License
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
br {
|
br {
|
||||||
@apply my-3;
|
@apply my-3;
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
#main a {
|
#main a {
|
||||||
@apply text-fuschia hover:underline visited:bg-rose-700;
|
@apply text-fuschia hover:underline visited:bg-rose-700;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,70 +1,69 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
useHead({
|
||||||
|
title: 'Juls07',
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="min-h-screen">
|
<div class="min-h-screen">
|
||||||
<Nav />
|
<Nav />
|
||||||
<main>
|
<main>
|
||||||
<div id="main" class="gap-4 justify-evenly py-2 grid grid-cols-[repeat(auto-fit,_minmax(50px,_450px))]">
|
<div id="main" class="gap-4 justify-evenly py-2 grid grid-cols-[repeat(auto-fit,_minmax(50px,_450px))]">
|
||||||
<ContentQuery path="blog" :only="['image', '_path', 'title', 'description', 'date', 'tags', 'excerpt']"
|
<ContentQuery path="blog"
|
||||||
:sort="{
|
:only="['image', '_path', 'title', 'description', 'date', 'tags', 'excerpt', 'readTime']" :sort="{
|
||||||
date: -1
|
date: 1
|
||||||
}" :where="{
|
}" :where="{
|
||||||
_draft: false
|
_draft: false
|
||||||
}" v-slot="{ data }">
|
}" v-slot="{ data }">
|
||||||
<div v-for="article in data" :key="article._path" class="mb-5 px-1.5">
|
<div v-for="article in data" :key="article._path" class="mb-5 px-1.5">
|
||||||
<div
|
<div
|
||||||
class="dark:bg-dark-slate bg-touched-lavender max-h-[563.25px] h-[563.25px] overflow-hidden rounded-lg border border-soft-lilac dark:border-midnight-slate/30 shadow-md">
|
class="dark:bg-dark-slate bg-touched-lavender max-h-[563.25px] h-[563.25px] overflow-hidden rounded-lg border border-soft-lilac dark:border-midnight-slate/30 shadow-md">
|
||||||
<NuxtImg v-if="article.image.src" :src="article.image.src"
|
<NuxtImg v-if="article.image" :src="article.image.src" width="464" densities="1x 2x"
|
||||||
class="w-full rounded-tl-lg rounded-tr-lg aspect-video" loading="lazy" />
|
quality="80" class="w-full rounded-tl-lg rounded-tr-lg aspect-video" loading="lazy" />
|
||||||
<div
|
<div
|
||||||
class="p-3 overflow-hidden pt-2 text-fade dark:before:bg-[linear-gradient(180deg,transparent_0,hsla(0,0%,5%,0)_36%,#0C0B0C_95%,#0C0B0C)] before:bg-[linear-gradient(180deg,transparent_0,hsla(0,0%,5%,0)_36%,#F5EDFE_95%,#F5EDFE)] mb-1 pb-1 relative">
|
class="p-3 overflow-hidden pt-2 text-fade dark:before:bg-[linear-gradient(180deg,transparent_0,hsla(0,0%,5%,0)_36%,#0C0B0C_95%,#0C0B0C)] before:bg-[linear-gradient(180deg,transparent_0,hsla(0,0%,5%,0)_36%,#F5EDFE_95%,#F5EDFE)] mb-1 pb-1 relative">
|
||||||
<h3>
|
<h3>
|
||||||
<nuxt-link tabindex="0" class="text-lg" :to="article._path">
|
<nuxt-link tabindex="0" class="text-lg" :to="article._path">
|
||||||
{{ article.title }}
|
{{ article.title }}
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
</h3>
|
</h3>
|
||||||
<p class="dark:text-zinc-400 text-zinc-600">
|
<p class="dark:text-zinc-400 text-zinc-600">
|
||||||
{{ article.description }}
|
{{ article.description }}
|
||||||
</p>
|
</p>
|
||||||
<p class="text-zinc-500">
|
<p class="text-zinc-500">
|
||||||
{{ new Date(article.date).toDateString().split(' ').slice(1).join(' ') }}
|
{{ new Date(article.date).toDateString().split(' ').slice(1).join(' ') }} |
|
||||||
</p>
|
{{ article.readTime }} minute read
|
||||||
<p class="dark:text-zinc-200 text-zinc-800 max-h-[13.75rem]">
|
</p>
|
||||||
<ContentDoc :head="false" :value="article" :path="article._path" v-slot="{ doc }">
|
<p class="dark:text-zinc-200 text-zinc-800 max-h-[13.75rem]">
|
||||||
<div class="flex flex-wrap w-full gap-2 justify-start my-1">
|
<div class="flex flex-wrap w-full gap-2 justify-start my-1">
|
||||||
<IconTag v-for="tag in doc.tags" :name="tag" :iconName='tag' isTag="true" />
|
<IconTag v-for="tag in article.tags" :name="tag" :iconName='tag' isTag="true" />
|
||||||
</div>
|
</div>
|
||||||
<div class="max-h-full leading-relaxed">
|
<div class="max-h-full leading-relaxed">
|
||||||
<ContentRenderer :value="doc">
|
<ContentRenderer :value="article">
|
||||||
<ContentRendererMarkdown :value="doc" :excerpt="true" />
|
<ContentRendererMarkdown :value="article" :excerpt="true" />
|
||||||
</ContentRenderer>
|
</ContentRenderer>
|
||||||
</div>
|
</div>
|
||||||
</ContentDoc>
|
</p>
|
||||||
</p>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</ContentQuery>
|
||||||
</ContentQuery>
|
</div>
|
||||||
</div>
|
</main>
|
||||||
</main>
|
</div>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.text-fade::before {
|
.text-fade::before {
|
||||||
content: '';
|
content: '';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 33.333333%;
|
height: 33.333333%;
|
||||||
left: 0px;
|
left: 0px;
|
||||||
bottom: 0px;
|
bottom: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#main a {
|
#main a {
|
||||||
@apply text-fuschia hover:underline visited:bg-rose-700;
|
@apply text-fuschia hover:underline visited:bg-rose-700;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
useHead({
|
|
||||||
title: 'Juls07',
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|||||||
325
pages/index.vue
325
pages/index.vue
@@ -1,9 +1,10 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
let colorMode = useColorMode();
|
||||||
let today = new Date();
|
let today = new Date();
|
||||||
let age = today.getFullYear() - 2008;
|
let age = today.getFullYear() - 2008;
|
||||||
|
|
||||||
if (new Date(today.getFullYear() + "-07-30") > today) {
|
if (new Date(today.getFullYear() + "-07-30") > today) {
|
||||||
age--;
|
age--;
|
||||||
}
|
}
|
||||||
|
|
||||||
let tagLine = ref("");
|
let tagLine = ref("");
|
||||||
@@ -13,182 +14,204 @@ let displayTextArrayIndex = ref(0);
|
|||||||
let charIndex = ref(0);
|
let charIndex = ref(0);
|
||||||
|
|
||||||
const typeText = () => {
|
const typeText = () => {
|
||||||
if (charIndex.value < displayTextArray[displayTextArrayIndex.value].length) {
|
if (charIndex.value < displayTextArray[displayTextArrayIndex.value].length) {
|
||||||
if (!typeStatus.value) typeStatus.value = true;
|
if (!typeStatus.value) typeStatus.value = true;
|
||||||
tagLine.value += displayTextArray[displayTextArrayIndex.value].charAt(
|
tagLine.value += displayTextArray[displayTextArrayIndex.value].charAt(
|
||||||
charIndex.value
|
charIndex.value
|
||||||
);
|
);
|
||||||
charIndex.value += 1;
|
charIndex.value += 1;
|
||||||
setTimeout(typeText, 100);
|
setTimeout(typeText, 100);
|
||||||
} else {
|
} else {
|
||||||
typeStatus.value = false;
|
typeStatus.value = false;
|
||||||
setTimeout(eraseText, Math.min(Math.floor(Math.random() * 2001), 1750));
|
setTimeout(eraseText, Math.min(Math.floor(Math.random() * 2001), 1750));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const eraseText = () => {
|
const eraseText = () => {
|
||||||
if (charIndex.value > 0) {
|
if (charIndex.value > 0) {
|
||||||
if (!typeStatus.value) typeStatus.value = true;
|
if (!typeStatus.value) typeStatus.value = true;
|
||||||
tagLine.value = displayTextArray[displayTextArrayIndex.value].substring(
|
tagLine.value = displayTextArray[displayTextArrayIndex.value].substring(
|
||||||
0,
|
0,
|
||||||
charIndex.value - 1
|
charIndex.value - 1
|
||||||
);
|
);
|
||||||
charIndex.value -= 1;
|
charIndex.value -= 1;
|
||||||
setTimeout(eraseText, 50);
|
setTimeout(eraseText, 50);
|
||||||
} else {
|
} else {
|
||||||
typeStatus.value = false;
|
typeStatus.value = false;
|
||||||
displayTextArrayIndex.value += 1;
|
displayTextArrayIndex.value += 1;
|
||||||
if (displayTextArrayIndex.value >= displayTextArray.length)
|
if (displayTextArrayIndex.value >= displayTextArray.length)
|
||||||
displayTextArrayIndex.value = 0;
|
displayTextArrayIndex.value = 0;
|
||||||
setTimeout(typeText, 1100);
|
setTimeout(typeText, 1100);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
setTimeout(typeText, 2200);
|
setTimeout(typeText, 2200);
|
||||||
})
|
})
|
||||||
|
|
||||||
useHead({
|
useHead({
|
||||||
title: 'Juls07',
|
title: 'Juls07',
|
||||||
})
|
})
|
||||||
|
|
||||||
|
type ProjectIcon = {
|
||||||
|
icon: string,
|
||||||
|
name: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
type Project = {
|
||||||
|
headerIcon: string | string[],
|
||||||
|
name: string,
|
||||||
|
githubLink?: string,
|
||||||
|
externalLink?: string,
|
||||||
|
body: string,
|
||||||
|
icons: ProjectIcon[],
|
||||||
|
}
|
||||||
|
|
||||||
|
let projects: Project[] = [
|
||||||
|
{
|
||||||
|
headerIcon: "mdi:language-rust",
|
||||||
|
name: "CappuccinOS",
|
||||||
|
githubLink: "https://github.com/juls0730/CappuccinOS",
|
||||||
|
body: "CappuccinOS is a Rust operating system built from scratch targeting x86_64. This project has oppened my eyes to low level development and let me learn a lot about Rust.",
|
||||||
|
icons: [{ icon: 'skill-icons:rust', name: 'Rust' }]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headerIcon: "tabler:brand-nuxt",
|
||||||
|
name: "Juls07.dev",
|
||||||
|
githubLink: "https://github.com/juls0730/juls07.dev",
|
||||||
|
body: "This is the site you're currently on, a simple but decent enough portfolio and blog site.",
|
||||||
|
icons: [{ icon: 'logos:nuxt-icon', name: 'nuxtjs v3' }]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headerIcon: "mdi:language-php",
|
||||||
|
name: "PHP Forum",
|
||||||
|
githubLink: "https://github.com/juls0730/php-forum",
|
||||||
|
body: "This was my first fullstack project and one of my first projects I ever released the source code of.",
|
||||||
|
icons: [{ icon: 'logos:laravel', name: 'php' }, { icon: 'logos:mysql-icon', name: 'MySQL' }]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headerIcon: "tabler:brand-vue",
|
||||||
|
name: "vuefullstack",
|
||||||
|
githubLink: "https://github.com/juls0730/vuefullstack",
|
||||||
|
body: "I used this project to learn more about fullstack web development using vue.js. It's just a simple forum, similar to reddit, but much simpler.",
|
||||||
|
icons: [{ icon: 'logos:vue', name: 'vue v3' }, { icon: 'logos:nodejs-icon-alt', name: 'node.js' }, { icon: 'logos:mongodb-icon', name: 'Mongodb' }]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headerIcon: "tabler:brand-nuxt",
|
||||||
|
name: "Discord Clone",
|
||||||
|
githubLink: "https://github.com/juls0730/discord-clone",
|
||||||
|
body: "Excited by Nuxt 3's recent stable release at the time, I jumped on the opportunity to build a fullstack application with it. This project was an amazing learning experience and I had a blast making it!",
|
||||||
|
icons: [{ icon: 'logos:nuxt-icon', name: 'nuxtjs v3' }, { icon: 'logos:docker-icon', name: 'Docker' }, { icon: 'logos:postgresql', name: 'Postgres' }]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headerIcon: "tabler:code-dots",
|
||||||
|
name: "100DaysOfCode",
|
||||||
|
githubLink: "https://github.com/juls0730/100DaysOfCode",
|
||||||
|
body: "In the middle of september 2022, I started a 100 days of code challenge, and I wanted to make a UI framework similar to Vue or Svelte. I learned a ton and had a bunch of fun!",
|
||||||
|
icons: [{ icon: 'logos:nodejs-icon-alt', name: 'node.js' }, { icon: 'logos:typescript-icon', name: 'Typescript' }]
|
||||||
|
}
|
||||||
|
];
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="min-h-screen">
|
<div class="min-h-screen">
|
||||||
<Nav class="absolute z-10 text-white" />
|
<Nav class="absolute z-10 text-white" />
|
||||||
<header class="w-full h-[50vh] sm:h-[40vh] md:h-[60vh]">
|
<header class="w-full h-[50vh] sm:h-[40vh] md:h-[60vh]">
|
||||||
<div
|
<div
|
||||||
class="p-6 bg-[#1212121A] justify-center grid sm:grid-cols-12 gap-5 items-center sm:justify-start w-full h-full blur-background">
|
class="p-6 bg-[#1212121A] justify-center grid sm:grid-cols-12 gap-5 items-center sm:justify-start w-full h-full blur-background">
|
||||||
<div
|
<div
|
||||||
class="sm:h-32 sm:!col-start-2 sm:col-span-8 md:col-span-6 lg:col-span-5 w-32 sm:w-fit max-h-full md:h-40 items-center grid grid-rows-1 grid-cols-1 sm:grid-cols-2 drop-shadow-md">
|
class="sm:h-32 sm:!col-start-2 sm:col-span-8 md:col-span-6 lg:col-span-5 w-32 sm:w-fit max-h-full md:h-40 items-center grid grid-rows-1 grid-cols-1 sm:grid-cols-2 drop-shadow-md">
|
||||||
<img alt="juls07 profile picture" src="~/assets/images/juls07.png"
|
<img alt="juls07 profile picture" src="~/assets/images/juls07.png"
|
||||||
class="h-32 md:h-40 max-h-full rounded-full mb-3 sm:mb-0 sm:mr-2" />
|
class="h-32 md:h-40 max-h-full rounded-full mb-3 sm:mb-0 sm:mr-2" />
|
||||||
<div class="grid grid-rows-5 grid-cols-1 h-fit">
|
<div class="grid grid-rows-5 grid-cols-1 h-fit">
|
||||||
<h1 class="text-4xl md:text-5xl row-span-3 font-jetbrains text-white">Juls07</h1>
|
<h1 class="text-4xl md:text-5xl row-span-3 font-jetbrains text-white">Juls07</h1>
|
||||||
<p class="text-sea-green row-span-2 md:text-lg h-fit">
|
<p class="text-sea-green row-span-2 md:text-lg h-fit">
|
||||||
<span
|
<span
|
||||||
class="after:border after:h-[1.0em] after:border-current after:inline-block after:ml-0.5 after:animate-blink">
|
class="after:border after:h-[1.0em] after:border-current after:inline-block after:ml-0.5 after:animate-blink">
|
||||||
{{ tagLine }}
|
{{ tagLine }}
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<main id="main" class="grid grid-cols-12 gap-5 justify-center">
|
<main id="main" class="grid grid-cols-12 gap-5 justify-center">
|
||||||
<section
|
<section
|
||||||
class="py-6 mb-4 !col-start-2 md:!col-start-3 lg:!col-start-4 lg:col-span-6 md:col-span-8 col-span-10">
|
class="py-6 mb-4 !col-start-2 md:!col-start-3 lg:!col-start-4 lg:col-span-6 md:col-span-8 col-span-10">
|
||||||
<h2 class="md:text-4xl text-3xl mb-1">About me</h2>
|
<h2 class="md:text-4xl text-3xl mb-1">About me</h2>
|
||||||
<hr class="border-2 my-1.5 border-fuschia rounded-md w-7/12 min-w-[200px] max-w-xs lg:max-w-sm mb-1" />
|
<hr class="border-2 my-1.5 border-fuschia rounded-md w-7/12 min-w-[200px] max-w-xs lg:max-w-sm mb-1" />
|
||||||
<p class="max-w-md sm:max-w-xl md:max-w-3xl lg:max-w-4xl mb-4">
|
<p class="max-w-md sm:max-w-xl md:max-w-3xl lg:max-w-4xl mb-4">
|
||||||
Hi there, I'm juls07, I am {{ age }} years old and I <span tabindex="0" class="love">love</span> web
|
Hi there, I'm juls07, I am {{ age }} years old and I <span tabindex="0" class="love">love</span> web
|
||||||
development. I first dabbled in web development
|
development. I first dabbled in web development when I was ten, and here we are today! I mainly use
|
||||||
when I was ten, and here we are today! I mainly use
|
<a href="https://nuxt.com">NuxtJs</a> to build my websites since I absolutely adore Vuejs. For me, I
|
||||||
<a href="https://nuxt.com">NuxtJs</a> to
|
love being able to imagine anything and it to come to life. Finally, go checkout my <a
|
||||||
build my websites since I absolutely
|
href="https://github.com/juls0730">Github</a> and also my <a
|
||||||
adore Vuejs. For me, I love being able to imagine anything and it to come to life. Finally, go
|
href="https://twitter.com/julie4055_">Twitter</a>.
|
||||||
checkout my <a href="https://github.com/juls0730">Github</a> and also my <a
|
</p>
|
||||||
href="https://twitter.com/julie4055_">Twitter</a>.
|
<h3 class="text-2xl md:text-3xl mb-1.5 ml-0.5">Skills</h3>
|
||||||
</p>
|
<section class="flex flex-wrap w-full gap-2 justify-start ml-1">
|
||||||
<h3 class="text-2xl md:text-3xl mb-1.5 ml-0.5">Skills</h3>
|
<IconTag name="TypeScript" iconName="skill-icons:typescript" />
|
||||||
<section class="flex flex-wrap w-full gap-2 justify-start ml-1">
|
<IconTag name="Nuxt.js" :iconName="'skill-icons:nuxtjs-' + $colorMode.value" />
|
||||||
<IconTag name="TypeScript" iconName="skill-icons:typescript" />
|
<IconTag name="Vue.js" :iconName="'skill-icons:vuejs-' + $colorMode.value" />
|
||||||
<IconTag name="Nuxt.js" iconName="skill-icons:nuxtjs-dark" />
|
<IconTag name="Ruby on rails" iconName="skill-icons:rails" />
|
||||||
<IconTag name="Vue.js" iconName="skill-icons:vuejs-dark" />
|
<IconTag name="php" :iconName="'skill-icons:php-' + $colorMode.value" />
|
||||||
<IconTag name="Ruby on rails" iconName="skill-icons:rails" />
|
<IconTag name="React" :iconName="'skill-icons:react-' + $colorMode.value" />
|
||||||
<IconTag name="php" iconName="skill-icons:php-dark" />
|
<IconTag name="Bash" :iconName="'skill-icons:bash-' + $colorMode.value" />
|
||||||
<IconTag name="React" iconName="skill-icons:react-dark" />
|
<IconTag name="Tailwindcss" :iconName="'skill-icons:tailwindcss-' + $colorMode.value" />
|
||||||
<IconTag name="Bash" iconName="skill-icons:bash-dark" />
|
<IconTag name="Rust" iconName="skill-icons:rust" />
|
||||||
<IconTag name="Tailwindcss" iconName="skill-icons:tailwindcss-dark" />
|
<IconTag name="Node.js" :iconName="'skill-icons:nodejs-' + $colorMode.value" />
|
||||||
<IconTag name="Rust" iconName="skill-icons:rust" />
|
<IconTag name="Svelte" iconName="skill-icons:svelte" />
|
||||||
<IconTag name="Node.js" iconName="skill-icons:nodejs-dark" />
|
<IconTag name="Figma" :iconName="'skill-icons:figma-' + $colorMode.value" />
|
||||||
<IconTag name="Svelte" iconName="skill-icons:svelte" />
|
<IconTag name="Electron" iconName="skill-icons:electron" />
|
||||||
<IconTag name="Figma" iconName="skill-icons:figma-dark" />
|
<IconTag name="Cypress" :iconName="'skill-icons:cypress-' + $colorMode.value" />
|
||||||
<IconTag name="Electron" iconName="skill-icons:electron" />
|
<IconTag name="Prisma" iconName="skill-icons:prisma" />
|
||||||
<IconTag name="Cypress" iconName="skill-icons:cypress-dark" />
|
<IconTag name="Docker" icon-name="skill-icons:docker" />
|
||||||
<IconTag name="Prisma" iconName="skill-icons:prisma" />
|
</section>
|
||||||
<IconTag name="Docker" icon-name="skill-icons:docker" />
|
</section>
|
||||||
</section>
|
<div class="!col-start-2 md:!col-start-3 lg:!col-start-4 lg:col-span-6 md:col-span-8 col-span-10">
|
||||||
</section>
|
<h2 class="md:text-4xl text-3xl mb-1">Projects</h2>
|
||||||
<div class="!col-start-2 md:!col-start-3 lg:!col-start-4 lg:col-span-6 md:col-span-8 col-span-10">
|
</div>
|
||||||
<h2 class="md:text-4xl text-3xl mb-1">Projects</h2>
|
<!-- <section
|
||||||
</div>
|
class="pb-6 grid mb-4 col-start-2 xs:!col-start-3 col-span-10 xs:col-span-8 grid-cols-12 gap-y-6 sm:gap-x-6 max-w-full"> -->
|
||||||
<section
|
<section
|
||||||
class="pb-6 grid mb-4 col-start-2 xs:!col-start-3 col-span-10 xs:col-span-8 grid-cols-12 gap-y-6 sm:gap-x-6 max-w-full">
|
class="max-w-full col-start-2 xs:!col-start-3 col-span-10 xs:col-span-8 pb-6 mb-4 gap-4 justify-evenly grid grid-cols-1 smyes :grid-cols-[repeat(auto-fit,_minmax(50px,_340px))] lg:grid-cols-[repeat(auto-fit,_minmax(50px,_300px))]">
|
||||||
<ProjectCard name="Juls07.dev V2" headerIcon="tabler:brand-nuxt"
|
<ProjectCard v-for="project in projects" :name="project.name" :headerIcon="project.headerIcon"
|
||||||
githubLink="https://github.com/juls0730/juls07.dev-v2"
|
:icons="project.icons" :githubLink="project.githubLink" :externalLink="project.externalLink">
|
||||||
:icons="[{ 'icon': 'logos:nuxt-icon', 'name': 'nuxtjs v3' }]">
|
<slot>
|
||||||
This is the site you're currently on, a simple but decent looking portfolio and blog site.
|
<p v-html="project.body" />
|
||||||
</ProjectCard>
|
</slot>
|
||||||
<ProjectCard name="PHP Forum" headerIcon="mdi:language-php"
|
</ProjectCard>
|
||||||
githubLink="https://github.com/juls0730/php-forum"
|
</section>
|
||||||
:icons="[{ 'icon': 'logos:laravel', 'name': 'php' }]">
|
</main>
|
||||||
This is my attempt at a forum written in php, its not great but it works.
|
</div>
|
||||||
</ProjectCard>
|
|
||||||
<ProjectCard name="Cyansplash.net" headerIcon="tabler:brand-nuxt"
|
|
||||||
githubLink="https://github.com/juls0730/cyansplash.net"
|
|
||||||
:icons="[{ 'icon': 'logos:nuxt-icon', 'name': 'nuxtjs v2' }]">
|
|
||||||
This is the old cyansplash.net site, not amazing but taught me a lot about web
|
|
||||||
development.
|
|
||||||
</ProjectCard>
|
|
||||||
<ProjectCard name="Discord.js bot" headerIcon="mdi:robot-excited-outline"
|
|
||||||
githubLink="https://github.com/juls0730/Echo"
|
|
||||||
:icons="[{ 'icon': 'logos:javascript', 'name': 'Javascript' }]">
|
|
||||||
This is one of the first projects that wasn't just a dinky website, this project brought me to where
|
|
||||||
I am today.
|
|
||||||
</ProjectCard>
|
|
||||||
<ProjectCard name="juls07.dev V1" headerIcon="tabler:brand-nuxt"
|
|
||||||
githubLink="https://github.com/juls0730/juls07.dev"
|
|
||||||
:icons="[{ 'icon': 'logos:nuxt-icon', 'name': 'nuxtjs v2' }, { 'icon': 'logos:tailwindcss-icon', 'name': 'tailwindcss' }]">
|
|
||||||
This is my original portfolio website, it isn't much and it definitely isn't the best thing I've
|
|
||||||
ever written, but it's something.
|
|
||||||
</ProjectCard>
|
|
||||||
<ProjectCard name="vuefullstack" headerIcon="tabler:brand-vue"
|
|
||||||
githubLink="https://github.com/juls0730/vuefullstack"
|
|
||||||
:icons="[{ 'icon': 'logos:vue', 'name': 'vue v3' }, { 'icon': 'logos:nodejs-icon', 'name': 'nodejs' }]">
|
|
||||||
This is a small forum project I made to mock my ruby on rails forum I am
|
|
||||||
working on but in vuejs.
|
|
||||||
</ProjectCard>
|
|
||||||
<ProjectCard name="100DaysOfCode" headerIcon="mdi:nodejs"
|
|
||||||
githubLink="https://github.com/juls0730/vuefullstack"
|
|
||||||
:icons="[{ 'icon': 'logos:nodejs-icon', 'name': 'nodejs' }]">
|
|
||||||
This is my #100DaysOfCode challenge, my challenge was to create my own UI framework thing, check out
|
|
||||||
a
|
|
||||||
<nuxt-link to="/blog/100daysofcode-challenge">large overview of my 100DaysOfCode
|
|
||||||
challenge</nuxt-link> on my
|
|
||||||
blog.
|
|
||||||
</ProjectCard>
|
|
||||||
</section>
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.blur-background {
|
.blur-background {
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
#main a {
|
#main a {
|
||||||
@apply text-fuschia hover:underline visited:bg-rose-700;
|
@apply text-fuschia hover:underline visited:bg-rose-700;
|
||||||
}
|
}
|
||||||
|
|
||||||
.blur-background::before {
|
.blur-background::before {
|
||||||
content: '';
|
content: '';
|
||||||
margin: -35px;
|
margin: -35px;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
filter: blur(16px) saturate(125%);
|
filter: blur(16px) saturate(125%);
|
||||||
}
|
}
|
||||||
|
|
||||||
header,
|
header,
|
||||||
.blur-background::before {
|
.blur-background::before {
|
||||||
background-image: url(~/assets/images/header.jpg);
|
background-image: url(~/assets/images/header.jpg);
|
||||||
background-position: center;
|
background-position: center;
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
16
server/plugins/read-time.ts
Normal file
16
server/plugins/read-time.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { visit } from 'unist-util-visit'
|
||||||
|
|
||||||
|
const wpm = 225
|
||||||
|
|
||||||
|
export default defineNitroPlugin((nitroApp) => {
|
||||||
|
nitroApp.hooks.hook('content:file:afterParse', (file) => {
|
||||||
|
if (file._id.endsWith('.md')) {
|
||||||
|
let wordCount = 0
|
||||||
|
visit(file.body, (n: any) => n.type === 'text', (node) => {
|
||||||
|
wordCount += node.value.trim().split(/\s+/).length
|
||||||
|
})
|
||||||
|
|
||||||
|
file.readTime = Math.ceil(wordCount / wpm)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -3,23 +3,23 @@ import { streamToPromise } from 'sitemap'
|
|||||||
import RSS from 'rss'
|
import RSS from 'rss'
|
||||||
|
|
||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async (event) => {
|
||||||
// Fetch all documents
|
// Fetch all documents
|
||||||
const docs = await serverQueryContent(event).where({ _draft: false }).find()
|
const docs = await serverQueryContent(event).where({ _draft: false }).find()
|
||||||
const feed = new RSS({
|
const feed = new RSS({
|
||||||
title: 'Juls07',
|
title: 'Juls07',
|
||||||
description: 'Juls07\'s blogs',
|
description: 'Juls07\'s blogs',
|
||||||
link: 'https://juls07.dev/blogs'
|
link: 'https://juls07.dev/blogs'
|
||||||
})
|
|
||||||
|
|
||||||
for (const doc of docs) {
|
|
||||||
feed.item({
|
|
||||||
title: doc.title,
|
|
||||||
description: doc.description,
|
|
||||||
url: `https://juls07.dev${doc._path}`,
|
|
||||||
image_url: doc.image.src,
|
|
||||||
date: doc.date
|
|
||||||
})
|
})
|
||||||
}
|
|
||||||
|
|
||||||
return feed.xml({ indent: true })
|
for (const doc of docs) {
|
||||||
|
feed.item({
|
||||||
|
title: doc.title,
|
||||||
|
description: doc.description,
|
||||||
|
url: `https://juls07.dev${doc._path}`,
|
||||||
|
image_url: doc.image.src,
|
||||||
|
date: doc.date
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return feed.xml({ indent: true })
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -2,19 +2,19 @@ import { serverQueryContent } from '#content/server'
|
|||||||
import { SitemapStream, streamToPromise } from 'sitemap'
|
import { SitemapStream, streamToPromise } from 'sitemap'
|
||||||
|
|
||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async (event) => {
|
||||||
// Fetch all documents
|
// Fetch all documents
|
||||||
const docs = await serverQueryContent(event).where({ _draft: false }).find()
|
const docs = await serverQueryContent(event).where({ _draft: false }).find()
|
||||||
const sitemap = new SitemapStream({
|
const sitemap = new SitemapStream({
|
||||||
hostname: 'https://juls07.dev'
|
hostname: 'https://juls07.dev'
|
||||||
})
|
|
||||||
|
|
||||||
for (const doc of docs) {
|
|
||||||
sitemap.write({
|
|
||||||
url: doc._path,
|
|
||||||
changefreq: 'monthly'
|
|
||||||
})
|
})
|
||||||
}
|
|
||||||
sitemap.end()
|
|
||||||
|
|
||||||
return streamToPromise(sitemap)
|
for (const doc of docs) {
|
||||||
|
sitemap.write({
|
||||||
|
url: doc._path,
|
||||||
|
changefreq: 'monthly'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
sitemap.end()
|
||||||
|
|
||||||
|
return streamToPromise(sitemap)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/** @type {import('tailwindcss').Config} */
|
/** @type {import('tailwindcss').Config} */
|
||||||
module.exports = {
|
module.exports = {
|
||||||
darkMode: 'class',
|
darkMode: "class",
|
||||||
content: [
|
content: [
|
||||||
"./components/**/*.{js,vue,ts}",
|
"./components/**/*.{js,vue,ts}",
|
||||||
"./layouts/**/*.vue",
|
"./layouts/**/*.vue",
|
||||||
@@ -13,31 +13,31 @@ module.exports = {
|
|||||||
theme: {
|
theme: {
|
||||||
extend: {
|
extend: {
|
||||||
colors: {
|
colors: {
|
||||||
'sea-green': '#00FFC2',
|
"sea-green": "#00FFC2",
|
||||||
'fuschia': '#EB0066',
|
fuschia: "#EB0066",
|
||||||
'soft-lavender': '#F6EEFE',
|
"soft-lavender": "#F6EEFE",
|
||||||
'touched-lavender': '#F5EDFE',
|
"touched-lavender": "#F5EDFE",
|
||||||
'midnight': '#080908',
|
midnight: "#080908",
|
||||||
'dark-slate': '#0C0B0C',
|
"dark-slate": "#0C0B0C",
|
||||||
'deep-indigo': '#393041',
|
"deep-indigo": "#393041",
|
||||||
'midnight-slate': '#2F353D',
|
"midnight-slate": "#2F353D",
|
||||||
'obsidian-night': '#131316',
|
"obsidian-night": "#131316",
|
||||||
'soft-lilac': '#DCD6E6',
|
"soft-lilac": "#DCD6E6",
|
||||||
},
|
},
|
||||||
screens: {
|
screens: {
|
||||||
'xs': '512px',
|
xs: "512px",
|
||||||
'3xl': '1792px'
|
"3xl": "1792px",
|
||||||
},
|
},
|
||||||
keyframes: {
|
keyframes: {
|
||||||
blink: {
|
blink: {
|
||||||
'from, to': { opacity: '0' },
|
"from, to": { opacity: "0" },
|
||||||
'50%': { opacity: '1' },
|
"50%": { opacity: "1" },
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
animation: {
|
animation: {
|
||||||
blink: 'blink 1s step-end infinite',
|
blink: "blink 1s step-end infinite",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
plugins: [],
|
plugins: [],
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
{
|
{
|
||||||
// https://nuxt.com/docs/guide/concepts/typescript
|
// https://nuxt.com/docs/guide/concepts/typescript
|
||||||
"extends": "./.nuxt/tsconfig.json"
|
"extends": "./.nuxt/tsconfig.json"
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user