Compare commits

...
8 Commits
Author SHA1 Message Date
klemek e956731642 feat: save ignored tlds in cookies
Lint / TypeScript (push) Successful in 4m31s
Lint / Oxlint (push) Successful in 4m31s
Lint / ESLint (push) Successful in 4m32s
Deploy / Build (push) Successful in 5m16s
Deploy / Deploy to Stapler (push) Successful in 5m24s
2026-06-26 14:47:20 +02:00
klemek e883073ea7 fix: tld-list link with correct extension
Lint / Oxlint (push) Successful in 3m57s
Deploy / Build (push) Successful in 4m14s
Lint / TypeScript (push) Successful in 3m57s
Lint / ESLint (push) Successful in 4m7s
Deploy / Deploy to Stapler (push) Successful in 6m5s
2026-06-26 14:28:22 +02:00
klemek e5ab509c29 feat: whois timeout
Lint / Oxlint (push) Successful in 50s
Lint / ESLint (push) Successful in 58s
Lint / TypeScript (push) Successful in 58s
Deploy / Build (push) Successful in 1m4s
Deploy / Deploy to Stapler (push) Successful in 7m9s
2026-06-26 14:16:33 +02:00
klemek e40a082c2a performances: async loading
Lint / Oxlint (push) Successful in 52s
Lint / TypeScript (push) Successful in 55s
Lint / ESLint (push) Successful in 59s
Deploy / Build (push) Successful in 1m3s
Deploy / Deploy to Stapler (push) Successful in 6m4s
2026-06-26 11:53:44 +02:00
klemek c41bdbec06 fix: factory ext
Lint / Oxlint (push) Successful in 7m41s
Lint / TypeScript (push) Successful in 7m41s
Deploy / Build (push) Successful in 8m6s
Lint / ESLint (push) Successful in 8m5s
Deploy / Deploy to Stapler (push) Successful in 3m58s
2026-06-26 11:06:51 +02:00
klemek ecf427b1e2 feat: dont generate xxx domains
Lint / Oxlint (push) Successful in 46s
Lint / TypeScript (push) Successful in 46s
Lint / ESLint (push) Successful in 1m19s
Deploy / Build (push) Successful in 1m19s
Deploy / Deploy to Stapler (push) Successful in 5m30s
2026-06-26 10:58:22 +02:00
klemek a6a07b7001 fix: restore all tlds 2026-06-26 10:58:09 +02:00
klemek 7f56c3d876 fix: vuejs warn 2026-06-26 10:58:00 +02:00
12 changed files with 241 additions and 152 deletions
+15 -30
View File
@@ -1,15 +1,20 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted, onBeforeMount } from "vue"; import { ref, onMounted, defineAsyncComponent, onBeforeMount } from "vue";
import PageRandom from "@/components/PageRandom.vue"; const PageRandom = defineAsyncComponent(() => import("@/components/PageRandom.vue"));
import PageInput from "@/components/PageInput.vue"; const PageInput = defineAsyncComponent(() => import("@/components/PageInput.vue"));
const PageHistory = defineAsyncComponent(() => import("./components/PageHistory.vue"));
import { DomainFactory } from "@/lib/factory"; import { DomainFactory } from "@/lib/factory";
import PageHistory from "./components/PageHistory.vue"; import { getCookie, IGNORED_TLDS_COOKIE } from "@/lib/cookies";
const visible = ref<boolean>(false); const visible = ref<boolean>(false);
const factory = ref<DomainFactory>(new DomainFactory()); const factory = ref<DomainFactory>(new DomainFactory());
onBeforeMount(() => { onBeforeMount(() => {
factory.value.init(); factory.value.ignoreTlds(
...getCookie(IGNORED_TLDS_COOKIE)
.split(".")
.filter((tld: string) => tld.length),
);
}); });
onMounted(() => { onMounted(() => {
@@ -22,37 +27,17 @@ onMounted(() => {
<template> <template>
<div :style="{ display: visible ? 'inherit' : 'none' }"> <div :style="{ display: visible ? 'inherit' : 'none' }">
<div class="tabs tabs-xs tabs-bottom w-xl max-w-svw shrink-0"> <div class="tabs tabs-xs tabs-bottom w-xl max-w-svw shrink-0">
<input <input type="radio" name="app_tabs" class="tab" :aria-label="$t('tab.random')" checked />
type="radio" <div class="tab-content bg-base-100 border-base-300 shadow-2xl p-6 overflow-hidden">
name="app_tabs"
class="tab"
:aria-label="$t('tab.random')"
checked
/>
<div
class="tab-content bg-base-100 border-base-300 shadow-2xl p-6 overflow-hidden"
>
<PageRandom :factory="factory" /> <PageRandom :factory="factory" />
</div> </div>
<input <input type="radio" name="app_tabs" class="tab" :aria-label="$t('tab.input')" />
type="radio" <div class="tab-content bg-base-100 border-base-300 shadow-2xl p-6 overflow-hidden">
name="app_tabs"
class="tab"
:aria-label="$t('tab.input')"
/>
<div
class="tab-content bg-base-100 border-base-300 shadow-2xl p-6 overflow-hidden"
>
<PageInput :factory="factory" /> <PageInput :factory="factory" />
</div> </div>
<input <input type="radio" name="app_tabs" class="tab" :aria-label="$t('tab.history')" />
type="radio"
name="app_tabs"
class="tab"
:aria-label="$t('tab.history')"
/>
<div <div
class="tab-content bg-base-100 border-base-300 shadow-2xl p-6 overflow-x-hidden overflow-y-scroll max-h-96" class="tab-content bg-base-100 border-base-300 shadow-2xl p-6 overflow-x-hidden overflow-y-scroll max-h-96"
> >
+4 -10
View File
@@ -1,21 +1,15 @@
<script setup lang="ts"> <script setup lang="ts">
import type { DomainFactory } from "@/lib/factory"; import type { DomainFactory } from "@/lib/factory";
import CoolDomain from "./CoolDomain.vue"; import { defineAsyncComponent } from "vue";
const CoolDomain = defineAsyncComponent(() => import("./CoolDomain.vue"));
defineProps<{ factory: DomainFactory }>(); defineProps<{ factory: DomainFactory }>();
</script> </script>
<template> <template>
<div> <div>
<p <p v-for="(domain, i) in factory.history.slice().reverse()" :key="`domain-${i}`" class="pt-3">
v-for="(domain, i) in factory.history.slice().reverse()" <CoolDomain :domain="domain"></CoolDomain>
:key="`domain-${i}`"
class="pt-3"
>
<CoolDomain
:id="domain.name + domain.searching + domain.available"
:domain="domain"
></CoolDomain>
<span v-if="domain.searching" <span v-if="domain.searching"
>&nbsp;<span class="loading loading-dots loading-xs"></span >&nbsp;<span class="loading loading-dots loading-xs"></span
></span> ></span>
+3 -3
View File
@@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref } from "vue"; import { defineAsyncComponent, ref } from "vue";
import type { DomainFactory } from "@/lib/factory"; import type { DomainFactory } from "@/lib/factory";
import CoolDomain from "./CoolDomain.vue"; const CoolDomain = defineAsyncComponent(() => import("./CoolDomain.vue"));
import type { Domain } from "@/types"; import type { Domain } from "@/types";
import { isDomainAvailable } from "@/lib/whois"; import { isDomainAvailable } from "@/lib/whois";
@@ -62,7 +62,7 @@ function newDomain() {
<span v-else-if="domain.available" <span v-else-if="domain.available"
>&nbsp;{{ $t("seems") }} >&nbsp;{{ $t("seems") }}
<a <a
:href="`https://tld-list.com/${$i18n.locale}/tld/${domain.name?.split('.')[1]}`" :href="`https://tld-list.com/${$i18n.locale}/tld/${domain.tld}`"
target="_blank" target="_blank"
class="underline" class="underline"
>{{ $t("available") }}</a >{{ $t("available") }}</a
+21 -3
View File
@@ -1,10 +1,11 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, onBeforeMount, watch } from "vue"; import { ref, onBeforeMount, watch, defineAsyncComponent } from "vue";
import { isDomainAvailable } from "@/lib/whois"; import { isDomainAvailable } from "@/lib/whois";
import type { DomainFactory } from "@/lib/factory"; import type { DomainFactory } from "@/lib/factory";
import CoolDomain from "./CoolDomain.vue"; const CoolDomain = defineAsyncComponent(() => import("./CoolDomain.vue"));
import type { Domain } from "@/types"; import type { Domain } from "@/types";
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
import { IGNORED_TLDS_COOKIE, setCookie } from "@/lib/cookies";
enum State { enum State {
Loading, Loading,
@@ -60,6 +61,14 @@ function checkAvailability() {
} }
} }
function removeTld() {
if (domain.value) {
props.factory.ignoreTlds(domain.value.tld);
setCookie(IGNORED_TLDS_COOKIE, props.factory.ignoredTldList.join(","));
newDomain();
}
}
function getTextClass() { function getTextClass() {
if (!domain.value || domain.value.name.length < 15) { if (!domain.value || domain.value.name.length < 15) {
return "text-5xl"; return "text-5xl";
@@ -101,7 +110,7 @@ watch(i18n.locale, newDomain);
<div v-else-if="state === State.Ready && domain?.available" class="py-6"> <div v-else-if="state === State.Ready && domain?.available" class="py-6">
{{ $t("this_domain") }} {{ $t("seems") }} {{ $t("this_domain") }} {{ $t("seems") }}
<a <a
:href="`https://tld-list.com/${$i18n.locale}/tld/${domain.name?.split('.')[1]}`" :href="`https://tld-list.com/${$i18n.locale}/tld/${domain.tld}`"
target="_blank" target="_blank"
class="underline" class="underline"
>{{ $t("available") }}</a >{{ $t("available") }}</a
@@ -121,5 +130,14 @@ watch(i18n.locale, newDomain);
> >
{{ $t("button.new_domain") }} {{ $t("button.new_domain") }}
</button> </button>
<br />
<p v-if="domain">
<small
><a class="underline" href="#" @click.prevent="removeTld">{{
$t("page_random.dont_suggest", { ext: `.${domain.tld}` })
}}</a></small
>
</p>
<p v-else>&nbsp;</p>
</div> </div>
</template> </template>
+38
View File
@@ -860,6 +860,7 @@ co.id
my.id my.id
or.id or.id
web.id web.id
ie
ac.il ac.il
co.il co.il
muni.il muni.il
@@ -875,6 +876,43 @@ org.im
plc.co.im plc.co.im
immo immo
immobilien immobilien
in
5g.in
6g.in
ahmdabad.in
ai.in
am.in
bihar.in
biz.in
business.in
ca.in
cn.in
co.in
com.in
coop.in
cs.in
delhi.in
dr.in
er.in
firm.in
gen.in
gujarat.in
ind.in
info.in
int.in
internet.in
io.in
me.in
net.in
org.in
pg.in
post.in
pro.in
travel.in
tv.in
uk.in
up.in
us.in
inc inc
industries industries
info info
+25 -24
View File
@@ -1,27 +1,28 @@
{ {
"tab": { "tab": {
"random": "Random", "random": "Random",
"input": "Input", "input": "Input",
"history": "History" "history": "History"
}, },
"this_domain": "This domain", "this_domain": "This domain",
"is_unavailable": "is not available", "is_unavailable": "is not available",
"seems": "seems", "seems": "seems",
"available": "available", "available": "available",
"error": "An error has occured, please reload the page.", "error": "An error has occured, please reload the page.",
"page_random": { "page_random": {
"state": { "state": {
"loading": "Loading the Cool Domain Factory™", "loading": "Loading the Cool Domain Factory™",
"generating": "Generating a new cool domain", "generating": "Generating a new cool domain",
"fetching": "Checking domain availability" "fetching": "Checking domain availability"
},
"dont_suggest": "Don't generate {ext} domains"
},
"page_input": {
"not_found": "No matching domain found.",
"placeholder": "Type a word or a phrase"
},
"button": {
"new_domain": "New domain",
"generate": "Generate"
} }
},
"page_input": {
"not_found": "No matching domain found.",
"placeholder": "Type a word or a phrase"
},
"button": {
"new_domain": "New domain",
"generate": "Generate"
}
} }
+25 -24
View File
@@ -1,27 +1,28 @@
{ {
"tab": { "tab": {
"random": "Aléatoire", "random": "Aléatoire",
"input": "Saisie", "input": "Saisie",
"history": "Historique" "history": "Historique"
}, },
"this_domain": "Ce domaine", "this_domain": "Ce domaine",
"is_unavailable": "n'est pas disponible", "is_unavailable": "n'est pas disponible",
"seems": "semble être", "seems": "semble être",
"available": "disponible", "available": "disponible",
"error": "Une erreur est survenue, merci de recharger la page.", "error": "Une erreur est survenue, merci de recharger la page.",
"page_random": { "page_random": {
"state": { "state": {
"loading": "Chargement de la Cool Domain Factory™", "loading": "Chargement de la Cool Domain Factory™",
"generating": "Création d'un domaine cool", "generating": "Création d'un domaine cool",
"fetching": "Vérification de la disponibilité" "fetching": "Vérification de la disponibilité"
},
"dont_suggest": "Ne pas génerer des domaines en {ext}"
},
"page_input": {
"not_found": "Aucun domaine correspondant.",
"placeholder": "Écrit un mot ou une phrase."
},
"button": {
"new_domain": "Nouveau domaine",
"generate": "Génerer"
} }
},
"page_input": {
"not_found": "Aucun domaine correspondant.",
"placeholder": "Écrit un mot ou une phrase."
},
"button": {
"new_domain": "Nouveau domaine",
"generate": "Génerer"
}
} }
+20
View File
@@ -0,0 +1,20 @@
export function setCookie(name: string, value: string, days = 400) {
const date = new Date();
date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000);
const expires = `expires=${date.toUTCString()}`;
document.cookie = `${name}=${value}; path=/; ${expires}`;
}
export function getCookie(name: string, defaultValue = ""): string {
const prefix = `${name}=`;
const decodedCookie = decodeURIComponent(document.cookie);
const cookies = decodedCookie.split(";");
for (const cookie of cookies) {
if (cookie.trim().startsWith(prefix)) {
return cookie.trim().substring(prefix.length, cookie.length);
}
}
return defaultValue;
}
export const IGNORED_TLDS_COOKIE = "ignored_tlds";
+80 -41
View File
@@ -1,20 +1,31 @@
import rawTldList from "@/data/tld.txt?raw"; const rawTldList = () => import("@/data/tld.txt?raw");
import englishWords from "@/data/english.txt?raw"; const englishWords = () => import("@/data/english.txt?raw");
import frenchWords from "@/data/french.txt?raw"; const frenchWords = () => import("@/data/french.txt?raw");
import { randomElement, shuffled } from "./random"; import { randomElement, shuffled } from "./random";
import type { Domain } from "@/types"; import type { Domain } from "@/types";
const TLD_LIST: string[] = rawTldList.trim().split("\n");
export class DomainFactory { export class DomainFactory {
error: string | null = null; error: string | null = null;
words: Record<string, string[]> = {}; words: Record<string, string[]> = {};
generated: string[] = []; generated: string[] = [];
history: Domain[] = []; history: Domain[] = [];
tldList: string[] = [];
ignoredTldList: string[] = [];
init() { async init(lang: string | null = null) {
this.words.fr = this.initWords(frenchWords); if (lang && !this.words[lang]) {
this.words.en = this.initWords(englishWords); switch (lang) {
case "en":
this.words.en = this.initWords((await englishWords()).default);
break;
case "fr":
this.words.fr = this.initWords((await frenchWords()).default);
break;
}
}
if (this.tldList.length == 0) {
this.tldList = (await rawTldList()).default.trim().split("\n");
}
} }
escapeWord(word: string): string { escapeWord(word: string): string {
@@ -47,7 +58,10 @@ export class DomainFactory {
} }
randomDomain(word: string): string | null { randomDomain(word: string): string | null {
for (const tld of shuffled(TLD_LIST.slice())) { for (const tld of shuffled(this.tldList.slice())) {
if (this.ignoredTld(tld)) {
continue;
}
const tldEscaped: string = tld.replace(".", ""); const tldEscaped: string = tld.replace(".", "");
if (word.endsWith(tldEscaped) && word.length > tldEscaped.length + 2) { if (word.endsWith(tldEscaped) && word.length > tldEscaped.length + 2) {
const domain = `${word.substring(0, word.length - tldEscaped.length)}.${tld}`; const domain = `${word.substring(0, word.length - tldEscaped.length)}.${tld}`;
@@ -62,45 +76,70 @@ export class DomainFactory {
next(lang: string): Promise<Domain> { next(lang: string): Promise<Domain> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let k = 10000; this.init(lang)
while (k > 0) { .then(() => {
k--; let k = 10000;
const word = this.randomWord(lang); while (k > 0) {
const domain = this.randomDomain(word); k--;
if (domain) { const word = this.randomWord(lang);
const output = { name: domain, searching: false, available: true }; const domain = this.randomDomain(word);
this.history.push(output); if (domain) {
resolve(output); const output = this.newDomain(domain);
return; this.history.push(output);
} resolve(output);
} return;
this.error = "Could not generate a valid domain"; }
reject(new Error(this.error)); }
this.error = "Could not generate a valid domain";
reject(new Error(this.error));
})
.catch(reject);
}); });
} }
ignoreTlds(...tlds: string[]): void {
this.ignoredTldList.push(...tlds);
}
newDomain(domain: string): Domain {
return {
name: domain,
tld: domain.split(".").slice(1).join("."),
searching: false,
available: true,
};
}
ignoredTld(tld: string): boolean {
return this.ignoredTldList.some((ignoredTld: string) => tld.endsWith(ignoredTld));
}
allFromWord(word: string): Promise<Domain[]> { allFromWord(word: string): Promise<Domain[]> {
const escapedWord = this.escapeWord(word.replace(/ /gm, "-")); const escapedWord = this.escapeWord(word.replace(/ /gm, "-"));
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const domains = []; this.init()
for (const tld of shuffled(TLD_LIST.slice())) { .then(() => {
const tldEscaped: string = tld.replace(".", ""); const domains = [];
if ( for (const tld of shuffled(this.tldList.slice())) {
escapedWord.endsWith(tldEscaped) && if (this.ignoredTld(tld)) {
escapedWord.length >= tldEscaped.length + 2 continue;
) { }
const domain = `${escapedWord.substring(0, escapedWord.length - tldEscaped.length)}.${tld}`; const tldEscaped: string = tld.replace(".", "");
if (!domain.includes("-.")) { if (escapedWord.endsWith(tldEscaped) && escapedWord.length >= tldEscaped.length + 2) {
const output = { name: domain, searching: false, available: true }; const domain = `${escapedWord.substring(0, escapedWord.length - tldEscaped.length)}.${tld}`;
this.history.push(output); if (!domain.includes("-.")) {
domains.push(output); const output = this.newDomain(domain);
this.history.push(output);
domains.push(output);
}
}
} }
} if (domains.length) {
} resolve(domains);
if (domains.length) { }
resolve(domains); reject(new Error("Could not generate a valid domain"));
} })
reject(new Error("Could not generate a valid domain")); .catch(reject);
}); });
} }
} }
+5 -13
View File
@@ -1,10 +1,4 @@
import type { import type { WhoIsResponse, WhoIsData, WhoIsEvent, WhoIsIpAddresses, Domain } from "@/types";
WhoIsResponse,
WhoIsData,
WhoIsEvent,
WhoIsIpAddresses,
Domain,
} from "@/types";
function checkEvents(events: WhoIsEvent[]): boolean { function checkEvents(events: WhoIsEvent[]): boolean {
for (const event of events) { for (const event of events) {
@@ -21,18 +15,16 @@ function checkIpAddresses(ipAddresses: WhoIsIpAddresses): boolean {
function checkWhoIsData(data: WhoIsData): boolean { function checkWhoIsData(data: WhoIsData): boolean {
return ( return (
checkEvents(data.events) && checkEvents(data.events) && checkIpAddresses(data.ipAddresses) && data.nameservers.length === 0
checkIpAddresses(data.ipAddresses) &&
data.nameservers.length === 0
); );
} }
export async function isDomainAvailable(domain: Domain): Promise<boolean> { export async function isDomainAvailable(domain: Domain): Promise<boolean> {
domain.searching = true; domain.searching = true;
try { try {
const response = await fetch( const response = await fetch(`https://whois.klemek.fr/api/lookup/${domain.name}`, {
`https://whois.klemek.fr/api/lookup/${domain.name}`, signal: AbortSignal.timeout(5000),
); });
const content: WhoIsResponse = await response.json(); const content: WhoIsResponse = await response.json();
domain.available = checkWhoIsData(content.data); domain.available = checkWhoIsData(content.data);
} catch { } catch {
+4 -4
View File
@@ -1,8 +1,8 @@
import { createApp } from "vue"; import { createApp, defineAsyncComponent } from "vue";
import { createI18n } from "vue-i18n"; import { createI18n } from "vue-i18n";
import App from "./App.vue"; const App = defineAsyncComponent(() => import("./App.vue"));
import en from "./i18n/en.json"; const en = await import("./i18n/en.json");
import fr from "./i18n/fr.json"; const fr = await import("./i18n/fr.json");
function getLang() { function getLang() {
return document.location.hash === "#french" ? "fr" : "en"; return document.location.hash === "#french" ? "fr" : "en";
+1
View File
@@ -34,6 +34,7 @@ export interface WhoIsEntity {
export interface Domain { export interface Domain {
name: string; name: string;
tld: string;
available: boolean; available: boolean;
searching: boolean; searching: boolean;
} }