Files
aritmija/resources/js/projects-renderer/ProjectPageRenderer.vue
2026-05-13 17:11:09 +02:00

283 lines
11 KiB
Vue

<script setup>
import { computed } from 'vue';
import PublicSlot from './components/blocks/PublicSlot.vue';
import ProjectBlockRenderer from './components/ProjectBlockRenderer.vue';
import ProjectHeadline from './components/ProjectHeadline.vue';
import ProjectHero from './components/ProjectHero.vue';
import ProjectMetadata from './components/ProjectMetadata.vue';
import { normalizeProjectSchema } from './schema/projectSchema';
const props = defineProps({
project: {
type: Object,
default: () => ({}),
},
editable: {
type: Boolean,
default: false,
},
selectedBlockId: {
type: String,
default: null,
},
activeLang: {
type: String,
default: '',
},
mobilePreview: {
type: Boolean,
default: false,
},
});
const emit = defineEmits(['select-block']);
const normalizedProject = computed(() => normalizeProjectSchema(props.project, props.activeLang));
const header = computed(() => normalizedProject.value.header);
const metadata = computed(() => normalizedProject.value.metadata);
// Map heroMedia → a slot-data shape PublicSlot understands
const heroSlot = computed(() => {
const m = normalizedProject.value.heroMedia;
if (!m) return null;
const isVideoType = ['youtube', 'frameio', 'bunny', 'video'].includes(m.type);
if (isVideoType) {
return { type: 'video', content: {}, image: { url: '' }, media: m };
}
return { type: 'image', content: {}, image: { url: m.url || '', alt: '' }, media: m };
});
const publicContentBlocks = computed(() => {
const hasMetaDescription = !!metadata.value.description;
return normalizedProject.value.contentBlocks.filter((block) => {
if (!hasMetaDescription) return true;
return !(block.type === 'FullWidth' && block.slot?.type === 'text');
});
});
</script>
<template>
<!-- ===================== PUBLIC VIEW ===================== -->
<template v-if="!editable">
<section class="projects-area">
<div class="container">
<!-- Headline + hero + metadata -->
<div class="row">
<div class="col-lg-12">
<div class="single-project">
<div class="project-info">
<div class="row">
<div class="col-lg-6 col-md-8">
<h2>{{ header.headline || 'Untitled project' }}</h2>
</div>
<div v-if="header.subline" class="col-lg-6 col-md-4">
<div class="subtitle"><p>{{ header.subline }}</p></div>
</div>
</div>
</div>
<div class="thumbnail-wrap">
<PublicSlot
v-if="heroSlot"
:slot-data="heroSlot"
:active-lang="activeLang"
/>
</div>
<div class="row mt-5">
<div class="col-lg-6">
<div class="project-info mb-0">
<div v-if="metadata.clientName" class="client-info">
<h4>Client</h4>
<span>{{ metadata.clientName }}</span>
</div>
<div v-if="metadata.awarded?.length" class="project-details">
<h4>Awarded</h4>
<p v-for="award in metadata.awarded" :key="award">{{ award }}</p>
</div>
</div>
</div>
<div class="col-lg-6">
<div v-if="metadata.description" class="description-text" v-html="metadata.description"></div>
</div>
</div>
</div>
</div>
</div>
<!-- Content blocks all in one shared row so columns flow together -->
<div v-if="publicContentBlocks.length" class="row">
<template v-for="block in publicContentBlocks" :key="block.id">
<!-- FullWidth text centered narrow column -->
<div v-if="block.type === 'FullWidth' && block.slot?.type === 'text'" class="col-lg-12">
<div class="row project-content justify-content-center">
<div class="col-lg-6 text-center">
<PublicSlot
:slot-data="block.slot"
:active-lang="activeLang"
/>
</div>
</div>
</div>
<!-- FullWidth non-text full 12-col -->
<div v-else-if="block.type === 'FullWidth'" class="col-lg-12">
<div class="single-project">
<div class="thumbnail-wrap">
<PublicSlot
:slot-data="block.slot"
:active-lang="activeLang"
/>
</div>
</div>
</div>
<!-- TwoColumns: text slots centered col-lg-10; image/video slots col-lg-12 thumbnail-wrap -->
<template v-else-if="block.type === 'TwoColumns'">
<!-- left text -->
<div v-if="block.left?.type === 'text'" class="col-lg-12">
<div class="row project-content justify-content-center">
<div class="col-lg-10 text-center">
<PublicSlot :slot-data="block.left" :active-lang="activeLang" />
</div>
</div>
</div>
<!-- left non-text -->
<div v-else-if="block.left" class="col-lg-6 col-md-6">
<div class="single-project">
<div class="thumbnail-wrap">
<PublicSlot :slot-data="block.left" :active-lang="activeLang" />
</div>
</div>
</div>
<!-- right text -->
<div v-if="block.right?.type === 'text'" class="col-lg-12">
<div class="row project-content justify-content-center">
<div class="col-lg-10 text-center">
<PublicSlot :slot-data="block.right" :active-lang="activeLang" />
</div>
</div>
</div>
<!-- right non-text -->
<div v-else-if="block.right" class="col-lg-6 col-md-6">
<div class="single-project">
<div class="thumbnail-wrap">
<PublicSlot :slot-data="block.right" :active-lang="activeLang" />
</div>
</div>
</div>
</template>
</template>
</div>
</div>
</section>
</template>
<!-- ===================== EDITOR PREVIEW ===================== -->
<template v-else>
<section class="project-page-renderer">
<div class="project-page-renderer__shell">
<!-- Hero block: clickable wrapper with same border treatment as dynamic blocks -->
<div
class="project-hero-block"
:class="{ 'project-hero-block--selected': selectedBlockId === '__hero__' }"
@click.stop="emit('select-block', '__hero__')"
>
<div class="project-hero-block__bar">
<span class="project-hero-block__badge">Hero / Header</span>
</div>
<ProjectHeadline
:key="activeLang"
:headline="header.headline"
:subline="header.subline"
:mobile-preview="mobilePreview"
/>
<ProjectHero :media="normalizedProject.heroMedia" />
<ProjectMetadata
:key="activeLang"
:metadata="metadata"
:mobile-preview="mobilePreview"
/>
</div>
<div class="project-page-renderer__blocks">
<ProjectBlockRenderer
v-for="block in normalizedProject.contentBlocks"
:key="block.id"
:block="block"
:editable="editable"
:selected="block.id === selectedBlockId"
:active-lang="activeLang"
@select="emit('select-block', $event)"
/>
</div>
</div>
</section>
</template>
</template>
<style scoped>
/* Editor preview only */
.project-page-renderer {
--project-ink: #0f172a;
--project-muted: #667085;
--project-surface: rgba(255, 255, 255, 0.72);
background:
radial-gradient(circle at top right, rgba(14, 116, 144, 0.12), transparent 26%),
linear-gradient(180deg, rgba(248, 250, 252, 0.95), rgba(241, 245, 249, 0.95));
padding: clamp(1rem, 4%, 3rem);
}
.project-page-renderer__shell {
display: grid;
gap: 2rem;
margin: 0 auto;
max-width: 100%;
width: 100%;
}
.project-page-renderer__blocks {
display: grid;
gap: 2rem;
}
/* Hero clickable wrapper */
.project-hero-block {
cursor: pointer;
display: grid;
gap: 0.85rem;
position: relative;
}
.project-hero-block__bar {
align-items: center;
display: flex;
gap: 0.5rem;
}
.project-hero-block__badge {
background: rgba(14, 116, 144, 0.1);
border-radius: 999px;
color: #0f766e;
font-size: 0.75rem;
font-weight: 600;
padding: 0.35rem 0.7rem;
}
.project-hero-block--selected {
outline: 2px solid rgba(14, 116, 144, 0.35);
outline-offset: 0.75rem;
}
@media (max-width: 768px) {
.project-page-renderer {
padding: 1rem;
}
}
</style>