Sort members list #45
20 changed files with 231 additions and 34 deletions
|
@ -23,3 +23,17 @@ steps:
|
||||||
when:
|
when:
|
||||||
- branch: main
|
- branch: main
|
||||||
event: push
|
event: push
|
||||||
|
|
||||||
|
- name: container-build-and-publish (staging)
|
||||||
|
image: docker
|
||||||
|
commands:
|
||||||
|
- docker login --username radical --password $PASSWORD git.gorb.app
|
||||||
|
- docker buildx build --platform linux/amd64,linux/arm64 --rm --push -t git.gorb.app/gorb/frontend:staging .
|
||||||
|
environment:
|
||||||
|
PASSWORD:
|
||||||
|
from_secret: docker_password
|
||||||
|
volumes:
|
||||||
|
- /var/run/podman/podman.sock:/var/run/docker.sock
|
||||||
|
when:
|
||||||
|
- branch: staging
|
||||||
|
event: push
|
||||||
|
|
|
@ -26,9 +26,7 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
const { fetchFriends } = useApi();
|
const { fetchFriends } = useApi();
|
||||||
|
|
||||||
const friends = await fetchFriends().then((response) => {
|
const friends = sortUsers(await fetchFriends())
|
||||||
return response.sort((a, b) => getDisplayName(a).localeCompare(getDisplayName(b)))
|
|
||||||
})
|
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
variant: string
|
variant: string
|
||||||
|
|
|
@ -21,8 +21,6 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { UserResponse } from '~/types/interfaces';
|
import type { UserResponse } from '~/types/interfaces';
|
||||||
|
|
||||||
const { fetchMembers } = useApi();
|
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
user: UserResponse
|
user: UserResponse
|
||||||
}>();
|
}>();
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
<template>
|
<template>
|
||||||
<div @click="props.callback()" class="button" :class="props.variant + '-button'">
|
<button @click="props.callback ? props.callback() : null" class="button" :class="props.variant + '-button'">
|
||||||
{{ props.text }}
|
{{ props.text }}
|
||||||
</div>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
text: string,
|
text: string,
|
||||||
callback: CallableFunction,
|
callback?: CallableFunction,
|
||||||
variant?: "normal" | "scary" | "neutral",
|
variant?: "normal" | "scary" | "neutral",
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
@ -28,6 +28,8 @@ const props = defineProps<{
|
||||||
border-radius: 0.7rem;
|
border-radius: 0.7rem;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
|
||||||
|
border: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.button:hover {
|
.button:hover {
|
||||||
|
|
|
@ -1,24 +1,36 @@
|
||||||
import type { ChannelResponse, GuildMemberResponse, GuildResponse, MessageResponse, StatsResponse, UserResponse } from "~/types/interfaces";
|
import type { ChannelResponse, GuildMemberResponse, GuildResponse, MessageResponse, StatsResponse, UserResponse } from "~/types/interfaces";
|
||||||
|
|
||||||
|
function ensureIsArray(list: any) {
|
||||||
|
if (Array.isArray(list)) {
|
||||||
|
return list
|
||||||
|
} else {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const useApi = () => {
|
export const useApi = () => {
|
||||||
async function fetchGuilds(): Promise<GuildResponse[] | undefined> {
|
async function fetchGuilds(): Promise<GuildResponse[]> {
|
||||||
return await fetchWithApi(`/guilds`);
|
return ensureIsArray(await fetchWithApi(`/guilds`));
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchGuild(guildId: string): Promise<GuildResponse | undefined> {
|
async function fetchGuild(guildId: string): Promise<GuildResponse | undefined> {
|
||||||
return await fetchWithApi(`/guilds/${guildId}`);
|
return await fetchWithApi(`/guilds/${guildId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchChannels(guildId: string): Promise<ChannelResponse[] | undefined> {
|
async function fetchMyGuilds(): Promise<GuildResponse[]> {
|
||||||
return await fetchWithApi(`/guilds/${guildId}/channels`);
|
return ensureIsArray(await fetchWithApi(`/me/guilds`));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchChannels(guildId: string): Promise<ChannelResponse[]> {
|
||||||
|
return ensureIsArray(await fetchWithApi(`/guilds/${guildId}/channels`));
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchChannel(channelId: string): Promise<ChannelResponse | undefined> {
|
async function fetchChannel(channelId: string): Promise<ChannelResponse | undefined> {
|
||||||
return await fetchWithApi(`/channels/${channelId}`)
|
return await fetchWithApi(`/channels/${channelId}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchMembers(guildId: string): Promise<GuildMemberResponse[] | undefined> {
|
async function fetchMembers(guildId: string): Promise<GuildMemberResponse[]> {
|
||||||
return await fetchWithApi(`/guilds/${guildId}/members`);
|
return ensureIsArray(await fetchWithApi(`/guilds/${guildId}/members`));
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchMember(guildId: string, memberId: string): Promise<GuildMemberResponse | undefined> {
|
async function fetchMember(guildId: string, memberId: string): Promise<GuildMemberResponse | undefined> {
|
||||||
|
@ -34,12 +46,7 @@ export const useApi = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchFriends(): Promise<UserResponse[]> {
|
async function fetchFriends(): Promise<UserResponse[]> {
|
||||||
const response = await fetchWithApi('/me/friends')
|
return ensureIsArray(await fetchWithApi('/me/friends'));
|
||||||
if (Array.isArray(response)) {
|
|
||||||
return response
|
|
||||||
} else {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function addFriend(username: string): Promise<void> {
|
async function addFriend(username: string): Promise<void> {
|
||||||
|
@ -79,9 +86,18 @@ export const useApi = () => {
|
||||||
await fetchWithApi("/auth/verify-email", { method: "POST", body: { email } });
|
await fetchWithApi("/auth/verify-email", { method: "POST", body: { email } });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function sendPasswordResetEmail(identifier: string): Promise<void> {
|
||||||
|
await fetchWithApi("/auth/reset-password", { method: "GET", query: { identifier } });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function resetPassword(password: string, token: string) {
|
||||||
|
await fetchWithApi("/auth/reset-password", { method: "POST", body: { password, token } });
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
fetchGuilds,
|
fetchGuilds,
|
||||||
fetchGuild,
|
fetchGuild,
|
||||||
|
fetchMyGuilds,
|
||||||
fetchChannels,
|
fetchChannels,
|
||||||
fetchChannel,
|
fetchChannel,
|
||||||
fetchMembers,
|
fetchMembers,
|
||||||
|
@ -97,6 +113,8 @@ export const useApi = () => {
|
||||||
joinGuild,
|
joinGuild,
|
||||||
createChannel,
|
createChannel,
|
||||||
fetchInstanceStats,
|
fetchInstanceStats,
|
||||||
sendVerificationEmail
|
sendVerificationEmail,
|
||||||
|
sendPasswordResetEmail,
|
||||||
|
resetPassword
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div v-else id="auth-form-container">
|
<div v-else id="auth-form-container">
|
||||||
<slot />
|
<slot />
|
||||||
|
<div v-if="!['/recover', '/reset-password'].includes(route.path)">Forgot password? Recover <NuxtLink href="/recover">here</NuxtLink>!</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="instanceUrl">
|
<div v-if="instanceUrl">
|
||||||
Instance URL is set to <span style="color: var(--primary-color);">{{ instanceUrl }}</span>
|
Instance URL is set to <span style="color: var(--primary-color);">{{ instanceUrl }}</span>
|
||||||
|
@ -36,7 +37,11 @@ const apiVersion = useRuntimeConfig().public.apiVersion;
|
||||||
const apiBase = useCookie("api_base");
|
const apiBase = useCookie("api_base");
|
||||||
const registrationEnabled = useState("registrationEnabled", () => true);
|
const registrationEnabled = useState("registrationEnabled", () => true);
|
||||||
|
|
||||||
const auth = useAuth();
|
const route = useRoute();
|
||||||
|
|
||||||
|
const query = route.query as Record<string, string>;
|
||||||
|
const searchParams = new URLSearchParams(query);
|
||||||
|
searchParams.delete("token");
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
instanceUrl.value = useCookie("instance_url").value;
|
instanceUrl.value = useCookie("instance_url").value;
|
||||||
|
@ -111,6 +116,7 @@ const form = reactive({
|
||||||
#auth-form-container form {
|
#auth-form-container form {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
margin-top: 10dvh;
|
margin-top: 10dvh;
|
||||||
gap: 1em;
|
gap: 1em;
|
||||||
|
|
|
@ -95,7 +95,7 @@ const options = [
|
||||||
if (invite.length == 6) {
|
if (invite.length == 6) {
|
||||||
try {
|
try {
|
||||||
const joinedGuild = await api.joinGuild(invite);
|
const joinedGuild = await api.joinGuild(invite);
|
||||||
guilds?.push(joinedGuild);
|
guilds.push(joinedGuild);
|
||||||
return await navigateTo(`/servers/${joinedGuild.uuid}`);
|
return await navigateTo(`/servers/${joinedGuild.uuid}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
alert(`Couldn't use invite: ${error}`);
|
alert(`Couldn't use invite: ${error}`);
|
||||||
|
@ -151,7 +151,7 @@ const options = [
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
const guilds: GuildResponse[] | undefined = await fetchWithApi("/me/guilds");
|
const guilds = await api.fetchMyGuilds();
|
||||||
|
|
||||||
function createDropdown() {
|
function createDropdown() {
|
||||||
const dropdown = h(GuildDropdown, { options });
|
const dropdown = h(GuildDropdown, { options });
|
||||||
|
|
|
@ -16,7 +16,7 @@ export default defineNuxtRouteMiddleware(async (to, from) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (["/login", "/register"].includes(to.path) && !Object.keys(to.query).includes("special")) {
|
if (["/login", "/register", "/recover", "/reset-password"].includes(to.path) && !Object.keys(to.query).includes("special")) {
|
||||||
console.log("path is login or register");
|
console.log("path is login or register");
|
||||||
const apiBase = useCookie("api_base");
|
const apiBase = useCookie("api_base");
|
||||||
console.log("apiBase gotten:", apiBase.value);
|
console.log("apiBase gotten:", apiBase.value);
|
||||||
|
|
|
@ -5,10 +5,10 @@ export default defineNuxtRouteMiddleware(async (to, from) => {
|
||||||
|
|
||||||
const guildId = to.params.serverId as string;
|
const guildId = to.params.serverId as string;
|
||||||
|
|
||||||
const channels: ChannelResponse[] | undefined = await fetchChannels(guildId);
|
const channels: ChannelResponse[] = await fetchChannels(guildId);
|
||||||
console.log("channels:", channels);
|
console.log("channels:", channels);
|
||||||
|
|
||||||
if (channels && channels.length > 0) {
|
if (channels.length > 0) {
|
||||||
console.log("wah");
|
console.log("wah");
|
||||||
return await navigateTo(`/servers/${guildId}/channels/${channels[0].uuid}`, { replace: true });
|
return await navigateTo(`/servers/${guildId}/channels/${channels[0].uuid}`, { replace: true });
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,6 +36,7 @@ const form = reactive({
|
||||||
|
|
||||||
const query = useRoute().query as Record<string, string>;
|
const query = useRoute().query as Record<string, string>;
|
||||||
const searchParams = new URLSearchParams(query);
|
const searchParams = new URLSearchParams(query);
|
||||||
|
searchParams.delete("token");
|
||||||
|
|
||||||
const registrationEnabled = ref<boolean>(true);
|
const registrationEnabled = ref<boolean>(true);
|
||||||
const apiBase = useCookie("api_base");
|
const apiBase = useCookie("api_base");
|
||||||
|
@ -48,7 +49,7 @@ if (apiBase.value) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const registerUrl = `/register?${searchParams}`
|
const registerUrl = `/register?${searchParams}`;
|
||||||
|
|
||||||
const { login } = useAuth();
|
const { login } = useAuth();
|
||||||
|
|
||||||
|
|
89
pages/recover.vue
Normal file
89
pages/recover.vue
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
<template>
|
||||||
|
<NuxtLayout name="auth">
|
||||||
|
<div v-if="errorValue">{{ errorValue }}</div>
|
||||||
|
<form v-if="!emailFormSent" @submit.prevent="sendEmail">
|
||||||
|
<div>
|
||||||
|
<label for="identifier">Email or username</label>
|
||||||
|
<br>
|
||||||
|
<input type="text" name="identifier" id="identifier" v-model="emailForm.identifier">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Button type="submit" text="Send email" variant="normal" />
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<div v-else>
|
||||||
|
If an account with that username or email exists, an email will be sent to it shortly.
|
||||||
|
</div>
|
||||||
|
<div v-if="registrationEnabled">
|
||||||
|
Don't have an account? <NuxtLink :href="registerUrl">Register</NuxtLink> one!
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
Already have an account? <NuxtLink :href="loginUrl">Log in</NuxtLink>!
|
||||||
|
</div>
|
||||||
|
</NuxtLayout>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import Button from '~/components/UserInterface/Button.vue';
|
||||||
|
|
||||||
|
const emailForm = reactive({
|
||||||
|
identifier: ""
|
||||||
|
});
|
||||||
|
|
||||||
|
const emailFormSent = ref(false);
|
||||||
|
|
||||||
|
const passwordForm = reactive({
|
||||||
|
password: ""
|
||||||
|
});
|
||||||
|
|
||||||
|
const errorValue = ref<string>();
|
||||||
|
|
||||||
|
const registrationEnabled = ref<boolean>(true);
|
||||||
|
const apiBase = useCookie("api_base");
|
||||||
|
|
||||||
|
const query = useRoute().query as Record<string, string>;
|
||||||
|
const searchParams = new URLSearchParams(query);
|
||||||
|
|
||||||
|
const token = ref(searchParams.get("token"))
|
||||||
|
searchParams.delete("token");
|
||||||
|
const { resetPassword } = useApi();
|
||||||
|
|
||||||
|
const registerUrl = `/register?${searchParams}`;
|
||||||
|
const loginUrl = `/login?${searchParams}`;
|
||||||
|
|
||||||
|
if (apiBase.value) {
|
||||||
|
console.log("apiBase:", apiBase.value);
|
||||||
|
const stats = await useApi().fetchInstanceStats(apiBase.value);
|
||||||
|
if (stats) {
|
||||||
|
registrationEnabled.value = stats.registration_enabled;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const { sendPasswordResetEmail } = useApi();
|
||||||
|
|
||||||
|
async function sendEmail() {
|
||||||
|
try {
|
||||||
|
await sendPasswordResetEmail(emailForm.identifier);
|
||||||
|
emailFormSent.value = true;
|
||||||
|
} catch (error) {
|
||||||
|
errorValue.value = (error as any).toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sendPassword() {
|
||||||
|
try {
|
||||||
|
console.log("pass:", passwordForm.password);
|
||||||
|
const hashedPass = await hashPassword(passwordForm.password)
|
||||||
|
console.log("hashed pass:", hashedPass);
|
||||||
|
await resetPassword(hashedPass, token.value!);
|
||||||
|
return await navigateTo(`/login?${searchParams}`);
|
||||||
|
} catch (error) {
|
||||||
|
errorValue.value = (error as any).toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
|
||||||
|
</style>
|
|
@ -86,6 +86,7 @@ const auth = useAuth();
|
||||||
const loggedIn = ref(await auth.getUser());
|
const loggedIn = ref(await auth.getUser());
|
||||||
|
|
||||||
const query = new URLSearchParams(useRoute().query as Record<string, string>);
|
const query = new URLSearchParams(useRoute().query as Record<string, string>);
|
||||||
|
query.delete("token");
|
||||||
|
|
||||||
const user = await useAuth().getUser();
|
const user = await useAuth().getUser();
|
||||||
|
|
||||||
|
|
56
pages/reset-password.vue
Normal file
56
pages/reset-password.vue
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
<template>
|
||||||
|
<NuxtLayout name="auth">
|
||||||
|
<div v-if="errorValue">{{ errorValue }}</div>
|
||||||
|
<form @submit.prevent="sendPassword">
|
||||||
|
<div>
|
||||||
|
<label for="password">Password</label>
|
||||||
|
<br>
|
||||||
|
<input type="password" name="password" id="password" v-model="passwordForm.password">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Button type="submit" text="Submit" variant="normal" />
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<div>
|
||||||
|
Already have an account? <NuxtLink :href="loginUrl">Log in</NuxtLink>!
|
||||||
|
</div>
|
||||||
|
</NuxtLayout>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import Button from '~/components/UserInterface/Button.vue';
|
||||||
|
|
||||||
|
const query = useRoute().query as Record<string, string>;
|
||||||
|
const searchParams = new URLSearchParams(query);
|
||||||
|
|
||||||
|
const loginUrl = `/login?${searchParams}`;
|
||||||
|
|
||||||
|
const token = ref(searchParams.get("token"))
|
||||||
|
|
||||||
|
if (!token.value) await navigateTo("/login");
|
||||||
|
|
||||||
|
const passwordForm = reactive({
|
||||||
|
password: ""
|
||||||
|
});
|
||||||
|
|
||||||
|
const errorValue = ref<string>();
|
||||||
|
|
||||||
|
const { resetPassword } = useApi();
|
||||||
|
|
||||||
|
async function sendPassword() {
|
||||||
|
try {
|
||||||
|
console.log("pass:", passwordForm.password);
|
||||||
|
const hashedPass = await hashPassword(passwordForm.password)
|
||||||
|
console.log("hashed pass:", hashedPass);
|
||||||
|
await resetPassword(hashedPass, token.value!);
|
||||||
|
return await navigateTo("/login?");
|
||||||
|
} catch (error) {
|
||||||
|
errorValue.value = (error as any).toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
|
||||||
|
</style>
|
|
@ -64,7 +64,7 @@ onActivated(async () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
async function setArrayVariables() {
|
async function setArrayVariables() {
|
||||||
members.value = await fetchMembers(route.params.serverId as string);
|
members.value = sortMembers(await fetchMembers(route.params.serverId as string))
|
||||||
const guildUrl = `guilds/${route.params.serverId}`;
|
const guildUrl = `guilds/${route.params.serverId}`;
|
||||||
channels.value = await fetchWithApi(`${guildUrl}/channels`);
|
channels.value = await fetchWithApi(`${guildUrl}/channels`);
|
||||||
console.log("channels:", channels.value);
|
console.log("channels:", channels.value);
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import type { GuildMemberResponse, UserResponse } from "~/types/interfaces";
|
import type { GuildMemberResponse, UserResponse } from "~/types/interfaces";
|
||||||
|
|
||||||
export function getDisplayName(user: UserResponse, member?: GuildMemberResponse): string {
|
export default (user: UserResponse, member?: GuildMemberResponse): string => {
|
||||||
if (member?.nickname) return member.nickname
|
if (member?.nickname) return member.nickname
|
||||||
if (user.display_name) return user.display_name
|
if (user.display_name) return user.display_name
|
||||||
return user.username
|
return user.username
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
export async function hashPassword(password: string) {
|
export default async (password: string) => {
|
||||||
const encodedPass = new TextEncoder().encode(password);
|
const encodedPass = new TextEncoder().encode(password);
|
||||||
const hashBuffer = await crypto.subtle.digest("SHA-384", encodedPass);
|
const hashBuffer = await crypto.subtle.digest("SHA-384", encodedPass);
|
||||||
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
7
utils/sortMembers.ts
Normal file
7
utils/sortMembers.ts
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
import type { GuildMemberResponse } from "~/types/interfaces";
|
||||||
|
|
||||||
|
export default (members: GuildMemberResponse[]): GuildMemberResponse[] => {
|
||||||
twig marked this conversation as resolved
Outdated
|
|||||||
|
return members.sort((a, b) => {
|
||||||
|
return getDisplayName(a.user, a).localeCompare(getDisplayName(b.user, b))
|
||||||
|
})
|
||||||
|
}
|
7
utils/sortUsers.ts
Normal file
7
utils/sortUsers.ts
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
import type { UserResponse } from "~/types/interfaces";
|
||||||
|
|
||||||
|
export default (users: UserResponse[]): UserResponse[] => {
|
||||||
twig marked this conversation as resolved
Outdated
sauceyred
commented
No need to name function here, name is derived from file name No need to name function here, name is derived from file name
|
|||||||
|
return users.sort((a, b) => {
|
||||||
|
return getDisplayName(a).localeCompare(getDisplayName(b))
|
||||||
|
})
|
||||||
|
}
|
3
utils/validateUsername.ts
Normal file
3
utils/validateUsername.ts
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
export default (username: string) => {
|
||||||
|
return /^[\w.-]+$/.test(username);
|
||||||
|
}
|
|
@ -1,3 +0,0 @@
|
||||||
export function validateUsername(username: string) {
|
|
||||||
return /^[\w.-]+$/.test(username);
|
|
||||||
}
|
|
Loading…
Add table
Add a link
Reference in a new issue
No need to name function here, name is derived from file name