Update
This commit is contained in:
282
resources/js/projects-renderer/ProjectPageRenderer.vue
Normal file
282
resources/js/projects-renderer/ProjectPageRenderer.vue
Normal file
@@ -0,0 +1,282 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user