Add 404 pages
This commit is contained in:
parent
481e30c1b3
commit
f593c1c638
8 changed files with 142 additions and 31 deletions
52
src/App.vue
52
src/App.vue
|
@ -1,12 +1,11 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="h-100vh w-100vw text-light-100 overflow-x-hidden">
|
<div class="h-100vh w-100vw text-light-100 overflow-x-hidden">
|
||||||
<suspense @pending="startLoading()" @resolve="stopLoading()">
|
<router-view v-slot="{ Component }">
|
||||||
<router-view v-slot="{ Component }">
|
<suspense @pending="startLoading()" @resolve="stopLoading()">
|
||||||
<suspense @pending="startLoading()" @resolve="stopLoading()">
|
<!-- The key makes that components are not reused if only params changed -->
|
||||||
<component :is="Component"/>
|
<component :is="Component" :key="$route.fullPath"/>
|
||||||
</suspense>
|
</suspense>
|
||||||
</router-view>
|
</router-view>
|
||||||
</suspense>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="bg-background fixed top-0 left-0 right-0 bottom-0 z-100 backdrop-filter" :style="loadingOverlayStyle">
|
<div class="bg-background fixed top-0 left-0 right-0 bottom-0 z-100 backdrop-filter" :style="loadingOverlayStyle">
|
||||||
<div class="font-bold text-light-900 text-2xl h-full w-full flex justify-center items-center overflow-hidden" :style="loadingOverlayContentStyle">
|
<div class="font-bold text-light-900 text-2xl h-full w-full flex justify-center items-center overflow-hidden" :style="loadingOverlayContentStyle">
|
||||||
|
@ -53,31 +52,21 @@
|
||||||
import { computed, ref } from "vue"
|
import { computed, ref } from "vue"
|
||||||
import { useWindowSize, whenever } from "@vueuse/core"
|
import { useWindowSize, whenever } from "@vueuse/core"
|
||||||
import { pageComponentLoading } from "./store"
|
import { pageComponentLoading } from "./store"
|
||||||
|
import { useRouter } from "vue-router"
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "App",
|
name: "App",
|
||||||
setup() {
|
setup() {
|
||||||
const loadingStartedTime = ref<null | number>(null)
|
const loadingStartedTime = ref<null | number>(null)
|
||||||
const isLoadingScreenActive = ref(false)
|
const isLoadingScreenActive = ref(false)
|
||||||
const START_TRANSITION_DURATION = 300
|
const START_TRANSITION_DURATION = 200
|
||||||
const START_TRANSITION_DELAY = 200
|
const START_TRANSITION_DELAY = 0
|
||||||
let suspenseLoading = false
|
|
||||||
|
|
||||||
const loadingTexts = ref<Array<{ x: number, y: number }>>([])
|
const loadingTexts = ref<Array<{ x: number, y: number }>>([])
|
||||||
|
|
||||||
const stopLoading = () => {
|
const stopLoading = () => {
|
||||||
if (loadingStartedTime.value === null) return
|
isLoadingScreenActive.value = false
|
||||||
|
loadingStartedTime.value = null
|
||||||
const stop = () => {
|
|
||||||
isLoadingScreenActive.value = false
|
|
||||||
loadingStartedTime.value = null
|
|
||||||
suspenseLoading = false
|
|
||||||
}
|
|
||||||
|
|
||||||
const timeSinceStart = Date.now() - loadingStartedTime.value
|
|
||||||
|
|
||||||
if (timeSinceStart > START_TRANSITION_DELAY) setTimeout(stop, START_TRANSITION_DURATION - timeSinceStart)
|
|
||||||
else stop()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const startLoading = () => {
|
const startLoading = () => {
|
||||||
|
@ -89,9 +78,22 @@
|
||||||
|
|
||||||
// Suspense @resolve is also called when the component is not async, so we don't need to handle stopping
|
// Suspense @resolve is also called when the component is not async, so we don't need to handle stopping
|
||||||
whenever(pageComponentLoading, () => {
|
whenever(pageComponentLoading, () => {
|
||||||
if (loadingStartedTime.value !== null) return
|
// Runs when a page component is imported
|
||||||
loadingStartedTime.value = Date.now()
|
startLoading()
|
||||||
isLoadingScreenActive.value = true
|
})
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
let isFirst = true
|
||||||
|
|
||||||
|
router.beforeResolve((to, from, next) => {
|
||||||
|
if (isFirst) {
|
||||||
|
isFirst = false
|
||||||
|
next()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
startLoading()
|
||||||
|
setTimeout(next, START_TRANSITION_DURATION)
|
||||||
})
|
})
|
||||||
|
|
||||||
const { width, height } = useWindowSize()
|
const { width, height } = useWindowSize()
|
||||||
|
|
52
src/components/BlogPostPageContent.vue
Normal file
52
src/components/BlogPostPageContent.vue
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
<template>
|
||||||
|
<NotFoundPage v-if="post === null" object-name="post" back-target="/blog"/>
|
||||||
|
<TopBarLayout title="Blog" back-target="/blog" v-else>
|
||||||
|
<article>
|
||||||
|
<h1 class="font-bold text-3xl sm:text-3xl sm:text-center font-special">
|
||||||
|
{{ post.title }}
|
||||||
|
</h1>
|
||||||
|
<XSpacer v="4"/>
|
||||||
|
<div class="flex justify-center -sm:flex-col -sm:space-y-1 -sm:pt-2 sm:space-x-3 text-sm sm:text-base">
|
||||||
|
<div>
|
||||||
|
Published at {{ post.published_at.slice(0, 10) }}
|
||||||
|
</div>
|
||||||
|
<div class="-sm:hidden">
|
||||||
|
|
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
Reading time: {{ post.reading_time_minutes }} minute{{ post.reading_time_minutes === 1 ? "" : "s" }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<XSpacer v="8"/>
|
||||||
|
<Prose v-html="html"/>
|
||||||
|
</article>
|
||||||
|
</TopBarLayout>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style module>
|
||||||
|
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import TopBarLayout from "./TopBarLayout.vue"
|
||||||
|
import { getPostBySlug } from "../posts"
|
||||||
|
import { useRoute } from "vue-router"
|
||||||
|
import Prose from "./Prose.vue"
|
||||||
|
import XSpacer from "./XSpacer.vue"
|
||||||
|
import NotFoundPage from "./NotFoundPage.vue"
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "BlogPostPageContent",
|
||||||
|
components: { NotFoundPage, XSpacer, Prose, TopBarLayout },
|
||||||
|
async setup() {
|
||||||
|
const route = useRoute()
|
||||||
|
const post = await getPostBySlug(route.params.slug as string)
|
||||||
|
|
||||||
|
return {
|
||||||
|
post,
|
||||||
|
// Yep, that's awful code
|
||||||
|
html: post?.body_html?.replaceAll("h2>", "h3>")?.replaceAll("h1>", "h2>")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
39
src/components/NotFoundPage.vue
Normal file
39
src/components/NotFoundPage.vue
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
<template>
|
||||||
|
<div class="pt-40 px-5 flex flex-col items-center">
|
||||||
|
<div class="font-bold text-3xl text-center">
|
||||||
|
This {{ objectName }} does not exist.
|
||||||
|
</div>
|
||||||
|
<XSpacer v="5"/>
|
||||||
|
<router-link :to="backTarget" class="flex items-center">
|
||||||
|
<ArrowLeftIcon/>
|
||||||
|
<XSpacer h="2"/>
|
||||||
|
<div class="text-lg">
|
||||||
|
Back
|
||||||
|
</div>
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style module>
|
||||||
|
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import ArrowLeftIcon from "~icons/ph/arrow-left"
|
||||||
|
import XSpacer from "../components/XSpacer.vue"
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "NotFoundPage",
|
||||||
|
components: { XSpacer, ArrowLeftIcon },
|
||||||
|
props: {
|
||||||
|
objectName: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
backTarget: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -56,7 +56,7 @@
|
||||||
@apply not-italic;
|
@apply not-italic;
|
||||||
}
|
}
|
||||||
|
|
||||||
:where(address, p, ol, ul) + :where(address, p, ol, ul) {
|
:where(address, p, ol, ul) + :is(address, p, ol, ul) {
|
||||||
@apply pt-4;
|
@apply pt-4;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
14
src/pages/[...all].vue
Normal file
14
src/pages/[...all].vue
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
<template>
|
||||||
|
<NotFoundPage object-name="page" back-target="/"/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import ArrowLeftIcon from "~icons/ph/arrow-left"
|
||||||
|
import XSpacer from "../components/XSpacer.vue"
|
||||||
|
import NotFoundPage from "../components/NotFoundPage.vue"
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "FallbackPage",
|
||||||
|
components: { NotFoundPage, XSpacer, ArrowLeftIcon }
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -1,5 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<TopBarLayout title="Blog" back-target="/blog">
|
<NotFoundPage v-if="post === null" object-name="post" back-target="/blog"/>
|
||||||
|
<TopBarLayout title="Blog" back-target="/blog" v-else>
|
||||||
<article>
|
<article>
|
||||||
<h1 class="font-bold text-3xl sm:text-3xl sm:text-center font-special">
|
<h1 class="font-bold text-3xl sm:text-3xl sm:text-center font-special">
|
||||||
{{ post.title }}
|
{{ post.title }}
|
||||||
|
@ -32,10 +33,11 @@
|
||||||
import { useRoute } from "vue-router"
|
import { useRoute } from "vue-router"
|
||||||
import Prose from "../../components/Prose.vue"
|
import Prose from "../../components/Prose.vue"
|
||||||
import XSpacer from "../../components/XSpacer.vue"
|
import XSpacer from "../../components/XSpacer.vue"
|
||||||
|
import NotFoundPage from "../../components/NotFoundPage.vue"
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "BlogPostPage",
|
name: "BlogPostPage",
|
||||||
components: { XSpacer, Prose, TopBarLayout },
|
components: { NotFoundPage, XSpacer, Prose, TopBarLayout },
|
||||||
async setup() {
|
async setup() {
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const post = await getPostBySlug(route.params.slug as string)
|
const post = await getPostBySlug(route.params.slug as string)
|
||||||
|
@ -43,7 +45,7 @@
|
||||||
return {
|
return {
|
||||||
post,
|
post,
|
||||||
// Yep, that's awful code
|
// Yep, that's awful code
|
||||||
html: post.body_html.replaceAll("h2>", "h3>").replaceAll("h1>", "h2>")
|
html: post?.body_html?.replaceAll("h2>", "h3>")?.replaceAll("h1>", "h2>")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,4 +16,4 @@ export interface FullPost extends Post {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getPosts = () => $fetch<Post[]>(`https://dev.to/api/articles?username=${USERNAME}&per_page=1000`)
|
export const getPosts = () => $fetch<Post[]>(`https://dev.to/api/articles?username=${USERNAME}&per_page=1000`)
|
||||||
export const getPostBySlug = (slug: string) => $fetch<FullPost>(`https://dev.to/api/articles/${USERNAME}/${slug}`)
|
export const getPostBySlug = (slug: string) => $fetch<FullPost>(`https://dev.to/api/articles/${USERNAME}/${slug}`).catch(() => null)
|
||||||
|
|
|
@ -7,7 +7,9 @@ import iconsPlugin from "unplugin-icons/vite"
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [
|
plugins: [
|
||||||
vuePlugin(),
|
vuePlugin(),
|
||||||
pagesPlugin(),
|
pagesPlugin({
|
||||||
|
syncIndex: false
|
||||||
|
}),
|
||||||
windicssPlugin(),
|
windicssPlugin(),
|
||||||
iconsPlugin()
|
iconsPlugin()
|
||||||
]
|
]
|
||||||
|
|
Loading…
Add table
Reference in a new issue