mirror of
https://github.com/CorentinTh/it-tools.git
synced 2025-05-11 16:41:36 -04:00
feat(new tools): Dice Roller, Cin Flipper, Card Picker and Fortune Wheel
Fix #1493 and #28
This commit is contained in:
parent
08d977b8cd
commit
c9dd5b9e11
19 changed files with 7250 additions and 8482 deletions
63
src/tools/card-picker/card-picker.vue
Normal file
63
src/tools/card-picker/card-picker.vue
Normal file
|
@ -0,0 +1,63 @@
|
|||
<script setup lang="ts">
|
||||
import cards from '@younestouati/playing-cards-standard-deck';
|
||||
import { computedRefreshable } from '@/composable/computedRefreshable';
|
||||
import { useCopy } from '@/composable/copy';
|
||||
import { useQueryParamOrStorage } from '@/composable/queryParams';
|
||||
import { randIntFromInterval } from '@/utils/random';
|
||||
|
||||
const cardKeys = Object.keys(cards);
|
||||
type CardNames = keyof typeof cards;
|
||||
|
||||
const numberOfCards = useQueryParamOrStorage({ name: 'cards', storageName: 'card-picker:n', defaultValue: 5 });
|
||||
const [cardPicked, refreshCardPicked] = computedRefreshable(() =>
|
||||
Array.from({ length: numberOfCards.value },
|
||||
() => cardKeys[randIntFromInterval(0, 51)]));
|
||||
|
||||
const suitNames = {
|
||||
c: 'clubs (♣)',
|
||||
d: 'diamonds (♦)',
|
||||
h: 'hearts (♥)',
|
||||
s: 'spades (♠)',
|
||||
};
|
||||
const numberNames = {
|
||||
1: 'Ace',
|
||||
11: 'Jack',
|
||||
12: 'Queen',
|
||||
13: 'King',
|
||||
};
|
||||
function translateName(cardId: string) {
|
||||
const [, number, suit] = /(\d+)([cdhs])/.exec(cardId) || [];
|
||||
if (!number && !suit) {
|
||||
return cardId;
|
||||
}
|
||||
return `${numberNames[number as never] || number} of ${suitNames[suit as never] || suit}`;
|
||||
}
|
||||
|
||||
const cardPickedString = computed(() => cardPicked.value.map(translateName).join(', '));
|
||||
|
||||
const { copy } = useCopy({ source: cardPickedString, text: 'Cards Picked copied to the clipboard' });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<c-card>
|
||||
<div mb-2 flex justify-center>
|
||||
<img v-for="(card, index) in cardPicked" :key="index" style="width:90px" mr-1 :src="`data:image/svg+xml;base64,${cards[card as CardNames]}`">
|
||||
</div>
|
||||
<div mb-2>
|
||||
<textarea-copyable :value="cardPickedString" readonly mb-1 />
|
||||
</div>
|
||||
<div flex justify-center gap-3>
|
||||
<n-form-item label="Number of cards:" label-placement="left">
|
||||
<n-input-number v-model:value="numberOfCards" min="1" placeholder="Width of the text" />
|
||||
</n-form-item>
|
||||
</div>
|
||||
<div flex justify-center gap-3>
|
||||
<c-button @click="copy()">
|
||||
Copy deck
|
||||
</c-button>
|
||||
<c-button @click="refreshCardPicked">
|
||||
Refresh deck
|
||||
</c-button>
|
||||
</div>
|
||||
</c-card>
|
||||
</template>
|
12
src/tools/card-picker/index.ts
Normal file
12
src/tools/card-picker/index.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
import { PlayCard } from '@vicons/tabler';
|
||||
import { defineTool } from '../tool';
|
||||
|
||||
export const tool = defineTool({
|
||||
name: 'Card Picker',
|
||||
path: '/card-picker',
|
||||
description: 'Generate a deck of playing cards',
|
||||
keywords: ['card', 'deck', 'picker'],
|
||||
component: () => import('./card-picker.vue'),
|
||||
icon: PlayCard,
|
||||
createdAt: new Date('2025-02-09'),
|
||||
});
|
46
src/tools/coin-flipper/coin-flipper.vue
Normal file
46
src/tools/coin-flipper/coin-flipper.vue
Normal file
|
@ -0,0 +1,46 @@
|
|||
<script setup lang="ts">
|
||||
import { computedRefreshable } from '@/composable/computedRefreshable';
|
||||
import { randIntFromInterval } from '@/utils/random';
|
||||
|
||||
const [coinFlip, refreshCoinFlip] = computedRefreshable(() => ({
|
||||
coin: randIntFromInterval(0, 10) % 2 === 0 ? 'Heads' : 'Tails',
|
||||
dt: Date.now(),
|
||||
}));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<Transition name="bounce" mode="out-in">
|
||||
<div :key="coinFlip.dt" flex items-center justify-center>
|
||||
<div flex items-center justify-center rounded-full outline style="width: 5em; height: 5em">
|
||||
{{ coinFlip.coin }}
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
<div mt-4 flex justify-center>
|
||||
<c-button @click="refreshCoinFlip">
|
||||
Re flip
|
||||
</c-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.bounce-enter-active {
|
||||
animation: bounce-in 0.5s;
|
||||
}
|
||||
.bounce-leave-active {
|
||||
animation: bounce-in 0.5s reverse;
|
||||
}
|
||||
@keyframes bounce-in {
|
||||
0% {
|
||||
transform: scale(0);
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.25);
|
||||
}
|
||||
100% {
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
</style>
|
12
src/tools/coin-flipper/index.ts
Normal file
12
src/tools/coin-flipper/index.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
import { Coin } from '@vicons/tabler';
|
||||
import { defineTool } from '../tool';
|
||||
|
||||
export const tool = defineTool({
|
||||
name: 'Coin Flipper',
|
||||
path: '/coin-flipper',
|
||||
description: 'Flip a coin',
|
||||
keywords: ['coin', 'flipper'],
|
||||
component: () => import('./coin-flipper.vue'),
|
||||
icon: Coin,
|
||||
createdAt: new Date('2025-02-09'),
|
||||
});
|
66
src/tools/dice-roller/dice-roller.vue
Normal file
66
src/tools/dice-roller/dice-roller.vue
Normal file
|
@ -0,0 +1,66 @@
|
|||
<script setup lang="ts">
|
||||
import { DiceRoll } from '@dice-roller/rpg-dice-roller';
|
||||
import { computedRefreshable } from '@/composable/computedRefreshable';
|
||||
import { useCopy } from '@/composable/copy';
|
||||
import { useQueryParamOrStorage } from '@/composable/queryParams';
|
||||
|
||||
const dicesNotations = useQueryParamOrStorage({ name: 'dices', storageName: 'dice-roll:dices', defaultValue: '3d6' });
|
||||
const [diceRollResult, refreshDiceRollResult] = computedRefreshable(() => {
|
||||
try {
|
||||
return {
|
||||
error: '',
|
||||
roll: new DiceRoll(dicesNotations.value),
|
||||
};
|
||||
}
|
||||
catch (e: any) {
|
||||
return {
|
||||
error: e.toString(),
|
||||
roll: null,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
const diceRollString = computed(() => diceRollResult.value.roll?.output || '');
|
||||
|
||||
const { copy } = useCopy({ source: diceRollString, text: 'Dice Roll copied to the clipboard' });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<c-card>
|
||||
<c-input-text
|
||||
v-model:value="dicesNotations"
|
||||
label="Dice Roll Notations:" label-position="left"
|
||||
placeholder="Dice configuration"
|
||||
mb-2
|
||||
/>
|
||||
<n-p mb-2>
|
||||
For more information about Dice Notation, see <n-a href="https://dice-roller.github.io/documentation/guide/notation/" target="_blank">
|
||||
here
|
||||
</n-a>
|
||||
</n-p>
|
||||
|
||||
<c-alert v-if="diceRollResult.error" mb-2>
|
||||
{{ diceRollResult.error }}
|
||||
</c-alert>
|
||||
|
||||
<c-card v-if="!diceRollResult.error" title="Roll Result" mb-2>
|
||||
<input-copyable :value="diceRollResult.roll?.output" readonly mb-1 />
|
||||
<input-copyable :value="diceRollResult.roll?.total" label="Total" readonly placeholder="Dice Roll total:" label-position="left" mb-1 />
|
||||
</c-card>
|
||||
|
||||
<div mb-2 flex justify-center gap-3>
|
||||
<c-button @click="copy()">
|
||||
Copy roll result
|
||||
</c-button>
|
||||
<c-button @click="refreshDiceRollResult">
|
||||
Refresh roll
|
||||
</c-button>
|
||||
</div>
|
||||
|
||||
<c-card v-if="!diceRollResult.error" title="Dice Notation Stats" mb-2>
|
||||
<input-copyable :value="diceRollResult.roll?.minTotal" readonly label="Min Total:" label-position="left" mb-1 />
|
||||
<input-copyable :value="diceRollResult.roll?.maxTotal" readonly label="Max Total:" label-position="left" mb-1 />
|
||||
<input-copyable :value="diceRollResult.roll?.averageTotal" readonly label="Avg Total:" label-position="left" mb-1 />
|
||||
</c-card>
|
||||
</c-card>
|
||||
</template>
|
12
src/tools/dice-roller/index.ts
Normal file
12
src/tools/dice-roller/index.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
import { Dice } from '@vicons/tabler';
|
||||
import { defineTool } from '../tool';
|
||||
|
||||
export const tool = defineTool({
|
||||
name: 'Dice Roller',
|
||||
path: '/dice-roller',
|
||||
description: 'RPG Dice Roller using Dice Notation',
|
||||
keywords: ['dice', 'rng', 'rpg', 'roller'],
|
||||
component: () => import('./dice-roller.vue'),
|
||||
icon: Dice,
|
||||
createdAt: new Date('2025-02-09'),
|
||||
});
|
11
src/tools/fortune-wheel/fortune-wheel.e2e.spec.ts
Normal file
11
src/tools/fortune-wheel/fortune-wheel.e2e.spec.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
import { expect, test } from '@playwright/test';
|
||||
|
||||
test.describe('Tool - Fortune wheel', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/fortune-wheel');
|
||||
});
|
||||
|
||||
test('Has correct title', async ({ page }) => {
|
||||
await expect(page).toHaveTitle('Fortune wheel - IT Tools');
|
||||
});
|
||||
});
|
85
src/tools/fortune-wheel/fortune-wheel.vue
Normal file
85
src/tools/fortune-wheel/fortune-wheel.vue
Normal file
|
@ -0,0 +1,85 @@
|
|||
<script setup lang="ts">
|
||||
import { FortuneWheel } from 'vue3-fortune-wheel';
|
||||
import type { Data, ImgParams } from 'vue3-fortune-wheel';
|
||||
import { useQueryParamOrStorage } from '@/composable/queryParams';
|
||||
import { computedRefreshable } from '@/composable/computedRefreshable';
|
||||
|
||||
const base = import.meta.env.BASE_URL;
|
||||
|
||||
const choices = useQueryParamOrStorage({ name: 'choices', storageName: 'fortune-wheel:chs', defaultValue: '' });
|
||||
const spinDuration = useQueryParamOrStorage({ name: 'duration', storageName: 'fortune-wheel:dur', defaultValue: 5 });
|
||||
|
||||
const colors = [
|
||||
{ bgColor: '#FF5733', color: '#FFFFFF' },
|
||||
{ bgColor: '#2E86C1', color: '#FFFFFF' },
|
||||
{ bgColor: '#28B463', color: '#FFFFFF' },
|
||||
{ bgColor: '#F1C40F', color: '#000000' },
|
||||
{ bgColor: '#8E44AD', color: '#FFFFFF' },
|
||||
{ bgColor: '#E74C3C', color: '#FFFFFF' },
|
||||
{ bgColor: '#1ABC9C', color: '#000000' },
|
||||
{ bgColor: '#34495E', color: '#FFFFFF' },
|
||||
{ bgColor: '#D35400', color: '#FFFFFF' },
|
||||
{ bgColor: '#2C3E50', color: '#FFFFFF' },
|
||||
];
|
||||
|
||||
const wheel = ref<InstanceType<typeof FortuneWheel> | null>(null);
|
||||
const data = computed(() => {
|
||||
return (choices.value || '').split('\n')
|
||||
.filter(s => s && s !== '')
|
||||
.map((choice, index) => ({ id: index + 1, value: choice, ...colors[index % colors.length] }));
|
||||
});
|
||||
|
||||
const logo: ImgParams = {
|
||||
src: `${base}logo.png`,
|
||||
width: 100,
|
||||
height: 100,
|
||||
};
|
||||
|
||||
const [randomGift, refreshGift] = computedRefreshable(() => {
|
||||
return Math.floor(Math.random() * data.value.length) + 1;
|
||||
});
|
||||
|
||||
const result = ref<Data>();
|
||||
function done(wheelResult: Data) {
|
||||
result.value = wheelResult;
|
||||
}
|
||||
|
||||
function restart() {
|
||||
refreshGift();
|
||||
wheel.value?.spin();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<n-form-item label="Spin Duration (seconds):" label-placement="left">
|
||||
<n-input-number v-model:value="spinDuration" :min="1" />
|
||||
</n-form-item>
|
||||
|
||||
<c-input-text
|
||||
v-model:value="choices"
|
||||
label="Wheel choices (one per line):"
|
||||
placeholder="Wheel choices (one per line)"
|
||||
multiline
|
||||
rows="5"
|
||||
/>
|
||||
|
||||
<n-divider />
|
||||
|
||||
<FortuneWheel
|
||||
ref="wheel"
|
||||
v-model="randomGift"
|
||||
middle-circle
|
||||
:img-params="logo"
|
||||
:anim-duration="spinDuration * 1000"
|
||||
:data="data"
|
||||
@done="done"
|
||||
/>
|
||||
|
||||
<div flex justify-center>
|
||||
<c-button @click="restart()">
|
||||
Restart
|
||||
</c-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
12
src/tools/fortune-wheel/index.ts
Normal file
12
src/tools/fortune-wheel/index.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
import { ArrowsShuffle } from '@vicons/tabler';
|
||||
import { defineTool } from '../tool';
|
||||
|
||||
export const tool = defineTool({
|
||||
name: 'Fortune wheel',
|
||||
path: '/fortune-wheel',
|
||||
description: '',
|
||||
keywords: ['fortune', 'wheel'],
|
||||
component: () => import('./fortune-wheel.vue'),
|
||||
icon: ArrowsShuffle,
|
||||
createdAt: new Date('2025-02-24'),
|
||||
});
|
|
@ -2,6 +2,10 @@ import { tool as base64FileConverter } from './base64-file-converter';
|
|||
import { tool as base64StringConverter } from './base64-string-converter';
|
||||
import { tool as basicAuthGenerator } from './basic-auth-generator';
|
||||
import { tool as emailNormalizer } from './email-normalizer';
|
||||
import { tool as fortuneWheel } from './fortune-wheel';
|
||||
import { tool as cardPicker } from './card-picker';
|
||||
import { tool as coinFlipper } from './coin-flipper';
|
||||
import { tool as diceRoller } from './dice-roller';
|
||||
|
||||
import { tool as asciiTextDrawer } from './ascii-text-drawer';
|
||||
|
||||
|
@ -168,7 +172,15 @@ export const toolsByCategory: ToolCategory[] = [
|
|||
},
|
||||
{
|
||||
name: 'Math',
|
||||
components: [mathEvaluator, etaCalculator, percentageCalculator],
|
||||
components: [
|
||||
mathEvaluator,
|
||||
etaCalculator,
|
||||
percentageCalculator,
|
||||
diceRoller,
|
||||
coinFlipper,
|
||||
cardPicker,
|
||||
fortuneWheel,
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Measurement',
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue