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">
import { ref, onMounted, onBeforeMount } from "vue";
import PageRandom from "@/components/PageRandom.vue";
import PageInput from "@/components/PageInput.vue";
import { ref, onMounted, defineAsyncComponent, onBeforeMount } from "vue";
const PageRandom = defineAsyncComponent(() => import("@/components/PageRandom.vue"));
const PageInput = defineAsyncComponent(() => import("@/components/PageInput.vue"));
const PageHistory = defineAsyncComponent(() => import("./components/PageHistory.vue"));
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 factory = ref<DomainFactory>(new DomainFactory());
onBeforeMount(() => {
factory.value.init();
factory.value.ignoreTlds(
...getCookie(IGNORED_TLDS_COOKIE)
.split(".")
.filter((tld: string) => tld.length),
);
});
onMounted(() => {
@@ -22,37 +27,17 @@ onMounted(() => {
<template>
<div :style="{ display: visible ? 'inherit' : 'none' }">
<div class="tabs tabs-xs tabs-bottom w-xl max-w-svw shrink-0">
<input
type="radio"
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"
>
<input type="radio" 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" />
</div>
<input
type="radio"
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"
>
<input type="radio" 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" />
</div>
<input
type="radio"
name="app_tabs"
class="tab"
:aria-label="$t('tab.history')"
/>
<input type="radio" name="app_tabs" class="tab" :aria-label="$t('tab.history')" />
<div
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">
import type { DomainFactory } from "@/lib/factory";
import CoolDomain from "./CoolDomain.vue";
import { defineAsyncComponent } from "vue";
const CoolDomain = defineAsyncComponent(() => import("./CoolDomain.vue"));
defineProps<{ factory: DomainFactory }>();
</script>
<template>
<div>
<p
v-for="(domain, i) in factory.history.slice().reverse()"
:key="`domain-${i}`"
class="pt-3"
>
<CoolDomain
:id="domain.name + domain.searching + domain.available"
:domain="domain"
></CoolDomain>
<p v-for="(domain, i) in factory.history.slice().reverse()" :key="`domain-${i}`" class="pt-3">
<CoolDomain :domain="domain"></CoolDomain>
<span v-if="domain.searching"
>&nbsp;<span class="loading loading-dots loading-xs"></span
></span>
+3 -3
View File
@@ -1,7 +1,7 @@
<script setup lang="ts">
import { ref } from "vue";
import { defineAsyncComponent, ref } from "vue";
import type { DomainFactory } from "@/lib/factory";
import CoolDomain from "./CoolDomain.vue";
const CoolDomain = defineAsyncComponent(() => import("./CoolDomain.vue"));
import type { Domain } from "@/types";
import { isDomainAvailable } from "@/lib/whois";
@@ -62,7 +62,7 @@ function newDomain() {
<span v-else-if="domain.available"
>&nbsp;{{ $t("seems") }}
<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"
class="underline"
>{{ $t("available") }}</a
+21 -3
View File
@@ -1,10 +1,11 @@
<script setup lang="ts">
import { ref, onBeforeMount, watch } from "vue";
import { ref, onBeforeMount, watch, defineAsyncComponent } from "vue";
import { isDomainAvailable } from "@/lib/whois";
import type { DomainFactory } from "@/lib/factory";
import CoolDomain from "./CoolDomain.vue";
const CoolDomain = defineAsyncComponent(() => import("./CoolDomain.vue"));
import type { Domain } from "@/types";
import { useI18n } from "vue-i18n";
import { IGNORED_TLDS_COOKIE, setCookie } from "@/lib/cookies";
enum State {
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() {
if (!domain.value || domain.value.name.length < 15) {
return "text-5xl";
@@ -101,7 +110,7 @@ watch(i18n.locale, newDomain);
<div v-else-if="state === State.Ready && domain?.available" class="py-6">
{{ $t("this_domain") }} {{ $t("seems") }}
<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"
class="underline"
>{{ $t("available") }}</a
@@ -121,5 +130,14 @@ watch(i18n.locale, newDomain);
>
{{ $t("button.new_domain") }}
</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>
</template>
+38
View File
@@ -860,6 +860,7 @@ co.id
my.id
or.id
web.id
ie
ac.il
co.il
muni.il
@@ -875,6 +876,43 @@ org.im
plc.co.im
immo
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
industries
info
+2 -1
View File
@@ -14,7 +14,8 @@
"loading": "Loading the Cool Domain Factory™",
"generating": "Generating a new cool domain",
"fetching": "Checking domain availability"
}
},
"dont_suggest": "Don't generate {ext} domains"
},
"page_input": {
"not_found": "No matching domain found.",
+2 -1
View File
@@ -14,7 +14,8 @@
"loading": "Chargement de la Cool Domain Factory™",
"generating": "Création d'un domaine cool",
"fetching": "Vérification de la disponibilité"
}
},
"dont_suggest": "Ne pas génerer des domaines en {ext}"
},
"page_input": {
"not_found": "Aucun domaine correspondant.",
+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";
+55 -16
View File
@@ -1,20 +1,31 @@
import rawTldList from "@/data/tld.txt?raw";
import englishWords from "@/data/english.txt?raw";
import frenchWords from "@/data/french.txt?raw";
const rawTldList = () => import("@/data/tld.txt?raw");
const englishWords = () => import("@/data/english.txt?raw");
const frenchWords = () => import("@/data/french.txt?raw");
import { randomElement, shuffled } from "./random";
import type { Domain } from "@/types";
const TLD_LIST: string[] = rawTldList.trim().split("\n");
export class DomainFactory {
error: string | null = null;
words: Record<string, string[]> = {};
generated: string[] = [];
history: Domain[] = [];
tldList: string[] = [];
ignoredTldList: string[] = [];
init() {
this.words.fr = this.initWords(frenchWords);
this.words.en = this.initWords(englishWords);
async init(lang: string | null = null) {
if (lang && !this.words[lang]) {
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 {
@@ -47,7 +58,10 @@ export class DomainFactory {
}
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(".", "");
if (word.endsWith(tldEscaped) && word.length > tldEscaped.length + 2) {
const domain = `${word.substring(0, word.length - tldEscaped.length)}.${tld}`;
@@ -62,13 +76,15 @@ export class DomainFactory {
next(lang: string): Promise<Domain> {
return new Promise((resolve, reject) => {
this.init(lang)
.then(() => {
let k = 10000;
while (k > 0) {
k--;
const word = this.randomWord(lang);
const domain = this.randomDomain(word);
if (domain) {
const output = { name: domain, searching: false, available: true };
const output = this.newDomain(domain);
this.history.push(output);
resolve(output);
return;
@@ -76,22 +92,43 @@ export class DomainFactory {
}
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[]> {
const escapedWord = this.escapeWord(word.replace(/ /gm, "-"));
return new Promise((resolve, reject) => {
this.init()
.then(() => {
const domains = [];
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(".", "");
if (
escapedWord.endsWith(tldEscaped) &&
escapedWord.length >= tldEscaped.length + 2
) {
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: true };
const output = this.newDomain(domain);
this.history.push(output);
domains.push(output);
}
@@ -101,6 +138,8 @@ export class DomainFactory {
resolve(domains);
}
reject(new Error("Could not generate a valid domain"));
})
.catch(reject);
});
}
}
+5 -13
View File
@@ -1,10 +1,4 @@
import type {
WhoIsResponse,
WhoIsData,
WhoIsEvent,
WhoIsIpAddresses,
Domain,
} from "@/types";
import type { WhoIsResponse, WhoIsData, WhoIsEvent, WhoIsIpAddresses, Domain } from "@/types";
function checkEvents(events: WhoIsEvent[]): boolean {
for (const event of events) {
@@ -21,18 +15,16 @@ function checkIpAddresses(ipAddresses: WhoIsIpAddresses): boolean {
function checkWhoIsData(data: WhoIsData): boolean {
return (
checkEvents(data.events) &&
checkIpAddresses(data.ipAddresses) &&
data.nameservers.length === 0
checkEvents(data.events) && checkIpAddresses(data.ipAddresses) && data.nameservers.length === 0
);
}
export async function isDomainAvailable(domain: Domain): Promise<boolean> {
domain.searching = true;
try {
const response = await fetch(
`https://whois.klemek.fr/api/lookup/${domain.name}`,
);
const response = await fetch(`https://whois.klemek.fr/api/lookup/${domain.name}`, {
signal: AbortSignal.timeout(5000),
});
const content: WhoIsResponse = await response.json();
domain.available = checkWhoIsData(content.data);
} catch {
+4 -4
View File
@@ -1,8 +1,8 @@
import { createApp } from "vue";
import { createApp, defineAsyncComponent } from "vue";
import { createI18n } from "vue-i18n";
import App from "./App.vue";
import en from "./i18n/en.json";
import fr from "./i18n/fr.json";
const App = defineAsyncComponent(() => import("./App.vue"));
const en = await import("./i18n/en.json");
const fr = await import("./i18n/fr.json");
function getLang() {
return document.location.hash === "#french" ? "fr" : "en";
+1
View File
@@ -34,6 +34,7 @@ export interface WhoIsEntity {
export interface Domain {
name: string;
tld: string;
available: boolean;
searching: boolean;
}