strapi integration
This commit is contained in:
parent
ea19ac6189
commit
5c38cd9bdd
12 changed files with 264 additions and 144 deletions
|
|
@ -1,7 +1,9 @@
|
|||
<template>
|
||||
<lenis root :options="{ duration: 1 }">
|
||||
<div :class="classes" :style="styles" id="container">
|
||||
<suspense>
|
||||
<layout />
|
||||
</suspense>
|
||||
</div>
|
||||
</lenis>
|
||||
</template>
|
||||
|
|
|
|||
3
src/components.d.ts
vendored
3
src/components.d.ts
vendored
|
|
@ -11,6 +11,8 @@ export {}
|
|||
/* prettier-ignore */
|
||||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
Dl: typeof import('./components/Dl.vue')['default']
|
||||
DlRow: typeof import('./components/DlRow.vue')['default']
|
||||
InfiniteScrollContainer: typeof import('./components/InfiniteScrollContainer.vue')['default']
|
||||
InfiniteScrollDemo: typeof import('./components/InfiniteScrollDemo.vue')['default']
|
||||
Layout: typeof import('./components/Layout.vue')['default']
|
||||
|
|
@ -20,5 +22,6 @@ declare module 'vue' {
|
|||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
SiteHeader: typeof import('./components/site/Header.vue')['default']
|
||||
StrapiContent: typeof import('./components/strapi/Content.vue')['default']
|
||||
}
|
||||
}
|
||||
|
|
|
|||
28
src/components/DlRow.vue
Normal file
28
src/components/DlRow.vue
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
<template>
|
||||
<div v-if="row" class="dl-row">
|
||||
<span class="label">{{ row.label }}:</span>
|
||||
|
||||
<strapi-content class="content" :content="row.content" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const props = defineProps({ row: Object })
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.dl-row {
|
||||
grid-template-columns: desktop-vw(40px) 1fr;
|
||||
display: grid;
|
||||
align-items: center;
|
||||
padding-top: desktop-vw(8px);
|
||||
border-top: 1px dashed currentColor;
|
||||
|
||||
.content {
|
||||
p,
|
||||
a {
|
||||
@include h6;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
<template>
|
||||
<div class="layout">
|
||||
<suspense>
|
||||
<site-header />
|
||||
</suspense>
|
||||
|
||||
<main class="main-content">
|
||||
<infinite-scroll-demo />
|
||||
|
|
@ -9,14 +11,11 @@
|
|||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useSeoMeta } from '@unhead/vue'
|
||||
import { useStrapiSingle } from '@/composables/useStrapi'
|
||||
import useStrapiHead from '@/composables/useStrapiHead'
|
||||
|
||||
// SEO
|
||||
useSeoMeta({
|
||||
title: 'nicwands.com - Infinite Grid Gallery',
|
||||
description:
|
||||
'Interactive infinite scrolling grid gallery with bidirectional content generation',
|
||||
})
|
||||
const { data } = await useStrapiSingle('home')
|
||||
useStrapiHead()
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
|
|
|||
|
|
@ -1,25 +1,20 @@
|
|||
<template>
|
||||
<header class="site-header">
|
||||
<header v-if="data" class="site-header">
|
||||
<img class="logo" src="/images/output_pixelated.gif" />
|
||||
|
||||
<div class="info theme-red">
|
||||
<p class="h6">Nic Wands</p>
|
||||
<p class="description p-small">
|
||||
Amet proin sit eleifend enim. Sit cursus nec vel donec odio
|
||||
nullam placerat.
|
||||
</p>
|
||||
<strapi-content class="copy" :content="data.header_copy" />
|
||||
|
||||
<hr />
|
||||
|
||||
<dl>
|
||||
<dt>Contact:</dt>
|
||||
<dd>nic@nicwands.com</dd>
|
||||
</dl>
|
||||
<dl-row v-for="row in data.header_dl" :row="row" />
|
||||
</div>
|
||||
</header>
|
||||
</template>
|
||||
|
||||
<script setup></script>
|
||||
<script setup>
|
||||
import { useStrapiGlobal } from '@/composables/useStrapi'
|
||||
|
||||
const { data } = await useStrapiGlobal()
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.site-header {
|
||||
|
|
@ -48,11 +43,16 @@
|
|||
border-radius: desktop-vw(10px);
|
||||
padding: desktop-vw(12px) desktop-vw(10px);
|
||||
|
||||
.description {
|
||||
margin: desktop-vw(6px) 0 desktop-vw(18px);
|
||||
.copy {
|
||||
margin-bottom: desktop-vw(18px);
|
||||
|
||||
strong {
|
||||
@include h6;
|
||||
}
|
||||
p {
|
||||
margin: desktop-vw(6px) 0;
|
||||
@include p-small;
|
||||
}
|
||||
hr {
|
||||
border: 1px dashed currentColor;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
24
src/components/strapi/Content.vue
Normal file
24
src/components/strapi/Content.vue
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
<template>
|
||||
<div class="strapi-content">
|
||||
<strapi-blocks :content="content" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { StrapiBlocks } from 'vue-strapi-blocks-renderer'
|
||||
|
||||
const props = defineProps({
|
||||
content: Array,
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.strapi-content {
|
||||
> *:first-child {
|
||||
margin-top: 0 !important;
|
||||
}
|
||||
> *:last-child {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
142
src/composables/useStrapi.js
Normal file
142
src/composables/useStrapi.js
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
import { inject, ref } from 'vue'
|
||||
|
||||
// Base composable to access the Strapi client
|
||||
const getStrapiClient = () => {
|
||||
const client = inject('strapi')
|
||||
if (!client)
|
||||
throw new Error(
|
||||
'Strapi client not initialized. Did you install the plugin?',
|
||||
)
|
||||
return client
|
||||
}
|
||||
|
||||
// Merge default and user options
|
||||
const mergeOptions = (userOptions = {}) => {
|
||||
return { pLevel: '5', ...userOptions }
|
||||
}
|
||||
|
||||
// Singleton instance for global data
|
||||
let globalInstance = null
|
||||
|
||||
/**
|
||||
* Use the global singleton data with caching
|
||||
* This composable ensures the global data is only fetched once,
|
||||
* even when called from multiple places
|
||||
* @param {object} options - optional query options
|
||||
*/
|
||||
export const useStrapiGlobal = async (options = {}) => {
|
||||
// Return existing instance if already fetched
|
||||
if (globalInstance) {
|
||||
return globalInstance
|
||||
}
|
||||
|
||||
// Create new instance
|
||||
const data = ref(null)
|
||||
const loading = ref(true)
|
||||
const error = ref(null)
|
||||
const client = getStrapiClient()
|
||||
|
||||
// Store the instance before making the request
|
||||
globalInstance = { data, loading, error }
|
||||
|
||||
try {
|
||||
const response = await client
|
||||
.single('global')
|
||||
.find(mergeOptions(options))
|
||||
data.value = response.data
|
||||
} catch (err) {
|
||||
error.value = err
|
||||
console.error(err)
|
||||
// Reset instance on error so it can be retried
|
||||
globalInstance = null
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
return globalInstance
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the global singleton cache
|
||||
* Useful for forcing a refetch of global data
|
||||
*/
|
||||
export const resetStrapiGlobal = () => {
|
||||
globalInstance = null
|
||||
}
|
||||
|
||||
/**
|
||||
* Use a singleton content type
|
||||
* @param {string} uid - Strapi singleton UID
|
||||
* @param {object} options - optional query options
|
||||
*/
|
||||
export const useStrapiSingle = async (uid, options = {}) => {
|
||||
const data = ref(null)
|
||||
const loading = ref(true)
|
||||
const error = ref(null)
|
||||
const client = getStrapiClient()
|
||||
|
||||
try {
|
||||
const response = await client.single(uid).find(mergeOptions(options))
|
||||
data.value = response.data
|
||||
} catch (err) {
|
||||
error.value = err
|
||||
console.error(err)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
return { data, loading, error }
|
||||
}
|
||||
|
||||
/**
|
||||
* Use a single entry by ID
|
||||
* @param {string} contentType
|
||||
* @param {number|string} id
|
||||
* @param {object} options - optional query options
|
||||
*/
|
||||
export const useStrapiOne = async (contentType, id, options = {}) => {
|
||||
const data = ref(null)
|
||||
const loading = ref(true)
|
||||
const error = ref(null)
|
||||
const client = getStrapiClient()
|
||||
|
||||
try {
|
||||
const response = await client
|
||||
.entity(contentType)
|
||||
.findOne({ id, ...mergeOptions(options) })
|
||||
data.value = response.data
|
||||
} catch (err) {
|
||||
error.value = err
|
||||
console.error(err)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
return { data, loading, error }
|
||||
}
|
||||
|
||||
/**
|
||||
* Use a collection of entries
|
||||
* @param {string} contentType
|
||||
* @param {object} options - optional query options
|
||||
*/
|
||||
export const useStrapiMany = async (contentType, options = {}) => {
|
||||
const data = ref(null)
|
||||
const loading = ref(true)
|
||||
const error = ref(null)
|
||||
const client = getStrapiClient()
|
||||
|
||||
try {
|
||||
const response = await client
|
||||
.entity(contentType)
|
||||
.find(mergeOptions(options))
|
||||
data.value = response.data
|
||||
} catch (err) {
|
||||
error.value = err
|
||||
console.error(err)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
return { data, loading, error }
|
||||
}
|
||||
24
src/composables/useStrapiHead.js
Normal file
24
src/composables/useStrapiHead.js
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import { computed } from 'vue'
|
||||
import { useSeoMeta } from '@unhead/vue'
|
||||
import { useStrapiGlobal } from '@/composables/useStrapi'
|
||||
|
||||
export default (pageSeo) => {
|
||||
const { data: globalData } = useStrapiGlobal()
|
||||
|
||||
const globalSeo = computed(() => globalData?.value?.seo)
|
||||
|
||||
const title = computed(() => {
|
||||
return pageSeo?.title || globalSeo.value?.title || ''
|
||||
})
|
||||
const description = computed(() => {
|
||||
return pageSeo?.description || globalSeo.value?.description || ''
|
||||
})
|
||||
const image = computed(() => {
|
||||
return pageSeo?.image || globalSeo.value?.image
|
||||
})
|
||||
|
||||
useSeoMeta({
|
||||
title: title.value,
|
||||
description: description.value,
|
||||
})
|
||||
}
|
||||
|
|
@ -1,109 +0,0 @@
|
|||
import { strapi } from '@strapi/client'
|
||||
|
||||
export const strapiClient = strapi({
|
||||
baseURL: 'https://cms.nicwands.com/api',
|
||||
auth: import.meta.env.VITE_STRAPI_API_TOKEN,
|
||||
})
|
||||
|
||||
/**
|
||||
* Enhanced fetch function with preview support
|
||||
* @param {string} contentType - The Strapi content type (e.g., 'global', 'articles')
|
||||
* @param {object} options - Fetch options including preview settings
|
||||
* @param {boolean} options.isPreview - Whether this is a preview request
|
||||
* @param {string} options.status - Content status ('draft' or 'published')
|
||||
* @param {boolean} options.isSingle - Whether this is a single type or collection
|
||||
* @param {object} options.params - Additional query parameters
|
||||
* @returns {Promise} Strapi response
|
||||
*/
|
||||
export const fetchWithPreview = async (contentType, options = {}) => {
|
||||
const {
|
||||
isPreview = false,
|
||||
status = 'published',
|
||||
isSingle = false,
|
||||
params = {},
|
||||
...otherOptions
|
||||
} = options
|
||||
|
||||
const queryParams = { ...params }
|
||||
const headers = {}
|
||||
|
||||
// Add status parameter for draft content in preview mode
|
||||
if (isPreview && status) {
|
||||
queryParams.status = status
|
||||
}
|
||||
|
||||
// Enable content source maps for Live Preview
|
||||
if (isPreview) {
|
||||
headers['strapi-encode-source-maps'] = 'true'
|
||||
}
|
||||
|
||||
// Disable caching for preview requests
|
||||
if (isPreview) {
|
||||
headers['Cache-Control'] = 'no-cache'
|
||||
}
|
||||
|
||||
try {
|
||||
if (isSingle) {
|
||||
const manager = strapiClient.single(contentType)
|
||||
return await manager.find(queryParams, { headers, ...otherOptions })
|
||||
} else {
|
||||
const manager = strapiClient.collection(contentType)
|
||||
return await manager.find(queryParams, { headers, ...otherOptions })
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error fetching ${contentType}:`, error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch global content with preview support
|
||||
*/
|
||||
export const getGlobal = async (previewOptions = {}) => {
|
||||
return await fetchWithPreview('global', {
|
||||
isSingle: true,
|
||||
...previewOptions,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a collection item by slug with preview support
|
||||
*/
|
||||
export const getBySlug = async (contentType, slug, previewOptions = {}) => {
|
||||
return await fetchWithPreview(contentType, {
|
||||
isSingle: false,
|
||||
params: {
|
||||
filters: { slug: { $eq: slug } },
|
||||
populate: '*',
|
||||
},
|
||||
...previewOptions,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a single item by document ID (useful for preview)
|
||||
*/
|
||||
export const getByDocumentId = async (
|
||||
contentType,
|
||||
documentId,
|
||||
previewOptions = {},
|
||||
) => {
|
||||
const { isSingle = false } = previewOptions
|
||||
|
||||
if (isSingle) {
|
||||
return await fetchWithPreview(contentType, {
|
||||
isSingle: true,
|
||||
params: { documentId },
|
||||
...previewOptions,
|
||||
})
|
||||
} else {
|
||||
return await fetchWithPreview(contentType, {
|
||||
isSingle: false,
|
||||
params: {
|
||||
filters: { documentId: { $eq: documentId } },
|
||||
populate: '*',
|
||||
},
|
||||
...previewOptions,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,9 @@
|
|||
import './styles/main.scss'
|
||||
import App from './App.vue'
|
||||
import { ViteSSG } from 'vite-ssg/single-page'
|
||||
import strapi from './plugins/strapi'
|
||||
|
||||
export const createApp = ViteSSG(App, ({ app }) => {
|
||||
// Plugins
|
||||
app.use(strapi)
|
||||
})
|
||||
|
|
|
|||
13
src/plugins/strapi.js
Normal file
13
src/plugins/strapi.js
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import { strapi } from '@strapi/client'
|
||||
|
||||
export default {
|
||||
install: (app) => {
|
||||
const client = strapi({
|
||||
baseURL: 'https://cms.nicwands.com/api',
|
||||
auth: import.meta.env.VITE_STRAPI_API_TOKEN,
|
||||
})
|
||||
|
||||
// Also provide
|
||||
app.provide('strapi', client)
|
||||
},
|
||||
}
|
||||
|
|
@ -61,14 +61,6 @@ code {
|
|||
@include code;
|
||||
}
|
||||
|
||||
/* DL styling */
|
||||
dl {
|
||||
grid-template-columns: desktop-vw(40px) 1fr;
|
||||
display: grid;
|
||||
align-items: center;
|
||||
padding-top: desktop-vw(8px);
|
||||
}
|
||||
|
||||
#app {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue