Commit
•
edb4af2
1
Parent(s):
fa79853
✨ Basic e-shop
Browse files- src/lib/components/Masonry.svelte +117 -0
- src/lib/server/db/index.ts +1 -0
- src/routes/realisations/+page.svelte +50 -0
- src/routes/vente/+page.server.ts +22 -0
- src/routes/vente/+page.svelte +75 -0
src/lib/components/Masonry.svelte
ADDED
@@ -0,0 +1,117 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<script lang="ts">
|
2 |
+
import { tick } from 'svelte';
|
3 |
+
export let items: any[] = []; // pass in data if it's dynamically updated
|
4 |
+
|
5 |
+
type GridItem = {
|
6 |
+
_el: HTMLElement;
|
7 |
+
gap: number;
|
8 |
+
items: HTMLElement[];
|
9 |
+
ncol: number;
|
10 |
+
mod: number;
|
11 |
+
};
|
12 |
+
|
13 |
+
let grids: GridItem[] = [];
|
14 |
+
let masonryElement: HTMLElement;
|
15 |
+
|
16 |
+
const refreshLayout = async () => {
|
17 |
+
masonryElement.querySelectorAll('img').forEach((img) => {
|
18 |
+
if (img.complete) {
|
19 |
+
img.classList.add('loaded');
|
20 |
+
}
|
21 |
+
});
|
22 |
+
|
23 |
+
grids.forEach(async (grid) => {
|
24 |
+
/* get the post relayout number of columns */
|
25 |
+
let ncol = getComputedStyle(grid._el).gridTemplateColumns.split(' ').length;
|
26 |
+
|
27 |
+
grid.items.forEach((c) => {
|
28 |
+
let new_h = c.getBoundingClientRect().height;
|
29 |
+
|
30 |
+
if (new_h !== Number(c.dataset.h)) {
|
31 |
+
c.dataset.h = String(new_h);
|
32 |
+
grid.mod++;
|
33 |
+
}
|
34 |
+
});
|
35 |
+
|
36 |
+
/* if the number of columns has changed */
|
37 |
+
if (grid.ncol !== ncol || grid.mod) {
|
38 |
+
/* update number of columns */
|
39 |
+
grid.ncol = ncol;
|
40 |
+
/* revert to initial positioning, no margin */
|
41 |
+
grid.items.forEach((c) => c.style.removeProperty('margin-top'));
|
42 |
+
/* if we have more than one column */
|
43 |
+
if (grid.ncol > 1) {
|
44 |
+
grid.items.slice(ncol).forEach((c, i) => {
|
45 |
+
let prev_fin =
|
46 |
+
grid.items[i].getBoundingClientRect().bottom /* bottom edge of item above */,
|
47 |
+
curr_ini = c.getBoundingClientRect().top; /* top edge of current item */
|
48 |
+
|
49 |
+
c.style.marginTop = `${prev_fin + grid.gap - curr_ini}px`;
|
50 |
+
});
|
51 |
+
}
|
52 |
+
|
53 |
+
grid.mod = 0;
|
54 |
+
}
|
55 |
+
});
|
56 |
+
};
|
57 |
+
|
58 |
+
const calcGrid = async (_masonryArr: HTMLElement[]) => {
|
59 |
+
await tick();
|
60 |
+
if (_masonryArr.length && getComputedStyle(_masonryArr[0]).gridTemplateRows !== 'masonry') {
|
61 |
+
grids = _masonryArr.map((grid) => {
|
62 |
+
return {
|
63 |
+
_el: grid,
|
64 |
+
gap: parseFloat(getComputedStyle(grid).rowGap),
|
65 |
+
items: [...(grid.childNodes as unknown as HTMLElement[])].filter(
|
66 |
+
(c) => c.nodeType === 1 && +getComputedStyle(c).gridColumnEnd !== -1
|
67 |
+
),
|
68 |
+
ncol: 0,
|
69 |
+
mod: 0
|
70 |
+
};
|
71 |
+
});
|
72 |
+
refreshLayout(); /* initial load */
|
73 |
+
}
|
74 |
+
};
|
75 |
+
|
76 |
+
$: if (masonryElement) {
|
77 |
+
calcGrid([masonryElement]);
|
78 |
+
}
|
79 |
+
|
80 |
+
$: if (items) {
|
81 |
+
// update if items are changed
|
82 |
+
masonryElement = masonryElement; // refresh masonryElement
|
83 |
+
|
84 |
+
if (masonryElement) {
|
85 |
+
const images = masonryElement.querySelectorAll('img');
|
86 |
+
|
87 |
+
images.forEach((img) => {
|
88 |
+
img.removeEventListener('load', refreshLayout);
|
89 |
+
img.addEventListener('load', refreshLayout);
|
90 |
+
});
|
91 |
+
}
|
92 |
+
}
|
93 |
+
</script>
|
94 |
+
|
95 |
+
<svelte:window on:resize={refreshLayout} on:load={refreshLayout} />
|
96 |
+
|
97 |
+
<div bind:this={masonryElement} class="__grid--masonry">
|
98 |
+
<slot />
|
99 |
+
</div>
|
100 |
+
|
101 |
+
<!--
|
102 |
+
$w: var(--col-width); // minmax(Min(20em, 100%), 1fr);
|
103 |
+
$s: var(--grid-gap); // .5em;
|
104 |
+
-->
|
105 |
+
<style>
|
106 |
+
:global(.__grid--masonry) {
|
107 |
+
display: grid;
|
108 |
+
grid-template-columns: repeat(auto-fit, var(--col-width, minmax(Min(20em, 100%), 1fr)));
|
109 |
+
grid-template-rows: masonry;
|
110 |
+
justify-content: start;
|
111 |
+
grid-gap: var(--grid-gap, 0.5em);
|
112 |
+
}
|
113 |
+
|
114 |
+
:global(.__grid--masonry > *) {
|
115 |
+
align-self: start;
|
116 |
+
}
|
117 |
+
</style>
|
src/lib/server/db/index.ts
CHANGED
@@ -19,3 +19,4 @@ const products = createProductCollection(db);
|
|
19 |
const { pictures, picturesFs } = createPictureCollections(db);
|
20 |
|
21 |
export { client, db, pages, users, pictures, picturesFs, products };
|
|
|
|
19 |
const { pictures, picturesFs } = createPictureCollections(db);
|
20 |
|
21 |
export { client, db, pages, users, pictures, picturesFs, products };
|
22 |
+
export const collections = { products, pictures };
|
src/routes/realisations/+page.svelte
ADDED
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<script lang="ts">
|
2 |
+
import Container from '$lib/components/Container.svelte';
|
3 |
+
import PictureComponent from '$lib/components/Picture.svelte';
|
4 |
+
import type { CreationsPage } from '$lib/types/Page';
|
5 |
+
import { marked } from 'marked';
|
6 |
+
import type { PageData } from './$types';
|
7 |
+
|
8 |
+
export let data: PageData;
|
9 |
+
|
10 |
+
const pageData = data.pageData as CreationsPage;
|
11 |
+
const pictures = data.pictures;
|
12 |
+
|
13 |
+
type PictureKey = keyof typeof pageData.pictures;
|
14 |
+
|
15 |
+
const picKeys = Object.keys(pageData.pictures).filter(
|
16 |
+
(key) => key.startsWith('realisation-') && pageData.pictures[key as PictureKey]
|
17 |
+
);
|
18 |
+
</script>
|
19 |
+
|
20 |
+
<Container>
|
21 |
+
<h1 class="text-4xl text-oxford mt-4">Nos réalisations</h1>
|
22 |
+
|
23 |
+
{#each picKeys as picKey, i}
|
24 |
+
<article
|
25 |
+
class="{i % 2
|
26 |
+
? 'bg-oxford'
|
27 |
+
: 'bg-sunray'} text-white text-lg md:h-xl my-16 flex flex-wrap md:flex-no-wrap rounded-3xl overflow-hidden"
|
28 |
+
>
|
29 |
+
<div class="grow h-full w-full md:w-3/6 basis-auto md:basis-0" class:md:order-last={i % 2}>
|
30 |
+
<PictureComponent
|
31 |
+
picture={pictures.find((p) => p._id === pageData.pictures[picKey])}
|
32 |
+
sizes="(max-width: 1024px) 50vw, 512px"
|
33 |
+
class="w-full h-full object-cover"
|
34 |
+
/>
|
35 |
+
</div>
|
36 |
+
<div class="grow basis-0 flex flex-col relative justify-center">
|
37 |
+
<div class="px-4 py-6">
|
38 |
+
{@html marked(pageData.text[picKey])}
|
39 |
+
<!-- svelte-ignore security-anchor-rel-noreferrer -->
|
40 |
+
<a
|
41 |
+
href="/photos/raw/{pictures.find((p) => p._id === pageData.pictures[picKey]).storage[0]
|
42 |
+
._id}"
|
43 |
+
class="underline"
|
44 |
+
target="_blank">Photo entière</a
|
45 |
+
>
|
46 |
+
</div>
|
47 |
+
</div>
|
48 |
+
</article>
|
49 |
+
{/each}
|
50 |
+
</Container>
|
src/routes/vente/+page.server.ts
ADDED
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import type { PageServerLoad } from './$types';
|
2 |
+
import '$lib/server/db';
|
3 |
+
import { collections } from '$lib/server/db';
|
4 |
+
|
5 |
+
export const load: PageServerLoad = async () => {
|
6 |
+
const products = await collections.products.find({ state: { $ne: 'draft' } }).toArray();
|
7 |
+
const pictures = await collections.pictures
|
8 |
+
.find({ productId: { $in: products.map((p) => p._id) } })
|
9 |
+
.sort({ createdAt: 1 })
|
10 |
+
.toArray();
|
11 |
+
|
12 |
+
const byId = Object.fromEntries(products.map((p) => [p._id, p]));
|
13 |
+
|
14 |
+
for (const picture of pictures) {
|
15 |
+
byId[picture.productId!].photos = [...(byId[picture.productId!].photos || []), picture];
|
16 |
+
}
|
17 |
+
|
18 |
+
return {
|
19 |
+
published: products.filter((p) => p.state === 'published'),
|
20 |
+
retired: products.filter((p) => p.state === 'retired')
|
21 |
+
};
|
22 |
+
};
|
src/routes/vente/+page.svelte
ADDED
@@ -0,0 +1,75 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<script lang="ts">
|
2 |
+
import Container from '$lib/components/Container.svelte';
|
3 |
+
import Masonry from '$lib/components/Masonry.svelte';
|
4 |
+
import Picture from '$lib/components/Picture.svelte';
|
5 |
+
import type { EshopPage } from '$lib/types/Page';
|
6 |
+
import type { PageData } from './$types';
|
7 |
+
|
8 |
+
export let data: PageData;
|
9 |
+
|
10 |
+
const pictures = data.pictures;
|
11 |
+
const pageData = data.pageData as EshopPage;
|
12 |
+
|
13 |
+
const { published, retired } = data;
|
14 |
+
</script>
|
15 |
+
|
16 |
+
<div class="h-full-without-banner w-full relative">
|
17 |
+
<Picture
|
18 |
+
picture={pictures.find((p) => p._id === pageData.pictures.background)}
|
19 |
+
class="h-full w-full object-cover absolute top-0 bottom-0 left-0 right-0 bg-brunswick text-white"
|
20 |
+
style="z-index: -1"
|
21 |
+
/>
|
22 |
+
<Container noPadding class="h-full flex flex-col items-stretch md:items-start">
|
23 |
+
<div class="grow basis-0" />
|
24 |
+
<div style="flex-grow: 2" class="basis-0 text-center md:px-8 xl:px-0">
|
25 |
+
<h1 class="text-7xl text-white text-center md:text-left">Découvrez <br />nos ventes</h1>
|
26 |
+
<a href="#produits" class="btn mt-10 inline-block">cliquez ici</a>
|
27 |
+
</div>
|
28 |
+
</Container>
|
29 |
+
</div>
|
30 |
+
|
31 |
+
<Container class="mb-4">
|
32 |
+
<h2 class="text-4xl text-oxford my-4" id="produits">Produits à la vente</h2>
|
33 |
+
|
34 |
+
<Masonry>
|
35 |
+
{#each published as product}
|
36 |
+
<a href="/vente/{product._id}" class="product">
|
37 |
+
<Picture picture={product.photos[0]} minStorage={1} class="picture mx-auto" />
|
38 |
+
<span class="name">{product.name}</span>
|
39 |
+
<span class="price text-right">{product.price} €</span>
|
40 |
+
</a>
|
41 |
+
{/each}
|
42 |
+
<!-- In case there is only one product. We don't want a product to take full row in desktop mode -->
|
43 |
+
<div />
|
44 |
+
</Masonry>
|
45 |
+
</Container>
|
46 |
+
|
47 |
+
<style>
|
48 |
+
.product {
|
49 |
+
display: grid;
|
50 |
+
gap: 0.5rem;
|
51 |
+
grid-template-columns: 1fr auto;
|
52 |
+
grid-template-rows: auto auto;
|
53 |
+
grid-template-areas:
|
54 |
+
'picture picture'
|
55 |
+
'name price';
|
56 |
+
}
|
57 |
+
|
58 |
+
:global(.product > .picture) {
|
59 |
+
grid-area: picture;
|
60 |
+
max-height: 24rem;
|
61 |
+
max-width: 100%;
|
62 |
+
}
|
63 |
+
|
64 |
+
.product > .name {
|
65 |
+
grid-area: name;
|
66 |
+
|
67 |
+
white-space: nowrap;
|
68 |
+
text-overflow: ellipsis;
|
69 |
+
overflow: hidden;
|
70 |
+
}
|
71 |
+
|
72 |
+
.product > .price {
|
73 |
+
grid-area: price;
|
74 |
+
}
|
75 |
+
</style>
|