dummy content scaffolding
This commit is contained in:
parent
5c38cd9bdd
commit
750af00e08
9 changed files with 292 additions and 83 deletions
3
src/components.d.ts
vendored
3
src/components.d.ts
vendored
|
|
@ -18,10 +18,13 @@ declare module 'vue' {
|
|||
Layout: typeof import('./components/Layout.vue')['default']
|
||||
Lenis: typeof import('./components/Lenis.vue')['default']
|
||||
LoadingSpinner: typeof import('./components/LoadingSpinner.vue')['default']
|
||||
MosaicModal: typeof import('./components/MosaicModal.vue')['default']
|
||||
MosaicScroll: typeof import('./components/MosaicScroll.vue')['default']
|
||||
MosaicViewport: typeof import('./components/MosaicViewport.vue')['default']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
SiteHeader: typeof import('./components/site/Header.vue')['default']
|
||||
StrapiContent: typeof import('./components/strapi/Content.vue')['default']
|
||||
StrapiMedia: typeof import('./components/strapi/Media.vue')['default']
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
</suspense>
|
||||
|
||||
<main class="main-content">
|
||||
<infinite-scroll-demo />
|
||||
<mosaic-scroll :items="mosaicItems" />
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -13,9 +13,12 @@
|
|||
<script setup>
|
||||
import { useStrapiSingle } from '@/composables/useStrapi'
|
||||
import useStrapiHead from '@/composables/useStrapiHead'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const { data } = await useStrapiSingle('home')
|
||||
useStrapiHead()
|
||||
|
||||
const mosaicItems = computed(() => data.value?.mosaic_items || [])
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
|
|
|||
75
src/components/MosaicModal.vue
Normal file
75
src/components/MosaicModal.vue
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
<template>
|
||||
<div
|
||||
class="mosaic-modal theme-dark layout-grid-inner"
|
||||
@click="emit('close')"
|
||||
>
|
||||
<div class="window dl" @click.stop>
|
||||
<dl-row v-for="row in item.dl" :row="row" />
|
||||
</div>
|
||||
|
||||
<div class="window content" @click.stop data-lenis-prevent>
|
||||
<h1 class="title">{{ item.title }}</h1>
|
||||
<strapi-content :content="item.content" />
|
||||
</div>
|
||||
|
||||
<button class="window close" @click="emit('close')">Close X</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const props = defineProps({ item: Object })
|
||||
|
||||
const emit = defineEmits(['close'])
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.mosaic-modal {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
align-items: flex-start;
|
||||
padding-top: desktop-vh(27px);
|
||||
|
||||
.window {
|
||||
background: var(--theme-bg);
|
||||
color: var(--theme-fg);
|
||||
|
||||
&.dl {
|
||||
padding: desktop-vw(12px) desktop-vw(10px);
|
||||
border-radius: desktop-vw(10px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: desktop-vw(8px);
|
||||
grid-column: 4 / span 2;
|
||||
|
||||
.dl-row:first-child {
|
||||
border-top: none;
|
||||
}
|
||||
}
|
||||
&.content {
|
||||
padding: desktop-vw(34px) desktop-vw(24px);
|
||||
border-radius: desktop-vw(10px);
|
||||
grid-column: 6 / span 7;
|
||||
max-height: desktop-vh(879px);
|
||||
overflow: auto;
|
||||
|
||||
.title {
|
||||
text-align: center;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
}
|
||||
&.close {
|
||||
padding: desktop-vw(9px);
|
||||
border-radius: desktop-vw(16px);
|
||||
cursor: pointer;
|
||||
/* position: absolute;
|
||||
right: calc(
|
||||
(var(--layout-column-width) * 3) +
|
||||
(var(--layout-column-gap) * 3) + var(--layout-margin)
|
||||
);
|
||||
top: desktop-vw(40px); */
|
||||
margin-right: auto;
|
||||
@include label;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div class="infinite-scroll-demo">
|
||||
<div class="mosaic-scroll">
|
||||
<infinite-scroll-container
|
||||
:bufferSize="2"
|
||||
:initialIndex="0"
|
||||
|
|
@ -21,20 +21,28 @@
|
|||
import _shuffle from 'lodash/shuffle'
|
||||
import { useMosaicLayout } from '@/composables/useMosaicLayout'
|
||||
|
||||
const props = defineProps({
|
||||
items: Array,
|
||||
})
|
||||
|
||||
// Initialize mosaic layout generator
|
||||
const { generateLayout, populateCells } = useMosaicLayout()
|
||||
|
||||
const generateGifContent = (cell) => {
|
||||
return {
|
||||
id: `gif-${cell.id}`,
|
||||
width: Math.floor(cell.width),
|
||||
height: Math.floor(cell.height),
|
||||
note: _shuffle(
|
||||
'curabitur euismod ultrices porttitor. Vivamus magna sapien, pretium at ullamcorper semper, sollicitudin et magna curabitur eget dolor a neque facilisis semper pellentesque euismod luctus urna, non aliquam justo aliquet id vestibulum quis lobortis diam nulla facilisi phasellus et rutrum lectus vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Praesent mattis felis condimentum, gravida lacus id, pretium neque. Donec cursus augue a dui placerat porttitor.Vestibulum rhoncus lectus nisl, nec gravida ex sollicitudin vel etiam dictum ac lacus ac elementum donec turpis eros, aliquam in nibh a, posuere tristique neque. Nunc cursus molestie consectetur.Integer placerat lorem eu nunc semper porta.'.split(
|
||||
' ',
|
||||
),
|
||||
).join(' '),
|
||||
}
|
||||
// const generateGifContent = (cell) => {
|
||||
// return {
|
||||
// id: `gif-${cell.id}`,
|
||||
// width: Math.floor(cell.width),
|
||||
// height: Math.floor(cell.height),
|
||||
// note: _shuffle(
|
||||
// 'curabitur euismod ultrices porttitor. Vivamus magna sapien, pretium at ullamcorper semper, sollicitudin et magna curabitur eget dolor a neque facilisis semper pellentesque euismod luctus urna, non aliquam justo aliquet id vestibulum quis lobortis diam nulla facilisi phasellus et rutrum lectus vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Praesent mattis felis condimentum, gravida lacus id, pretium neque. Donec cursus augue a dui placerat porttitor.Vestibulum rhoncus lectus nisl, nec gravida ex sollicitudin vel etiam dictum ac lacus ac elementum donec turpis eros, aliquam in nibh a, posuere tristique neque. Nunc cursus molestie consectetur.Integer placerat lorem eu nunc semper porta.'.split(
|
||||
// ' ',
|
||||
// ),
|
||||
// ).join(' '),
|
||||
// }
|
||||
// }
|
||||
const generateCellContent = () => {
|
||||
const itemIndex = Math.floor(Math.random() * props.items.length)
|
||||
return props.items[itemIndex]
|
||||
}
|
||||
|
||||
// Generate mosaic content for each viewport
|
||||
|
|
@ -44,8 +52,6 @@ const generateMosaicContent = async (index) => {
|
|||
const variance = Math.abs(index % 2) * 2
|
||||
const targetCells = baseCellCount + variance
|
||||
|
||||
// Generate layout with different parameters based on viewport index
|
||||
// This adds variety to each viewport
|
||||
const layoutOptions = {
|
||||
minCellWidth: 200,
|
||||
minCellHeight: 200,
|
||||
|
|
@ -56,17 +62,16 @@ const generateMosaicContent = async (index) => {
|
|||
const cells = generateLayout(layoutOptions)
|
||||
|
||||
// Populate cells with GIF content
|
||||
const populatedCells = populateCells(cells, generateGifContent)
|
||||
const populatedCells = populateCells(cells, generateCellContent)
|
||||
|
||||
return {
|
||||
cells: populatedCells,
|
||||
viewportIndex: index,
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.infinite-scroll-demo {
|
||||
.mosaic-scroll {
|
||||
position: relative;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -3,7 +3,10 @@
|
|||
<div class="mosaic-container">
|
||||
<div
|
||||
v-for="cell in content.cells"
|
||||
class="mosaic-cell"
|
||||
:class="[
|
||||
'mosaic-cell',
|
||||
`layout-${cell.width >= cell.height ? 'landscape' : 'portrait'}`,
|
||||
]"
|
||||
:style="{
|
||||
left: cell.x + 'px',
|
||||
top: cell.y + 'px',
|
||||
|
|
@ -11,38 +14,31 @@
|
|||
height: cell.height + 'px',
|
||||
}"
|
||||
:key="`${viewport.index}-${cell.id}`"
|
||||
@click="openNote(cell.content)"
|
||||
@click="openModal(cell.content)"
|
||||
>
|
||||
<div class="gif-placeholder">
|
||||
<div class="gif-info">
|
||||
<span class="gif-id">{{ cell.content.id }}</span>
|
||||
<span class="gif-size"
|
||||
>{{ Math.round(cell.width) }}x{{
|
||||
Math.round(cell.height)
|
||||
}}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<strapi-media
|
||||
:field="cell.content.image"
|
||||
:aspect="100"
|
||||
fit="contain"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Note modal -->
|
||||
<!-- Content modal -->
|
||||
<teleport to="#container">
|
||||
<div v-if="selectedNote" class="note-modal-backdrop" @click="closeNote">
|
||||
<div class="note-modal" @click.stop>
|
||||
<button class="note-close" @click="closeNote">×</button>
|
||||
<div class="note-content">
|
||||
<p>{{ selectedNote.note }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<mosaic-modal
|
||||
v-if="selectedItem"
|
||||
:item="selectedItem"
|
||||
@close="closeModal"
|
||||
/>
|
||||
</teleport>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch } from 'vue'
|
||||
import { useEventListener } from '@vueuse/core'
|
||||
import { ref } from 'vue'
|
||||
import useLenis from '@/composables/useLenis'
|
||||
|
||||
const props = defineProps({
|
||||
viewport: {
|
||||
|
|
@ -63,26 +59,33 @@ const props = defineProps({
|
|||
},
|
||||
})
|
||||
|
||||
const selectedNote = ref(null)
|
||||
const selectedItem = ref(null)
|
||||
|
||||
const openNote = (cellContent) => {
|
||||
if (cellContent.note) {
|
||||
selectedNote.value = cellContent
|
||||
}
|
||||
const openModal = (cell) => {
|
||||
selectedItem.value = cell
|
||||
}
|
||||
|
||||
const closeNote = () => {
|
||||
selectedNote.value = null
|
||||
const closeModal = () => {
|
||||
selectedItem.value = null
|
||||
}
|
||||
|
||||
// Close note on Escape key
|
||||
const handleKeydown = (event) => {
|
||||
if (event.key === 'Escape' && selectedNote.value) {
|
||||
closeNote()
|
||||
if (event.key === 'Escape' && selectedItem.value) {
|
||||
closeModal()
|
||||
}
|
||||
}
|
||||
|
||||
useEventListener('keydown', handleKeydown)
|
||||
|
||||
// Pause scroll when modal is open
|
||||
const lenis = useLenis()
|
||||
watch(selectedItem, (newItem, oldItem) => {
|
||||
if (newItem && !oldItem) {
|
||||
lenis.value.stop()
|
||||
} else if (oldItem && !newItem) {
|
||||
lenis.value.start()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
|
@ -101,37 +104,22 @@ useEventListener('keydown', handleKeydown)
|
|||
overflow: hidden;
|
||||
border: 1px solid var(--theme-fg);
|
||||
cursor: pointer;
|
||||
|
||||
.gif-placeholder {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.gif-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.note-modal-backdrop {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
.note-modal {
|
||||
background: var(--theme-bg);
|
||||
width: 50vw;
|
||||
padding: 2rem;
|
||||
&.layout-portrait {
|
||||
.strapi-media {
|
||||
width: 50%;
|
||||
}
|
||||
}
|
||||
&.layout-landscape {
|
||||
.strapi-media {
|
||||
height: 50%;
|
||||
aspect-ratio: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<header v-if="data" class="site-header">
|
||||
<header v-if="data" class="site-header layout-grid-inner">
|
||||
<img class="logo" src="/images/output_pixelated.gif" />
|
||||
|
||||
<div class="info theme-red">
|
||||
|
|
@ -24,7 +24,6 @@ const { data } = await useStrapiGlobal()
|
|||
right: 0;
|
||||
pointer-events: none;
|
||||
padding: desktop-vw(27px);
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
|
||||
> * {
|
||||
|
|
@ -39,9 +38,9 @@ const { data } = await useStrapiGlobal()
|
|||
.info {
|
||||
background: var(--theme-bg);
|
||||
color: var(--theme-fg);
|
||||
width: desktop-vw(173px);
|
||||
border-radius: desktop-vw(10px);
|
||||
padding: desktop-vw(12px) desktop-vw(10px);
|
||||
grid-column: 2 / span 2;
|
||||
|
||||
.copy {
|
||||
margin-bottom: desktop-vw(18px);
|
||||
|
|
|
|||
|
|
@ -14,6 +14,11 @@ const props = defineProps({
|
|||
|
||||
<style lang="scss">
|
||||
.strapi-content {
|
||||
> * {
|
||||
margin-top: 1em;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
> *:first-child {
|
||||
margin-top: 0 !important;
|
||||
}
|
||||
|
|
|
|||
131
src/components/strapi/Media.vue
Normal file
131
src/components/strapi/Media.vue
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
<template>
|
||||
<div
|
||||
v-if="image"
|
||||
:class="['strapi-media', { 'fill-space': fillSpace }, `fit-${fit}`]"
|
||||
:style="styles"
|
||||
>
|
||||
<div class="image-sizer">
|
||||
<img
|
||||
:src="addURLBase(image.url)"
|
||||
:srcset="srcset"
|
||||
:sizes="sizes"
|
||||
:alt="image.alternativeText || ''"
|
||||
:width="image.width"
|
||||
:height="image.height"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { breakpoints } from '@/libs/theme'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
field: Object,
|
||||
aspect: {
|
||||
type: [String, Number],
|
||||
default: () => -1,
|
||||
},
|
||||
fillSpace: {
|
||||
type: Boolean,
|
||||
default: () => false,
|
||||
},
|
||||
fit: {
|
||||
type: String,
|
||||
default: () => 'cover',
|
||||
},
|
||||
mobileSize: {
|
||||
type: String,
|
||||
default: () => '100vw',
|
||||
},
|
||||
desktopSize: {
|
||||
type: String,
|
||||
default: () => '100vw',
|
||||
},
|
||||
})
|
||||
|
||||
const addURLBase = (path) => `https://cms.nicwands.com${path}`
|
||||
|
||||
// const image = computed(() => props.field.image)
|
||||
const image = computed(() => props.field)
|
||||
const formats = computed(() => image.value?.formats || {})
|
||||
const ratio = computed(() => {
|
||||
if (!image.value) return 1
|
||||
return image.value.height / image.value.width
|
||||
})
|
||||
const aspect = computed(() => {
|
||||
// calculate if no aspect provided
|
||||
if (props.aspect === -1) {
|
||||
return ratio.value * 100
|
||||
}
|
||||
|
||||
// otherwise, parse provided aspect, handling both 56.25 and 0.5625 style
|
||||
const toParse = parseFloat(props.aspect)
|
||||
return toParse <= 1 ? toParse * 100 : toParse
|
||||
})
|
||||
const styles = computed(() => ({ '--aspect': aspect.value + '%' }))
|
||||
const srcset = computed(() =>
|
||||
Object.values(formats.value)
|
||||
.map((f) => `${addURLBase(f.url)} ${f.width}w`)
|
||||
.join(', '),
|
||||
)
|
||||
const sizes = computed(
|
||||
() =>
|
||||
`(max-width: ${breakpoints.mobile}) ${props.mobileSize}, ${props.desktopSize}`,
|
||||
)
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.strapi-media {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
|
||||
.image-sizer {
|
||||
overflow: hidden;
|
||||
padding-bottom: var(--aspect);
|
||||
|
||||
& > * {
|
||||
display: block;
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
left: 0;
|
||||
top: 0;
|
||||
}
|
||||
.svg {
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
}
|
||||
|
||||
// fill space
|
||||
&.fill-space {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
left: 0;
|
||||
top: 0;
|
||||
|
||||
.image-sizer {
|
||||
padding-bottom: 0;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// fits
|
||||
&.fit-cover .image-sizer > * {
|
||||
object-fit: cover;
|
||||
}
|
||||
&.fit-contain .image-sizer > * {
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -21,15 +21,15 @@
|
|||
$layout: (
|
||||
'columns-count': (
|
||||
5,
|
||||
18,
|
||||
15,
|
||||
),
|
||||
'columns-gap': (
|
||||
20px,
|
||||
20px,
|
||||
),
|
||||
'margin': (
|
||||
30px,
|
||||
60px,
|
||||
20px,
|
||||
20px,
|
||||
),
|
||||
);
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue