refactor: move public static pages
Some checks failed
Backend CI/CD / build_and_deploy (push) Has been cancelled
Frontend CI/CD / build_and_deploy (push) Has been cancelled

This commit is contained in:
2026-05-05 11:39:02 -04:00
parent 5baacbceea
commit f6c351c31e
22 changed files with 47 additions and 28 deletions

View File

@@ -0,0 +1,407 @@
<template>
<header class="site-menu">
<div class="site-menu-inner">
<router-link
class="site-brand"
to="/"
>
<span class="site-brand-mark">S</span>
<span class="site-brand-text">Socialize</span>
</router-link>
<nav
class="site-nav"
:aria-label="t('public.nav.ariaLabel')"
>
<div
ref="productMenuRef"
class="site-nav-group site-nav-product"
@pointerenter="openProductMenu"
@pointerleave="scheduleProductMenuClose"
@focusin="openProductMenu"
@focusout="scheduleProductMenuClose"
>
<button
type="button"
:aria-expanded="isProductMenuOpen"
aria-haspopup="true"
>
{{ t('public.nav.product') }}
</button>
<div
class="site-product-panel"
:class="{ open: isProductMenuOpen }"
>
<router-link
v-for="feature in productFeatureItems"
:key="feature.slug"
class="site-product-link"
:to="`/product/${feature.slug}`"
@click="closeProductMenu"
>
<span class="site-product-icon">
<svg
viewBox="0 0 24 24"
aria-hidden="true"
>
<path :d="feature.icon" />
</svg>
</span>
<span>
<strong>{{ t(`public.features.${feature.slug}.title`) }}</strong>
<small>{{ t(`public.features.${feature.slug}.description`) }}</small>
</span>
</router-link>
</div>
</div>
<router-link to="/pricing">{{ t('public.nav.pricing') }}</router-link>
<div
ref="resourcesMenuRef"
class="site-nav-group"
@pointerenter="openResourcesMenu"
@pointerleave="scheduleResourcesMenuClose"
@focusin="openResourcesMenu"
@focusout="scheduleResourcesMenuClose"
>
<button
type="button"
:aria-expanded="isResourcesMenuOpen"
aria-haspopup="true"
@click="toggleResourcesMenu"
>
{{ t('public.nav.resources') }}
</button>
<div
class="site-nav-menu"
:class="{ open: isResourcesMenuOpen }"
>
<router-link
to="/blogs"
@click="closeResourcesMenu"
>
{{ t('public.nav.blogs') }}
</router-link>
<router-link
to="/guides"
@click="closeResourcesMenu"
>
{{ t('public.nav.guides') }}
</router-link>
</div>
</div>
</nav>
<div
class="site-language-toggle"
:aria-label="t('public.nav.language')"
>
<button
type="button"
:class="{ active: currentLocale === 'en' }"
@click="setLocale('en')"
>
En
</button>
<button
type="button"
:class="{ active: currentLocale === 'fr' }"
@click="setLocale('fr')"
>
Fr
</button>
</div>
<router-link
class="site-login"
:to="authLink"
>
{{ authLabel }}
</router-link>
</div>
</header>
</template>
<script setup>
import { computed, onBeforeUnmount, onMounted, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { useAuthStore } from '@/features/auth/stores/authStore.js';
import { productFeatureItems } from '@/static/productFeatures.js';
const allowedLocales = ['en', 'fr'];
const localeStorageKey = 'user-locale';
const authStore = useAuthStore();
const { locale, t } = useI18n();
const isProductMenuOpen = ref(false);
const isResourcesMenuOpen = ref(false);
const productMenuRef = ref(null);
const resourcesMenuRef = ref(null);
let productMenuCloseTimer = null;
let resourcesMenuCloseTimer = null;
const authLink = computed(() =>
authStore.isAuthenticated
? '/app/dashboard'
: '/login'
);
const authLabel = computed(() =>
authStore.isAuthenticated
? t('public.nav.openApp')
: t('public.nav.login')
);
const currentLocale = computed(() =>
allowedLocales.includes(locale.value) ? locale.value : 'en'
);
function setLocale(nextLocale) {
if (!allowedLocales.includes(nextLocale)) {
return;
}
locale.value = nextLocale;
if (typeof window !== 'undefined') {
window.sessionStorage.setItem(localeStorageKey, nextLocale);
}
}
function clearProductMenuCloseTimer() {
if (productMenuCloseTimer === null) {
return;
}
window.clearTimeout(productMenuCloseTimer);
productMenuCloseTimer = null;
}
function clearResourcesMenuCloseTimer() {
if (resourcesMenuCloseTimer === null) {
return;
}
window.clearTimeout(resourcesMenuCloseTimer);
resourcesMenuCloseTimer = null;
}
function openProductMenu() {
clearProductMenuCloseTimer();
isProductMenuOpen.value = true;
}
function openResourcesMenu() {
clearResourcesMenuCloseTimer();
isResourcesMenuOpen.value = true;
}
function closeProductMenu() {
clearProductMenuCloseTimer();
isProductMenuOpen.value = false;
}
function closeResourcesMenu() {
clearResourcesMenuCloseTimer();
isResourcesMenuOpen.value = false;
}
function toggleResourcesMenu() {
clearResourcesMenuCloseTimer();
isResourcesMenuOpen.value = !isResourcesMenuOpen.value;
}
function scheduleProductMenuClose() {
clearProductMenuCloseTimer();
productMenuCloseTimer = window.setTimeout(() => {
const activeElement = document.activeElement;
if (productMenuRef.value?.contains(activeElement)) {
return;
}
closeProductMenu();
}, 120);
}
function scheduleResourcesMenuClose() {
clearResourcesMenuCloseTimer();
resourcesMenuCloseTimer = window.setTimeout(() => {
const activeElement = document.activeElement;
if (resourcesMenuRef.value?.contains(activeElement)) {
return;
}
closeResourcesMenu();
}, 120);
}
onMounted(() => {
const storedLocale = window.sessionStorage.getItem(localeStorageKey);
if (allowedLocales.includes(storedLocale)) {
locale.value = storedLocale;
}
});
onBeforeUnmount(() => {
clearProductMenuCloseTimer();
clearResourcesMenuCloseTimer();
});
</script>
<style scoped>
.site-menu {
@apply sticky top-0 z-30 w-full;
background: rgba(255, 250, 242, 0.9);
backdrop-filter: blur(18px);
border-bottom: 1px solid rgba(23, 32, 51, 0.08);
}
.site-menu-inner {
@apply mx-auto flex w-full max-w-7xl items-center justify-between gap-4 px-5 py-4 md:px-8;
}
.site-brand {
@apply flex min-w-0 items-center gap-3 no-underline;
color: #172033;
}
.site-brand-mark {
@apply flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-2xl text-base font-black;
background: linear-gradient(135deg, #ff8a3d 0%, #ef4444 100%);
color: #fffaf2;
}
.site-brand-text {
@apply truncate text-lg font-black uppercase tracking-[0.18em];
}
.site-nav {
@apply hidden items-center justify-center gap-2 sm:flex;
}
.site-nav a {
@apply rounded-[0.65rem] px-4 py-2 text-sm font-semibold no-underline transition-colors;
color: #44516a;
}
.site-nav a:hover,
.site-nav-group:hover > button {
background: rgba(23, 32, 51, 0.06);
color: #172033;
}
.site-nav-group {
@apply relative;
}
.site-nav-group > button {
@apply rounded-[0.65rem] px-4 py-2 text-sm font-semibold transition-colors;
color: #44516a;
}
.site-nav-menu {
@apply invisible absolute left-0 top-[calc(100%+0.5rem)] flex min-w-36 flex-col gap-1 rounded-[1rem] border p-2 opacity-0 transition-opacity;
background: rgba(255, 255, 255, 0.98);
border-color: rgba(23, 32, 51, 0.08);
box-shadow: 0 18px 40px rgba(23, 32, 51, 0.12);
}
.site-nav-group:hover .site-nav-menu,
.site-nav-group:focus-within .site-nav-menu,
.site-nav-menu.open {
@apply visible opacity-100;
}
.site-nav-menu a {
@apply rounded-[0.75rem] px-3 py-2;
}
.site-product-panel {
@apply invisible absolute left-1/2 top-full grid w-[min(56rem,calc(100vw-2rem))] -translate-x-1/2 grid-cols-3 gap-2 rounded-[1.25rem] border p-3 opacity-0 transition-opacity;
background: rgba(255, 255, 255, 0.98);
border-color: rgba(23, 32, 51, 0.08);
box-shadow: 0 24px 60px rgba(23, 32, 51, 0.14);
}
.site-nav-product:hover .site-product-panel,
.site-nav-product:focus-within .site-product-panel,
.site-product-panel.open {
@apply visible opacity-100;
}
.site-product-link {
@apply grid grid-cols-[2.5rem_1fr] gap-3 rounded-[0.9rem] p-3 no-underline transition-colors;
}
.site-product-link:hover {
background: rgba(23, 32, 51, 0.05);
}
.site-product-icon {
@apply flex h-10 w-10 items-center justify-center rounded-[0.75rem];
background: rgba(15, 118, 110, 0.1);
color: #0f766e;
}
.site-product-icon svg {
@apply h-5 w-5;
fill: currentColor;
}
.site-product-link strong {
@apply block text-sm font-black leading-5;
color: #172033;
}
.site-product-link small {
@apply mt-1 block text-xs font-medium leading-5;
color: #44516a;
}
.site-language-toggle {
@apply flex items-center rounded-full border p-1;
background: rgba(255, 255, 255, 0.64);
border-color: rgba(23, 32, 51, 0.1);
}
.site-language-toggle button {
@apply flex h-8 min-w-9 items-center justify-center rounded-full px-3 text-xs font-black uppercase transition-colors;
color: #44516a;
}
.site-language-toggle button:hover {
color: #172033;
}
.site-language-toggle button.active {
background: #172033;
color: #fffaf2;
}
.site-login {
@apply flex h-10 min-w-[7.75rem] items-center justify-center rounded-full px-4 text-sm font-bold no-underline transition-colors;
background: #172033;
color: #fffaf2;
}
.site-login:hover {
background: #0f766e;
}
@media (max-width: 420px) {
.site-brand-text {
@apply hidden;
}
.site-menu-inner {
@apply gap-3;
}
}
@media (max-width: 820px) {
.site-product-panel {
@apply w-[min(30rem,calc(100vw-2rem))] grid-cols-1;
}
}
</style>

View File

@@ -0,0 +1,56 @@
import {
mdiAccountGroupOutline,
mdiCalendarCheckOutline,
mdiCheckDecagramOutline,
mdiClipboardTextClockOutline,
mdiCommentMultipleOutline,
mdiFileDocumentOutline,
mdiHistory,
mdiLinkVariant,
mdiShieldAccountOutline,
} from '@mdi/js';
export const productFeatureItems = [
{
slug: 'content-planning',
icon: mdiCalendarCheckOutline,
},
{
slug: 'asset-revisions',
icon: mdiHistory,
},
{
slug: 'comment-threads',
icon: mdiCommentMultipleOutline,
},
{
slug: 'approval-workflows',
icon: mdiCheckDecagramOutline,
},
{
slug: 'client-review',
icon: mdiLinkVariant,
},
{
slug: 'review-queues',
icon: mdiClipboardTextClockOutline,
},
{
slug: 'audit-trail',
icon: mdiFileDocumentOutline,
},
{
slug: 'workspace-access',
icon: mdiShieldAccountOutline,
},
{
slug: 'team-collaboration',
icon: mdiAccountGroupOutline,
},
];
export function getProductFeature(slug) {
const normalizedSlug = slug === 'assets-revisions' ? 'asset-revisions' : slug;
return productFeatureItems.find(feature => feature.slug === normalizedSlug) ?? productFeatureItems[0];
}

View File

@@ -0,0 +1,52 @@
import { useHead } from '@vueuse/head';
import { computed, unref } from 'vue';
function getCanonicalUrl(path) {
const configuredSiteUrl = import.meta.env.VITE_PUBLIC_SITE_URL
?? (typeof process !== 'undefined' ? process.env.VITE_PUBLIC_SITE_URL : undefined)
?? (typeof process !== 'undefined' ? process.env.SITE_URL : undefined);
if (configuredSiteUrl) {
return new URL(path, `${configuredSiteUrl.replace(/\/$/, '')}/`).toString();
}
if (typeof window === 'undefined') {
return path;
}
return new URL(path, window.location.origin).toString();
}
export function usePublicPageMeta({ title, description, path }) {
useHead({
title,
meta: [
{
name: 'description',
content: description,
},
{
name: 'robots',
content: 'index,follow',
},
{
property: 'og:title',
content: title,
},
{
property: 'og:description',
content: description,
},
{
property: 'og:type',
content: 'website',
},
],
link: [
{
rel: 'canonical',
href: computed(() => getCanonicalUrl(unref(path))),
},
],
});
}

View File

@@ -0,0 +1,9 @@
<template>
<svg fill="currentColor"
viewBox="0 0 512 512"
xmlns="http://www.w3.org/2000/svg">
<!--! Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2024 Fonticons, Inc. -->
<path
d="M512 256C512 114.6 397.4 0 256 0S0 114.6 0 256C0 376 82.7 476.8 194.2 504.5V334.2H141.4V256h52.8V222.3c0-87.1 39.4-127.5 125-127.5c16.2 0 44.2 3.2 55.7 6.4V172c-6-.6-16.5-1-29.6-1c-42 0-58.2 15.9-58.2 57.2V256h83.6l-14.4 78.2H287V510.1C413.8 494.8 512 386.9 512 256h0z"/>
</svg>
</template>

View File

@@ -0,0 +1,12 @@
<template>
<svg fill="currentColor"
viewBox="0 0 540 540"
xmlns="http://www.w3.org/2000/svg">
<path
d="M0 0 C2.69849689 0.0031451 5.39587704 -0.02033719 8.09423828 -0.0456543 C24.64095924 -0.10755047 40.6877731 1.83145667 56.59814453 6.50317383 C57.64365479 6.80505615 58.68916504 7.10693848 59.76635742 7.41796875 C92.79656028 17.21576522 120.14877162 34.84217931 145.09814453 58.31567383 C145.80712891 58.97051758 146.51611328 59.62536133 147.24658203 60.30004883 C156.43027452 69.07141226 164.10800524 79.75645166 171.09814453 90.31567383 C171.99726562 91.64985352 171.99726562 91.64985352 172.91455078 93.01098633 C202.24624509 138.40104257 210.21713133 193.46657021 199.45556641 245.92919922 C192.90962056 275.68852135 179.08829353 305.20779517 159.09814453 328.31567383 C158.12705075 329.46844293 157.15708766 330.62216508 156.18798828 331.77661133 C145.74534244 344.15953661 145.74534244 344.15953661 140.09814453 349.31567383 C139.43814453 349.31567383 138.77814453 349.31567383 138.09814453 349.31567383 C138.09814453 349.97567383 138.09814453 350.63567383 138.09814453 351.31567383 C125.46428062 363.48565937 109.61785837 373.30307857 94.09814453 381.31567383 C93.49325195 381.62843262 92.88835937 381.94119141 92.26513672 382.26342773 C64.72703152 396.27155233 34.49385055 402.76232703 3.72314453 402.69067383 C2.84274506 402.69013 1.96234558 402.68958618 1.05526733 402.68902588 C-13.25349056 402.64848426 -26.95433873 401.73425935 -40.90185547 398.31567383 C-42.23821045 397.99421387 -42.23821045 397.99421387 -43.6015625 397.66625977 C-72.07884569 390.61747809 -98.87807183 378.00655878 -121.53857422 359.26879883 C-124.98230298 356.422742 -128.53024689 353.7103623 -132.06591797 350.97973633 C-132.97470703 350.15602539 -132.97470703 350.15602539 -133.90185547 349.31567383 C-133.90185547 348.65567383 -133.90185547 347.99567383 -133.90185547 347.31567383 C-134.56185547 347.31567383 -135.22185547 347.31567383 -135.90185547 347.31567383 C-135.90185547 346.65567383 -135.90185547 345.99567383 -135.90185547 345.31567383 C-136.56185547 345.31567383 -137.22185547 345.31567383 -137.90185547 345.31567383 C-139.24169922 343.98754883 -139.24169922 343.98754883 -140.83935547 342.06567383 C-143.10188476 339.38611053 -145.39815773 336.76489417 -147.80810547 334.21801758 C-148.30834229 333.68660156 -148.8085791 333.15518555 -149.32397461 332.60766602 C-150.32555716 331.54718583 -151.33248849 330.49172924 -152.3449707 329.44165039 C-155.90185547 325.64913211 -155.90185547 325.64913211 -155.90185547 322.31567383 C-151.7526342 322.86293323 -149.09751951 325.32327358 -145.96435547 327.87817383 C-112.49996321 354.28872765 -70.28107399 364.92725762 -28.16088867 360.37231445 C11.7404784 355.37903429 50.51367105 333.83116901 75.28564453 302.00317383 C102.2073581 266.1942078 113.74287629 223.77918143 108.03564453 179.19067383 C103.36182966 146.79964046 88.57557441 116.98148746 66.09814453 93.31567383 C65.60604492 92.78570801 65.11394531 92.25574219 64.60693359 91.7097168 C40.8738918 66.25446714 8.58347239 50.05740641 -25.90185547 45.31567383 C-27.07103516 45.13391602 -28.24021484 44.9521582 -29.44482422 44.76489258 C-71.61533582 39.65438979 -112.89208728 51.7135382 -146.31201172 77.46020508 C-149.8276055 80.21952147 -153.19809546 83.10839876 -156.42138672 86.20629883 C-157.90185547 87.31567383 -157.90185547 87.31567383 -160.90185547 87.31567383 C-160.35570146 83.08040407 -157.80081795 80.22554824 -155.21435547 77.00317383 C-154.72813721 76.3936084 -154.24191895 75.78404297 -153.7409668 75.15600586 C-143.89916607 62.99985508 -133.13277345 52.06245136 -120.90185547 42.31567383 C-120.33450684 41.85741211 -119.7671582 41.39915039 -119.18261719 40.92700195 C-97.55281967 23.63297615 -71.10136924 11.56026197 -44.27685547 5.06567383 C-43.26478027 4.81954346 -42.25270508 4.57341309 -41.20996094 4.31982422 C-27.42871573 1.10789603 -14.10433559 -0.03420308 0 0 Z "
transform="translate(258.90185546875,69.684326171875)"/>
<path
d="M0 0 C23.93418963 20.21356155 38.18828481 47.87411238 43.30859375 78.609375 C43.72796962 83.82104607 43.79621346 89.00908218 43.74609375 94.234375 C43.7423877 94.93147583 43.73868164 95.62857666 43.73486328 96.34680176 C43.51079954 126.08447361 33.84943539 152.98108038 14.30859375 175.609375 C13.37273437 176.74439453 13.37273437 176.74439453 12.41796875 177.90234375 C7.3119221 183.93204517 1.58692987 188.83219983 -4.69140625 193.609375 C-5.65691406 194.351875 -6.62242187 195.094375 -7.6171875 195.859375 C-33.76277727 214.99515608 -66.44544142 222.27976227 -98.37890625 217.421875 C-129.2224086 212.211016 -156.10496998 196.18588064 -175.99609375 172.10546875 C-177.69140625 169.609375 -177.69140625 169.609375 -177.69140625 166.609375 C-176.37140625 166.939375 -175.05140625 167.269375 -173.69140625 167.609375 C-173.69140625 168.269375 -173.69140625 168.929375 -173.69140625 169.609375 C-172.85222656 170.00576172 -172.85222656 170.00576172 -171.99609375 170.41015625 C-169.47801141 171.72041265 -167.18096648 173.22649078 -164.81640625 174.796875 C-141.95286732 189.35191305 -116.13355444 194.94004479 -89.48388672 189.12792969 C-74.00965166 185.43003002 -61.09388018 178.62819028 -48.69140625 168.609375 C-48.05976563 168.14273438 -47.428125 167.67609375 -46.77734375 167.1953125 C-29.95140734 154.19707727 -19.18769244 130.97695501 -16.03515625 110.39453125 C-15.865 109.01587891 -15.865 109.01587891 -15.69140625 107.609375 C-15.55734375 106.55621094 -15.42328125 105.50304687 -15.28515625 104.41796875 C-12.8813248 80.40697048 -19.85309354 55.11934523 -34.69921875 36.02734375 C-35.35664063 35.22941406 -36.0140625 34.43148437 -36.69140625 33.609375 C-37.32820312 32.82175781 -37.965 32.03414062 -38.62109375 31.22265625 C-54.38763523 12.57508008 -77.98681734 0.98006353 -102.19970703 -1.04858398 C-115.33840527 -1.9297872 -128.14870541 -0.56547477 -140.69140625 3.609375 C-141.71621094 3.94710937 -142.74101562 4.28484375 -143.796875 4.6328125 C-155.05737548 8.64192736 -164.81345042 14.73678833 -173.984375 22.359375 C-175.69140625 23.609375 -175.69140625 23.609375 -177.69140625 23.609375 C-176.02661937 12.90007435 -162.17010016 2.50403181 -153.91601562 -3.73193359 C-107.16913616 -37.54750108 -44.81974776 -36.84949238 0 0 Z "
transform="translate(291.69140625,175.390625)"/>
</svg>
</template>

View File

@@ -0,0 +1,9 @@
<template>
<svg fill="currentColor"
viewBox="0 0 448 512"
xmlns="http://www.w3.org/2000/svg">
<!--! Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2024 Fonticons, Inc. -->
<path
d="M224.1 141c-63.6 0-114.9 51.3-114.9 114.9s51.3 114.9 114.9 114.9S339 319.5 339 255.9 287.7 141 224.1 141zm0 189.6c-41.1 0-74.7-33.5-74.7-74.7s33.5-74.7 74.7-74.7 74.7 33.5 74.7 74.7-33.6 74.7-74.7 74.7zm146.4-194.3c0 14.9-12 26.8-26.8 26.8-14.9 0-26.8-12-26.8-26.8s12-26.8 26.8-26.8 26.8 12 26.8 26.8zm76.1 27.2c-1.7-35.9-9.9-67.7-36.2-93.9-26.2-26.2-58-34.4-93.9-36.2-37-2.1-147.9-2.1-184.9 0-35.8 1.7-67.6 9.9-93.9 36.1s-34.4 58-36.2 93.9c-2.1 37-2.1 147.9 0 184.9 1.7 35.9 9.9 67.7 36.2 93.9s58 34.4 93.9 36.2c37 2.1 147.9 2.1 184.9 0 35.9-1.7 67.7-9.9 93.9-36.2 26.2-26.2 34.4-58 36.2-93.9 2.1-37 2.1-147.8 0-184.8zM398.8 388c-7.8 19.6-22.9 34.7-42.6 42.6-29.5 11.7-99.5 9-132.1 9s-102.7 2.6-132.1-9c-19.6-7.8-34.7-22.9-42.6-42.6-11.7-29.5-9-99.5-9-132.1s-2.6-102.7 9-132.1c7.8-19.6 22.9-34.7 42.6-42.6 29.5-11.7 99.5-9 132.1-9s102.7-2.6 132.1 9c19.6 7.8 34.7 22.9 42.6 42.6 11.7 29.5 9 99.5 9 132.1s2.7 102.7-9 132.1z"/>
</svg>
</template>

View File

@@ -0,0 +1,9 @@
<template>
<svg fill="currentColor"
viewBox="0 0 448 512"
xmlns="http://www.w3.org/2000/svg">
<!--! Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2024 Fonticons, Inc. -->
<path
d="M416 32H31.9C14.3 32 0 46.5 0 64.3v383.4C0 465.5 14.3 480 31.9 480H416c17.6 0 32-14.5 32-32.3V64.3c0-17.8-14.4-32.3-32-32.3zM135.4 416H69V202.2h66.5V416zm-33.2-243c-21.3 0-38.5-17.3-38.5-38.5S80.9 96 102.2 96c21.2 0 38.5 17.3 38.5 38.5 0 21.3-17.2 38.5-38.5 38.5zm282.1 243h-66.4V312c0-24.8-.5-56.7-34.5-56.7-34.6 0-39.9 27-39.9 54.9V416h-66.4V202.2h63.7v29.2h.9c8.9-16.8 30.6-34.5 62.9-34.5 67.2 0 79.7 44.3 79.7 101.9V416z"/>
</svg>
</template>

View File

@@ -0,0 +1,9 @@
<template>
<svg fill="currentColor"
viewBox="0 0 512 512"
xmlns="http://www.w3.org/2000/svg">
<!--! Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2024 Fonticons, Inc. -->
<path
d="M0 256C0 114.6 114.6 0 256 0S512 114.6 512 256s-114.6 256-256 256L37.1 512c-13.7 0-20.5-16.5-10.9-26.2L75 437C28.7 390.7 0 326.7 0 256zM349.6 153.6c23.6 0 42.7-19.1 42.7-42.7s-19.1-42.7-42.7-42.7c-20.6 0-37.8 14.6-41.8 34c-34.5 3.7-61.4 33-61.4 68.4l0 .2c-37.5 1.6-71.8 12.3-99 29.1c-10.1-7.8-22.8-12.5-36.5-12.5c-33 0-59.8 26.8-59.8 59.8c0 24 14.1 44.6 34.4 54.1c2 69.4 77.6 125.2 170.6 125.2s168.7-55.9 170.6-125.3c20.2-9.6 34.1-30.2 34.1-54c0-33-26.8-59.8-59.8-59.8c-13.7 0-26.3 4.6-36.4 12.4c-27.4-17-62.1-27.7-100-29.1l0-.2c0-25.4 18.9-46.5 43.4-49.9l0 0c4.4 18.8 21.3 32.8 41.5 32.8zM177.1 246.9c16.7 0 29.5 17.6 28.5 39.3s-13.5 29.6-30.3 29.6s-31.4-8.8-30.4-30.5s15.4-38.3 32.1-38.3zm190.1 38.3c1 21.7-13.7 30.5-30.4 30.5s-29.3-7.9-30.3-29.6c-1-21.7 11.8-39.3 28.5-39.3s31.2 16.6 32.1 38.3zm-48.1 56.7c-10.3 24.6-34.6 41.9-63 41.9s-52.7-17.3-63-41.9c-1.2-2.9 .8-6.2 3.9-6.5c18.4-1.9 38.3-2.9 59.1-2.9s40.7 1 59.1 2.9c3.1 .3 5.1 3.6 3.9 6.5z"/>
</svg>
</template>

View File

@@ -0,0 +1,8 @@
<template>
<svg fill="currentColor"
viewBox="0 0 448 512"
xmlns="http://www.w3.org/2000/svg">
<path
d="M448,209.91a210.06,210.06,0,0,1-122.77-39.25V349.38A162.55,162.55,0,1,1,185,188.31V278.2a74.62,74.62,0,1,0,52.23,71.18V0l88,0a121.18,121.18,0,0,0,1.86,22.17h0A122.18,122.18,0,0,0,381,102.39a121.43,121.43,0,0,0,67,20.14Z"/>
</svg>
</template>

View File

@@ -0,0 +1,9 @@
<template>
<svg fill="currentColor"
viewBox="0 0 512 512"
xmlns="http://www.w3.org/2000/svg">
<!--! Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2024 Fonticons, Inc. -->
<path
d="M352 256c0 22.2-1.2 43.6-3.3 64l-185.3 0c-2.2-20.4-3.3-41.8-3.3-64s1.2-43.6 3.3-64l185.3 0c2.2 20.4 3.3 41.8 3.3 64zm28.8-64l123.1 0c5.3 20.5 8.1 41.9 8.1 64s-2.8 43.5-8.1 64l-123.1 0c2.1-20.6 3.2-42 3.2-64s-1.1-43.4-3.2-64zm112.6-32l-116.7 0c-10-63.9-29.8-117.4-55.3-151.6c78.3 20.7 142 77.5 171.9 151.6zm-149.1 0l-176.6 0c6.1-36.4 15.5-68.6 27-94.7c10.5-23.6 22.2-40.7 33.5-51.5C239.4 3.2 248.7 0 256 0s16.6 3.2 27.8 13.8c11.3 10.8 23 27.9 33.5 51.5c11.6 26 20.9 58.2 27 94.7zm-209 0L18.6 160C48.6 85.9 112.2 29.1 190.6 8.4C165.1 42.6 145.3 96.1 135.3 160zM8.1 192l123.1 0c-2.1 20.6-3.2 42-3.2 64s1.1 43.4 3.2 64L8.1 320C2.8 299.5 0 278.1 0 256s2.8-43.5 8.1-64zM194.7 446.6c-11.6-26-20.9-58.2-27-94.6l176.6 0c-6.1 36.4-15.5 68.6-27 94.6c-10.5 23.6-22.2 40.7-33.5 51.5C272.6 508.8 263.3 512 256 512s-16.6-3.2-27.8-13.8c-11.3-10.8-23-27.9-33.5-51.5zM135.3 352c10 63.9 29.8 117.4 55.3 151.6C112.2 482.9 48.6 426.1 18.6 352l116.7 0zm358.1 0c-30 74.1-93.6 130.9-171.9 151.6c25.5-34.2 45.2-87.7 55.3-151.6l116.7 0z"/>
</svg>
</template>

View File

@@ -0,0 +1,9 @@
<template>
<svg fill="currentColor"
viewBox="0 0 512 512"
xmlns="http://www.w3.org/2000/svg">
<!--! Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2024 Fonticons, Inc. -->
<path
d="M389.2 48h70.6L305.6 224.2 487 464H345L233.7 318.6 106.5 464H35.8L200.7 275.5 26.8 48H172.4L272.9 180.9 389.2 48zM364.4 421.8h39.1L151.1 88h-42L364.4 421.8z"/>
</svg>
</template>

View File

@@ -0,0 +1,10 @@
<template>
<svg fill="currentColor"
viewBox="0 0 576 512"
xmlns="http://www.w3.org/2000/svg"
>
<!--! Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2024 Fonticons, Inc. -->
<path
d="M549.655 124.083c-6.281-23.65-24.787-42.276-48.284-48.597C458.781 64 288 64 288 64S117.22 64 74.629 75.486c-23.497 6.322-42.003 24.947-48.284 48.597-11.412 42.867-11.412 132.305-11.412 132.305s0 89.438 11.412 132.305c6.281 23.65 24.787 41.5 48.284 47.821C117.22 448 288 448 288 448s170.78 0 213.371-11.486c23.497-6.321 42.003-24.171 48.284-47.821 11.412-42.867 11.412-132.305 11.412-132.305s0-89.438-11.412-132.305zm-317.51 213.508V175.185l142.739 81.205-142.739 81.201z"/>
</svg>
</template>

View File

@@ -0,0 +1,60 @@
<script setup>
import { computed } from 'vue';
import { useI18n } from 'vue-i18n';
import LandingSiteMenu from '@/static/components/LandingSiteMenu.vue';
import { usePublicPageMeta } from '@/static/publicPageMeta.js';
const { t } = useI18n();
usePublicPageMeta({
title: computed(() => t('public.blogs.meta.title')),
description: computed(() => t('public.blogs.meta.description')),
path: '/blogs',
});
</script>
<template>
<div class="public-page">
<LandingSiteMenu />
<main class="public-page-content">
<section class="public-page-panel">
<div class="eyebrow">{{ t('public.blogs.eyebrow') }}</div>
<h1>{{ t('public.blogs.title') }}</h1>
<p>{{ t('public.blogs.description') }}</p>
</section>
</main>
</div>
</template>
<style scoped>
.public-page {
@apply min-h-screen w-full;
}
.public-page-content {
@apply mx-auto flex w-full max-w-7xl flex-col px-5 py-8 md:px-8 md:py-12;
}
.public-page-panel {
@apply rounded-[2rem] p-6 md:p-10;
background: rgba(255, 255, 255, 0.84);
border: 1px solid rgba(23, 32, 51, 0.08);
box-shadow: 0 18px 40px rgba(23, 32, 51, 0.06);
}
.eyebrow {
@apply text-xs font-bold uppercase tracking-[0.26em];
color: #ff8a3d;
}
h1 {
@apply mt-4 max-w-4xl text-4xl font-black leading-tight md:text-6xl;
color: #172033;
}
p {
@apply mt-5 max-w-3xl text-base leading-7 md:text-lg;
color: #44516a;
}
</style>

View File

@@ -0,0 +1,60 @@
<script setup>
import { computed } from 'vue';
import { useI18n } from 'vue-i18n';
import LandingSiteMenu from '@/static/components/LandingSiteMenu.vue';
import { usePublicPageMeta } from '@/static/publicPageMeta.js';
const { t } = useI18n();
usePublicPageMeta({
title: computed(() => t('public.guides.meta.title')),
description: computed(() => t('public.guides.meta.description')),
path: '/guides',
});
</script>
<template>
<div class="public-page">
<LandingSiteMenu />
<main class="public-page-content">
<section class="public-page-panel">
<div class="eyebrow">{{ t('public.guides.eyebrow') }}</div>
<h1>{{ t('public.guides.title') }}</h1>
<p>{{ t('public.guides.description') }}</p>
</section>
</main>
</div>
</template>
<style scoped>
.public-page {
@apply min-h-screen w-full;
}
.public-page-content {
@apply mx-auto flex w-full max-w-7xl flex-col px-5 py-8 md:px-8 md:py-12;
}
.public-page-panel {
@apply rounded-[2rem] p-6 md:p-10;
background: rgba(255, 255, 255, 0.84);
border: 1px solid rgba(23, 32, 51, 0.08);
box-shadow: 0 18px 40px rgba(23, 32, 51, 0.06);
}
.eyebrow {
@apply text-xs font-bold uppercase tracking-[0.26em];
color: #ff8a3d;
}
h1 {
@apply mt-4 max-w-4xl text-4xl font-black leading-tight md:text-6xl;
color: #172033;
}
p {
@apply mt-5 max-w-3xl text-base leading-7 md:text-lg;
color: #44516a;
}
</style>

View File

@@ -0,0 +1,353 @@
<script setup>
import { computed } from 'vue';
import { useI18n } from 'vue-i18n';
import LandingSiteMenu from '@/static/components/LandingSiteMenu.vue';
import { usePublicPageMeta } from '@/static/publicPageMeta.js';
const { t } = useI18n();
usePublicPageMeta({
title: computed(() => t('public.landing.meta.title')),
description: computed(() => t('public.landing.meta.description')),
path: '/',
});
const pillarKeys = ['singleSource', 'collaboration', 'ownership'];
const focusMetricKeys = ['clients', 'campaigns', 'contentItems'];
const proofKeys = ['reviews', 'handoff', 'traceability'];
const mockupRowKeys = ['reel', 'launch', 'newsletter'];
const pillars = computed(() => [
...pillarKeys.map(key => ({
eyebrow: t(`public.landing.pillars.${key}.eyebrow`),
title: t(`public.landing.pillars.${key}.title`),
})),
]);
const focusMetrics = computed(() =>
focusMetricKeys.map(key => ({
label: t(`public.landing.focus.${key}.label`),
description: t(`public.landing.focus.${key}.description`),
}))
);
const proofPoints = computed(() =>
proofKeys.map(key => ({
label: t(`public.landing.proof.${key}.label`),
value: t(`public.landing.proof.${key}.value`),
}))
);
const mockupRows = computed(() =>
mockupRowKeys.map(key => ({
title: t(`public.landing.mockup.${key}.title`),
meta: t(`public.landing.mockup.${key}.meta`),
status: t(`public.landing.mockup.${key}.status`),
}))
);
</script>
<template>
<div class="landing-page">
<LandingSiteMenu />
<main class="landing-shell">
<section
id="products"
class="hero-card"
>
<div class="hero-copy">
<div class="eyebrow">{{ t('public.landing.hero.eyebrow') }}</div>
<h1>{{ t('public.landing.hero.title') }}</h1>
<p>{{ t('public.landing.hero.description') }}</p>
<div class="proof-row">
<div
v-for="proof in proofPoints"
:key="proof.label"
>
<strong>{{ proof.value }}</strong>
<span>{{ proof.label }}</span>
</div>
</div>
<div class="hero-actions">
<router-link to="/register">
<button class="primary">{{ t('public.landing.hero.primaryAction') }}</button>
</router-link>
</div>
<span class="cta-note">{{ t('public.landing.hero.ctaNote') }}</span>
</div>
<div
class="workflow-preview"
aria-hidden="true"
>
<div class="preview-bar">
<span>{{ t('public.landing.mockup.title') }}</span>
<strong>{{ t('public.landing.mockup.badge') }}</strong>
</div>
<div class="preview-list">
<div
v-for="row in mockupRows"
:key="row.title"
class="preview-row"
>
<div>
<strong>{{ row.title }}</strong>
<span>{{ row.meta }}</span>
</div>
<em>{{ row.status }}</em>
</div>
</div>
<div class="approval-card">
<span>{{ t('public.landing.mockup.approvalLabel') }}</span>
<strong>{{ t('public.landing.mockup.approvalValue') }}</strong>
</div>
</div>
</section>
<section class="pillars-grid">
<article
v-for="pillar in pillars"
:key="pillar.title"
class="pillar-card"
>
<div class="eyebrow">{{ pillar.eyebrow }}</div>
<p>{{ pillar.title }}</p>
</article>
</section>
<section class="focus-card">
<div>
<div class="eyebrow">{{ t('public.landing.focus.eyebrow') }}</div>
<h2>{{ t('public.landing.focus.title') }}</h2>
<router-link
class="inline-cta"
to="/register"
>
{{ t('public.landing.hero.primaryAction') }}
</router-link>
</div>
<div class="focus-metrics">
<div
v-for="metric in focusMetrics"
:key="metric.label"
>
<strong>{{ metric.label }}</strong>
<span>{{ metric.description }}</span>
</div>
</div>
</section>
<section class="final-cta">
<div>
<div class="eyebrow">{{ t('public.landing.finalCta.eyebrow') }}</div>
<h2>{{ t('public.landing.finalCta.title') }}</h2>
<p>{{ t('public.landing.finalCta.description') }}</p>
</div>
<router-link to="/register">
<button class="primary">{{ t('public.landing.hero.primaryAction') }}</button>
</router-link>
</section>
</main>
</div>
</template>
<style scoped>
.landing-page {
@apply min-h-screen w-full;
}
.landing-shell {
@apply mx-auto flex w-full max-w-7xl flex-col gap-8 px-5 py-8 md:px-8 md:py-12;
}
.hero-card {
@apply grid gap-8 overflow-hidden rounded-[2rem] p-6 md:grid-cols-[1fr_0.88fr] md:p-10;
background: linear-gradient(145deg, #172033 0%, #25324b 65%, #314766 100%);
color: #fffaf2;
box-shadow: 0 30px 80px rgba(23, 32, 51, 0.18);
}
.hero-copy {
@apply flex flex-col gap-5;
}
.hero-copy h1 {
@apply max-w-3xl text-4xl font-black leading-tight md:text-6xl;
}
.hero-copy p {
@apply max-w-2xl text-base leading-7 md:text-lg;
color: rgba(255, 250, 242, 0.84);
}
.eyebrow {
@apply text-xs font-bold uppercase tracking-[0.26em];
color: #ffb26b;
}
.hero-actions {
@apply flex flex-col gap-3 sm:flex-row;
}
.hero-actions .primary,
.final-cta .primary {
@apply min-h-12 px-7 text-base;
box-shadow: 0 18px 40px rgba(255, 138, 61, 0.28);
}
.cta-note {
@apply text-sm font-semibold;
color: rgba(255, 250, 242, 0.72);
}
.proof-row {
@apply grid max-w-2xl gap-3 sm:grid-cols-3;
}
.proof-row div {
@apply rounded-[1rem] border p-4;
background: rgba(255, 250, 242, 0.08);
border-color: rgba(255, 250, 242, 0.12);
}
.proof-row strong {
@apply block text-2xl font-black;
color: #7dd3c7;
}
.proof-row span {
@apply mt-1 block text-xs font-bold uppercase leading-5 tracking-[0.12em];
color: rgba(255, 250, 242, 0.72);
}
.workflow-preview {
@apply flex flex-col gap-4 rounded-[1.5rem] p-4;
background: rgba(255, 250, 242, 0.1);
border: 1px solid rgba(255, 250, 242, 0.14);
}
.preview-bar {
@apply flex items-center justify-between gap-3 rounded-[1rem] bg-white/90 px-4 py-3;
color: #172033;
}
.preview-bar span,
.approval-card span {
@apply text-xs font-black uppercase tracking-[0.18em];
color: #0f766e;
}
.preview-bar strong {
@apply rounded-full px-3 py-1 text-xs font-black;
background: rgba(255, 138, 61, 0.14);
color: #b45309;
}
.preview-list {
@apply flex flex-col gap-3;
}
.preview-row {
@apply flex items-center justify-between gap-4 rounded-[1rem] bg-white/90 p-4;
color: #172033;
}
.preview-row strong {
@apply block text-sm font-black;
}
.preview-row span {
@apply mt-1 block text-xs font-semibold;
color: #44516a;
}
.preview-row em {
@apply rounded-full px-3 py-1 text-xs font-black not-italic;
background: rgba(15, 118, 110, 0.1);
color: #0f766e;
}
.approval-card {
@apply rounded-[1rem] bg-white/95 p-5;
color: #172033;
}
.approval-card strong {
@apply mt-2 block text-2xl font-black leading-tight;
}
.pillars-grid {
@apply grid gap-4 md:grid-cols-3;
}
.pillar-card {
@apply rounded-[1.5rem] p-6;
background: rgba(255, 255, 255, 0.84);
border: 1px solid rgba(23, 32, 51, 0.08);
box-shadow: 0 18px 40px rgba(23, 32, 51, 0.06);
}
.pillar-card p {
@apply mt-3 text-lg font-semibold leading-7;
color: #172033;
}
.focus-card {
@apply grid gap-6 rounded-[1.75rem] p-6 md:grid-cols-[1fr_1.1fr] md:p-8;
background: linear-gradient(135deg, rgba(255, 138, 61, 0.12), rgba(52, 211, 153, 0.1));
border: 1px solid rgba(23, 32, 51, 0.08);
}
.focus-card h2 {
@apply mt-3 text-2xl font-black leading-tight md:text-3xl;
color: #172033;
}
.inline-cta {
@apply mt-5 inline-flex h-11 items-center rounded-full px-5 text-sm font-black no-underline transition-colors;
background: #172033;
color: #fffaf2;
}
.inline-cta:hover {
background: #0f766e;
}
.focus-metrics {
@apply grid gap-4 md:grid-cols-3;
}
.focus-metrics div {
@apply rounded-[1.25rem] bg-white/70 p-5;
border: 1px solid rgba(23, 32, 51, 0.06);
}
.focus-metrics strong {
@apply block text-sm font-black uppercase tracking-[0.18em];
color: #0f766e;
}
.focus-metrics span {
@apply mt-2 block text-sm leading-6;
color: #3f4d63;
}
.final-cta {
@apply flex flex-col gap-6 rounded-[2rem] p-6 md:flex-row md:items-center md:justify-between md:p-10;
background: #172033;
color: #fffaf2;
box-shadow: 0 30px 80px rgba(23, 32, 51, 0.18);
}
.final-cta h2 {
@apply mt-3 max-w-3xl text-3xl font-black leading-tight md:text-5xl;
}
.final-cta p {
@apply mt-4 max-w-2xl text-base leading-7;
color: rgba(255, 250, 242, 0.78);
}
</style>

View File

@@ -0,0 +1,249 @@
<script setup>
import { computed } from 'vue';
import { useI18n } from 'vue-i18n';
import LandingSiteMenu from '@/static/components/LandingSiteMenu.vue';
import { usePublicPageMeta } from '@/static/publicPageMeta.js';
const { t } = useI18n();
const tierKeys = ['free', 'freelance', 'agency', 'enterprise'];
const listIndexes = [0, 1, 2, 3, 4];
const limitIndexes = [0, 1, 2, 3];
const tiers = computed(() =>
tierKeys.map(key => ({
key,
isFeatured: key === 'agency',
features: listIndexes.map(index => t(`public.pricing.tiers.${key}.features.${index}`)),
limits: limitIndexes.map(index => t(`public.pricing.tiers.${key}.limits.${index}`)),
}))
);
usePublicPageMeta({
title: computed(() => t('public.pricing.meta.title')),
description: computed(() => t('public.pricing.meta.description')),
path: '/pricing',
});
</script>
<template>
<div class="public-page">
<LandingSiteMenu />
<main class="public-page-content">
<section class="public-page-panel">
<div class="eyebrow">{{ t('public.pricing.eyebrow') }}</div>
<h1>{{ t('public.pricing.title') }}</h1>
<p>{{ t('public.pricing.description') }}</p>
</section>
<section class="pricing-grid">
<article
v-for="tier in tiers"
:key="tier.key"
class="pricing-tier"
:class="{ featured: tier.isFeatured }"
>
<div class="tier-top">
<span
v-if="tier.isFeatured"
class="tier-badge"
>
{{ t('public.pricing.bestValue') }}
</span>
<h2>{{ t(`public.pricing.tiers.${tier.key}.name`) }}</h2>
<p>{{ t(`public.pricing.tiers.${tier.key}.description`) }}</p>
</div>
<div class="tier-price">
<strong>{{ t(`public.pricing.tiers.${tier.key}.price`) }}</strong>
<span>{{ t(`public.pricing.tiers.${tier.key}.priceNote`) }}</span>
</div>
<router-link
class="tier-action"
to="/register"
>
{{ t(`public.pricing.tiers.${tier.key}.action`) }}
</router-link>
<div class="tier-section">
<h3>{{ t('public.pricing.includedLabel') }}</h3>
<ul>
<li
v-for="feature in tier.features"
:key="feature"
>
{{ feature }}
</li>
</ul>
</div>
<div class="tier-section limits">
<h3>{{ t('public.pricing.limitsLabel') }}</h3>
<ul>
<li
v-for="limit in tier.limits"
:key="limit"
>
{{ limit }}
</li>
</ul>
</div>
</article>
</section>
</main>
</div>
</template>
<style scoped>
.public-page {
@apply min-h-screen w-full;
}
.public-page-content {
@apply mx-auto flex w-full max-w-7xl flex-col gap-8 px-5 py-8 md:px-8 md:py-12;
}
.public-page-panel {
@apply rounded-[2rem] p-6 md:p-10;
background: rgba(255, 255, 255, 0.84);
border: 1px solid rgba(23, 32, 51, 0.08);
box-shadow: 0 18px 40px rgba(23, 32, 51, 0.06);
}
.eyebrow {
@apply text-xs font-bold uppercase tracking-[0.26em];
color: #ff8a3d;
}
h1 {
@apply mt-4 max-w-4xl text-4xl font-black leading-tight md:text-6xl;
color: #172033;
}
p {
@apply mt-5 max-w-3xl text-base leading-7 md:text-lg;
color: #44516a;
}
.pricing-grid {
@apply grid gap-4 lg:grid-cols-4;
}
.pricing-tier {
@apply relative flex min-h-[42rem] flex-col gap-6 rounded-[1.5rem] border bg-white/85 p-5;
border-color: rgba(23, 32, 51, 0.08);
box-shadow: 0 18px 40px rgba(23, 32, 51, 0.06);
}
.pricing-tier.featured {
background: #172033;
color: #fffaf2;
border-color: rgba(255, 178, 107, 0.44);
box-shadow: 0 28px 70px rgba(23, 32, 51, 0.2);
}
.tier-badge {
@apply mb-4 inline-flex w-fit rounded-full px-3 py-1 text-xs font-black uppercase tracking-[0.16em];
background: #ffb26b;
color: #172033;
}
.tier-top h2 {
@apply text-2xl font-black;
color: #172033;
}
.featured .tier-top h2 {
color: #fffaf2;
}
.tier-top p {
@apply mt-3 text-sm leading-6;
color: #44516a;
}
.featured .tier-top p,
.featured .tier-price span,
.featured .tier-section li {
color: rgba(255, 250, 242, 0.78);
}
.tier-price {
@apply flex items-end gap-2;
}
.tier-price strong {
@apply text-4xl font-black leading-none;
color: #172033;
}
.featured .tier-price strong {
color: #fffaf2;
}
.tier-price span {
@apply pb-1 text-sm font-semibold;
color: #44516a;
}
.tier-action {
@apply flex h-11 items-center justify-center rounded-full px-5 text-sm font-black no-underline transition-colors;
background: #172033;
color: #fffaf2;
}
.tier-action:hover {
background: #0f766e;
}
.featured .tier-action {
background: #ffb26b;
color: #172033;
}
.featured .tier-action:hover {
background: #fffaf2;
}
.tier-section {
@apply flex flex-col gap-3;
}
.tier-section h3 {
@apply text-xs font-black uppercase tracking-[0.18em];
color: #0f766e;
}
.featured .tier-section h3 {
color: #ffb26b;
}
.tier-section ul {
@apply flex flex-col gap-2;
}
.tier-section li {
@apply text-sm leading-6;
color: #172033;
}
.tier-section li::before {
content: "✓";
@apply mr-2 font-black;
color: #0f766e;
}
.featured .tier-section li::before {
color: #ffb26b;
}
.limits {
@apply mt-auto border-t pt-5;
border-color: rgba(23, 32, 51, 0.08);
}
.featured .limits {
border-color: rgba(255, 250, 242, 0.14);
}
</style>

View File

@@ -0,0 +1,200 @@
<script setup>
import { computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router';
import LandingSiteMenu from '@/static/components/LandingSiteMenu.vue';
import { getProductFeature, productFeatureItems } from '@/static/productFeatures.js';
import { usePublicPageMeta } from '@/static/publicPageMeta.js';
const route = useRoute();
const { t } = useI18n();
const feature = computed(() => getProductFeature(route.params.featureSlug));
const featurePath = computed(() => `/product/${feature.value.slug}`);
const relatedFeatures = computed(() =>
productFeatureItems.filter(item => item.slug !== feature.value.slug).slice(0, 3)
);
const bullets = computed(() =>
[0, 1, 2].map(index => t(`public.features.${feature.value.slug}.bullets.${index}`))
);
usePublicPageMeta({
title: computed(() => `${t(`public.features.${feature.value.slug}.title`)} | Socialize`),
description: computed(() => t(`public.features.${feature.value.slug}.description`)),
path: featurePath,
});
</script>
<template>
<div class="public-page">
<LandingSiteMenu />
<main class="feature-page-content">
<section class="feature-hero">
<span class="feature-icon">
<svg
viewBox="0 0 24 24"
aria-hidden="true"
>
<path :d="feature.icon" />
</svg>
</span>
<div class="eyebrow">{{ t('public.product.eyebrow') }}</div>
<h1>{{ t(`public.features.${feature.slug}.detailTitle`) }}</h1>
<p>{{ t(`public.features.${feature.slug}.detailDescription`) }}</p>
</section>
<section class="feature-detail-panel">
<div class="eyebrow">{{ t('public.product.featureEyebrow') }}</div>
<ul>
<li
v-for="bullet in bullets"
:key="bullet"
>
<span aria-hidden="true"></span>
<strong>{{ bullet }}</strong>
</li>
</ul>
</section>
<section class="related-features">
<div>
<div class="eyebrow">{{ t('public.product.featureEyebrow') }}</div>
<h2>{{ t('public.product.featureTitle') }}</h2>
</div>
<div class="related-grid">
<router-link
v-for="item in relatedFeatures"
:key="item.slug"
class="related-card"
:to="`/product/${item.slug}`"
>
<span class="related-icon">
<svg
viewBox="0 0 24 24"
aria-hidden="true"
>
<path :d="item.icon" />
</svg>
</span>
<strong>{{ t(`public.features.${item.slug}.title`) }}</strong>
<span>{{ t(`public.features.${item.slug}.description`) }}</span>
</router-link>
</div>
</section>
</main>
</div>
</template>
<style scoped>
.public-page {
@apply min-h-screen w-full;
}
.feature-page-content {
@apply mx-auto flex w-full max-w-7xl flex-col gap-8 px-5 py-8 md:px-8 md:py-12;
}
.feature-hero {
@apply rounded-[2rem] p-6 md:p-10;
background: rgba(255, 255, 255, 0.84);
border: 1px solid rgba(23, 32, 51, 0.08);
box-shadow: 0 18px 40px rgba(23, 32, 51, 0.06);
}
.feature-icon,
.related-icon {
@apply flex h-12 w-12 items-center justify-center rounded-[0.9rem];
background: rgba(15, 118, 110, 0.1);
color: #0f766e;
}
.feature-icon svg,
.related-icon svg {
@apply h-5 w-5;
fill: currentColor;
}
.eyebrow {
@apply mt-5 text-xs font-bold uppercase tracking-[0.26em];
color: #ff8a3d;
}
h1 {
@apply mt-4 max-w-4xl text-4xl font-black leading-tight md:text-6xl;
color: #172033;
}
p {
@apply mt-5 max-w-3xl text-base leading-7 md:text-lg;
color: #44516a;
}
.related-grid {
@apply grid gap-4 md:grid-cols-3;
}
.related-card {
@apply rounded-[1.25rem] border bg-white/80 p-5;
border-color: rgba(23, 32, 51, 0.08);
box-shadow: 0 18px 40px rgba(23, 32, 51, 0.05);
}
.feature-detail-panel {
@apply rounded-[1.5rem] border bg-white/80 p-5 md:p-7;
border-color: rgba(23, 32, 51, 0.08);
box-shadow: 0 18px 40px rgba(23, 32, 51, 0.05);
}
.feature-detail-panel .eyebrow {
@apply mt-0;
}
.feature-detail-panel ul {
@apply mt-5 grid gap-3;
}
.feature-detail-panel li {
@apply grid grid-cols-[2rem_1fr] items-start gap-3 rounded-[1rem] p-4;
background: rgba(15, 118, 110, 0.08);
}
.feature-detail-panel li span {
@apply flex h-8 w-8 items-center justify-center rounded-full text-sm font-black;
background: #0f766e;
color: #fffaf2;
}
.feature-detail-panel li strong {
@apply pt-1 text-base font-semibold leading-7;
color: #172033;
}
.related-features {
@apply flex flex-col gap-5;
}
.related-features h2 {
@apply mt-3 max-w-3xl text-2xl font-black leading-tight md:text-3xl;
color: #172033;
}
.related-card {
@apply flex min-h-48 flex-col gap-3 no-underline transition-colors;
}
.related-card:hover {
background: #fff;
border-color: rgba(15, 118, 110, 0.28);
}
.related-card strong {
@apply text-base font-black;
color: #172033;
}
.related-card span:last-child {
@apply text-sm leading-6;
color: #44516a;
}
</style>

View File

@@ -0,0 +1,133 @@
<script setup>
import { computed } from 'vue';
import { useI18n } from 'vue-i18n';
import LandingSiteMenu from '@/static/components/LandingSiteMenu.vue';
import { productFeatureItems } from '@/static/productFeatures.js';
import { usePublicPageMeta } from '@/static/publicPageMeta.js';
const { t } = useI18n();
usePublicPageMeta({
title: computed(() => t('public.product.meta.title')),
description: computed(() => t('public.product.meta.description')),
path: '/product',
});
</script>
<template>
<div class="public-page">
<LandingSiteMenu />
<main class="public-page-content">
<section class="public-page-panel">
<div class="eyebrow">{{ t('public.product.eyebrow') }}</div>
<h1>{{ t('public.product.title') }}</h1>
<p>{{ t('public.product.description') }}</p>
</section>
<section class="product-features">
<div>
<div class="eyebrow">{{ t('public.product.featureEyebrow') }}</div>
<h2>{{ t('public.product.featureTitle') }}</h2>
</div>
<div class="feature-grid">
<router-link
v-for="feature in productFeatureItems"
:key="feature.slug"
class="feature-card"
:to="`/product/${feature.slug}`"
>
<span class="feature-icon">
<svg
viewBox="0 0 24 24"
aria-hidden="true"
>
<path :d="feature.icon" />
</svg>
</span>
<strong>{{ t(`public.features.${feature.slug}.title`) }}</strong>
<span>{{ t(`public.features.${feature.slug}.description`) }}</span>
</router-link>
</div>
</section>
</main>
</div>
</template>
<style scoped>
.public-page {
@apply min-h-screen w-full;
}
.public-page-content {
@apply mx-auto flex w-full max-w-7xl flex-col gap-8 px-5 py-8 md:px-8 md:py-12;
}
.public-page-panel {
@apply rounded-[2rem] p-6 md:p-10;
background: rgba(255, 255, 255, 0.84);
border: 1px solid rgba(23, 32, 51, 0.08);
box-shadow: 0 18px 40px rgba(23, 32, 51, 0.06);
}
.eyebrow {
@apply text-xs font-bold uppercase tracking-[0.26em];
color: #ff8a3d;
}
h1 {
@apply mt-4 max-w-4xl text-4xl font-black leading-tight md:text-6xl;
color: #172033;
}
p {
@apply mt-5 max-w-3xl text-base leading-7 md:text-lg;
color: #44516a;
}
.product-features {
@apply flex flex-col gap-5;
}
.product-features h2 {
@apply mt-3 max-w-3xl text-2xl font-black leading-tight md:text-3xl;
color: #172033;
}
.feature-grid {
@apply grid gap-4 md:grid-cols-3;
}
.feature-card {
@apply flex min-h-48 flex-col gap-3 rounded-[1.25rem] border bg-white/80 p-5 no-underline transition-colors;
border-color: rgba(23, 32, 51, 0.08);
box-shadow: 0 18px 40px rgba(23, 32, 51, 0.05);
}
.feature-card:hover {
background: #fff;
border-color: rgba(15, 118, 110, 0.28);
}
.feature-icon {
@apply flex h-11 w-11 items-center justify-center rounded-[0.85rem];
background: rgba(15, 118, 110, 0.1);
color: #0f766e;
}
.feature-icon svg {
@apply h-5 w-5;
fill: currentColor;
}
.feature-card strong {
@apply text-base font-black;
color: #172033;
}
.feature-card span:last-child {
@apply text-sm leading-6;
color: #44516a;
}
</style>