Edit global style and component structure

This commit is contained in:
Paul Nicoué 2023-02-02 17:02:53 +01:00
parent 88bac001d8
commit 53e513a55b
15 changed files with 816 additions and 215 deletions

View file

@ -26,10 +26,11 @@
<style lang="scss"> <style lang="scss">
// -------------------------------------------------- // --------------------------------------------------
// LAYOUT // STYLE
// -------------------------------------------------- // --------------------------------------------------
.app { .app {
width: 100%;
min-height: 100vh; min-height: 100vh;
min-height: 100svh; min-height: 100svh;
display: grid; display: grid;

View file

@ -11,15 +11,6 @@
} }
} }
@keyframes expand-width {
0% {
width: 0;
}
100% {
width: 100%;
}
}
@keyframes expand-height { @keyframes expand-height {
0% { 0% {
height: 0; height: 0;
@ -29,28 +20,6 @@
} }
} }
@keyframes fade-in-from-bottom {
0% {
opacity: 0;
transform: translateY(0.25rem);
}
100% {
opacity: 1;
transform: translateY(0);
}
}
@keyframes fade-in-from-top {
0% {
opacity: 0;
transform: translateY(-0.25rem);
}
100% {
opacity: 1;
transform: translateY(0);
}
}
@keyframes rotate-360 { @keyframes rotate-360 {
0% { 0% {
transform: rotate(0deg); transform: rotate(0deg);

View file

@ -24,6 +24,14 @@
--caption-font-size: 1.1rem; --caption-font-size: 1.1rem;
--footnote-font-size: 1rem; --footnote-font-size: 1rem;
--line-height: 1.2; --line-height: 1.2;
--h1-font-height: calc(var(--h1-font-size) * var(--line-height));
--h2-font-height: calc(var(--h2-font-size) * var(--line-height));
--h3-font-height: calc(var(--h3-font-size) * var(--line-height));
--button-font-height: calc(var(--button-font-size) * var(--line-height));
--text-font-height: calc(var(--text-font-size) * var(--line-height));
--caption-font-height: calc(var(--caption-font-size) * var(--line-height));
--footnote-font-height: calc(var(--footnote-font-size) * var(--line-height));
// Dimensions // Dimensions
@ -175,11 +183,13 @@ button {
justify-content: center; justify-content: center;
align-items: center; align-items: center;
&__text,
&-text { &-text {
transform: translateX(calc((var(--button-gap) + var(--small-icon-size)) / 2)); transform: translateX(calc((var(--button-gap) + var(--small-icon-size)) / 2));
transition: transform 200ms ease-in-out; transition: transform 200ms ease-in-out;
} }
&__icon,
&-icon { &-icon {
display: inherit; display: inherit;
flex-shrink: 0; flex-shrink: 0;
@ -193,14 +203,20 @@ button {
transform 200ms ease-in-out; transform 200ms ease-in-out;
} }
&:hover &__text,
&:hover &-text, &:hover &-text,
&:focus &__text,
&:focus &-text, &:focus &-text,
&:active &__text,
&:active &-text { &:active &-text {
transform: translateX(0); transform: translateX(0);
} }
&:hover &__icon,
&:hover &-icon, &:hover &-icon,
&:focus &__icon,
&:focus &-icon, &:focus &-icon,
&:active &__icon,
&:active &-icon { &:active &-icon {
opacity: 1; opacity: 1;
transform: translateX(0); transform: translateX(0);
@ -248,16 +264,22 @@ form {
} }
} }
textarea {
resize: vertical;
min-height: 15rem;
}
p { p {
font-size: var(--caption-font-size); font-size: var(--caption-font-size);
color: var(--accent-color); color: var(--accent-color);
} }
} }
} }
// --------------------------------------------------
// SMOOTH SCROLLING
// --------------------------------------------------
textarea { html {
resize: vertical; scroll-behavior: smooth;
min-height: 15rem;
} }

View file

@ -1,10 +1,16 @@
<template> <template>
<main> <main>
<div class="error"> <section class="error">
<h1 class="error__title">{{ errorMessage }}</h1> <Transition name="fade-in-from-bottom">
<div class="error__separator" aria-hidden="true"></div> <h1 class="error__title" v-show="isVisible">{{ errorMessage }}</h1>
<div class="error__emoticon" aria-hidden="true">¯\(°_o)/¯</div> </Transition>
<Transition name="expand-width">
<div class="error__separator" aria-hidden="true" v-show="isVisible"></div>
</Transition>
<Transition name="fade-in-from-top">
<div class="error__emoticon" aria-hidden="true" v-show="isVisible">¯\(°_o)/¯</div>
</Transition>
<button class="error__button" @click="$emit('handleError')"> <button class="error__button" @click="$emit('handleError')">
<span class="error__button-text">Retourner à la page d'accueil</span> <span class="error__button-text">Retourner à la page d'accueil</span>
<svg class="error__button-icon" aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"> <svg class="error__button-icon" aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg">
@ -12,7 +18,7 @@
<polyline points="8.667 23 8.667 12 15.333 12 15.333 23"/> <polyline points="8.667 23 8.667 12 15.333 12 15.333 23"/>
</svg> </svg>
</button> </button>
</div> </section>
</main> </main>
</template> </template>
@ -23,10 +29,25 @@
// DATA // DATA
// -------------------------------------------------- // --------------------------------------------------
const props = defineProps({ defineProps({
errorMessage: String errorMessage: String
}); });
const emit = defineEmits([
const isVisible = ref(false);
// --------------------------------------------------
// PROGRAM
// --------------------------------------------------
onMounted(() => {
isVisible.value = true;
})
// --------------------------------------------------
// FORWARDING
// --------------------------------------------------
defineEmits([
'handleError' 'handleError'
]); ]);
@ -39,53 +60,55 @@
// -------------------------------------------------- // --------------------------------------------------
main { main {
padding: 2rem; width: 100%;
padding: 0 2rem;
} }
.error { .error {
display: flex; width: 100%;
flex-direction: column; display: grid;
justify-content: center; grid:
align-items: center; 'title' minmax(var(--h1-font-height), auto)
min-width: 30vw; 'separator' 1px
'emoticon' minmax(var(--h1-font-height), auto)
'.' 5rem
'button' auto
/ minmax(30%, auto);
place-content: center;
place-items: center;
gap: 0.5rem;
text-align: center; text-align: center;
&__title { &__title {
opacity: 0; grid-area: title;
margin: 0 2rem; margin: 0 2rem;
animation: fade-in-from-bottom 400ms ease-in-out 600ms forwards;
} }
&__separator { &__separator {
width: 0; grid-area: separator;
width: 100%;
height: 1px; height: 1px;
margin: 0.5rem 0;
background-color: var(--accent-color); background-color: var(--accent-color);
animation: expand-width 400ms ease-in-out 200ms forwards;
} }
&__emoticon { &__emoticon {
opacity: 0; grid-area: emoticon;
margin: 0 2rem; margin: 0 2rem;
font-family: var(--title-font-family); font-family: var(--title-font-family);
font-size: var(--h1-font-size); font-size: var(--h1-font-size);
font-weight: var(--medium-font-weight); font-weight: var(--medium-font-weight);
animation: fade-in-from-top 400ms ease-in-out 600ms forwards;
} }
&__button { &__button {
margin: 6rem 2rem 0 2rem; grid-area: button;
margin: 0 2rem;
&-icon { &-icon {
display: none; display: none;
} }
} }
} @media screen and (min-width: $tablet-media-query) {
@media screen and (min-width: $tablet-media-query) {
.error {
&__button { &__button {
@include button-with-icon; @include button-with-icon;
@ -93,4 +116,51 @@
} }
} }
// Transition components
.fade-in-from-bottom {
&-enter-from,
&-leave-to {
opacity: 0;
transform: translateY(0.25rem);
}
&-enter-active,
&-leave-active {
transition:
opacity 400ms ease-in-out 600ms,
transform 400ms ease-in-out 600ms;
}
}
.fade-in-from-top {
&-enter-from,
&-leave-to {
opacity: 0;
transform: translateY(-0.25rem);
}
&-enter-active,
&-leave-active {
transition:
opacity 400ms ease-in-out 600ms,
transform 400ms ease-in-out 600ms;
}
}
.expand-width {
&-enter-from,
&-leave-to {
width: 0;
}
&-enter-active,
&-leave-active {
transition: width 400ms ease-in-out 200ms;
}
}
</style> </style>

View file

@ -18,39 +18,43 @@
<style lang="scss" scoped> <style lang="scss" scoped>
// -------------------------------------------------- // --------------------------------------------------
// LAYOUT & STYLE // STYLE
// -------------------------------------------------- // --------------------------------------------------
.contact-decoration { .contact-decoration {
display: flex; display: none;
flex-direction: column;
justify-content: flex-start;
align-items: center;
height: 100%;
margin: 0 -1rem;
&__icon { @media screen and (min-width: $tablet-media-query) {
width: calc(var(--regular-icon-size) + 1rem);
height: calc(var(--regular-icon-size) + 1rem);
display: flex; display: flex;
justify-content: center; flex-direction: column;
justify-content: flex-start;
align-items: center; align-items: center;
background-color: var(--primary-color); height: 100%;
border: 1px solid var(--accent-color); margin: 0 -1rem;
border-radius: 50%;
svg { &__icon {
stroke: var(--secondary-color); width: calc(var(--regular-icon-size) + 1rem);
width: var(--regular-icon-size); height: calc(var(--regular-icon-size) + 1rem);
height: var(--regular-icon-size); display: flex;
justify-content: center;
align-items: center;
background-color: var(--primary-color);
border: 1px solid var(--accent-color);
border-radius: 50%;
svg {
stroke: var(--secondary-color);
width: var(--regular-icon-size);
height: var(--regular-icon-size);
}
} }
}
&__separator { &__separator {
width: 1px; width: 1px;
height: 0; height: 0;
background-color: var(--accent-color); background-color: var(--accent-color);
animation: expand-height 600ms ease-in-out 600ms forwards; animation: expand-height 600ms ease-in-out 600ms forwards;
}
} }
} }

View file

@ -4,48 +4,33 @@
<div class="contact-form__name"> <div class="contact-form__name">
<label for="name">Nom</label> <label for="name">Nom</label>
<VeeField name="name" /> <VeeField name="name" />
<Transition name="validation"> <Transition name="fade-in-expand-height">
<VeeError name="name" as="p" /> <VeeError name="name" as="p" />
</Transition> </Transition>
</div> </div>
<div class="contact-form__email"> <div class="contact-form__email">
<label for="email">Adresse e-mail</label> <label for="email">Adresse e-mail</label>
<VeeField name="email" /> <VeeField name="email" />
<Transition name="validation"> <Transition name="fade-in-expand-height">
<VeeError name="email" as="p" /> <VeeError name="email" as="p" />
</Transition> </Transition>
</div> </div>
<div class="contact-form__subject"> <div class="contact-form__subject">
<label for="subject">Sujet</label> <label for="subject">Sujet</label>
<VeeField name="subject" /> <VeeField name="subject" />
<Transition name="validation"> <Transition name="fade-in-expand-height">
<VeeError name="subject" as="p" /> <VeeError name="subject" as="p" />
</Transition> </Transition>
</div> </div>
<div class="contact-form__message"> <div class="contact-form__message">
<label for="message">Message</label> <label for="message">Message</label>
<VeeField name="message" as="textarea" /> <VeeField name="message" as="textarea" />
<Transition name="validation"> <Transition name="fade-in-expand-height">
<VeeError name="message" as="p" /> <VeeError name="message" as="p" />
</Transition> </Transition>
</div> </div>
<VeeField name="honeypot" type="hidden" /> <VeeField name="honeypot" type="hidden" />
<button class="contact-form__button"> <ContactFormValidation class="contact-form__validation" ref="contactFormValidation" @send-email="sendEmail" />
<span class="contact-form__button-text">Envoyer</span>
<svg class="contact-form__button-icon" aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg">
<line x1="23" y1="1" x2="10.9" y2="13.1"/>
<polygon points="23 1 15.3 23 10.9 13.1 1 8.7 23 1"/>
</svg>
</button>
<Transition name="loader">
<div class="contact-form__loader" aria-hidden="true" ref="loader" v-show="isLoading"></div>
</Transition>
<Transition name="validation">
<div class="contact-form__validation" v-if="isValidated" v-click-outside="hideValidationMessage">
<p class="contact-form__validation-success" v-if="isSuccessful">Votre message a bien été envoyé !</p>
<p class="contact-form__validation-error" v-else>Une erreur est survenue... Veuillez me contacter à l'adresse e-mail contact@paulnicoue.com</p>
</div>
</Transition>
</VeeForm> </VeeForm>
</template> </template>
@ -78,22 +63,16 @@
.max(0) .max(0)
}); });
// Form validation // Form validation component
const isValidated = ref(false); const contactFormValidation = ref();
const isSuccessful = ref(undefined);
// Loader
const isLoading = ref(false);
const loader = ref(null);
// -------------------------------------------------- // --------------------------------------------------
// LOGIC // LOGIC
// -------------------------------------------------- // --------------------------------------------------
const sendEmail = async (values) => { const sendEmail = async (values) => {
isLoading.value = true; contactFormValidation.value.isLoading = true;
try { try {
await $fetch('/api/contact', { await $fetch('/api/contact', {
method: 'post', method: 'post',
@ -104,26 +83,24 @@
message: values.message message: values.message
} }
}); });
isSuccessful.value = true; contactFormValidation.value.isSuccessful = true;
} catch { } catch {
isSuccessful.value = false; contactFormValidation.value.isSuccessful = false;
} finally { } finally {
loader.value.addEventListener('animationiteration', function hideLoader(event) { contactFormValidation.value.loader.addEventListener('animationiteration', function hideLoader(event) {
event.currentTarget.removeEventListener('animationiteration', hideLoader); event.currentTarget.removeEventListener('animationiteration', hideLoader);
isLoading.value = false; contactFormValidation.value.isLoading = false;
isValidated.value = true; contactFormValidation.value.isValidated = true;
}); });
} }
} }
const hideValidationMessage = () => isValidated.value = false;
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
// -------------------------------------------------- // --------------------------------------------------
// LAYOUT & STYLE // STYLE
// -------------------------------------------------- // --------------------------------------------------
.contact-form { .contact-form {
@ -134,7 +111,6 @@
'email email' auto 'email email' auto
'subject subject' auto 'subject subject' auto
'message message' auto 'message message' auto
'button loader' auto
'validation validation' auto 'validation validation' auto
/ 1fr 1fr; / 1fr 1fr;
place-content: start; place-content: start;
@ -164,80 +140,42 @@
width: 100%; width: 100%;
} }
&__button {
grid-area: button;
@include button-with-icon;
}
&__loader {
grid-area: loader;
place-self: center end;
width: 2.5rem;
height: 2.5rem;
border-width: 2px;
border-style: solid;
border-color: var(--accent-color) transparent;
border-radius: 50%;
animation: rotate-360 800ms ease-in-out infinite;
}
&__validation { &__validation {
grid-area: validation; grid-area: validation;
&-error {
color: var(--error-color);
}
} }
@media screen and (min-width: $tablet-media-query) { @media screen and (min-width: $tablet-media-query) {
grid: grid:
'name name .' auto 'name name .' auto
'email email .' auto 'email email .' auto
'subject subject subject' auto 'subject subject subject' auto
'message message message' auto 'message message message' auto
'button loader loader' auto
'validation validation validation' auto 'validation validation validation' auto
/ 1fr 1fr 1fr; / 1fr 1fr 1fr;
} }
} }
// -------------------------------------------------- // Transition components
// TRANSITION COMPONENTS
// --------------------------------------------------
.validation-enter-from, .fade-in-expand-height {
.validation-leave-to {
opacity: 0;
max-height: 0;
}
.validation-enter-active, &-enter-from,
.validation-leave-active { &-leave-to {
transition: opacity: 0;
opacity 400ms linear, max-height: 0;
max-height 400ms linear; }
}
.validation-enter-to, &-enter-active,
.validation-leave-from { &-leave-active {
opacity: 1; transition:
max-height: calc(3 * (var(--caption-font-size) * var(--line-height))); opacity 600ms ease-in-out,
} max-height 600ms ease-in-out;
}
.loader-enter-from, &-enter-to,
.loader-leave-to { &-leave-from {
opacity: 0; max-height: calc(3 * var(--caption-font-height));
} }
.loader-enter-active,
.loader-leave-active {
transition: opacity 400ms linear;
}
.loader-enter-to,
.loader-leave-from {
opacity: 1;
} }
</style> </style>

View file

@ -0,0 +1,144 @@
<template>
<div class="contact-form-validation">
<button class="contact-form-validation__button" @click="$emit('sendEmail')">
<span class="contact-form-validation__button-text">Envoyer</span>
<svg class="contact-form-validation__button-icon" aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg">
<line x1="23" y1="1" x2="10.9" y2="13.1"/>
<polygon points="23 1 15.3 23 10.9 13.1 1 8.7 23 1"/>
</svg>
</button>
<Transition name="fade-in">
<div class="contact-form-validation__loader" aria-hidden="true" ref="loader" v-show="isLoading"></div>
</Transition>
<Transition name="fade-in-expand-height">
<div class="contact-form-validation__message" v-show="isValidated" v-click-outside="hideValidationMessage">
<p class="contact-form-validation__message-success" v-if="isSuccessful">Votre message a bien été envoyé !</p>
<p class="contact-form-validation__message-error" v-else>Une erreur est survenue... Veuillez me contacter à l'adresse e-mail contact@paulnicoue.com</p>
</div>
</Transition>
</div>
</template>
<script setup>
// --------------------------------------------------
// DATA
// --------------------------------------------------
// Form validation
const isValidated = ref(false);
const isSuccessful = ref(undefined);
// Loader
const isLoading = ref(false);
const loader = ref();
// --------------------------------------------------
// LOGIC
// --------------------------------------------------
const hideValidationMessage = () => isValidated.value = false;
// --------------------------------------------------
// FORWARDING
// --------------------------------------------------
defineEmits([
'sendEmail'
]);
defineExpose({
isValidated,
isSuccessful,
isLoading,
loader,
hideValidationMessage
});
</script>
<style lang="scss" scoped>
// --------------------------------------------------
// STYLE
// --------------------------------------------------
.contact-form-validation {
width: 100%;
display: grid;
grid:
'button loader' auto
'message message' auto
/ auto auto;
place-content: start stretch;
place-items: start;
column-gap: 1rem;
row-gap: 0.5rem;
&__button {
grid-area: button;
@include button-with-icon;
}
&__loader {
grid-area: loader;
place-self: center end;
width: 2.5rem;
height: 2.5rem;
border-width: 2px;
border-style: solid;
border-color: var(--accent-color) transparent;
border-radius: 50%;
animation: rotate-360 800ms ease-in-out infinite;
}
&__message {
grid-area: message;
&-error {
color: var(--error-color);
}
}
}
// Transition components
.fade-in-expand-height {
&-enter-from,
&-leave-to {
opacity: 0;
max-height: 0;
}
&-enter-active,
&-leave-active {
transition:
opacity 600ms ease-in-out,
max-height 600ms ease-in-out;
}
&-enter-to,
&-leave-from {
max-height: calc(1rem + (3 * var(--caption-font-height)));
}
}
.fade-in {
&-enter-from,
&-leave-to {
opacity: 0;
}
&-enter-active,
&-leave-active {
transition: opacity 400ms ease-in-out;
}
}
</style>

View file

@ -0,0 +1,240 @@
<template>
<VeeForm class="contact-form" @submit="sendEmail" :validation-schema="contactFormSchema">
<div class="contact-form__name">
<label for="name">Nom</label>
<VeeField name="name" />
<Transition name="fade-in-expand-height">
<VeeError name="name" as="p" />
</Transition>
</div>
<div class="contact-form__email">
<label for="email">Adresse e-mail</label>
<VeeField name="email" />
<Transition name="fade-in-expand-height">
<VeeError name="email" as="p" />
</Transition>
</div>
<div class="contact-form__subject">
<label for="subject">Sujet</label>
<VeeField name="subject" />
<Transition name="fade-in-expand-height">
<VeeError name="subject" as="p" />
</Transition>
</div>
<div class="contact-form__message">
<label for="message">Message</label>
<VeeField name="message" as="textarea" />
<Transition name="fade-in-expand-height">
<VeeError name="message" as="p" />
</Transition>
</div>
<VeeField name="honeypot" type="hidden" />
<button class="contact-form__button">
<span class="contact-form__button-text">Envoyer</span>
<svg class="contact-form__button-icon" aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg">
<line x1="23" y1="1" x2="10.9" y2="13.1"/>
<polygon points="23 1 15.3 23 10.9 13.1 1 8.7 23 1"/>
</svg>
</button>
<Transition name="fade-in">
<div class="contact-form__loader" aria-hidden="true" ref="loader" v-show="isLoading"></div>
</Transition>
<Transition name="fade-in-expand-height">
<div class="contact-form__validation" v-show="isValidated" v-click-outside="hideValidationMessage">
<p class="contact-form__validation-success" v-if="isSuccessful">Votre message a bien été envoyé !</p>
<p class="contact-form__validation-error" v-else>Une erreur est survenue... Veuillez me contacter à l'adresse e-mail contact@paulnicoue.com</p>
</div>
</Transition>
</VeeForm>
</template>
<script setup>
// --------------------------------------------------
// DATA
// --------------------------------------------------
// Yup schema builder
const yupObject = useNuxtApp().$yupObject;
const yupString = useNuxtApp().$yupString;
const contactFormSchema = yupObject({
name: yupString()
.min(2, 'Votre nom doit comprendre au moins ${min} caractères.')
.required('Veuillez saisir votre nom.'),
email: yupString()
.email('Veuillez saisir une adresse e-mail valide.')
.required('Veuillez saisir votre adresse e-mail.'),
subject: yupString()
.min(2, 'Le sujet de votre message doit comprendre au moins ${min} caractères.')
.required('Veuillez saisir le sujet de votre message.'),
message: yupString()
.min(10, 'Votre message doit comprendre au moins ${min} caractères.')
.required('Veuillez saisir votre message.'),
honeypot: yupString()
.max(0)
});
// Form validation
const isValidated = ref(false);
const isSuccessful = ref(undefined);
// Loader
const isLoading = ref(false);
const loader = ref();
// --------------------------------------------------
// LOGIC
// --------------------------------------------------
const sendEmail = async (values) => {
isLoading.value = true;
try {
await $fetch('/api/contact', {
method: 'post',
body: {
name: values.name,
emailAddress: values.email,
subject: values.subject,
message: values.message
}
});
isSuccessful.value = true;
} catch {
isSuccessful.value = false;
} finally {
loader.value.addEventListener('animationiteration', function hideLoader(event) {
event.currentTarget.removeEventListener('animationiteration', hideLoader);
isLoading.value = false;
isValidated.value = true;
});
}
}
const hideValidationMessage = () => isValidated.value = false;
</script>
<style lang="scss" scoped>
// --------------------------------------------------
// STYLE
// --------------------------------------------------
.contact-form {
max-width: var(--small-content-max-width);
display: grid;
grid:
'name name' auto
'email email' auto
'subject subject' auto
'message message' auto
'button loader' auto
'validation validation' auto
/ 1fr 1fr;
place-content: start;
place-items: start;
gap: 1.5rem;
&__name {
grid-area: name;
}
&__email {
grid-area: email;
}
&__subject {
grid-area: subject;
}
&__message {
grid-area: message;
}
&__name,
&__email,
&__subject,
&__message {
width: 100%;
}
&__button {
grid-area: button;
@include button-with-icon;
}
&__loader {
grid-area: loader;
place-self: center end;
width: 2.5rem;
height: 2.5rem;
border-width: 2px;
border-style: solid;
border-color: var(--accent-color) transparent;
border-radius: 50%;
animation: rotate-360 800ms ease-in-out infinite;
}
&__validation {
grid-area: validation;
&-error {
color: var(--error-color);
}
}
@media screen and (min-width: $tablet-media-query) {
grid:
'name name .' auto
'email email .' auto
'subject subject subject' auto
'message message message' auto
'button loader loader' auto
'validation validation validation' auto
/ 1fr 1fr 1fr;
}
}
// Transition components
.fade-in-expand-height {
&-enter-from,
&-leave-to {
opacity: 0;
max-height: 0;
}
&-enter-active,
&-leave-active {
transition:
opacity 600ms ease-in-out,
max-height 600ms ease-in-out;
}
&-enter-to,
&-leave-from {
max-height: calc(3 * var(--caption-font-height));
}
}
.fade-in {
&-enter-from,
&-leave-to {
opacity: 0;
}
&-enter-active,
&-leave-active {
transition: opacity 400ms ease-in-out;
}
}
</style>

View file

@ -37,7 +37,7 @@
<style lang="scss" scoped> <style lang="scss" scoped>
// -------------------------------------------------- // --------------------------------------------------
// LAYOUT & STYLE // STYLE
// -------------------------------------------------- // --------------------------------------------------
.contact-header { .contact-header {
@ -67,7 +67,7 @@
width: 100%; width: 100%;
height: 100%; height: 100%;
transition: transition:
fill 200ms ease-in-out, filter 200ms ease-in-out,
transform 200ms ease-in-out; transform 200ms ease-in-out;
} }
@ -76,7 +76,7 @@
&:active { &:active {
svg { svg {
fill: var(--accent-color-light); filter: brightness(1.1);
transform: scale(1.1) rotate(22.5deg); transform: scale(1.1) rotate(22.5deg);
} }
} }

View file

@ -1,6 +1,6 @@
<template> <template>
<section class="contact"> <section id="contact" class="contact">
<ContactDecoration class="contact__decoration" /> <ContactDecoration class="contact__decoration" />
<ContactHeader class="contact__header" /> <ContactHeader class="contact__header" />
<ContactForm class="contact__form" /> <ContactForm class="contact__form" />
@ -14,7 +14,7 @@
<style lang="scss" scoped> <style lang="scss" scoped>
// -------------------------------------------------- // --------------------------------------------------
// LAYOUT & STYLE // STYLE
// -------------------------------------------------- // --------------------------------------------------
.contact { .contact {
@ -27,7 +27,6 @@
place-content: start stretch; place-content: start stretch;
place-items: start stretch; place-items: start stretch;
row-gap: 4rem; row-gap: 4rem;
column-gap: 2rem;
&__decoration { &__decoration {
grid-area: decoration; grid-area: decoration;
@ -46,7 +45,6 @@
} }
@media screen and (min-width: $desktop-media-query) { @media screen and (min-width: $desktop-media-query) {
grid: grid:
'decoration header form' auto 'decoration header form' auto
/ auto 1fr 2fr; / auto 1fr 2fr;

View file

@ -0,0 +1,52 @@
<template>
<a class="hero-arrow-down" tabindex="-1" aria-hidden="true" href="#contact" target="_self">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg">
<line x1="12" y1="5" x2="12" y2="19"></line>
<polyline points="19 12 12 19 5 12"></polyline>
</svg>
</a>
</template>
<script setup>
</script>
<style lang="scss" scoped>
// --------------------------------------------------
// STYLE
// --------------------------------------------------
.hero-arrow-down {
width: calc(var(--regular-icon-size) + 1rem);
height: calc(var(--regular-icon-size) + 1rem);
display: flex;
justify-content: center;
align-items: center;
background-image: var(--button-gradient);
background-size: 100%;
background-position: right center;
border-radius: 50%;
cursor: pointer;
transition: background-size 200ms ease-in-out;
svg {
width: var(--regular-icon-size);
height: var(--regular-icon-size);
stroke: var(--primary-color);
transition: transform 200ms ease-in-out;
}
&:hover,
&:focus,
&:active {
background-size: 300%;
svg {
transform: translateY(0.25rem);
}
}
}
</style>

View file

@ -0,0 +1,95 @@
<template>
<section class="hero" ref="hero">
<HeroTitle class="hero__title" />
<Transition name="fade-in">
<HeroArrowDown class="hero__arrow-down" v-show="isVisible" />
</Transition>
</section>
</template>
<script setup>
// --------------------------------------------------
// DATA
// --------------------------------------------------
const hero = ref();
const isVisible = ref(true);
// --------------------------------------------------
// LOGIC
// --------------------------------------------------
const toggleArrowDown = () => {
if (hero.value) {
if (window.innerHeight / 2 < hero.value.getBoundingClientRect().bottom) {
isVisible.value = true;
} else {
isVisible.value = false;
}
}
}
// --------------------------------------------------
// PROGRAM
// --------------------------------------------------
onMounted(() => {
toggleArrowDown();
window.addEventListener('scroll', toggleArrowDown);
})
onUnmounted(() => {
window.removeEventListener('scroll', toggleArrowDown);
})
</script>
<style lang="scss" scoped>
// --------------------------------------------------
// STYLE
// --------------------------------------------------
.hero {
width: 100%;
height: 100vh;
height: 100svh;
display: grid;
grid:
'.' 1fr
'title' auto
'.' minmax(6rem, 1fr)
'arrow-down' auto
'.' 2rem
/ 100%;
place-content: center;
place-items: center;
&__title {
grid-area: title;
}
&__arrow-down {
grid-area: arrow-down;
}
}
// Transition component
.fade-in {
&-enter-from,
&-leave-to {
opacity: 0;
}
&-enter-active,
&-leave-active {
transition: opacity 400ms ease-in-out;
}
}
</style>

View file

@ -1,14 +1,35 @@
<template> <template>
<div class="hero-title"> <h1 class="hero-title">
<h1 class="hero-title__name">Paul Nicoué</h1> <Transition name="fade-in-from-bottom">
<div class="hero-title__separator" aria-hidden="true"></div> <div class="hero-title__name" v-show="isVisible">Paul Nicoué</div>
<h2 class="hero-title__job">Intégrateur web & développeur full stack</h2> </Transition>
</div> <Transition name="expand-width">
<div class="hero-title__separator" aria-hidden="true" v-show="isVisible"></div>
</Transition>
<Transition name="fade-in-from-top">
<div class="hero-title__job" v-show="isVisible">Intégrateur web & développeur full stack</div>
</Transition>
</h1>
</template> </template>
<script setup> <script setup>
// --------------------------------------------------
// DATA
// --------------------------------------------------
const isVisible = ref(false);
// --------------------------------------------------
// PROGRAM
// --------------------------------------------------
onMounted(() => {
isVisible.value = true;
})
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@ -18,35 +39,82 @@
// -------------------------------------------------- // --------------------------------------------------
.hero-title { .hero-title {
min-width: 50vw; width: 100%;
height: 100vh; display: grid;
height: 100svh; grid:
display: flex; 'name' auto
flex-direction: column; 'separator' auto
justify-content: center; 'job' auto
align-items: center; / minmax(50%, auto);
place-content: center;
place-items: center;
gap: 0.5rem;
text-align: center; text-align: center;
&__name { &__name {
opacity: 0; grid-area: name;
margin: 0 2rem; margin: 0 2rem;
animation: fade-in-from-bottom 400ms ease-in-out 600ms forwards;
} }
&__separator { &__separator {
width: 0; grid-area: separator;
width: 100%;
height: 1px; height: 1px;
margin: 0.5rem auto;
background-color: var(--accent-color); background-color: var(--accent-color);
animation: expand-width 400ms ease-in-out 200ms forwards;
} }
&__job { &__job {
opacity: 0; grid-area: job;
font-size: var(--h2-font-size); font-size: var(--h2-font-size);
font-weight: var(--light-font-weight); font-weight: var(--light-font-weight);
margin: 0 2rem; margin: 0 2rem;
animation: fade-in-from-top 400ms ease-in-out 600ms forwards; }
}
// Transition components
.fade-in-from-bottom {
&-enter-from,
&-leave-to {
opacity: 0;
transform: translateY(0.25rem);
}
&-enter-active,
&-leave-active {
transition:
opacity 400ms ease-in-out 600ms,
transform 400ms ease-in-out 600ms;
}
}
.fade-in-from-top {
&-enter-from,
&-leave-to {
opacity: 0;
transform: translateY(-0.25rem);
}
&-enter-active,
&-leave-active {
transition:
opacity 400ms ease-in-out 600ms,
transform 400ms ease-in-out 600ms;
}
}
.expand-width {
&-enter-from,
&-leave-to {
width: 0;
}
&-enter-active,
&-leave-active {
transition: width 400ms ease-in-out 200ms;
} }
} }

View file

@ -37,7 +37,7 @@
<style lang="scss" scoped> <style lang="scss" scoped>
// -------------------------------------------------- // --------------------------------------------------
// LAYOUT // STYLE
// -------------------------------------------------- // --------------------------------------------------
.app { .app {

View file

@ -1,7 +1,7 @@
<template> <template>
<main> <main>
<HeroTitle class="hero-title" /> <HeroSection class="hero-section" />
<ContactSection class="contact-section" /> <ContactSection class="contact-section" />
</main> </main>
@ -22,23 +22,23 @@
<style lang="scss" scoped> <style lang="scss" scoped>
// -------------------------------------------------- // --------------------------------------------------
// LAYOUT // STYLE
// -------------------------------------------------- // --------------------------------------------------
main { main {
width: 100%; width: 100%;
padding: 2rem; padding: 0 2rem;
display: grid; display: grid;
grid: grid:
'hero-title' auto 'hero-section' auto
'contact-section' auto 'contact-section' auto
/ 1fr; / 1fr;
place-content: start center; place-content: start center;
place-items: start center; place-items: start center;
row-gap: 4rem; row-gap: 4rem;
.hero-title { .hero-section {
grid-area: hero-title; grid-area: hero-section;
} }
.contact-section { .contact-section {
@ -46,11 +46,11 @@
} }
@media screen and (min-width: $tablet-media-query) { @media screen and (min-width: $tablet-media-query) {
padding: 2rem 4rem; padding: 0 4rem;
} }
@media screen and (min-width: $desktop-media-query) { @media screen and (min-width: $desktop-media-query) {
padding: 2rem 6rem; padding: 0 6rem;
} }
} }