Initial Montech monorepo: Nuxt landing, Vue admin, Nest API.
Scaffold SSR site with CMS/blog/gallery/SEO admin and deploy configs for montech.ir. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
<template>
|
||||
<article v-if="post" class="min-h-screen px-6 pb-24 pt-28 md:px-10">
|
||||
<SiteNavbar brand="Montech" />
|
||||
<div class="mx-auto max-w-[760px]">
|
||||
<NuxtLink to="/blog" class="font-mono text-[11px] tracking-[0.22em] text-ash hover:text-volt">← BACK</NuxtLink>
|
||||
<h1 class="mt-8 font-display text-4xl font-extrabold uppercase tracking-[-0.03em] md:text-6xl">
|
||||
{{ post.title }}
|
||||
</h1>
|
||||
<p class="mt-4 font-mono text-[11px] tracking-[0.2em] text-ash">
|
||||
{{ post.publishedAt ? new Date(post.publishedAt).toLocaleDateString("en-GB") : "" }}
|
||||
</p>
|
||||
<div class="prose-montech mt-10 space-y-4 text-base leading-relaxed text-bone/90" v-html="post.content" />
|
||||
</div>
|
||||
</article>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const route = useRoute();
|
||||
const slug = String(route.params.slug);
|
||||
const { data: post } = await useAsyncData(`blog-${slug}`, () => fetchPublicPost(slug));
|
||||
|
||||
if (!post.value) {
|
||||
throw createError({ statusCode: 404, statusMessage: "Post not found" });
|
||||
}
|
||||
|
||||
useSeoMeta({
|
||||
title: post.value.seoTitle || post.value.title,
|
||||
description: post.value.seoDesc || post.value.excerpt,
|
||||
ogTitle: post.value.seoTitle || post.value.title,
|
||||
ogDescription: post.value.seoDesc || post.value.excerpt,
|
||||
ogImage: post.value.ogImage || post.value.coverUrl || undefined,
|
||||
});
|
||||
if (post.value.canonical) {
|
||||
useHead({ link: [{ rel: "canonical", href: post.value.canonical }] });
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,29 @@
|
||||
<template>
|
||||
<div class="min-h-screen px-6 pb-24 pt-28 md:px-10">
|
||||
<SiteNavbar :brand="'Montech'" />
|
||||
<div class="mx-auto max-w-[1100px]">
|
||||
<h1 class="font-display text-5xl font-extrabold uppercase tracking-[-0.03em] md:text-7xl">Blog</h1>
|
||||
<p class="mt-4 max-w-xl text-ash">Notes on product, engineering and shipping software.</p>
|
||||
<ul class="mt-16 divide-y divide-white/[0.07] border-y border-white/[0.07]">
|
||||
<li v-for="post in posts" :key="post.id" class="py-8">
|
||||
<NuxtLink :to="`/blog/${post.slug}`" class="group block">
|
||||
<p class="font-mono text-[10px] tracking-[0.22em] text-ash">
|
||||
{{ post.publishedAt ? new Date(post.publishedAt).toLocaleDateString("en-GB") : "" }}
|
||||
</p>
|
||||
<h2 class="mt-2 font-display text-2xl font-bold uppercase tracking-[-0.02em] group-hover:text-volt md:text-3xl">
|
||||
{{ post.title }}
|
||||
</h2>
|
||||
<p class="mt-3 max-w-2xl text-sm text-ash">{{ post.excerpt }}</p>
|
||||
</NuxtLink>
|
||||
</li>
|
||||
</ul>
|
||||
<p v-if="!posts?.length" class="mt-10 text-ash">No posts yet.</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const { data: posts } = await useAsyncData("blog-list", () => fetchPublicBlog());
|
||||
const { data: page } = await useAsyncData("page-blog", () => fetchPage("blog"));
|
||||
applyPageSeo(page.value?.seo);
|
||||
</script>
|
||||
@@ -0,0 +1,28 @@
|
||||
<template>
|
||||
<div class="min-h-screen px-6 pb-24 pt-28 md:px-10">
|
||||
<SiteNavbar brand="Montech" />
|
||||
<div class="mx-auto max-w-[1500px]">
|
||||
<h1 class="font-display text-5xl font-extrabold uppercase tracking-[-0.03em] md:text-7xl">Gallery</h1>
|
||||
<p class="mt-4 max-w-xl text-ash">Selected visuals from projects and studio life.</p>
|
||||
<div class="mt-16 grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
<figure v-for="item in items" :key="item.id" class="overflow-hidden border border-white/[0.07] bg-coal">
|
||||
<div
|
||||
class="aspect-[4/3] bg-cover bg-center"
|
||||
:style="item.imageUrl ? { backgroundImage: `url(${item.imageUrl})` } : {}"
|
||||
/>
|
||||
<figcaption class="p-4">
|
||||
<p class="font-display text-lg font-bold uppercase">{{ item.title }}</p>
|
||||
<p v-if="item.category" class="mt-1 font-mono text-[10px] tracking-[0.2em] text-ash">{{ item.category }}</p>
|
||||
</figcaption>
|
||||
</figure>
|
||||
</div>
|
||||
<p v-if="!items?.length" class="mt-10 text-ash">Gallery is empty.</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const { data: items } = await useAsyncData("gallery", () => fetchPublicGallery());
|
||||
const { data: page } = await useAsyncData("page-gallery", () => fetchPage("gallery"));
|
||||
applyPageSeo(page.value?.seo);
|
||||
</script>
|
||||
@@ -0,0 +1,35 @@
|
||||
<template>
|
||||
<div v-if="site">
|
||||
<SiteNavbar :brand="site.settings.brandName" :timezone="site.settings.timezone" />
|
||||
<main>
|
||||
<HeroSection :data="site.sections.hero" />
|
||||
<section class="relative overflow-hidden py-16 md:py-24">
|
||||
<div class="-ml-[5%] w-[110%] -rotate-2 bg-volt py-4 text-ink md:py-5">
|
||||
<SiteMarquee
|
||||
:items="site.sections.marquee.items"
|
||||
:duration="22"
|
||||
class-name="font-display text-xl font-bold uppercase tracking-[-0.01em] md:text-3xl"
|
||||
item-class-name="mx-4 md:mx-6"
|
||||
separator-class-name="text-ink/50"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
<ManifestoSection :data="site.sections.manifesto" />
|
||||
<ServicesSection :data="site.sections.services" />
|
||||
<WorkSection :data="site.sections.work" />
|
||||
<ProcessSection :data="site.sections.process" />
|
||||
<StatsSection :data="site.sections.stats" />
|
||||
<TeamSection :data="site.sections.team" />
|
||||
</main>
|
||||
<SiteFooter :brand="site.settings.brandName" :data="site.sections.footer" />
|
||||
</div>
|
||||
<div v-else class="grid min-h-screen place-items-center text-ash">
|
||||
Unable to load site content. Is the API running?
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const { data: site } = await useAsyncData("public-site", () => fetchPublicSite());
|
||||
const homePage = computed(() => site.value?.pages?.find((p: any) => p.key === "home"));
|
||||
applyPageSeo(homePage.value?.seo);
|
||||
</script>
|
||||
Reference in New Issue
Block a user