first commit
This commit is contained in:
commit
e1362c0d36
24
.gitignore
vendored
Normal file
24
.gitignore
vendored
Normal file
@ -0,0 +1,24 @@
|
||||
# Nuxt dev/build outputs
|
||||
.output
|
||||
.data
|
||||
.nuxt
|
||||
.nitro
|
||||
.cache
|
||||
dist
|
||||
|
||||
# Node dependencies
|
||||
node_modules
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
|
||||
# Misc
|
||||
.DS_Store
|
||||
.fleet
|
||||
.idea
|
||||
|
||||
# Local env files
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
75
README.md
Normal file
75
README.md
Normal file
@ -0,0 +1,75 @@
|
||||
# Nuxt 3 Minimal Starter
|
||||
|
||||
Look at the [Nuxt 3 documentation](https://nuxt.com/docs/getting-started/introduction) to learn more.
|
||||
|
||||
## Setup
|
||||
|
||||
Make sure to install the dependencies:
|
||||
|
||||
```bash
|
||||
# npm
|
||||
npm install
|
||||
|
||||
# pnpm
|
||||
pnpm install
|
||||
|
||||
# yarn
|
||||
yarn install
|
||||
|
||||
# bun
|
||||
bun install
|
||||
```
|
||||
|
||||
## Development Server
|
||||
|
||||
Start the development server on `http://localhost:3000`:
|
||||
|
||||
```bash
|
||||
# npm
|
||||
npm run dev
|
||||
|
||||
# pnpm
|
||||
pnpm run dev
|
||||
|
||||
# yarn
|
||||
yarn dev
|
||||
|
||||
# bun
|
||||
bun run dev
|
||||
```
|
||||
|
||||
## Production
|
||||
|
||||
Build the application for production:
|
||||
|
||||
```bash
|
||||
# npm
|
||||
npm run build
|
||||
|
||||
# pnpm
|
||||
pnpm run build
|
||||
|
||||
# yarn
|
||||
yarn build
|
||||
|
||||
# bun
|
||||
bun run build
|
||||
```
|
||||
|
||||
Locally preview production build:
|
||||
|
||||
```bash
|
||||
# npm
|
||||
npm run preview
|
||||
|
||||
# pnpm
|
||||
pnpm run preview
|
||||
|
||||
# yarn
|
||||
yarn preview
|
||||
|
||||
# bun
|
||||
bun run preview
|
||||
```
|
||||
|
||||
Check out the [deployment documentation](https://nuxt.com/docs/getting-started/deployment) for more information.
|
130
app.vue
Normal file
130
app.vue
Normal file
@ -0,0 +1,130 @@
|
||||
<script setup>
|
||||
// ITEM FETCHING //
|
||||
// set initial sort order
|
||||
const sortVariable = ref(["reserved", "date_created"]);
|
||||
const reservedVariable = ref(true);
|
||||
// fetch data
|
||||
const { homepage } = await GqlGetHomepage();
|
||||
const { data, refresh } = await useAsyncData("gifts", () =>
|
||||
GqlGifts({ sort: sortVariable.value })
|
||||
);
|
||||
// update sort param
|
||||
function sortList(param) {
|
||||
sortVariable.value = param;
|
||||
// refetch query (expose token or access pubic data)
|
||||
refresh();
|
||||
}
|
||||
// filter out already reserved items
|
||||
const filteredList = computed(() => {
|
||||
const list = data;
|
||||
return list;
|
||||
});
|
||||
|
||||
// CHANGE DISPLAY //
|
||||
const listDisplay = ref(false);
|
||||
function toggleDisplay() {
|
||||
listDisplay.value = !listDisplay.value;
|
||||
}
|
||||
|
||||
// ITEM RESERVATION //
|
||||
// no error to start
|
||||
const errorStatus = ref(false);
|
||||
const statusMessage = ref("");
|
||||
// update the chosen item's reservation status
|
||||
async function runMutation(payload) {
|
||||
try {
|
||||
const result = await GqlUpdateItem({
|
||||
id: payload.id,
|
||||
reserved: payload.reserve,
|
||||
name: payload.name,
|
||||
});
|
||||
} catch (error) {
|
||||
// if error, set the status to true
|
||||
console.log("---ERREUR---", error);
|
||||
errorStatus.value = true;
|
||||
} finally {
|
||||
if (errorStatus.value) {
|
||||
// if error is true, show an error notice
|
||||
statusMessage.value = "Une erreur est survenue… ¯\\_(ツ)_/¯";
|
||||
// cleanup the error status
|
||||
errorStatus.value = false;
|
||||
} else {
|
||||
// if all is well, show a success notice
|
||||
statusMessage.value = `C'est noté merci beaucoup ${payload.name} 🥰`;
|
||||
setTimeout(() => {
|
||||
refresh();
|
||||
}, 3500);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main class="flow">
|
||||
<h1>Liste de naissance</h1>
|
||||
<section class="intro sidebar--reverse" data-direction="rtl">
|
||||
<article v-html="homepage.content" class="editorial" />
|
||||
<img
|
||||
src="~/assets/images/jungle.jpg"
|
||||
class="intro__image"
|
||||
width="400"
|
||||
height="600"
|
||||
alt=""
|
||||
/>
|
||||
</section>
|
||||
<hr />
|
||||
<SortFilter :onSort="sortList" :onDisplay="toggleDisplay" />
|
||||
<hr />
|
||||
<ul
|
||||
class="gifts gifts--grid"
|
||||
:class="{ 'gifts--list': listDisplay }"
|
||||
role="list"
|
||||
>
|
||||
<li v-for="gift of data?.gifts" :key="gift.id" class="gift-item">
|
||||
<GiftCard
|
||||
:item="gift"
|
||||
:onReserved="runMutation"
|
||||
v-model:status-message="statusMessage"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
h1 {
|
||||
text-align: center;
|
||||
}
|
||||
.intro {
|
||||
--sidebar-target-width: 400px;
|
||||
--sidebar-target-grow: 0;
|
||||
margin: var(--space-m-l) auto;
|
||||
max-inline-size: 90ch;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.intro__image {
|
||||
mix-blend-mode: multiply;
|
||||
filter: brightness(110%);
|
||||
height: 600px;
|
||||
width: 400px;
|
||||
object-fit: cover;
|
||||
}
|
||||
hr {
|
||||
margin-block: var(--space-xs-s);
|
||||
border-bottom: 3px dotted var(--color-yellow-dark);
|
||||
}
|
||||
.gifts {
|
||||
display: grid;
|
||||
gap: var(--space-s-m);
|
||||
}
|
||||
.gifts--grid {
|
||||
grid-template-columns: repeat(auto-fit, minmax(min(280px, 100%), 1fr));
|
||||
}
|
||||
.gifts--grid .gift-item > :deep(:first-child) {
|
||||
height: 100%;
|
||||
}
|
||||
.gifts--list {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
</style>
|
55
assets/css/compositions/sidebar.css
Normal file
55
assets/css/compositions/sidebar.css
Normal file
@ -0,0 +1,55 @@
|
||||
/*
|
||||
CUSTOM PROPERTIES AND CONFIGURATION
|
||||
--gutter (var(--space-size-1)): This defines the space
|
||||
between the sidebar and main content.
|
||||
|
||||
--sidebar-target-width (20rem): How large the sidebar should be
|
||||
|
||||
--sidebar-content-min-width(50%): The minimum size of the main content area
|
||||
|
||||
EXCEPTIONS
|
||||
.sidebar[data-direction='rtl']: flips the sidebar to be on the right
|
||||
*/
|
||||
.sidebar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--gutter, var(--space-xs-s));
|
||||
}
|
||||
|
||||
.sidebar > :first-child {
|
||||
flex-basis: var(--sidebar-target-width, 20rem);
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.sidebar > :last-child {
|
||||
flex-basis: 0;
|
||||
flex-grow: 999;
|
||||
min-width: var(--sidebar-content-min-width, 50%);
|
||||
}
|
||||
|
||||
/*
|
||||
A flipped version where the sidebar is on the right
|
||||
*/
|
||||
.sidebar[data-direction="rtl"],
|
||||
.sidebar--reverse[data-direction="rtl"] {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
|
||||
/* REVERSE sidebar on right*/
|
||||
|
||||
.sidebar--reverse {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--gutter, var(--space-xs-s));
|
||||
}
|
||||
|
||||
.sidebar--reverse > :last-child {
|
||||
flex-basis: var(--sidebar-target-width, 20rem);
|
||||
flex-grow: var(--sidebar-target-grow, 1);
|
||||
}
|
||||
|
||||
.sidebar--reverse > :first-child {
|
||||
flex-basis: 0;
|
||||
flex-grow: 999;
|
||||
min-width: var(--sidebar-content-min-width, 50%);
|
||||
}
|
122
assets/css/global/01-reset.css
Normal file
122
assets/css/global/01-reset.css
Normal file
@ -0,0 +1,122 @@
|
||||
/* RESET */
|
||||
* {
|
||||
/* Remove default margin on everything */
|
||||
margin: 0;
|
||||
/* Remove default padding on everything */
|
||||
padding: 0;
|
||||
/* Calc `em` based line height, bigger line height for smaller font size and smaller line height for bigger font size: https://kittygiraudel.com/2020/05/18/using-calc-to-figure-out-optimal-line-height/ */
|
||||
line-height: calc(0.25rem + 1em + 0.25rem);
|
||||
}
|
||||
/* Use a more-intuitive box-sizing model on everything */
|
||||
*,
|
||||
::before,
|
||||
::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
/* Remove border and set sensible defaults for backgrounds, on all elements except fieldset progress and meter */
|
||||
*:where(:not(fieldset, progress, meter)) {
|
||||
border-width: 0;
|
||||
border-style: solid;
|
||||
background-origin: border-box;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
html {
|
||||
/* Allow percentage-based heights in the application */
|
||||
block-size: 100%;
|
||||
/* Making sure text size is only controlled by font-size */
|
||||
-webkit-text-size-adjust: none;
|
||||
/* Improve text rendering */
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
/* Smooth scrolling for users that don't prefer reduced motion */
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
html:focus-within {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
}
|
||||
body {
|
||||
overflow-x: hidden;
|
||||
font-family: Arial, sans-serif;
|
||||
font-weight: normal;
|
||||
/* Allow percentage-based heights in the application */
|
||||
min-block-size: 100%;
|
||||
/* https://www.sarasoueidan.com/blog/safari-fluid-typography-bug-fix/ */
|
||||
-webkit-marquee-increment: 0vw;
|
||||
}
|
||||
/* Improve media defaults */
|
||||
:where(img, svg, video, canvas, audio, iframe, embed, object) {
|
||||
display: block;
|
||||
}
|
||||
:where(img, svg, video) {
|
||||
block-size: auto;
|
||||
max-inline-size: 100%;
|
||||
}
|
||||
/* Smaller line height for titles */
|
||||
:where(h1, h2, h3, h1 a, h2 a, h3 a) {
|
||||
line-height: 1.1;
|
||||
}
|
||||
/* Avoid text overflows */
|
||||
:where(p, h1, h2, h3, h4, h5, h6) {
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
/* Remove list styles on ul, ol elements with a list role, which suggests default styling will be removed */
|
||||
:where(ul, ol)[role="list"] {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
/* More readable underline style for anchor tags without a class. This could be set on anchor tags globally, but it can cause conflicts. */
|
||||
a:not([class]) {
|
||||
text-decoration-skip-ink: auto;
|
||||
}
|
||||
/* Make it clear that interactive elements are interactive */
|
||||
:where(
|
||||
a[href],
|
||||
area,
|
||||
button,
|
||||
input,
|
||||
label[for],
|
||||
select,
|
||||
summary,
|
||||
textarea,
|
||||
[tabindex]:not([tabindex*="-"])
|
||||
) {
|
||||
cursor: pointer;
|
||||
touch-action: manipulation;
|
||||
}
|
||||
:where(input[type="file"]) {
|
||||
cursor: auto;
|
||||
}
|
||||
:where(input[type="file"])::-webkit-file-upload-button,
|
||||
:where(input[type="file"])::file-selector-button {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Make sure users can't select button text */
|
||||
:where(
|
||||
button,
|
||||
button[type],
|
||||
input[type="button"],
|
||||
input[type="submit"],
|
||||
input[type="reset"]
|
||||
),
|
||||
:where(input[type="file"])::-webkit-file-upload-button,
|
||||
:where(input[type="file"])::file-selector-button {
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
-webkit-touch-callout: none;
|
||||
user-select: none;
|
||||
text-align: center;
|
||||
}
|
||||
/* Disabled cursor for disabled buttons */
|
||||
:where(
|
||||
button,
|
||||
button[type],
|
||||
input[type="button"],
|
||||
input[type="submit"],
|
||||
input[type="reset"]
|
||||
)[disabled] {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
/* END RESET */
|
35
assets/css/global/02-fonts.css
Normal file
35
assets/css/global/02-fonts.css
Normal file
@ -0,0 +1,35 @@
|
||||
@font-face {
|
||||
font-family: "wotfard";
|
||||
src: url("~/assets/fonts/wotfard/wotfard-medium-fr.woff2") format("woff2");
|
||||
font-weight: 500;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
@font-face {
|
||||
font-family: "wotfard";
|
||||
src: url("~/assets/fonts/wotfard/wotfard-semibold-fr.woff2") format("woff2");
|
||||
font-weight: bold;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
@font-face {
|
||||
font-family: "wotfard";
|
||||
src: url("~/assets/fonts/wotfard/wotfard-regular-fr.woff2") format("woff2");
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
/*
|
||||
* reduces Cumulative Layout Shift
|
||||
* https://www.24joursdeweb.fr/2021/performance-web-lintegrateur-ce-heros/
|
||||
*/
|
||||
@font-face {
|
||||
font-family: "ArialReplace";
|
||||
src: local("Arial");
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
size-adjust: 94%;
|
||||
letter-spacing: 1px;
|
||||
}
|
48
assets/css/global/03-variables.css
Normal file
48
assets/css/global/03-variables.css
Normal file
@ -0,0 +1,48 @@
|
||||
:root {
|
||||
/* font sizes */
|
||||
--size--2: clamp(0.79rem, calc(0.76rem + 0.16vw), 0.89rem);
|
||||
--size--1: clamp(0.89rem, calc(0.85rem + 0.19vw), 1rem);
|
||||
--size-0: clamp(1rem, calc(0.96rem + 0.21vw), 1.13rem);
|
||||
--size-1: clamp(1.13rem, calc(1.08rem + 0.23vw), 1.27rem);
|
||||
--size-2: clamp(1.27rem, calc(1.21rem + 0.26vw), 1.42rem);
|
||||
--size-3: clamp(1.42rem, calc(1.36rem + 0.3vw), 1.6rem);
|
||||
--size-4: clamp(1.6rem, calc(1.54rem + 0.33vw), 1.8rem);
|
||||
--size-5: clamp(1.8rem, calc(1.73rem + 0.38vw), 2.03rem);
|
||||
|
||||
/* spaces */
|
||||
--space-3xs: clamp(0.31rem, calc(0.31rem + 0vw), 0.31rem);
|
||||
--space-2xs: clamp(0.56rem, calc(0.53rem + 0.14vw), 0.63rem);
|
||||
--space-xs: clamp(0.88rem, calc(0.85rem + 0.14vw), 0.94rem);
|
||||
--space-s: clamp(1.13rem, calc(1.07rem + 0.28vw), 1.25rem);
|
||||
--space-m: clamp(1.69rem, calc(1.6rem + 0.42vw), 1.88rem);
|
||||
--space-l: clamp(2.25rem, calc(2.14rem + 0.56vw), 2.5rem);
|
||||
--space-xl: clamp(3.38rem, calc(3.21rem + 0.83vw), 3.75rem);
|
||||
--space-2xl: clamp(4.5rem, calc(4.28rem + 1.11vw), 5rem);
|
||||
--space-3xl: clamp(6.75rem, calc(6.42rem + 1.67vw), 7.5rem);
|
||||
|
||||
/* One-up pairs */
|
||||
--space-3xs-2xs: clamp(0.31rem, calc(0.17rem + 0.69vw), 0.63rem);
|
||||
--space-2xs-xs: clamp(0.56rem, calc(0.4rem + 0.83vw), 0.94rem);
|
||||
--space-xs-s: clamp(0.88rem, calc(0.71rem + 0.83vw), 1.25rem);
|
||||
--space-s-m: clamp(1.13rem, calc(0.79rem + 1.67vw), 1.88rem);
|
||||
--space-m-l: clamp(1.69rem, calc(1.33rem + 1.81vw), 2.5rem);
|
||||
--space-l-xl: clamp(2.25rem, calc(1.58rem + 3.33vw), 3.75rem);
|
||||
--space-xl-2xl: clamp(3.38rem, calc(2.65rem + 3.61vw), 5rem);
|
||||
--space-2xl-3xl: clamp(4.5rem, calc(3.17rem + 6.67vw), 7.5rem);
|
||||
|
||||
/* multi steps */
|
||||
--space-3xs-s: clamp(0.31rem, calc(-0.1rem + 2.08vw), 1.25rem);
|
||||
--space-2xs-s: clamp(0.56rem, calc(0.26rem + 1.53vw), 1.25rem);
|
||||
--space-2xs-m: clamp(0.56rem, calc(-0.02rem + 2.92vw), 1.88rem);
|
||||
--space-xs-m: clamp(0.88rem, calc(0.43rem + 2.22vw), 1.88rem);
|
||||
--space-xs-l: clamp(0.88rem, calc(0.15rem + 3.61vw), 2.5rem);
|
||||
--space-s-l: clamp(1.13rem, calc(0.51rem + 3.06vw), 2.5rem);
|
||||
--space-s-xl: clamp(1.13rem, calc(-0.04rem + 5.83vw), 3.75rem);
|
||||
--space-l-2xl: clamp(2.25rem, calc(1.03rem + 6.11vw), 5rem);
|
||||
|
||||
/* COLORS */
|
||||
--color-yellow-light: hsl(20, 56%, 95%);
|
||||
--color-yellow-dark: hsl(353, 13%, 27%);
|
||||
--color-green-light: hsl(36, 95%, 66%);
|
||||
--color-green-dark: hsl(145, 59%, 11%);
|
||||
}
|
45
assets/css/global/04-base.css
Normal file
45
assets/css/global/04-base.css
Normal file
@ -0,0 +1,45 @@
|
||||
body {
|
||||
margin: 0 auto;
|
||||
padding: var(--space-s-l) var(--space-xs-s);
|
||||
max-inline-size: 80rem;
|
||||
font-family: "wotfard", "ArialReplace", Arial, sans-serif;
|
||||
font-size: var(--size-0);
|
||||
font-weight: normal;
|
||||
color: var(--color-green-dark);
|
||||
background-color: var(--color-yellow-light);
|
||||
}
|
||||
a {
|
||||
color: inherit;
|
||||
}
|
||||
a:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
h1,
|
||||
.h1 {
|
||||
font-size: var(--size-4);
|
||||
}
|
||||
h2,
|
||||
.h2 {
|
||||
font-size: var(--size-2);
|
||||
}
|
||||
h3,
|
||||
.h3 {
|
||||
font-size: var(--size-1);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.flow > * + * {
|
||||
margin-block-start: var(--space-xs);
|
||||
}
|
||||
|
||||
ul,
|
||||
ol {
|
||||
padding-inline-start: 1em;
|
||||
}
|
||||
|
||||
.editorial > * + * {
|
||||
margin-block-start: var(--space-2xs);
|
||||
}
|
||||
.editorial h2 + * {
|
||||
margin-block-start: var(--space-xs);
|
||||
}
|
7
assets/css/style.css
Normal file
7
assets/css/style.css
Normal file
@ -0,0 +1,7 @@
|
||||
@import-glob './global/*.css';
|
||||
|
||||
/* @import-glob './blocks/*.css'; */
|
||||
|
||||
@import-glob './compositions/*.css';
|
||||
|
||||
/* @import-glob './utilities/*.css'; */
|
BIN
assets/fonts/wotfard/wotfard-medium-fr.woff2
Normal file
BIN
assets/fonts/wotfard/wotfard-medium-fr.woff2
Normal file
Binary file not shown.
BIN
assets/fonts/wotfard/wotfard-regular-fr.woff2
Normal file
BIN
assets/fonts/wotfard/wotfard-regular-fr.woff2
Normal file
Binary file not shown.
BIN
assets/fonts/wotfard/wotfard-semibold-fr.woff2
Normal file
BIN
assets/fonts/wotfard/wotfard-semibold-fr.woff2
Normal file
Binary file not shown.
BIN
assets/images/jungle.jpg
Normal file
BIN
assets/images/jungle.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 100 KiB |
288
components/GiftCard.vue
Normal file
288
components/GiftCard.vue
Normal file
@ -0,0 +1,288 @@
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
item: { type: Object, default: () => {} },
|
||||
statusMessage: { type: String, default: "" },
|
||||
});
|
||||
|
||||
let showDetails = ref(true);
|
||||
|
||||
const formError = ref("");
|
||||
const isFormVisible = ref(false);
|
||||
function showReservation() {
|
||||
isFormVisible.value = !isFormVisible.value;
|
||||
}
|
||||
|
||||
const reserved = ref(false);
|
||||
const fromName = ref("");
|
||||
const emit = defineEmits(["reserved"]);
|
||||
|
||||
const sendReservation = (id, reserve, name) => {
|
||||
console.log(fromName);
|
||||
if (reserved.value && fromName.value.length > 1) {
|
||||
emit("reserved", { id, reserve, name });
|
||||
formError.value = "";
|
||||
} else {
|
||||
formError.value = "Veuillez remplir le formulaire";
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="gift flow">
|
||||
<div class="gift__info">
|
||||
<h2 class="h3">
|
||||
<NuxtLink :to="item.url">{{ item.name }}</NuxtLink>
|
||||
</h2>
|
||||
<div class="gift__status">
|
||||
<p v-if="!item.reserved">🎉 Ce cadeau est disponible !</p>
|
||||
<p v-else>
|
||||
Ce cadeau est déjà réservé par
|
||||
<span class="from-name">{{ item.from || "¯\_(ツ)_/¯" }}</span> 🤗
|
||||
</p>
|
||||
</div>
|
||||
<p v-if="!item.reserved && item.prix" class="h2 gift__price">
|
||||
{{ item.prix }} €
|
||||
</p>
|
||||
<button v-if="!item.reserved" @click="showReservation" class="btn">
|
||||
Réserver
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="!item.reserved" class="gift__actions">
|
||||
<div v-if="isFormVisible" class="flow">
|
||||
<form>
|
||||
<div class="field form__confirmation">
|
||||
<label :for="`name-${item.id}`">Votre nom (requis)</label>
|
||||
<input
|
||||
type="text"
|
||||
name="name"
|
||||
:id="`name-${item.id}`"
|
||||
placeholder="Jeanne"
|
||||
v-model="fromName"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div class="field form__reservation">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="check"
|
||||
:id="`check-${item.id}`"
|
||||
v-model="reserved"
|
||||
required
|
||||
/>
|
||||
<label :for="`check-${item.id}`">Confirmer la réservation</label>
|
||||
</div>
|
||||
<button
|
||||
@click.prevent="sendReservation(item.id, reserved, fromName)"
|
||||
:disabled="reserved && fromName.length > 1 ? false : true"
|
||||
>
|
||||
Valider le cadeau
|
||||
</button>
|
||||
</form>
|
||||
<div
|
||||
v-if="statusMessage || formError"
|
||||
aria-live="polite"
|
||||
aria-atomic="false"
|
||||
class="form__errors"
|
||||
>
|
||||
<p v-if="statusMessage">
|
||||
{{ statusMessage }}
|
||||
</p>
|
||||
<p v-if="formError">
|
||||
{{ formError }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<details
|
||||
v-if="!item.reserved"
|
||||
class="flow"
|
||||
:open="showDetails"
|
||||
@click.prevent="showDetails = !showDetails"
|
||||
>
|
||||
<summary>
|
||||
<span v-if="showDetails">Masquer les détails</span>
|
||||
<span v-else>Voir les détails</span>
|
||||
</summary>
|
||||
<div class="gift__details">
|
||||
<NuxtPicture
|
||||
v-if="item.photo"
|
||||
format="avif,webp"
|
||||
class="gift__photo"
|
||||
:src="`http://localhost:8055/assets/${item.photo.id}?width=200&height=200&fit=inside`"
|
||||
loading="lazy"
|
||||
width="200"
|
||||
height="200"
|
||||
fit="inside"
|
||||
alt=""
|
||||
/>
|
||||
<div class="gift__buying">
|
||||
<span v-if="item.used" class="gift__used">Occasion ok !</span>
|
||||
<p
|
||||
v-if="!item.reserved && item.description"
|
||||
class="gift__description"
|
||||
>
|
||||
{{ item.description }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.gift {
|
||||
container-type: inline-size;
|
||||
container-name: card;
|
||||
border-radius: 8px;
|
||||
padding: var(--space-xs-s);
|
||||
box-shadow: 0 0 8px 0 hsla(24, 91%, 35%, 0.2);
|
||||
background-color: white;
|
||||
}
|
||||
.gift__info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-s-m);
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
.gift__status {
|
||||
color: var(--color-yellow-dark);
|
||||
}
|
||||
details {
|
||||
margin-block-start: var(--space-xs);
|
||||
text-align: center;
|
||||
}
|
||||
details summary {
|
||||
font-size: var(--size--1);
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-yellow-dark);
|
||||
}
|
||||
.gift__details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-xs);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.gift__photo {
|
||||
align-self: center;
|
||||
}
|
||||
.gift__buying {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: var(--space-2xs);
|
||||
}
|
||||
.gift__price {
|
||||
margin-block: var(--space-3xs);
|
||||
padding: var(--space-2xs);
|
||||
text-align: center;
|
||||
font-weight: 500;
|
||||
align-self: center;
|
||||
border-block: 2px solid var(--color-green-dark);
|
||||
}
|
||||
.gift__used {
|
||||
padding: var(--space-3xs) var(--space-2xs);
|
||||
font-size: var(--size--1);
|
||||
background-color: var(--color-green-light);
|
||||
border-radius: 40px;
|
||||
}
|
||||
.gift__description {
|
||||
padding: var(--space-2xs) var(--space-xs);
|
||||
font-size: var(--size-0);
|
||||
border-radius: 8px;
|
||||
color: var(--color-green-light);
|
||||
background-color: var(--color-green-dark);
|
||||
}
|
||||
|
||||
.from-name {
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.btn {
|
||||
margin-block-start: auto;
|
||||
padding: var(--space-2xs) var(--space-xs);
|
||||
position: relative;
|
||||
align-self: center;
|
||||
font-size: var(--size-0);
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--color-green-light);
|
||||
background-color: white;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
.btn:focus-within,
|
||||
.btn:hover {
|
||||
background-color: var(--color-yellow-light);
|
||||
}
|
||||
|
||||
form,
|
||||
.form__confirmation {
|
||||
display: flex;
|
||||
flex-flow: column wrap;
|
||||
gap: var(--space-xs);
|
||||
}
|
||||
.form__reservation label {
|
||||
margin-inline-start: var(--space-3xs);
|
||||
}
|
||||
.form__confirmation input {
|
||||
padding: var(--space-3xs) var(--space-2xs);
|
||||
font-size: var(--size--1);
|
||||
font-weight: bold;
|
||||
border: 1px solid var(--color-green-dark);
|
||||
}
|
||||
form button {
|
||||
padding: var(--space-2xs) var(--space-2xs);
|
||||
font-size: var(--size--1);
|
||||
border: 1px solid transparent;
|
||||
border-radius: 8px;
|
||||
transition-property: background-color, color;
|
||||
transition-duration: 0.3s;
|
||||
transition-timing-function: ease;
|
||||
}
|
||||
form button:not([disabled]) {
|
||||
color: var(--color-yellow-light);
|
||||
border-color: var(--color-green-dark);
|
||||
background-color: var(--color-green-dark);
|
||||
}
|
||||
form button:not([disabled]):is(:hover, :focus) {
|
||||
color: var(--color-green-dark);
|
||||
background-color: var(--color-yellow-light);
|
||||
}
|
||||
.form__errors {
|
||||
font-size: var(--size--1);
|
||||
}
|
||||
|
||||
@container card (min-inline-size: 60rem) {
|
||||
.gift__info {
|
||||
flex-flow: row wrap;
|
||||
justify-content: flex-start;
|
||||
row-gap: var(--space-2xs);
|
||||
}
|
||||
.gift__price {
|
||||
margin: 0;
|
||||
margin-inline-start: auto;
|
||||
}
|
||||
.gift__actions {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 2fr;
|
||||
gap: var(--space-s-m);
|
||||
align-content: end;
|
||||
}
|
||||
details {
|
||||
margin: 0;
|
||||
text-align: right;
|
||||
grid-column: 2 / 3;
|
||||
}
|
||||
.gift__details {
|
||||
flex-flow: row nowrap;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.gift__description {
|
||||
text-align: left;
|
||||
}
|
||||
.gift__buying {
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
</style>
|
47
components/SortFilter.vue
Normal file
47
components/SortFilter.vue
Normal file
@ -0,0 +1,47 @@
|
||||
<script setup>
|
||||
const selectedSort = ref("");
|
||||
|
||||
const emit = defineEmits(["sort", "display"]);
|
||||
const sortData = (sortParam) => {
|
||||
emit("sort", sortParam);
|
||||
};
|
||||
const changeDisplay = () => {
|
||||
emit("display");
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<form>
|
||||
<div>
|
||||
<label for="sort">Trier par : </label>
|
||||
<select
|
||||
name="sort"
|
||||
id="sort"
|
||||
v-model="selectedSort"
|
||||
@change="sortData(selectedSort)"
|
||||
>
|
||||
<option disabled value="">Sélectionnez une option</option>
|
||||
<option value="prix">Prix croissant</option>
|
||||
<option value="-prix">Prix décroissant</option>
|
||||
<option :value="['reserved', 'prix']">Non réservés</option>
|
||||
</select>
|
||||
</div>
|
||||
<!-- <div>
|
||||
<input type="checkbox" name="list" id="list" @change="changeDisplay" />
|
||||
<label for="list"> Affichage en liste</label>
|
||||
</div> -->
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
form {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
align-items: center;
|
||||
gap: var(--space-2xs);
|
||||
}
|
||||
select {
|
||||
padding: var(--space-2xs);
|
||||
background-color: white;
|
||||
}
|
||||
</style>
|
37
nuxt.config.ts
Normal file
37
nuxt.config.ts
Normal file
@ -0,0 +1,37 @@
|
||||
// https://nuxt.com/docs/api/configuration/nuxt-config
|
||||
export default defineNuxtConfig({
|
||||
app: {
|
||||
head: {
|
||||
meta: [
|
||||
{ charset: "utf-8" },
|
||||
{ name: "viewport", content: "width=device-width, initial-scale=1" },
|
||||
{
|
||||
name: "robots",
|
||||
content: "noindex,nofollow",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
css: ["@/assets/css/style.css"],
|
||||
modules: ["nuxt-graphql-client", "@nuxt/image"],
|
||||
|
||||
runtimeConfig: { public: { GQL_HOST: "http://0.0.0.0:8055/graphql" } },
|
||||
|
||||
image: {
|
||||
dir: "assets/images",
|
||||
domains: ["http://0.0.0.0:8055"],
|
||||
alias: { directus: "http://0.0.0.0:8055/assets/" },
|
||||
presets: {
|
||||
gift: { modifiers: { width: 200, height: 200, fit: "contain" } },
|
||||
},
|
||||
},
|
||||
postcss: {
|
||||
plugins: {
|
||||
"postcss-import-ext-glob": {},
|
||||
},
|
||||
},
|
||||
telemetry: false,
|
||||
typescript: {
|
||||
shim: false,
|
||||
},
|
||||
});
|
12129
package-lock.json
generated
Normal file
12129
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
23
package.json
Normal file
23
package.json
Normal file
@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "nuxt-app",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "nuxt build",
|
||||
"dev": "nuxt dev",
|
||||
"generate": "nuxt generate",
|
||||
"preview": "nuxt preview",
|
||||
"postinstall": "nuxt prepare"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nuxt/devtools": "latest",
|
||||
"@nuxt/image": "npm:@nuxt/image-edge@latest",
|
||||
"nuxt": "^3.8.0",
|
||||
"postcss-import-ext-glob": "^2.1.1",
|
||||
"vue": "^3.3.7",
|
||||
"vue-router": "^4.2.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"nuxt-graphql-client": "^0.2.31"
|
||||
}
|
||||
}
|
BIN
public/favicon.ico
Normal file
BIN
public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.2 KiB |
BIN
public/jungle.jpg
Normal file
BIN
public/jungle.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 100 KiB |
22
queries/gifts.gql
Normal file
22
queries/gifts.gql
Normal file
@ -0,0 +1,22 @@
|
||||
query gifts($sort: [String!]) {
|
||||
gifts(filter: { status: { _eq: "published" } }, sort: $sort) {
|
||||
status
|
||||
id
|
||||
name
|
||||
url
|
||||
prix
|
||||
used
|
||||
description
|
||||
from
|
||||
reserved
|
||||
photo {
|
||||
id
|
||||
filename_disk
|
||||
}
|
||||
}
|
||||
}
|
||||
mutation UpdateItem($id: ID!, $reserved: Boolean!, $name: String!) {
|
||||
update_gifts_item(id: $id, data: { reserved: $reserved, from: $name }) {
|
||||
id
|
||||
}
|
||||
}
|
6
queries/singles/homepage.gql
Normal file
6
queries/singles/homepage.gql
Normal file
@ -0,0 +1,6 @@
|
||||
query getHomepage {
|
||||
homepage {
|
||||
date_updated
|
||||
content
|
||||
}
|
||||
}
|
3
server/tsconfig.json
Normal file
3
server/tsconfig.json
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": "../.nuxt/tsconfig.server.json"
|
||||
}
|
4
tsconfig.json
Normal file
4
tsconfig.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
// https://nuxt.com/docs/guide/concepts/typescript
|
||||
"extends": "./.nuxt/tsconfig.json"
|
||||
}
|
Loading…
Reference in New Issue
Block a user