diff --git a/src/App.vue b/src/App.vue index 5ea55a2..7757ff4 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,7 +1,9 @@ diff --git a/src/components.d.ts b/src/components.d.ts index 9ace4c9..1d613aa 100644 --- a/src/components.d.ts +++ b/src/components.d.ts @@ -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'] } } diff --git a/src/components/DlRow.vue b/src/components/DlRow.vue new file mode 100644 index 0000000..c904f29 --- /dev/null +++ b/src/components/DlRow.vue @@ -0,0 +1,28 @@ + + + + + diff --git a/src/components/Layout.vue b/src/components/Layout.vue index 47e53dd..966e8bb 100644 --- a/src/components/Layout.vue +++ b/src/components/Layout.vue @@ -1,6 +1,8 @@ diff --git a/src/composables/useStrapi.js b/src/composables/useStrapi.js new file mode 100644 index 0000000..7ed3f96 --- /dev/null +++ b/src/composables/useStrapi.js @@ -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 } +} diff --git a/src/composables/useStrapiHead.js b/src/composables/useStrapiHead.js new file mode 100644 index 0000000..0e40172 --- /dev/null +++ b/src/composables/useStrapiHead.js @@ -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, + }) +} diff --git a/src/libs/strapi.js b/src/libs/strapi.js deleted file mode 100644 index 27897d5..0000000 --- a/src/libs/strapi.js +++ /dev/null @@ -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, - }) - } -} diff --git a/src/main.js b/src/main.js index ac15ebb..cb3cec0 100644 --- a/src/main.js +++ b/src/main.js @@ -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) }) diff --git a/src/plugins/strapi.js b/src/plugins/strapi.js new file mode 100644 index 0000000..54388e8 --- /dev/null +++ b/src/plugins/strapi.js @@ -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) + }, +} diff --git a/src/styles/main.scss b/src/styles/main.scss index acd9de1..d80dbe7 100644 --- a/src/styles/main.scss +++ b/src/styles/main.scss @@ -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; }