Sharing is ok
This commit is contained in:
+2
-8
@@ -16,18 +16,12 @@
|
||||
</div>
|
||||
<SideMenu />
|
||||
</div>
|
||||
<ToastMessage />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted } from 'vue'
|
||||
|
||||
import AppHeader from '@/components/AppHeader.vue'
|
||||
import SideMenu from '@/components/SideMenu.vue'
|
||||
import { darkMode } from '@/composables/settings'
|
||||
import { LSK_DARKMODE } from '@/globals'
|
||||
import { getItem } from '@/storage'
|
||||
import ToastMessage from '@/components/ToastMessage.vue'
|
||||
|
||||
onMounted(() => {
|
||||
darkMode.value = JSON.parse(getItem(LSK_DARKMODE, 'true'))
|
||||
})
|
||||
</script>
|
||||
|
||||
+4
-1
@@ -72,7 +72,10 @@ export function isSolvable(result: number, plaquettes: number[]): boolean {
|
||||
if (a < b) [a, b] = [b, a]
|
||||
const ops = shuffle([...operators])
|
||||
for (const op of ops) {
|
||||
if (op === '/' && a % b !== 0) continue
|
||||
// Avoid non-integer divisions, or divisions by 1
|
||||
if (op === '/' && (a % b !== 0 || b === 1)) continue
|
||||
// Avoid multiplications by 1
|
||||
if (op === '*' && (a === 1 || b === 1)) continue
|
||||
recursOperation(op, a, b, plaquettes, history)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,11 +8,10 @@
|
||||
</RouterLink>
|
||||
<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
|
||||
N<span class="text-cyan-500">0</span>mbers<span class="text-xs">beta</span>
|
||||
</h1>
|
||||
<button @click="isSideMenuVisible = true">
|
||||
<IconMenu
|
||||
class="text-xl btn" />
|
||||
<IconMenu class="text-xl btn" />
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -86,8 +86,8 @@ function canOperationBeDeleted(op: Operation): boolean {
|
||||
|
||||
function undoOperation(index: number): void {
|
||||
if (gameState.value !== GameState.Playing) return
|
||||
const l = operations.length
|
||||
for (let i = operations.length - 1; i >= index; --i) {
|
||||
const len = operations.length
|
||||
for (let i = len - 1; i >= index; --i) {
|
||||
let popped: Operation
|
||||
if (i === index) {
|
||||
popped = operations[index]
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<div class="flex flex-col flex-1 content-center items-center">
|
||||
<div class="h-12" />
|
||||
<InputSwitch
|
||||
class="mb-4 "
|
||||
class="mb-4"
|
||||
id="toggleNight"
|
||||
lbl-left="Day"
|
||||
lbl-right="Night"
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
<template>
|
||||
<Transition>
|
||||
<div
|
||||
v-if="toastVisible"
|
||||
class="fixed inset-x-0 top-4 z-20 p-3 mx-auto w-max text-center bg-stone-800 rounded border border-cyan-500"
|
||||
v-html="toastMessage" />
|
||||
</Transition>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.v-enter-active,
|
||||
.v-leave-active {
|
||||
transition: opacity 0.5s ease;
|
||||
}
|
||||
|
||||
.v-enter-from,
|
||||
.v-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { toastMessage, toastVisible } from '@/composables/toast-manager'
|
||||
</script>
|
||||
@@ -34,7 +34,7 @@ export function numberOfGamesSinceStart(): number {
|
||||
new Date(GAME_STARTING_DATE as string),
|
||||
BXL_TZ,
|
||||
)
|
||||
return differenceInDays(startDate, getCurrentDate())
|
||||
return differenceInDays(getCurrentDate(), startDate)
|
||||
}
|
||||
|
||||
export function clearOperationsList(): void {
|
||||
|
||||
+26
-17
@@ -1,20 +1,25 @@
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { ref, watch } from 'vue'
|
||||
|
||||
import { LSK_DARKMODE, LSK_LOCALE } from '@/globals'
|
||||
import { setItem } from '@/storage'
|
||||
import { i18n } from '@/i18n'
|
||||
import { getItem, setItem } from '@/storage'
|
||||
|
||||
// #region Dark Mode
|
||||
|
||||
export const darkMode = ref(true)
|
||||
watch(darkMode, val => {
|
||||
if (val) {
|
||||
document.documentElement.classList.add('dark')
|
||||
}
|
||||
else {
|
||||
document.documentElement.classList.remove('dark')
|
||||
}
|
||||
setItem(LSK_DARKMODE, val ? 'true' : 'false')
|
||||
})
|
||||
export const darkMode = ref(getItem(LSK_DARKMODE, 'true') === 'true')
|
||||
watch(
|
||||
darkMode,
|
||||
val => {
|
||||
if (val) {
|
||||
document.documentElement.classList.add('dark')
|
||||
}
|
||||
else {
|
||||
document.documentElement.classList.remove('dark')
|
||||
}
|
||||
setItem(LSK_DARKMODE, val ? 'true' : 'false')
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
export function isDarkModeDefault(): boolean {
|
||||
return (
|
||||
@@ -27,10 +32,14 @@ export function isDarkModeDefault(): boolean {
|
||||
|
||||
// #region Locale
|
||||
|
||||
export const isLocaleFrench = ref(true)
|
||||
|
||||
watch(isLocaleFrench, v => {
|
||||
setItem(LSK_LOCALE, v ? 'fr' : 'en')
|
||||
})
|
||||
export const isLocaleFrench = ref(getItem(LSK_LOCALE, 'en') === 'fr')
|
||||
watch(
|
||||
isLocaleFrench,
|
||||
v => {
|
||||
setItem(LSK_LOCALE, v ? 'fr' : 'en')
|
||||
i18n.global.locale.value = v ? 'fr' : 'en'
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
// #endregion Locale
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import { i18n } from '@/i18n'
|
||||
import { percentageDiff } from '@/utils'
|
||||
|
||||
import { numberOfGamesSinceStart, operations, result } from './game-state'
|
||||
import { showToast } from './toast-manager'
|
||||
|
||||
function getSharingText(): string {
|
||||
// × ÷ + -
|
||||
const emojis: string[] = []
|
||||
for (const op of operations) {
|
||||
switch (op.operator) {
|
||||
case '+':
|
||||
emojis.push('+')
|
||||
break
|
||||
case '-':
|
||||
emojis.push('-')
|
||||
break
|
||||
case '*':
|
||||
emojis.push('×')
|
||||
break
|
||||
case '/':
|
||||
emojis.push('÷')
|
||||
break
|
||||
}
|
||||
}
|
||||
const endResult = operations[operations.length - 1].result?.value ?? 0
|
||||
return `N0mbers #${numberOfGamesSinceStart()}
|
||||
===========
|
||||
${emojis.join(' ')} = ${endResult}
|
||||
Score: ${100 - percentageDiff(result.value, endResult) * 100}%
|
||||
===========
|
||||
https://n0mbers.scambier.xyz`
|
||||
}
|
||||
|
||||
export function toClipboard(): void {
|
||||
navigator.clipboard.writeText(getSharingText())
|
||||
showToast(i18n.global.t('copiedInClipboard'), 3000)
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { ref } from 'vue'
|
||||
|
||||
export const toastMessage = ref<string>()
|
||||
export const toastVisible = ref(false)
|
||||
const timeout = ref<NodeJS.Timeout>()
|
||||
|
||||
/**
|
||||
* Adds a toast to the list, that will be destroyed after `duration`
|
||||
* @param message
|
||||
* @param duration In milliseconds
|
||||
*/
|
||||
export function showToast(txt: string, duration: number): void {
|
||||
if (timeout.value) {
|
||||
clearTimeout(timeout.value)
|
||||
}
|
||||
// Set toast message
|
||||
toastMessage.value = txt
|
||||
// Show toast
|
||||
toastVisible.value = true
|
||||
// Hide toast after duration
|
||||
timeout.value = setTimeout(() => {
|
||||
toastVisible.value = false
|
||||
}, duration)
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import messages from '@intlify/vite-plugin-vue-i18n/messages'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
export const i18n = createI18n({
|
||||
legacy: false,
|
||||
locale: 'fr',
|
||||
fallbackLocale: 'en',
|
||||
messages,
|
||||
})
|
||||
+2
-2
@@ -59,7 +59,7 @@
|
||||
.route-move,
|
||||
.route-enter-active,
|
||||
.route-leave-active {
|
||||
transition: all 1s ease;
|
||||
transition: all 0.8s ease;
|
||||
}
|
||||
|
||||
.route-enter-from {
|
||||
@@ -82,7 +82,7 @@
|
||||
.route_back-move,
|
||||
.route_back-enter-active,
|
||||
.route_back-leave-active {
|
||||
transition: all 1s ease;
|
||||
transition: all 0.8s ease;
|
||||
}
|
||||
|
||||
.route_back-enter-from {
|
||||
|
||||
+6
-2
@@ -9,6 +9,10 @@
|
||||
"medium": "medium",
|
||||
"randomGame": "Random number",
|
||||
"startGame": "Start",
|
||||
"dailyDescription": "The daily challenge changes every day at midnight, and is common to all players.",
|
||||
"share": "Share"
|
||||
"dailyDescription": "The daily challenge changes every day at midnight (CET), and is common to all players.",
|
||||
"share": "Share",
|
||||
"finishDailyToPlayRandom": "Finish the daily challenge to unlock.",
|
||||
"gameDescription": "Combine the numbers to reach the result.",
|
||||
"randomDescription": "A challenge at random, just for fun.",
|
||||
"copiedInClipboard": "Copied in clipboard"
|
||||
}
|
||||
|
||||
+4
-3
@@ -9,9 +9,10 @@
|
||||
"impossible": "☠",
|
||||
"dailyGame": "Défi quotidien",
|
||||
"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.",
|
||||
"gameDescription": "Combinez les nombres imposés afin d'atteindre le résultat.",
|
||||
"dailyDescription": "Le défi quotidien change chaque jour à minuit (CET), et est commun à tous les joueurs.",
|
||||
"randomDescription": "Une partie au hasard, pour le plaisir.",
|
||||
"share": "Partager",
|
||||
"finishDailyToPlayRandom": "Terminez le défi quotidien pour débloquer."
|
||||
"finishDailyToPlayRandom": "Terminez le défi quotidien pour débloquer.",
|
||||
"copiedInClipboard": "Copié dans le presse-papier"
|
||||
}
|
||||
|
||||
+2
-19
@@ -1,29 +1,12 @@
|
||||
import './index.css'
|
||||
|
||||
import messages from '@intlify/vite-plugin-vue-i18n/messages'
|
||||
import { registerSW } from 'virtual:pwa-register'
|
||||
import { createApp, watch } from 'vue'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
import { createApp } from 'vue'
|
||||
|
||||
import App from '@/App.vue'
|
||||
import router from '@/router'
|
||||
|
||||
import { isLocaleFrench } from './composables/settings'
|
||||
import { LSK_LOCALE } from './globals'
|
||||
import { getItem } from './storage'
|
||||
|
||||
const i18n = createI18n({
|
||||
legacy: false,
|
||||
locale: 'fr',
|
||||
fallbackLocale: 'en',
|
||||
messages,
|
||||
})
|
||||
|
||||
// Set locale
|
||||
watch(isLocaleFrench, v => {
|
||||
i18n.global.locale.value = v ? 'fr' : 'en'
|
||||
})
|
||||
isLocaleFrench.value = getItem(LSK_LOCALE, 'en') === 'fr'
|
||||
import { i18n } from './i18n'
|
||||
|
||||
registerSW({
|
||||
onOfflineReady() {
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
import { hasPlayed } from './composables/statistics'
|
||||
import { getCurrentSessionKey } from './utils'
|
||||
|
||||
export function setItem(k: string, v: string): void {
|
||||
return localStorage.setItem(k, v)
|
||||
}
|
||||
|
||||
@@ -14,6 +14,16 @@ export function getCurrentSessionKey(): string {
|
||||
return localISOTime.slice(0, 10)
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param a
|
||||
* @param b
|
||||
* @returns Percentage rounded to 2 decimals
|
||||
*/
|
||||
export function percentageDiff(a: number, b: number): number {
|
||||
return Math.round(((a - b) / ((a + b) / 2)) * 100) / 100
|
||||
}
|
||||
|
||||
// #region RNG
|
||||
|
||||
export let random = Math.random
|
||||
|
||||
@@ -42,6 +42,7 @@
|
||||
</div>
|
||||
</Transition>
|
||||
|
||||
<!-- END GAME RESULT -->
|
||||
<Transition name="slide_up">
|
||||
<div
|
||||
v-if="isEndGame"
|
||||
@@ -60,7 +61,9 @@
|
||||
</button>
|
||||
<button
|
||||
v-else
|
||||
class="btn-border">
|
||||
@click="toClipboard"
|
||||
class="inline-flex items-center btn-border">
|
||||
<IconShare class="mr-2" />
|
||||
{{ t('share') }}
|
||||
</button>
|
||||
</div>
|
||||
@@ -94,6 +97,7 @@ import {
|
||||
plaquettes,
|
||||
result,
|
||||
} from '@/composables/game-state'
|
||||
import { toClipboard } from '@/composables/sharing'
|
||||
import { hasPlayed } from '@/composables/statistics'
|
||||
import { GameState, pools } from '@/globals'
|
||||
import { OperatorType, Plaquette } from '@/types'
|
||||
@@ -105,6 +109,7 @@ import {
|
||||
setDailyPRNG,
|
||||
setMathPRNG,
|
||||
} from '@/utils'
|
||||
import IconShare from '~icons/ph/share-network'
|
||||
|
||||
const { t } = useI18n() // call `useI18n`, and spread `t` from `useI18n` returning
|
||||
|
||||
@@ -113,7 +118,7 @@ const { t } = useI18n() // call `useI18n`, and spread `t` from `useI18n` return
|
||||
*/
|
||||
|
||||
const difficultyLevel = ref<1 | 2>(1)
|
||||
const cmpPlaquettes = ref<InstanceType<typeof PlaquettesList>>()
|
||||
const cmpPlaquettes = ref<InstanceType<typeof PlaquettesList> | null>(null)
|
||||
|
||||
const isDaily = computed(() => useRoute()?.name === 'daily')
|
||||
const shownPlaquettes = computed(() =>
|
||||
|
||||
Reference in New Issue
Block a user