diff --git a/bun.lock b/bun.lock index 485550a..f062eca 100644 --- a/bun.lock +++ b/bun.lock @@ -1,6 +1,5 @@ { "lockfileVersion": 1, - "configVersion": 0, "workspaces": { "": { "name": "vue-boilerplate", @@ -8,6 +7,7 @@ "@tailwindcss/vite": "^4.3.1", "daisyui": "^5.5.23", "vue": "^3.5.38", + "vue-i18n": "11", }, "devDependencies": { "@tsconfig/node24": "^24.0.4", @@ -171,6 +171,14 @@ "@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="], + "@intlify/core-base": ["@intlify/core-base@11.4.5", "", { "dependencies": { "@intlify/devtools-types": "11.4.5", "@intlify/message-compiler": "11.4.5", "@intlify/shared": "11.4.5" } }, "sha512-lja3F/iKVIvTa48mIwmrIeDcQUFZ0F0drvFvT8AwINOvbwnAzl/S/p8p2DxILZpWEUHRi1qewfWNIkMvhD3kKA=="], + + "@intlify/devtools-types": ["@intlify/devtools-types@11.4.5", "", { "dependencies": { "@intlify/core-base": "11.4.5", "@intlify/shared": "11.4.5" } }, "sha512-W5vydP9Yq3t82IyWqCM6aR0BTWCZrN5RAwjZEPpH8I2OQWp2RLy03Evh2ANZlSMhcvGAoyDg25k0so85Kwncpw=="], + + "@intlify/message-compiler": ["@intlify/message-compiler@11.4.5", "", { "dependencies": { "@intlify/shared": "11.4.5", "source-map-js": "^1.0.2" } }, "sha512-IEOZiHtbQopyPc/Dz2M869lOlZYX1SdcniNJwphATDYHhovvIneEKf1EFF37DE7NAABZtza1FNtnwwqZWInfpw=="], + + "@intlify/shared": ["@intlify/shared@11.4.5", "", {}, "sha512-g/i5mtdUa9ia/8BaJ4w6ZRHgAXYQd9XyCaQPRMvsd8d5qmZwkjoTmHrNsI28Q/7I8h+2ijUkI4uEnnMCziKupQ=="], + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], @@ -383,6 +391,8 @@ "@vue/compiler-ssr": ["@vue/compiler-ssr@3.5.38", "", { "dependencies": { "@vue/compiler-dom": "3.5.38", "@vue/shared": "3.5.38" } }, "sha512-7s+W5Gc42FGxZMcuwl8H5B29T8BJPMdBT7KHFE+BbAuZ/iTEdTtv7z2XiMjiaUUw4w3ZcCEdHs36RuYJ2VA7bA=="], + "@vue/devtools-api": ["@vue/devtools-api@6.6.4", "", {}, "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g=="], + "@vue/devtools-core": ["@vue/devtools-core@8.1.3", "", { "dependencies": { "@vue/devtools-kit": "^8.1.3", "@vue/devtools-shared": "^8.1.3" }, "peerDependencies": { "vue": "^3.0.0" } }, "sha512-xezkv5/CPH/o5C8PE2Len9MnTJMsctYYQbKbbUiNOJpKd+fRHj27nKDb/sbtYI8NSQduegeQhCJGKRgAiOV6Uw=="], "@vue/devtools-kit": ["@vue/devtools-kit@8.1.3", "", { "dependencies": { "@vue/devtools-shared": "^8.1.3", "birpc": "^2.6.1", "hookable": "^5.5.3", "perfect-debounce": "^2.0.0" } }, "sha512-cRn7GXiCQkMYU2Z3h3pM4YO/ndbx9FY1yLDAqIqPLcmIq4H6zAOJHein6tvZU3AfPwgrodqLiPBEF+YQaS8AxA=="], @@ -833,6 +843,8 @@ "vue-eslint-parser": ["vue-eslint-parser@10.4.0", "", { "dependencies": { "debug": "^4.4.0", "eslint-scope": "^8.2.0 || ^9.0.0", "eslint-visitor-keys": "^4.2.0 || ^5.0.0", "espree": "^10.3.0 || ^11.0.0", "esquery": "^1.6.0", "semver": "^7.6.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0" } }, "sha512-Vxi9pJdbN3ZnVGLODVtZ7y4Y2kzAAE2Cm0CZ3ZDRvydVYxZ6VrnBhLikBsRS+dpwj4Jv4UCv21PTEwF5rQ9WXg=="], + "vue-i18n": ["vue-i18n@11.4.5", "", { "dependencies": { "@intlify/core-base": "11.4.5", "@intlify/devtools-types": "11.4.5", "@intlify/shared": "11.4.5", "@vue/devtools-api": "^6.5.0" }, "peerDependencies": { "vue": "^3.0.0" } }, "sha512-rm8YJ6RpjOrkcgS2GLrZwLvs/VbhxbTSuEspbyXDo233+fPK0OMFNLOj3fdQYVKdOgcpSfLW91JhbqgpkkcBWA=="], + "vue-tsc": ["vue-tsc@3.3.5", "", { "dependencies": { "@volar/typescript": "2.4.28", "@vue/language-core": "3.3.5" }, "peerDependencies": { "typescript": ">=5.0.0" }, "bin": { "vue-tsc": "bin/vue-tsc.js" } }, "sha512-Rzh/G2MmNlMSAMTiQEjDrsb4dgB/jbtEM47rVN2NtidF1dfb/q4w4QvpQBtW5+y3y5H27Hjh7deVwk+YB02fNg=="], "which": ["which@5.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ=="], diff --git a/package.json b/package.json index 2b4ecb3..c04d9e1 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,8 @@ "dependencies": { "@tailwindcss/vite": "^4.3.1", "daisyui": "^5.5.23", - "vue": "^3.5.38" + "vue": "^3.5.38", + "vue-i18n": "11" }, "devDependencies": { "@tsconfig/node24": "^24.0.4", diff --git a/src/App.vue b/src/App.vue index 95107d6..61d8d51 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,15 +1,11 @@ @@ -36,27 +26,40 @@ onMounted(() => { type="radio" name="app_tabs" class="tab" - :aria-label="lang === 'french' ? 'Aléatoire' : 'Random'" + :aria-label="$t('tab.random')" checked />
- +
- + + + + +
+ +
diff --git a/src/components/CoolDomain.vue b/src/components/CoolDomain.vue index 4e977b8..c96e4a1 100644 --- a/src/components/CoolDomain.vue +++ b/src/components/CoolDomain.vue @@ -1,16 +1,16 @@ diff --git a/src/components/PageHistory.vue b/src/components/PageHistory.vue new file mode 100644 index 0000000..f6fe9bd --- /dev/null +++ b/src/components/PageHistory.vue @@ -0,0 +1,30 @@ + + + diff --git a/src/components/PageInput.vue b/src/components/PageInput.vue index d5fce47..3b771fc 100644 --- a/src/components/PageInput.vue +++ b/src/components/PageInput.vue @@ -2,15 +2,19 @@ import { ref } from "vue"; import type { DomainFactory } from "@/lib/factory"; import CoolDomain from "./CoolDomain.vue"; +import type { Domain } from "@/types"; +import { isDomainAvailable } from "@/lib/whois"; -const props = defineProps<{ lang: string; factory: DomainFactory }>(); +const props = defineProps<{ factory: DomainFactory }>(); const input = ref(""); -const domains = ref([]); +const domains = ref([]); +const noDomain = ref(false); function start() { + noDomain.value = false; + domains.value.splice(0, domains.value.length); if (input.value.length) { - domains.value.splice(0, domains.value.length); newDomain(); } } @@ -18,15 +22,17 @@ function start() { function newDomain() { const startInput = input.value; props.factory - .nextFromWord(input.value) - .then((domain) => { + .allFromWord(input.value) + .then((newDomains: Domain[]) => { if (input.value === startInput) { - domains.value.push(domain); - newDomain(); + domains.value.push(...newDomains); + domains.value.forEach((domain) => void isDomainAvailable(domain)); } }) .catch(() => { - // TODO handle 0 domains + if (input.value === startInput && !domains.value.length) { + noDomain.value = true; + } }); } @@ -37,25 +43,34 @@ function newDomain() { -

+

+   +  {{ $t("seems") }} + {{ $t("available") }} {{ $t("is_unavailable") }} +

+

+ {{ $t("page_input.not_found") }}

diff --git a/src/components/PageRandom.vue b/src/components/PageRandom.vue index 516e227..74e56c0 100644 --- a/src/components/PageRandom.vue +++ b/src/components/PageRandom.vue @@ -3,6 +3,8 @@ import { ref, onBeforeMount, watch } from "vue"; import { isDomainAvailable } from "@/lib/whois"; import type { DomainFactory } from "@/lib/factory"; import CoolDomain from "./CoolDomain.vue"; +import type { Domain } from "@/types"; +import { useI18n } from "vue-i18n"; enum State { Loading, @@ -12,19 +14,20 @@ enum State { Error, } -const props = defineProps<{ lang: string; factory: DomainFactory }>(); +const props = defineProps<{ factory: DomainFactory }>(); -const domain = ref(null); -const domainAvailable = ref(false); +const domain = ref(null); const state = ref(State.Loading); +const i18n = useI18n(); + function newDomain() { if (!props.factory.error) { state.value = State.Generating; props.factory - .next(props.lang) - .then((value: string) => { - domain.value = value; + .next(i18n.locale.value) + .then((newDomain: Domain) => { + domain.value = newDomain; checkAvailability(); }) .catch((err: unknown) => { @@ -37,13 +40,13 @@ function newDomain() { function checkAvailability() { if (domain.value) { const promiseDomain = domain.value; + domain.value.searching = true; state.value = State.Fetching; - isDomainAvailable(promiseDomain) + isDomainAvailable(domain.value) .then((available: boolean) => { - if (domain.value !== promiseDomain) { + if (domain.value?.name !== promiseDomain.name) { return; // domain changed } - domainAvailable.value = available; if (!available) { newDomain(); } else { @@ -58,86 +61,58 @@ function checkAvailability() { } function getTextClass() { - if (!domain.value || domain.value.length < 15) { + if (!domain.value || domain.value.name.length < 15) { return "text-5xl"; } - if (domain.value.length < 20) { + if (domain.value.name.length < 20) { return "text-4xl"; } - if (domain.value.length < 25) { + if (domain.value.name.length < 25) { return "text-3xl"; } - if (domain.value.length < 30) { + if (domain.value.name.length < 30) { return "text-2xl"; } return "text-xl"; } -onBeforeMount(() => { - newDomain(); -}); -watch(() => props.lang, newDomain); +onBeforeMount(newDomain); +watch(i18n.locale, newDomain); diff --git a/src/i18n/en.json b/src/i18n/en.json new file mode 100644 index 0000000..7653850 --- /dev/null +++ b/src/i18n/en.json @@ -0,0 +1,27 @@ +{ + "tab": { + "random": "Random", + "input": "Input", + "history": "History" + }, + "this_domain": "This domain", + "is_unavailable": "is not available.", + "seems": "seems", + "available": "available", + "error": "An error has occured, please reload the page.", + "page_random": { + "state": { + "loading": "Loading the Cool Domain Factory™", + "generating": "Generating a new cool domain", + "fetching": "Checking domain availability" + } + }, + "page_input": { + "not_found": "No matching domain found.", + "placeholder": "Type a word or a phrase" + }, + "button": { + "new_domain": "New domain", + "generate": "Generate" + } +} diff --git a/src/i18n/fr.json b/src/i18n/fr.json new file mode 100644 index 0000000..4af8464 --- /dev/null +++ b/src/i18n/fr.json @@ -0,0 +1,27 @@ +{ + "tab": { + "random": "Aléatoire", + "input": "Saisie", + "history": "Historique" + }, + "this_domain": "Ce domaine", + "is_unavailable": "n'est pas disponible.", + "seems": "semble être", + "available": "disponible", + "error": "Une erreur est survenue, merci de recharger la page.", + "page_random": { + "state": { + "loading": "Chargement de la Cool Domain Factory™", + "generating": "Création d'un domaine cool", + "fetching": "Vérification de la disponibilité" + } + }, + "page_input": { + "not_found": "Aucun domaine correspondant.", + "placeholder": "Écrit un mot ou une phrase." + }, + "button": { + "new_domain": "Nouveau domaine", + "generate": "Génerer" + } +} diff --git a/src/lib/factory.ts b/src/lib/factory.ts index 05c2dd7..4f22bba 100644 --- a/src/lib/factory.ts +++ b/src/lib/factory.ts @@ -2,6 +2,7 @@ import rawTldList from "@/data/tld.txt?raw"; import englishWords from "@/data/english.txt?raw"; import frenchWords from "@/data/french.txt?raw"; import { randomElement, shuffled } from "./random"; +import type { Domain } from "@/types"; const TLD_LIST: string[] = rawTldList.trim().split("\n"); @@ -9,10 +10,11 @@ export class DomainFactory { error: string | null = null; words: Record = {}; generated: string[] = []; + history: Domain[] = []; init() { - this.words.french = this.initWords(frenchWords); - this.words.english = this.initWords(englishWords); + this.words.fr = this.initWords(frenchWords); + this.words.en = this.initWords(englishWords); } escapeWord(word: string): string { @@ -58,7 +60,7 @@ export class DomainFactory { return null; } - next(lang: string): Promise { + next(lang: string): Promise { return new Promise((resolve, reject) => { let k = 10000; while (k > 0) { @@ -66,7 +68,9 @@ export class DomainFactory { const word = this.randomWord(lang); const domain = this.randomDomain(word); if (domain) { - resolve(domain); + const output = { name: domain, searching: false, available: false }; + this.history.push(output); + resolve(output); return; } } @@ -75,18 +79,27 @@ export class DomainFactory { }); } - nextFromWord(word: string): Promise { + allFromWord(word: string): Promise { const escapedWord = this.escapeWord(word.replace(/ /gm, "-")); return new Promise((resolve, reject) => { - let k = 10000; - while (k > 0) { - k--; - const domain = this.randomDomain(escapedWord); - if (domain) { - resolve(domain); - return; + const domains = []; + for (const tld of shuffled(TLD_LIST.slice())) { + const tldEscaped: string = tld.replace(".", ""); + if ( + escapedWord.endsWith(tldEscaped) && + escapedWord.length >= tldEscaped.length + 2 + ) { + const domain = `${escapedWord.substring(0, escapedWord.length - tldEscaped.length)}.${tld}`; + if (!domain.includes("-.")) { + const output = { name: domain, searching: false, available: false }; + this.history.push(output); + domains.push(output); + } } } + if (domains.length) { + resolve(domains); + } reject(new Error("Could not generate a valid domain")); }); } diff --git a/src/lib/whois.ts b/src/lib/whois.ts index 503d675..b47bec3 100644 --- a/src/lib/whois.ts +++ b/src/lib/whois.ts @@ -3,6 +3,7 @@ import type { WhoIsData, WhoIsEvent, WhoIsIpAddresses, + Domain, } from "@/types"; function checkEvents(events: WhoIsEvent[]): boolean { @@ -26,14 +27,17 @@ function checkWhoIsData(data: WhoIsData): boolean { ); } -export async function isDomainAvailable(domain: string): Promise { +export async function isDomainAvailable(domain: Domain): Promise { + domain.searching = true; try { const response = await fetch( - `https://whois.klemek.fr/api/lookup/${domain}`, + `https://whois.klemek.fr/api/lookup/${domain.name}`, ); const content: WhoIsResponse = await response.json(); - return checkWhoIsData(content.data); + domain.available = checkWhoIsData(content.data); } catch { - return false; + domain.available = false; } + domain.searching = false; + return domain.available; } diff --git a/src/main.ts b/src/main.ts index b670de8..f640e7a 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,4 +1,27 @@ import { createApp } from "vue"; +import { createI18n } from "vue-i18n"; import App from "./App.vue"; +import en from "./i18n/en.json"; +import fr from "./i18n/fr.json"; -createApp(App).mount("#app"); +function getLang() { + return document.location.hash === "#french" ? "fr" : "en"; +} + +const i18n = createI18n({ + locale: getLang(), + fallbackLocale: "en", + messages: { + en: en, + fr: fr, + }, +}); + +setInterval(() => { + const newLang = getLang(); + if (newLang !== i18n.global.locale) { + i18n.global.locale = newLang; + } +}, 100); + +createApp(App).use(i18n).mount("#app"); diff --git a/src/types.ts b/src/types.ts index e7136e3..e1111f5 100644 --- a/src/types.ts +++ b/src/types.ts @@ -31,3 +31,9 @@ export interface WhoIsEntity { roles: string[]; vcardArray: [string, [string, object, string, string][]]; } + +export interface Domain { + name: string; + available: boolean; + searching: boolean; +}