feat: initial commit — site + admin panel + Postgres content pipeline
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
import { onMounted, onBeforeUnmount } from "vue";
|
||||
import type { Ref } from "vue";
|
||||
|
||||
/**
|
||||
* useClickOutside composable:
|
||||
* - Detects clicks outside of a given element reference.
|
||||
* - Executes a callback when a click occurs outside the target.
|
||||
* - Automatically registers and cleans up listeners on mount/unmount.
|
||||
*/
|
||||
|
||||
export function useClickOutside(
|
||||
elRef: Ref<HTMLElement | null>,
|
||||
callback: (event: MouseEvent) => void
|
||||
): void {
|
||||
if (!elRef) return;
|
||||
|
||||
// Event handler: invokes callback when click is outside the element
|
||||
const handleClick = (event: MouseEvent) => {
|
||||
if (elRef.value && !elRef.value.contains(event.target as Node)) {
|
||||
callback(event);
|
||||
}
|
||||
};
|
||||
|
||||
// Register click listener on document when component mounts
|
||||
onMounted(() => {
|
||||
document.addEventListener("click", handleClick);
|
||||
});
|
||||
|
||||
// Remove click listener when component unmounts
|
||||
onBeforeUnmount(() => {
|
||||
document.removeEventListener("click", handleClick);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import { h, type Component } from "vue";
|
||||
import { Icon } from "@iconify/vue";
|
||||
|
||||
export const iconNames = {
|
||||
mail: "heroicons-solid:envelope",
|
||||
document: "heroicons-solid:document-text",
|
||||
code: "heroicons-solid:code-bracket",
|
||||
|
||||
chevronRightIcon: "heroicons-solid:chevron-right",
|
||||
chevronLeftIcon: "heroicons-solid:chevron-left",
|
||||
|
||||
arrowRight: "heroicons-solid:arrow-right",
|
||||
arrowLeft: "heroicons-solid:arrow-left",
|
||||
arrowUp: "heroicons-solid:arrow-up",
|
||||
external: "heroicons-solid:arrow-top-right-on-square",
|
||||
phone: "heroicons-solid:phone",
|
||||
location: "heroicons-solid:map-pin",
|
||||
check: "heroicons-solid:check-circle",
|
||||
plus: "heroicons-solid:plus",
|
||||
|
||||
user: "heroicons-solid:user",
|
||||
home: "mdi:home",
|
||||
share: "heroicons-solid:share",
|
||||
clipboard: "heroicons-solid:clipboard-document",
|
||||
link: "heroicons-solid:link",
|
||||
chat: "heroicons-solid:chat-bubble-left-right",
|
||||
close: "heroicons-solid:x-mark",
|
||||
|
||||
instagram: "mdi:instagram",
|
||||
facebook: "mdi:facebook",
|
||||
tiktok: "ic:baseline-tiktok",
|
||||
} as const;
|
||||
|
||||
export type IconName = keyof typeof iconNames;
|
||||
|
||||
export function useIcons(): Record<IconName, Component> {
|
||||
const registry = {} as Record<IconName, Component>;
|
||||
|
||||
for (const name of Object.keys(iconNames) as IconName[]) {
|
||||
registry[name] = {
|
||||
name: `${name}Icon`,
|
||||
render() {
|
||||
return h(Icon, {
|
||||
icon: iconNames[name],
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return registry;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
// src/composables/useLocale.ts
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { computed } from "vue";
|
||||
|
||||
/**
|
||||
* useLocale composable:
|
||||
* - Provides the current locale from vue-i18n.
|
||||
* - Returns a computed `path` that prefixes URLs with /en for English locale.
|
||||
* - Helps route components generate localized links.
|
||||
*/
|
||||
|
||||
// Return localized path and current locale reference
|
||||
export function useLocale() {
|
||||
const { locale } = useI18n();
|
||||
const path = computed(() => (locale.value === "en" ? "/en/" : "/"));
|
||||
return { path };
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
import { useHead } from '@vueuse/head'
|
||||
import { computed, unref } from 'vue'
|
||||
/**
|
||||
* useMeta composable:
|
||||
* - Dynamically sets page meta tags and title using @vueuse/head.
|
||||
* - Supports Open Graph, Twitter Card, and standard SEO metadata.
|
||||
* - Localizes `og:locale` automatically based on current locale.
|
||||
*/
|
||||
|
||||
// Interface for supported meta tag fields
|
||||
interface MetaData {
|
||||
title?: string
|
||||
description?: string
|
||||
keywords?: string[]
|
||||
image?: string
|
||||
type?: string
|
||||
author?: string
|
||||
canonical?: string
|
||||
robots?: string
|
||||
structuredData?: Record<string, any>
|
||||
['og:locale']?: string
|
||||
['og:site_name']?: string
|
||||
['twitter:card']?: string
|
||||
['twitter:creator']?: string
|
||||
['twitter:title']?: string
|
||||
['twitter:description']?: string
|
||||
['twitter:image']?: string
|
||||
}
|
||||
|
||||
export function useMeta(data: MetaData | any, locale: string | any) {
|
||||
const d = () => unref(data) as MetaData
|
||||
const loc = () => unref(locale) as string
|
||||
|
||||
const withHost = (u?: string) => {
|
||||
if (!u) return undefined
|
||||
if (/^https?:\/\//i.test(u)) return u
|
||||
if (u.startsWith('//')) return 'https:' + u
|
||||
|
||||
const base = 'https://www.mucomutfak.com'
|
||||
|
||||
if (u.startsWith('/')) return base + u
|
||||
|
||||
return `${base}/${u}`
|
||||
}
|
||||
|
||||
const ogLocale = computed(() => {
|
||||
return d()['og:locale'] || (loc() === 'tr' ? 'tr_TR' : 'en_US')
|
||||
})
|
||||
|
||||
const canonical = computed(() => {
|
||||
return withHost(d().canonical)
|
||||
})
|
||||
|
||||
const structuredData = computed(() => {
|
||||
const schema = d().structuredData
|
||||
|
||||
if (!schema) return undefined
|
||||
|
||||
return JSON.stringify({
|
||||
...schema,
|
||||
url: withHost(schema.url),
|
||||
image: withHost(schema.image),
|
||||
})
|
||||
})
|
||||
|
||||
useHead({
|
||||
title: computed(() => d().title ?? '') as any,
|
||||
|
||||
htmlAttrs: {
|
||||
lang: computed(() => loc() || 'en'),
|
||||
},
|
||||
|
||||
meta: [
|
||||
{ name: 'description', content: computed(() => d().description) },
|
||||
{
|
||||
name: 'keywords',
|
||||
content: computed(() =>
|
||||
d().keywords?.length ? d().keywords!.join(', ') : undefined,
|
||||
),
|
||||
},
|
||||
{ name: 'author', content: computed(() => d().author) },
|
||||
{ name: 'robots', content: computed(() => d().robots) },
|
||||
|
||||
{ property: 'og:title', content: computed(() => d().title) },
|
||||
{ property: 'og:description', content: computed(() => d().description) },
|
||||
{ property: 'og:image', content: computed(() => withHost(d().image)) },
|
||||
{ property: 'og:url', content: canonical },
|
||||
{ property: 'og:type', content: computed(() => d().type) },
|
||||
{ property: 'og:locale', content: ogLocale },
|
||||
{ property: 'og:site_name', content: computed(() => d()['og:site_name']) },
|
||||
|
||||
{ name: 'twitter:card', content: computed(() => d()['twitter:card']) },
|
||||
{ name: 'twitter:creator', content: computed(() => d()['twitter:creator']) },
|
||||
{ name: 'twitter:title', content: computed(() => d()['twitter:title'] || d().title) },
|
||||
{
|
||||
name: 'twitter:description',
|
||||
content: computed(() => d()['twitter:description'] || d().description),
|
||||
},
|
||||
{
|
||||
name: 'twitter:image',
|
||||
content: computed(() => withHost(d()['twitter:image'] || d().image)),
|
||||
},
|
||||
],
|
||||
|
||||
link: [
|
||||
{ rel: 'canonical', href: canonical },
|
||||
],
|
||||
|
||||
script: computed(() =>
|
||||
structuredData.value
|
||||
? [
|
||||
{
|
||||
type: 'application/ld+json',
|
||||
children: structuredData.value,
|
||||
},
|
||||
]
|
||||
: [],
|
||||
) as any,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import { shallowRef } from "vue";
|
||||
/**
|
||||
* usePageData composable:
|
||||
* - Loads localized JSON content that was eagerly bundled at build time.
|
||||
* - If the JSON is an array and a `slug` is provided, returns the matching item.
|
||||
* - Otherwise returns the whole JSON payload.
|
||||
* - Exposes the result as a shallowRef for lightweight reactivity.
|
||||
*/
|
||||
// Preload JSON files eagerly (SSG-safe)
|
||||
|
||||
const jsonModules = import.meta.glob("../data/**/*.json", { eager: true });
|
||||
|
||||
// Fetch page data for a locale; optionally filter by slug
|
||||
export function usePageData<T>(page: string, locale: string, slug?: string) {
|
||||
// Result holder (reactive)
|
||||
const data = shallowRef<T | null>(null);
|
||||
// Build module path for the requested page and locale
|
||||
const path = `../data/${page}/${locale}.json`;
|
||||
// Look up the eagerly-imported JSON module
|
||||
const module = jsonModules[path] as any;
|
||||
// Module found: extract its default export
|
||||
if (module?.default) {
|
||||
// JSON payload (object or array)
|
||||
const payload = module?.default ?? null;
|
||||
// If a slug is provided and payload is a list, return the matching item
|
||||
if (slug && Array.isArray(payload)) {
|
||||
// Find item with matching slug
|
||||
const found =
|
||||
(payload as any[]).find((item: any) => item && item.slug === slug) ??
|
||||
null;
|
||||
data.value = (found as T) ?? null;
|
||||
} else {
|
||||
data.value = payload as T;
|
||||
}
|
||||
}
|
||||
// Module missing: warn and set null
|
||||
else {
|
||||
console.warn(`[usePageData] Missing data for ${page}/${locale}.json`);
|
||||
data.value = null;
|
||||
}
|
||||
// Return the shallowRef so consumers can watch/react to it
|
||||
return data;
|
||||
}
|
||||
Reference in New Issue
Block a user