Dark mode
This commit is contained in:
		
							parent
							
								
									b480885e7a
								
							
						
					
					
						commit
						0cd8eaf580
					
				| 
						 | 
				
			
			@ -1,5 +1,5 @@
 | 
			
		|||
<!DOCTYPE html>
 | 
			
		||||
<html lang="en" translate="no" class="dark h-full">
 | 
			
		||||
<html lang="en" translate="no" class="dark h-full bg-stone-900">
 | 
			
		||||
 | 
			
		||||
<head>
 | 
			
		||||
  <meta charset="UTF-8" />
 | 
			
		||||
| 
						 | 
				
			
			@ -8,7 +8,7 @@
 | 
			
		|||
  <title>N0mbers</title>
 | 
			
		||||
</head>
 | 
			
		||||
 | 
			
		||||
<body class="bg-stone-200 text-stone-900 dark:bg-stone-900 dark:text-stone-200 h-full">
 | 
			
		||||
<body class="bg-stone-300 text-stone-900 dark:bg-stone-900 dark:text-stone-200 h-full relative transition-colors">
 | 
			
		||||
  <div id="app" class="h-full"></div>
 | 
			
		||||
  <script type="module" src="/src/main.ts"></script>
 | 
			
		||||
</body>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										45
									
								
								src/App.vue
									
									
									
									
									
								
							
							
						
						
									
										45
									
								
								src/App.vue
									
									
									
									
									
								
							| 
						 | 
				
			
			@ -1,19 +1,34 @@
 | 
			
		|||
<script setup lang="ts">
 | 
			
		||||
import AppHeader from './components/AppHeader.vue'
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
  <div class="container flex overflow-hidden flex-col px-2 mx-auto max-w-md h-full">
 | 
			
		||||
    <AppHeader />
 | 
			
		||||
    <!-- keep relative for transition -->
 | 
			
		||||
    <div class="relative">
 | 
			
		||||
      <RouterView v-slot="{ Component, route }">
 | 
			
		||||
        <Transition :name="route.meta.transition">
 | 
			
		||||
          <Component
 | 
			
		||||
            :is="Component"
 | 
			
		||||
            :key="route.path" />
 | 
			
		||||
        </Transition>
 | 
			
		||||
      </RouterView>
 | 
			
		||||
  <div
 | 
			
		||||
    class="container flex overflow-hidden relative flex-col mx-auto max-w-md h-full border border-cyan-500">
 | 
			
		||||
    <div class="px-2 w-full h-full">
 | 
			
		||||
      <AppHeader class="" />
 | 
			
		||||
      <!-- keep relative for transition -->
 | 
			
		||||
      <div class="relative">
 | 
			
		||||
        <RouterView v-slot="{ Component, route }">
 | 
			
		||||
          <Transition :name="route.meta.transition">
 | 
			
		||||
            <Component
 | 
			
		||||
              :is="Component"
 | 
			
		||||
              :key="route.path" />
 | 
			
		||||
          </Transition>
 | 
			
		||||
        </RouterView>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
    <SideMenu />
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { onMounted } from 'vue'
 | 
			
		||||
 | 
			
		||||
import { darkMode, isDarkModeDefault } from '@/composables/settings'
 | 
			
		||||
 | 
			
		||||
import AppHeader from './components/AppHeader.vue'
 | 
			
		||||
import SideMenu from './components/SideMenu.vue'
 | 
			
		||||
 | 
			
		||||
onMounted(() => {
 | 
			
		||||
  darkMode.value = localStorage.getItem('n0_dark')
 | 
			
		||||
    ? localStorage.getItem('n0_dark') === 'true'
 | 
			
		||||
    : isDarkModeDefault()
 | 
			
		||||
})
 | 
			
		||||
</script>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3,14 +3,17 @@
 | 
			
		|||
    <RouterLink
 | 
			
		||||
      :class="{ 'opacity-0': isHome }"
 | 
			
		||||
      to="/"
 | 
			
		||||
      class="text-xl text-stone-400 hover:text-cyan-500 transition-opacity duration-200">
 | 
			
		||||
      class="text-xl hover:text-cyan-500 dark:text-stone-400 transition-opacity duration-200">
 | 
			
		||||
      <IconHouse />
 | 
			
		||||
    </RouterLink>
 | 
			
		||||
    <h1 class="py-2 font-mono text-3xl text-center">
 | 
			
		||||
    <h1
 | 
			
		||||
      class="z-20 py-2 font-mono text-3xl text-center bg-stone-300 dark:bg-stone-900">
 | 
			
		||||
      N<span class="text-cyan-500">0</span>mbers
 | 
			
		||||
    </h1>
 | 
			
		||||
    <IconMenu
 | 
			
		||||
      class="text-xl text-stone-400 hover:text-cyan-500 transition-opacity" />
 | 
			
		||||
    <button @click="isSideMenuVisible = true">
 | 
			
		||||
      <IconMenu
 | 
			
		||||
        class="text-xl hover:text-cyan-500 dark:text-stone-400 transition-opacity" />
 | 
			
		||||
    </button>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -18,6 +21,7 @@
 | 
			
		|||
import { computed } from 'vue'
 | 
			
		||||
import { useRoute } from 'vue-router'
 | 
			
		||||
 | 
			
		||||
import { isSideMenuVisible } from '@/composables/side-menu'
 | 
			
		||||
import IconHouse from '~icons/ph/house'
 | 
			
		||||
import IconMenu from '~icons/ph/list'
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										44
									
								
								src/components/SideMenu.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								src/components/SideMenu.vue
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,44 @@
 | 
			
		|||
<template>
 | 
			
		||||
  <Transition name="menu">
 | 
			
		||||
    <div
 | 
			
		||||
      v-if="isSideMenuVisible"
 | 
			
		||||
      class="flex absolute top-[1px] right-0 z-10 flex-col w-[50%] h-full bg-stone-300 dark:bg-stone-900 border-l border-l-cyan-500">
 | 
			
		||||
      <div class="h-12">
 | 
			
		||||
        <button @click="isSideMenuVisible = false">
 | 
			
		||||
          <IconClose
 | 
			
		||||
            class="absolute top-0 right-1 h-12 text-xl hover:text-cyan-500" />
 | 
			
		||||
        </button>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div class="flex-1 ">
 | 
			
		||||
        <div class="h-12" />
 | 
			
		||||
        <InputSwitch
 | 
			
		||||
          id="toggleNight"
 | 
			
		||||
          lbl-left="Day"
 | 
			
		||||
          lbl-right="Night"
 | 
			
		||||
          v-model="darkMode" />
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  </Transition>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<style scoped>
 | 
			
		||||
.menu-move,
 | 
			
		||||
.menu-enter-active,
 | 
			
		||||
.menu-leave-active {
 | 
			
		||||
  transition: all 0.5s ease;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.menu-enter-from,
 | 
			
		||||
.menu-leave-to {
 | 
			
		||||
  /* opacity: 0; */
 | 
			
		||||
  transform: translateX(100%);
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { darkMode } from '@/composables/settings'
 | 
			
		||||
import { isSideMenuVisible } from '@/composables/side-menu'
 | 
			
		||||
import IconClose from '~icons/ph/x'
 | 
			
		||||
 | 
			
		||||
import InputSwitch from './common/InputSwitch.vue'
 | 
			
		||||
</script>
 | 
			
		||||
							
								
								
									
										46
									
								
								src/components/common/InputSwitch.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								src/components/common/InputSwitch.vue
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,46 @@
 | 
			
		|||
<template>
 | 
			
		||||
  <!-- Toggle A -->
 | 
			
		||||
  <div class="flex justify-center items-center mb-12 w-full">
 | 
			
		||||
    <label
 | 
			
		||||
      :for="id"
 | 
			
		||||
      class="flex items-center cursor-pointer">
 | 
			
		||||
      <div>{{ lblLeft }}</div>
 | 
			
		||||
      <!-- toggle -->
 | 
			
		||||
      <div class="relative mx-2 ml-3">
 | 
			
		||||
        <!-- input -->
 | 
			
		||||
        <input
 | 
			
		||||
          :checked="modelValue"
 | 
			
		||||
          @change="e => emit('update:modelValue', (e?.target as any)?.checked)"
 | 
			
		||||
          :id="id"
 | 
			
		||||
          type="checkbox"
 | 
			
		||||
          class="sr-only">
 | 
			
		||||
        <!-- line -->
 | 
			
		||||
        <div class="w-10 h-4 bg-cyan-500 rounded-full shadow-inner" />
 | 
			
		||||
        <!-- dot -->
 | 
			
		||||
        <div
 | 
			
		||||
          class="absolute -top-1 -left-1 w-6 h-6 bg-stone-400 rounded-full shadow transition dot" />
 | 
			
		||||
      </div>
 | 
			
		||||
      <!-- label -->
 | 
			
		||||
      <div>{{ lblRight }}</div>
 | 
			
		||||
    </label>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
<style scoped>
 | 
			
		||||
/* Toggle A */
 | 
			
		||||
input:checked ~ .dot {
 | 
			
		||||
  transform: translateX(100%);
 | 
			
		||||
  /* background-color: #48bb78; */
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
defineProps<{
 | 
			
		||||
  modelValue: boolean
 | 
			
		||||
  id: string
 | 
			
		||||
  lblLeft: string
 | 
			
		||||
  lblRight: string
 | 
			
		||||
}>()
 | 
			
		||||
const emit = defineEmits<{
 | 
			
		||||
  (e: 'update:modelValue', value: boolean): void
 | 
			
		||||
}>()
 | 
			
		||||
</script>
 | 
			
		||||
							
								
								
									
										20
									
								
								src/composables/settings.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								src/composables/settings.ts
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,20 @@
 | 
			
		|||
import { ref, watch } from 'vue'
 | 
			
		||||
 | 
			
		||||
export const darkMode = ref(true)
 | 
			
		||||
 | 
			
		||||
watch(darkMode, val => {
 | 
			
		||||
  if (val) {
 | 
			
		||||
    document.documentElement.classList.add('dark')
 | 
			
		||||
  }
 | 
			
		||||
  else {
 | 
			
		||||
    document.documentElement.classList.remove('dark')
 | 
			
		||||
  }
 | 
			
		||||
  localStorage.setItem('n0_dark', val ? 'true' : 'false')
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
export function isDarkModeDefault(): boolean {
 | 
			
		||||
  return (
 | 
			
		||||
    window.matchMedia &&
 | 
			
		||||
    window.matchMedia('(prefers-color-scheme: dark)').matches
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										3
									
								
								src/composables/side-menu.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								src/composables/side-menu.ts
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,3 @@
 | 
			
		|||
import { ref } from 'vue'
 | 
			
		||||
 | 
			
		||||
export const isSideMenuVisible = ref(false)
 | 
			
		||||
| 
						 | 
				
			
			@ -142,10 +142,12 @@
 | 
			
		|||
  opacity: 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@layer utils {
 | 
			
		||||
}
 | 
			
		||||
@layer components {
 | 
			
		||||
  .btn {
 | 
			
		||||
    @apply w-fit p-2;
 | 
			
		||||
    @apply rounded border border-stone-600 transition-opacity;
 | 
			
		||||
    @apply hover:border-cyan-500 hover:text-cyan-500;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,3 +1,13 @@
 | 
			
		|||
{
 | 
			
		||||
 | 
			
		||||
  "playAgain": "New number",
 | 
			
		||||
  "easy": "easy",
 | 
			
		||||
  "dailyGame": "Daily challenge\n",
 | 
			
		||||
  "endGame.failureLabel": "Too bad...<br>Another game?",
 | 
			
		||||
  "endGame.victoryLabel": "Well done!",
 | 
			
		||||
  "hard": "hard",
 | 
			
		||||
  "impossible": "☠",
 | 
			
		||||
  "medium": "medium",
 | 
			
		||||
  "randomGame": "Random number",
 | 
			
		||||
  "startGame": "Start",
 | 
			
		||||
  "dailyDescription": "The daily challenge changes every day at midnight, and is common to all players."
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,6 +1,6 @@
 | 
			
		|||
{
 | 
			
		||||
  "playAgain": "Nouveau nombre",
 | 
			
		||||
  "endGame.victoryLabel": "Le compte est bon !",
 | 
			
		||||
  "endGame.victoryLabel": "Bien joué !",
 | 
			
		||||
  "endGame.failureLabel": "Dommage...<br>Une autre partie ?",
 | 
			
		||||
  "startGame": "Démarrer",
 | 
			
		||||
  "easy": "facile",
 | 
			
		||||
| 
						 | 
				
			
			@ -8,5 +8,8 @@
 | 
			
		|||
  "hard": "difficile",
 | 
			
		||||
  "impossible": "☠",
 | 
			
		||||
  "dailyGame": "Défi quotidien",
 | 
			
		||||
  "randomGame": "Nombre au hasard"
 | 
			
		||||
  "randomGame": "Nombre au hasard",
 | 
			
		||||
  "gameDescription": "Combinez les nombres imposés afin d'atteindre le résultat, ou de vous en         approcher le plus possible.",
 | 
			
		||||
  "dailyDescription": "Le défi quotidien change chaque jour à minuit, et est commun à tous les joueurs.",
 | 
			
		||||
  "randomDescription": "Une partie au hasard, pour le plaisir."
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,9 +1,9 @@
 | 
			
		|||
import { createRouter, createWebHistory } from 'vue-router'
 | 
			
		||||
import { createRouter, createWebHashHistory, createWebHistory } from 'vue-router'
 | 
			
		||||
 | 
			
		||||
import HomeView from '../views/HomeView.vue'
 | 
			
		||||
 | 
			
		||||
const router = createRouter({
 | 
			
		||||
  history: createWebHistory(import.meta.env.BASE_URL),
 | 
			
		||||
  history: createWebHashHistory(import.meta.env.BASE_URL),
 | 
			
		||||
  routes: [
 | 
			
		||||
    {
 | 
			
		||||
      path: '/',
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -12,8 +12,7 @@
 | 
			
		|||
        class="text-center"
 | 
			
		||||
        v-if="gameState <= GameState.Waiting">
 | 
			
		||||
        <div v-if="isDaily">
 | 
			
		||||
          Le défi quotidien vous propose un résultat différent chaque jour,
 | 
			
		||||
          commun à tous les joueurs.<br>
 | 
			
		||||
          {{ t('dailyDescription') }}<br>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <button
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,8 +2,7 @@
 | 
			
		|||
  <div class="flex flex-col grow p-1 w-full text-center">
 | 
			
		||||
    <div class="flex flex-col grow gap-16 justify-center md:mt-16">
 | 
			
		||||
      <div>
 | 
			
		||||
        Combinez les nombres imposés afin d'atteindre le résultat, ou de vous en
 | 
			
		||||
        approcher le plus possible.<br>
 | 
			
		||||
        {{ t('gameDescription') }}<br>
 | 
			
		||||
        <br>
 | 
			
		||||
      </div>
 | 
			
		||||
      <!-- Daily -->
 | 
			
		||||
| 
						 | 
				
			
			@ -13,9 +12,8 @@
 | 
			
		|||
          class="text-2xl btn">
 | 
			
		||||
          {{ t('dailyGame') }}
 | 
			
		||||
        </RouterLink>
 | 
			
		||||
        <span>
 | 
			
		||||
          Un nombre à atteindre, unique pour la journée,<br>commun à tous les
 | 
			
		||||
          joueurs
 | 
			
		||||
        <span v-html="t('dailyDescription')">
 | 
			
		||||
 | 
			
		||||
        </span>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -26,7 +24,7 @@
 | 
			
		|||
          class="text-2xl btn">
 | 
			
		||||
          {{ t('randomGame') }}
 | 
			
		||||
        </RouterLink>
 | 
			
		||||
        Une partie au hasard, pour le plaisir
 | 
			
		||||
        {{ t('randomDescription') }}
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue
	
	Block a user