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>
|
||||
<div class="h-100vh w-100vw text-light-100 overflow-x-hidden">
|
||||
<suspense @pending="startLoading()" @resolve="stopLoading()">
|
||||
<router-view v-slot="{ Component }">
|
||||
<suspense @pending="startLoading()" @resolve="stopLoading()">
|
||||
<component :is="Component"/>
|
||||
</suspense>
|
||||
</router-view>
|
||||
</suspense>
|
||||
<router-view v-slot="{ Component }">
|
||||
<suspense @pending="startLoading()" @resolve="stopLoading()">
|
||||
<!-- The key makes that components are not reused if only params changed -->
|
||||
<component :is="Component" :key="$route.fullPath"/>
|
||||
</suspense>
|
||||
</router-view>
|
||||
</div>
|
||||
<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">
|
||||
|
@ -53,31 +52,21 @@
|
|||
import { computed, ref } from "vue"
|
||||
import { useWindowSize, whenever } from "@vueuse/core"
|
||||
import { pageComponentLoading } from "./store"
|
||||
import { useRouter } from "vue-router"
|
||||
|
||||
export default {
|
||||
name: "App",
|
||||
setup() {
|
||||
const loadingStartedTime = ref<null | number>(null)
|
||||
const isLoadingScreenActive = ref(false)
|
||||
const START_TRANSITION_DURATION = 300
|
||||
const START_TRANSITION_DELAY = 200
|
||||
let suspenseLoading = false
|
||||
const START_TRANSITION_DURATION = 200
|
||||
const START_TRANSITION_DELAY = 0
|
||||
|
||||
const loadingTexts = ref<Array<{ x: number, y: number }>>([])
|
||||
|
||||
const stopLoading = () => {
|
||||
if (loadingStartedTime.value === null) return
|
||||
|
||||
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()
|
||||
isLoadingScreenActive.value = false
|
||||
loadingStartedTime.value = null
|
||||
}
|
||||
|
||||
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
|
||||
whenever(pageComponentLoading, () => {
|
||||
if (loadingStartedTime.value !== null) return
|
||||
loadingStartedTime.value = Date.now()
|
||||
isLoadingScreenActive.value = true
|
||||
// Runs when a page component is imported
|
||||
startLoading()
|
||||
})
|
||||
|
||||
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()
|
||||
|
|
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;
|
||||
}
|
||||
|
||||
:where(address, p, ol, ul) + :where(address, p, ol, ul) {
|
||||
:where(address, p, ol, ul) + :is(address, p, ol, ul) {
|
||||
@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>
|
||||
<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>
|
||||
<h1 class="font-bold text-3xl sm:text-3xl sm:text-center font-special">
|
||||
{{ post.title }}
|
||||
|
@ -32,10 +33,11 @@
|
|||
import { useRoute } from "vue-router"
|
||||
import Prose from "../../components/Prose.vue"
|
||||
import XSpacer from "../../components/XSpacer.vue"
|
||||
import NotFoundPage from "../../components/NotFoundPage.vue"
|
||||
|
||||
export default {
|
||||
name: "BlogPostPage",
|
||||
components: { XSpacer, Prose, TopBarLayout },
|
||||
components: { NotFoundPage, XSpacer, Prose, TopBarLayout },
|
||||
async setup() {
|
||||
const route = useRoute()
|
||||
const post = await getPostBySlug(route.params.slug as string)
|
||||
|
@ -43,7 +45,7 @@
|
|||
return {
|
||||
post,
|
||||
// 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 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({
|
||||
plugins: [
|
||||
vuePlugin(),
|
||||
pagesPlugin(),
|
||||
pagesPlugin({
|
||||
syncIndex: false
|
||||
}),
|
||||
windicssPlugin(),
|
||||
iconsPlugin()
|
||||
]
|
||||
|
|
Loading…
Add table
Reference in a new issue