Add new mobile menu

This commit is contained in:
2021-08-12 22:04:47 +02:00
parent 164238d624
commit e8484f98d5
17 changed files with 467 additions and 56 deletions

View File

@@ -24,6 +24,7 @@
"nuxt-i18n": "^6.27.1",
"prism-themes": "^1.7.0",
"sass": "^1.35.1",
"vuex": "^3.6.2",
"windicss": "^3.1.3"
},
"devDependencies": {
@@ -31,7 +32,7 @@
"@nuxt/typescript-build": "^2.1.0",
"@nuxtjs/color-mode": "^2.0.10",
"markdown-it-prism": "^2.1.6",
"sass-loader": "10.1.1",
"nuxt-windicss": "^1.1.1"
"nuxt-windicss": "^1.1.1",
"sass-loader": "10.1.1"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 KiB

View File

@@ -1,23 +1,37 @@
<template>
<div
v-if="announce"
class="h-16 bg-black text-center"
class="h-12 flex justify-center items-center"
:class="[getBackgroundColor, getHoverColor]"
>
{{ announce }}
{{ announce.translation.code }}
<img
v-if="announce.file"
:src="`https://athena.arthurdanjou.fr/files/${announce.file}`"
alt="Announce Cover File"
>
</div>
</template>
<script lang="ts">
import {ref, useAsync, useContext} from "@nuxtjs/composition-api";
import {computed, ref, useAsync, useContext} from "@nuxtjs/composition-api";
import {Translation} from "~/types/types";
interface Announce {
color: string,
hover_color: string,
translation: Translation
file: null
}
export default {
name: "Announcement",
setup() {
const {$axios, $sentry} = useContext()
const announce = ref("")
const announce = ref<Announce>()
useAsync(async () => {
const response = await $axios.get('/api/announce', {
const response = await $axios.get('/api/announces', {
headers: {
'Authorization': `Bearer ${process.env.API_TOKEN}`
}
@@ -29,8 +43,26 @@ export default {
}
})
const getBackgroundColor = computed(() => {
switch (announce.value!.color) {
case 'black': {
return 'bg-black text-white dark:(bg-white text-black)'
}
}
})
const getHoverColor = computed(() => {
switch (announce.value!.color) {
case 'gray': {
return 'hover:bg-gray-600'
}
}
})
return {
announce
announce,
getBackgroundColor,
getHoverColor
}
}
}

View File

@@ -1,6 +1,6 @@
<template>
<header class="dark:bg-black dark:text-white fixed z-50 top-0 left-0 bg-white w-full duration-400"
:class="scrollPosition > 50 ? ' shadow-md dark:shadow-white h-16 lg:h-20' : 'h-20 lg:h-24'">
<header class="hidden md:block dark:bg-black dark:text-white sticky z-50 top-0 left-0 bg-white w-full duration-400"
:class="scrollPosition > 65 ? ' shadow-md dark:shadow-white h-16 lg:h-20' : 'h-20 lg:h-24'">
<div class="header-container z-index-50 flex justify-between items-center h-full px-5 xl:px-32">
<nuxt-link to="/">
<img src="~/assets/images/logo-header.png" alt="Logo Circle" class="h-10 left cursor-pointer duration-500"/>
@@ -21,37 +21,6 @@
</nuxt-link>
</div>
</nav>
<div class="w-full z-50 fixed bottom-0 left-0 md:hidden">
<ul
class="bg-gray-300 dark:bg-gray-700 m-4 rounded-xl dark:text-white text-sm flex items-center justify-around h-20 navbar-bottom-items"
>
<nuxt-link to="/" class="w-1/5">
<li class="h-full w-full font-medium flex flex-col items-center justify-center">
<HomeIcon :active="isWindow('')"/>
</li>
</nuxt-link>
<nuxt-link to="/about" class="w-1/5">
<li class="font-medium flex flex-col items-center justify-center">
<UserIcon :active="isWindow('/about')"/>
</li>
</nuxt-link>
<nuxt-link to="/blog" class="w-1/5">
<li class="font-medium flex flex-col items-center justify-center">
<BookIcon :active="isWindow('/blog')"/>
</li>
</nuxt-link>
<nuxt-link to="/projects" class="w-1/5">
<li class="font-medium flex flex-col items-center justify-center">
<LightbulbIcon :active="isWindow('/projects')"/>
</li>
</nuxt-link>
<nuxt-link to="/contact" class="w-1/5">
<li class="font-medium flex flex-col items-center justify-center">
<ContactIcon :active="isWindow('/contact')"/>
</li>
</nuxt-link>
</ul>
</div>
<div>
<ul class="flex items-center">
<li @click="changeLanguage()"
@@ -75,14 +44,13 @@
<script lang="ts">
import {
computed,
defineComponent,
onMounted,
onUnmounted,
ref,
useAsync,
useContext,
useRouter, watch
useRouter
} from "@nuxtjs/composition-api";
export default defineComponent({
@@ -115,17 +83,11 @@ export default defineComponent({
}
})
const isWindow = (loc: string) => {
if (loc === '') return $router.currentRoute.path === "/"
else return $router.currentRoute.path.includes(loc)
}
return {
scrollPosition,
changeColorMode,
updateScroll,
changeLanguage,
isWindow
}
}
})

View File

@@ -0,0 +1,87 @@
<template>
<div
class="w-full z-100 fixed bottom-0 left-0 md:hidden duration-500"
:class="{'opened': isOpened}"
>
<ul
class="bg-gray-200 dark:bg-gray-800 m-4 rounded-3xl dark:text-white text-sm flex items-center justify-around h-20 navbar-bottom-items"
>
<nuxt-link to="/" class="w-1/5">
<li class="h-full w-full font-medium flex flex-col items-center justify-center">
<HomeIcon :active="isWindow('')"/>
{{ debug }}
</li>
</nuxt-link>
<nuxt-link to="/about" class="w-1/5">
<li class="font-medium flex flex-col items-center justify-center">
<UserIcon :active="isWindow('/about')"/>
</li>
</nuxt-link>
<nuxt-link to="/blog" class="w-1/5">
<li class="font-medium flex flex-col items-center justify-center">
<BookIcon :active="isWindow('/blog')"/>
</li>
</nuxt-link>
<nuxt-link to="/projects" class="w-1/5">
<li class="font-medium flex flex-col items-center justify-center">
<LightbulbIcon :active="isWindow('/projects')"/>
</li>
</nuxt-link>
<li @click='toggleMenu' class="w-1/5 flex flex-col items-center justify-center cursor-pointer">
<MenuIcon :type="getMenuIconType"/>
</li>
</ul>
</div>
</template>
<script lang="ts">
import {computed, defineComponent, useRouter, useStore} from "@nuxtjs/composition-api";
import {State} from "~/types/types";
const PAGE_TYPE = {
projects: 1,
}
export default defineComponent({
name: "MobileNavbar",
setup() {
const $router = useRouter()
const debug = computed(() => $router.currentRoute.path)
const isWindow = (loc: string) => {
if (loc === '') return $router.currentRoute.path === "/"
else return $router.currentRoute.path.includes(loc)
}
const getMenuIconType = computed(() => PAGE_TYPE[$router.currentRoute.path.split('/')[1]] || 0)
const store = useStore<State>()
const toggleMenu = () => {
window.scrollTo({
top: 0,
})
store.commit('TOGGLE_OPENED', !store.state.opened)
}
$router.afterEach(() => {
store.commit('TOGGLE_OPENED', false)
})
const isOpened = computed(() => store.state.opened)
return {
isWindow,
toggleMenu,
isOpened,
getMenuIconType,
debug
}
}
})
</script>
<style scoped lang="scss">
.opened {
@apply transform translate-x-9/12 scale-90 -translate-y-10 duration-500;
}
</style>

170
src/components/SideMenu.vue Normal file
View File

@@ -0,0 +1,170 @@
<template>
<div class="md:hidden w-full min-w-screen">
<div class="bg-gray-100 dark:bg-gray-900 min-h-screen duration-500 absolute top-0 left-0 right-0 py-4 pr-20 pl-4 flex items-center">
<nav class="w-auto">
<div class="mb-8">
<div class="flex justify-between mb-4">
<div @click="closeMenu" class="flex justify-center items-center cursor-pointer cross text-sm">
<CrossIcon class="duration-300" />
<div class="ml-4">{{ $t('sidebar.close') }}</div>
</div>
<div class="ml-6">
<ul class="flex items-center">
<li @click="changeLanguage()"
class="mx-1 h-9 w-9 cursor-pointer flex items-center justify-center p-1.5 rounded-xl hover:bg-gray-300 duration-200 dark:hover:bg-dark-400">
<TranslateIcon/>
</li>
<li @click="changeColorMode()"
class="mx-1 h-9 w-9 cursor-pointer flex items-center p-1.5 rounded-xl hover:bg-gray-300 dark:hover:bg-dark-400 duration-200">
<div v-if="this.$colorMode.preference === 'light'">
<MoonIcon/>
</div>
<div v-else>
<SunIcon/>
</div>
</li>
</ul>
</div>
</div>
<nuxt-link class="profile ml-4 flex items-center" to="/">
<img class="h-12 w-12 duration-500" src="@/assets/images/photo-rounded.png" alt="Photo of me" />
<h1 class="ml-4 font-bold text-lg">Arthur Danjou</h1>
</nuxt-link>
</div>
<div class="w-auto flex">
<div class="flex flex-col ml-4 mb-8 space-y-1.5 font-bold text-lg">
<div class="nav-link" :class="{ 'link-active': isWindow('') }">
<nuxt-link to="/">
{{ $t('header.home') }}
</nuxt-link>
</div>
<div class="nav-link" :class="{ 'link-active': isWindow('') }">
<nuxt-link to="/about">
{{ $t('header.about') }}
</nuxt-link>
</div>
<div class="nav-link" :class="{ 'link-active': isWindow('blog') }">
<nuxt-link to="/blog">
{{ $t('header.blog') }}
</nuxt-link>
</div>
<div class="nav-link" :class="{ 'link-active': isWindow('projects') }">
<nuxt-link to="/projects">
{{ $t('header.projects') }}
</nuxt-link>
</div>
<div class="nav-link" :class="{ 'link-active': isWindow('services') }">
<nuxt-link to="/services">
{{ $t('header.services') }}
</nuxt-link>
</div>
<div class="nav-link" :class="{ 'link-active': isWindow('env') }">
<nuxt-link to="/env">
{{ $t('header.env') }}
</nuxt-link>
</div>
<div class="nav-link" :class="{ 'link-active': isWindow('guestbook') }">
<nuxt-link to="/guestbook">
{{ $t('header.guestbook') }}
</nuxt-link>
</div>
<div class="nav-link" :class="{ 'link-active': isWindow('contact') }">
<nuxt-link to="/contact">
{{ $t('header.contact') }}
</nuxt-link>
</div>
</div>
</div>
<div class="social-links flex justify-between space-x-1">
<a target="_blank" href="https://twitter.com/ArthurDanj" rel="noopener noreferrer">
<TwitterIcon />
</a>
<a target="_blank" href="https://github.com/ArthurDanjou" rel="noopener noreferrer">
<GithubIcon />
</a>
<a target="_blank" href="https://www.polywork.com/arthurdanjou" rel="noopener noreferrer">
<PolyworkIcon />
</a>
<a target="_blank" href="https://www.twitch.tv/arthurdanjou" rel="noopener noreferrer">
<TwitchIcon />
</a>
<a target="_blank" href="https://discord.gg/RQhjE5UkxD" rel="noopener noreferrer">
<DiscordIcon />
</a>
<a target="_blank" href="mailto:contact@arthurdanjou.fr" rel="noopener noreferrer">
<MailIcon />
</a>
</div>
</nav>
</div>
</div>
</template>
<script lang="ts">
import {defineComponent, useAsync, useContext, useRouter, useStore} from "@nuxtjs/composition-api";
import {State} from "~/types/types";
export default defineComponent({
name: "SideMenu",
setup() {
const {$colorMode} = useContext()
const changeColorMode = () => {
$colorMode.preference = $colorMode.value === 'light' ? 'dark' : 'light'
}
const {i18n} = useContext()
const $router = useRouter()
const changeLanguage = () => useAsync(() => {
i18n.setLocale(i18n.locale === 'fr' ? 'en' : 'fr')
if ($router.currentRoute.fullPath.includes('blog') || $router.currentRoute.fullPath === '/') {
window.location.reload()
}
})
const isWindow = (loc: string) => {
if (loc === '') return $router.currentRoute.path === "/"
else return $router.currentRoute.path.includes(loc)
}
const store = useStore<State>()
const closeMenu = () => {
store.commit('TOGGLE_OPENED', false)
}
return {
isWindow,
changeColorMode,
changeLanguage,
closeMenu
}
}
})
</script>
<style lang="scss">
.cross:hover svg {
@apply transform scale-140;
}
.profile:hover img {
@apply transform rotate-360;
}
.nav-link a, .link-active a {
@apply duration-300 border-b-2 border-transparent;
&:hover {
@apply border-indigo-600;
}
}
.social-links a {
svg {
@apply h-8 w-8 duration-300
}
&:hover svg {
@apply transform hover:scale-120
}
}
</style>

View File

@@ -0,0 +1,18 @@
<template>
<svg class="inline" width="1.5em" height="1.5em" viewBox="0 0 15 15" focusable="false">
<g fill="none">
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M11.782 4.032a.575.575 0 1 0-.813-.814L7.5 6.687L4.032 3.218a.575.575 0 0 0-.814.814L6.687 7.5l-3.469 3.468a.575.575 0 0 0 .814.814L7.5 8.313l3.469 3.469a.575.575 0 0 0 .813-.814L8.313 7.5l3.469-3.468z"
fill="currentColor"
/>
</g>
</svg>
</template>
<script>
export default {
name: "CrossIcon"
}
</script>

View File

@@ -0,0 +1,13 @@
<template>
<svg class="inline" width="1.5em" height="1.5em" viewBox="0 0 24 24" focusable="false">
<path
d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515a.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0a12.64 12.64 0 0 0-.617-1.25a.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057a19.9 19.9 0 0 0 5.993 3.03a.078.078 0 0 0 .084-.028c.462-.63.874-1.295 1.226-1.994a.076.076 0 0 0-.041-.106a13.107 13.107 0 0 1-1.872-.892a.077.077 0 0 1-.008-.128a10.2 10.2 0 0 0 .372-.292a.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127a12.299 12.299 0 0 1-1.873.892a.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028a19.839 19.839 0 0 0 6.002-3.03a.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.956-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.955-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.946 2.418-2.157 2.418z"
fill="currentColor" />
</svg>
</template>
<script>
export default {
name: "DiscordIcon"
}
</script>

View File

@@ -0,0 +1,32 @@
<template>
<div>
<svg v-if="type === 0" class="inline" width="2.5em" height="2.5em" viewBox="0 0 24 24" focusable="false">
<g fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M3 12h18"/>
<path d="M3 6h18"/>
<path d="M3 18h18/"/>
</g>
</svg>
<svg v-else-if="type === 1" class="inline" width="2.5em" height="2.5em" viewBox="0 0 48 48" focusable="false">
<g fill="none" stroke="currentColor" stroke-width="4" stroke-linecap="round" stroke-linejoin="round">
<path
clip-rule="evenodd"
d="M44 22c0-9.941-8.954-18-20-18S4 12.059 4 22h40z" />
<path d="M4 38h40v6H4z" />
<path d="M4 28l5.455 4l7.272-4L24 32l7.273-4l7.272 4L44 28" />
</g>
</svg>
</div>
</template>
<script>
export default {
name: "MenuIcon",
props: {
type: {
type: Number,
default: 0
}
}
}
</script>

View File

@@ -0,0 +1,13 @@
<template>
<svg class="inline" width="1.5em" height="1.5em" viewBox="0 0 24 24" focusable="false">
<path
d="M19.125 0H4.875A4.865 4.865 0 0 0 0 4.875v14.25C0 21.825 2.175 24 4.875 24h6.6c2.7 0 4.875-2.175 4.875-4.875V16.65h2.775c2.7 0 4.875-2.175 4.875-4.875v-6.9C24 2.175 21.825 0 19.125 0zM16.5 1.275h2.625a3.6 3.6 0 0 1 3.6 3.6v2.7H16.5v-6.3zM15.075 9v6.45H8.85V9h6.225zM8.85 1.2h6.225v6.375H8.85V1.2zM1.275 4.8a3.6 3.6 0 0 1 3.6-3.6H7.5v6.375H1.275V4.8zM7.5 9v6.45H1.2V9h6.3zm0 13.725H4.8a3.6 3.6 0 0 1-3.6-3.6V16.8h6.3v5.925zm7.575-3.525a3.6 3.6 0 0 1-3.6 3.6H8.85v-5.925h6.225V19.2zm7.65-7.35a3.6 3.6 0 0 1-3.6 3.6H16.5V9h6.225v2.85z"
fill="currentColor" />
</svg>
</template>
<script>
export default {
name: "PolyworkIcon"
}
</script>

View File

@@ -1,5 +1,5 @@
<template>
<svg class="inline" width="1em" height="1em" viewBox="0 0 24 24" focusable="false">
<svg class="inline" width="1.5em" height="1.5em" viewBox="0 0 24 24" focusable="false">
<path
d="M12.87 15.07l-2.54-2.51l.03-.03A17.52 17.52 0 0 0 14.07 6H17V4h-7V2H8v2H1v1.99h11.17C11.5 7.92 10.44 9.75 9 11.35C8.07 10.32 7.3 9.19 6.69 8h-2c.73 1.63 1.73 3.17 2.98 4.56l-5.09 5.02L4 19l5-5l3.11 3.11l.76-2.04zM18.5 10h-2L12 22h2l1.12-3h4.75L21 22h2l-4.5-12zm-2.62 7l1.62-4.33L19.12 17h-3.24z"
fill="currentColor"

View File

@@ -1,7 +1,52 @@
<template>
<div>
<Header />
<Nuxt class="z-10 pt-16 lg:pt-24 content"/>
<Footer />
<SideMenu />
<div
class="z-50 relative top-0 left-0 w-full duration-500 h-1/2"
:class="{'opened': opened}"
:style="`max-height: ${height}px`"
>
<div @click="closeMenu" class="z-50 bg-white dark:bg-black overflow-hidden overflow-x-hidden w-full min-w-screen" :class="{'cursor-pointer': opened}">
<Announcement />
<Header />
<Nuxt class="z-10 pt-16 lg:pt-24 content"/>
<Footer />
</div>
</div>
<MobileNavbar />
</div>
</template>
<script lang="ts">
import {computed, onMounted, ref, useStore} from "@nuxtjs/composition-api";
import {State} from "~/types/types";
export default {
setup() {
const store = useStore<State>()
const opened = computed(() => store.state.opened)
const closeMenu = () => {
store.commit('TOGGLE_OPENED', false)
}
const height = ref(0)
onMounted(() => {
height.value = window.innerHeight
})
return {
opened,
closeMenu,
height
}
}
}
</script>
<style scoped lang="scss">
.opened {
@apply rounded-lg overflow-hidden overflow-x-hidden transform scale-90 translate-x-9/12 transition-all duration-500;
}
</style>

View File

@@ -1,12 +1,18 @@
export default {
header: {
home: 'Home',
about: 'About',
blog: 'Blog',
contact: 'Contact',
projects: 'Projects',
env: 'Tools',
guestbook: 'Guestbook',
newsletter: 'Newsletter'
newsletter: 'Newsletter',
services: 'Services'
},
sidebar: {
close: 'Close the menu'
},
part: {

View File

@@ -1,12 +1,18 @@
export default {
header: {
home: 'Accueil',
about: 'A Propos',
blog: 'Blog',
contact: 'Contact',
projects: 'Projets',
env: 'Outils',
guestbook: "Livre d'or",
newsletter: 'Newsletter'
newsletter: 'Newsletter',
services: 'Services'
},
sidebar: {
close: 'Fermer le menu'
},
part: {

15
src/store/index.ts Executable file → Normal file
View File

@@ -0,0 +1,15 @@
import {GetterTree, MutationTree} from "vuex";
export const state = () => ({
opened: false
})
export type RootState = ReturnType<typeof state>
export const getters: GetterTree<RootState, RootState> = {
opened: state => state.opened,
}
export const mutations: MutationTree<RootState> = {
TOGGLE_OPENED: (state, opened: boolean) => (state.opened = opened),
}

1
types/index.d.ts vendored
View File

@@ -3,6 +3,7 @@ import VueI18n, {IVueI18n} from "vue-i18n";
import {ColorModeInstance} from "@nuxtjs/color-mode/types/color-mode";
import {NuxtApp} from "@nuxt/types/app";
import {NuxtStorage} from "@nuxtjs/universal-storage";
import {NuxtOptionsRouter} from "@nuxt/types/config/router";
declare module 'vue/types/vue' {

View File

@@ -69,4 +69,14 @@ interface NewsletterForm {
email: string
}
export { Form, InfoData, Skill, Experience, Formation, Post, Tag, Project, GuestbookForm, NewsletterForm }
interface Translation {
code: string,
english: string,
french: string
}
interface State {
opened: boolean
}
export { Form, InfoData, Skill, Experience, Formation, Post, Tag, Project, GuestbookForm, NewsletterForm, Translation, State }