Edit global style and component structure
This commit is contained in:
parent
88bac001d8
commit
53e513a55b
15 changed files with 816 additions and 215 deletions
3
app.vue
3
app.vue
|
@ -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;
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
144
components/ContactFormValidation.vue
Normal file
144
components/ContactFormValidation.vue
Normal 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>
|
240
components/ContactForm_old.vue
Normal file
240
components/ContactForm_old.vue
Normal 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>
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
52
components/HeroArrowDown.vue
Normal file
52
components/HeroArrowDown.vue
Normal 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>
|
95
components/HeroSection.vue
Normal file
95
components/HeroSection.vue
Normal 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>
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -37,7 +37,7 @@
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|
||||||
// --------------------------------------------------
|
// --------------------------------------------------
|
||||||
// LAYOUT
|
// STYLE
|
||||||
// --------------------------------------------------
|
// --------------------------------------------------
|
||||||
|
|
||||||
.app {
|
.app {
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue