Added Team System
This commit is contained in:
180
web/components/team/Default.vue
Normal file
180
web/components/team/Default.vue
Normal file
@@ -0,0 +1,180 @@
|
||||
<template>
|
||||
<Modal v-if="modalOpen" title="Invite team member" @close="modalOpen = false" @submit="modalOpen = false">
|
||||
<div>
|
||||
<h2 class="text-lg font-bold text-primary">Users</h2>
|
||||
<div class="h-48 space-y-3 overflow-y-auto">
|
||||
<div v-for="unaffiliatedUser in unaffilatedUsers" :key="unaffiliatedUser._id.toString()" class="flex items-center rounded bg-neutral-700 px-3 py-1 text-gray-200">
|
||||
{{ unaffiliatedUser.username }}
|
||||
<Button v-if="!unaffiliatedUser.teamInvites.includes(user.team.id)" class="ml-auto" @click="inviteUser(unaffiliatedUser)">Invite</Button>
|
||||
<Button v-else class="ml-auto" type="danger" @click="cancelInvite(unaffiliatedUser)">Cancel Invite</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
<Modal v-if="editTeamModal.open" title="Edit team" @close="editTeamModal.open = false" @submit="editTeam">
|
||||
<Input v-model:value="editTeamModal.name" background-class="bg-neutral-800" class="w-full max-w-sm">Naam / Prefix</Input>
|
||||
<Colorpicker v-model:value="editTeamModal.color" input-background-class="bg-neutral-800" class="w-full max-w-sm" />
|
||||
</Modal>
|
||||
<div class="mx-auto my-10 max-w-2xl">
|
||||
<h2 class="mb-2 text-xl font-bold text-primary">Team Information</h2>
|
||||
<div class="rounded border-[1px] border-primary px-5 py-2 text-primary">
|
||||
<table class="w-full table-auto">
|
||||
<tbody class="divide-y divide-gray-700">
|
||||
<tr>
|
||||
<td class="py-3">Name</td>
|
||||
<td class="font-bold">{{ team.name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="py-3">Color</td>
|
||||
<td class="font-bold">{{ team.color }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="py-3">ID</td>
|
||||
<td class="font-bold">{{ team._id }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="mb-10 mt-5 flex justify-center gap-x-3">
|
||||
<Button type="danger" @click="leaveTeam">Leave Team</Button>
|
||||
<Button @click="openTeamModal">Edit Team</Button>
|
||||
</div>
|
||||
<h2 class="mb-2 text-xl font-bold text-primary">Team Members</h2>
|
||||
<div class="space-y-5 rounded border-[1px] border-primary p-5 text-primary">
|
||||
<div v-for="teamMember in teamMembers" :key="teamMember._id" class="flex h-12 items-center rounded bg-neutral-800 px-5 font-bold text-gray-200">
|
||||
{{ teamMember.username }}
|
||||
<span v-if="teamMember.team.admin" class="ml-3 text-sm text-gray-400">Admin</span>
|
||||
<div v-if="user.team.admin" class="ml-auto">
|
||||
<Button v-if="!teamMember.team.admin && teamMember._id !== user._id" @click="promoteUser(teamMember)">Promote</Button>
|
||||
<Button v-if="teamMember.team.admin && teamMember._id !== user._id" @click="demoteUser(teamMember)">Demote</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="user.team.admin" class="rounded border-2 border-dashed border-neutral-500 bg-neutral-800 p-3 text-center font-bold text-gray-200 hover:cursor-pointer hover:bg-neutral-700" @click="modalOpen = true">
|
||||
Invite new member
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const user = useState('user');
|
||||
const { data: team } = await useFetch('/api/team');
|
||||
const { data: teamMembers } = await useFetch('/api/team/members');
|
||||
const { data: unaffilatedUsers } = await useFetch('/api/team/unaffiliated')
|
||||
|
||||
const modalOpen = ref(false)
|
||||
|
||||
const editTeamModal = ref({
|
||||
open: false,
|
||||
name: '',
|
||||
color: '',
|
||||
})
|
||||
|
||||
const openTeamModal = () => {
|
||||
editTeamModal.value.name = team.value.name
|
||||
editTeamModal.value.color = team.value.color
|
||||
editTeamModal.value.open = true
|
||||
}
|
||||
|
||||
const leaveTeam = async () => {
|
||||
try {
|
||||
await $fetch('/api/team/leave');
|
||||
|
||||
user.value.team = null;
|
||||
|
||||
useToast().success('Successfully left team')
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
useToast().error(e.statusMessage)
|
||||
}
|
||||
}
|
||||
|
||||
const editTeam = async () => {
|
||||
try {
|
||||
await $fetch('/api/team/edit', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
name: editTeamModal.value.name,
|
||||
color: editTeamModal.value.color,
|
||||
}
|
||||
});
|
||||
|
||||
team.value.name = editTeamModal.value.name
|
||||
team.value.color = editTeamModal.value.color
|
||||
|
||||
useToast().success('Successfully modified team')
|
||||
|
||||
editTeamModal.value.open = false
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
useToast().error(e.statusMessage)
|
||||
}
|
||||
}
|
||||
|
||||
const inviteUser = async (usr) => {
|
||||
try {
|
||||
await $fetch('/api/team/invite', {
|
||||
method: 'POST',
|
||||
body: { id: usr._id }
|
||||
});
|
||||
|
||||
usr.teamInvites.push(user.value.team.id);
|
||||
|
||||
useToast().success(`Successfully invited ${usr.username}`)
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
useToast().error(e.statusMessage)
|
||||
}
|
||||
}
|
||||
|
||||
const cancelInvite = async (usr) => {
|
||||
try {
|
||||
await $fetch('/api/team/cancelinvite', {
|
||||
method: 'POST',
|
||||
body: { id: usr._id }
|
||||
});
|
||||
|
||||
usr.teamInvites = usr.teamInvites.filter(a => a !== user.value.team.id);
|
||||
|
||||
useToast().success('Successfully cancelled invited')
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
useToast().error(e.statusMessage)
|
||||
}
|
||||
}
|
||||
|
||||
const promoteUser = async (usr) => {
|
||||
try {
|
||||
await $fetch('/api/team/promote', {
|
||||
method: 'POST',
|
||||
body: { userId: usr._id }
|
||||
});
|
||||
|
||||
usr.team.admin = true
|
||||
|
||||
console.log(usr)
|
||||
console.log(teamMembers)
|
||||
|
||||
useToast().success('Successfully promted user')
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
useToast().error(e.statusMessage)
|
||||
}
|
||||
}
|
||||
|
||||
const demoteUser = async (usr) => {
|
||||
try {
|
||||
await $fetch('/api/team/demote', {
|
||||
method: 'POST',
|
||||
body: { userId: usr._id }
|
||||
});
|
||||
|
||||
usr.team.admin = false
|
||||
|
||||
useToast().success('Successfully demoted user')
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
useToast().error(e.statusMessage)
|
||||
}
|
||||
}
|
||||
</script>
|
@@ -4,28 +4,83 @@
|
||||
</p>
|
||||
<div class="mb-5 flex w-full justify-center gap-10">
|
||||
<span
|
||||
class="font-bold text-primary hover:cursor-pointer" :class="{ 'underline underline-offset-4': !createTeam }"
|
||||
@click="createTeam = false"
|
||||
class="font-bold text-primary hover:cursor-pointer" :class="{ 'underline underline-offset-4': !createTeam.show }"
|
||||
@click="createTeam.show = false"
|
||||
>
|
||||
Team Invites
|
||||
</span>
|
||||
<span
|
||||
class="font-bold text-primary hover:cursor-pointer" :class="{ 'underline underline-offset-4': createTeam }"
|
||||
@click="createTeam = true"
|
||||
class="font-bold text-primary hover:cursor-pointer" :class="{ 'underline underline-offset-4': createTeam.show }"
|
||||
@click="createTeam.show = true"
|
||||
>
|
||||
Create Team
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="!createTeam" class="text-center text-gray-300">
|
||||
You don't have any team invites
|
||||
<div v-if="!createTeam.show" class="text-center text-gray-300">
|
||||
<h2 v-if="!user.teamInvites.length">You don't have any team invites</h2>
|
||||
<div v-else class="mx-auto flex max-w-lg flex-col">
|
||||
<div v-for="team in filteredTeams" :key="team._id" class="flex items-center rounded bg-neutral-800 px-3 py-1 text-left" @click="acceptInvite(team)">
|
||||
<span>
|
||||
{{ team.name }}
|
||||
</span>
|
||||
<Button class="ml-auto">Accept</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="flex w-full flex-col items-center gap-5">
|
||||
<Input class="w-full max-w-sm">Naam / Prefix</Input>
|
||||
<Colorpicker class="w-full max-w-sm" />
|
||||
<Button>Create Team</Button>
|
||||
<Input v-model:value="createTeam.name" class="w-full max-w-sm">Naam / Prefix</Input>
|
||||
<Colorpicker v-model:value="createTeam.color" class="w-full max-w-sm" />
|
||||
<Button @click="handleCreateTeam">Create Team</Button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const createTeam = ref(false)
|
||||
const user = useState('user')
|
||||
|
||||
const teams = await $fetch('/api/team/all')
|
||||
|
||||
const filteredTeams = computed(() => {
|
||||
return teams.filter(a => user.value.teamInvites.includes(a._id));
|
||||
})
|
||||
|
||||
const createTeam = ref({
|
||||
show: false,
|
||||
name: '',
|
||||
color: '',
|
||||
})
|
||||
|
||||
const acceptInvite = async (team) => {
|
||||
try {
|
||||
const response = await $fetch('/api/team/acceptinvite', {
|
||||
method: 'POST',
|
||||
body: { teamId: team._id }
|
||||
})
|
||||
|
||||
console.log(response)
|
||||
|
||||
user.value.team = { id: response._id, admin: false }
|
||||
|
||||
useToast().success('Successfully joined team')
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
useToast().error(e.statusMessage)
|
||||
}
|
||||
}
|
||||
|
||||
const handleCreateTeam = async () => {
|
||||
try {
|
||||
const response = await $fetch('/api/team/create', {
|
||||
method: 'POST',
|
||||
body: { teamName: createTeam.value.name, teamColor: createTeam.value.color }
|
||||
})
|
||||
|
||||
user.value.team = { id: response.insertedId.toString(), admin: true }
|
||||
|
||||
useToast().success('Successfully created team')
|
||||
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
useToast().error(e.statusMessage)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
@@ -1,13 +1,13 @@
|
||||
<template>
|
||||
<div class="h-full bg-neutral-900">
|
||||
<div class="hidden h-full grid-cols-desktoplayout grid-rows-desktoplayout sm:grid">
|
||||
<div v-if="user" class="hidden h-full grid-cols-desktoplayout grid-rows-desktoplayout sm:grid">
|
||||
<LayoutNavbar class="col-span-2" />
|
||||
<LayoutSidebar v-if="user.minecraft.uuid" class="" />
|
||||
<div class="overflow-y-auto px-10 pt-5" :class="{ 'col-span-2': !user.minecraft.uuid }">
|
||||
<NuxtPage />
|
||||
</div>
|
||||
</div>
|
||||
<div class="h-full sm:hidden">
|
||||
<div v-if="user" class="h-full sm:hidden">
|
||||
<div class="overflow-y-auto p-2">
|
||||
<NuxtPage />
|
||||
</div>
|
||||
|
4
web/package-lock.json
generated
4
web/package-lock.json
generated
@@ -3160,7 +3160,7 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@xeovalyte/nuxt-xvui": {
|
||||
"resolved": "git+https://gitea.xeovalyte.dev/xeovalyte/nuxt-xvui.git#e5bb7302f0626b39aa7b3a73bd4109b03702a9c7",
|
||||
"resolved": "git+https://gitea.xeovalyte.dev/xeovalyte/nuxt-xvui.git#53b253beae85b014c2556291aaa9f8f94fc765d5",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@nuxt/eslint-config": "^0.1.1",
|
||||
@@ -15838,7 +15838,7 @@
|
||||
}
|
||||
},
|
||||
"@xeovalyte/nuxt-xvui": {
|
||||
"version": "git+https://gitea.xeovalyte.dev/xeovalyte/nuxt-xvui.git#e5bb7302f0626b39aa7b3a73bd4109b03702a9c7",
|
||||
"version": "git+https://gitea.xeovalyte.dev/xeovalyte/nuxt-xvui.git#53b253beae85b014c2556291aaa9f8f94fc765d5",
|
||||
"from": "@xeovalyte/nuxt-xvui@git+https://gitea.xeovalyte.dev/xeovalyte/nuxt-xvui.git",
|
||||
"requires": {
|
||||
"@nuxt/eslint-config": "^0.1.1",
|
||||
|
@@ -3,6 +3,15 @@
|
||||
<h1 class="text-2xl font-bold text-primary">
|
||||
Team
|
||||
</h1>
|
||||
<TeamNone />
|
||||
<TeamNone v-if="!user.team" />
|
||||
<TeamDefault v-else />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const user = useState('user')
|
||||
|
||||
definePageMeta({
|
||||
middleware: ["auth"]
|
||||
})
|
||||
</script>
|
||||
|
@@ -37,7 +37,7 @@ export default defineEventHandler(async (event) => {
|
||||
},
|
||||
}
|
||||
|
||||
await coll.updateOne({ 'discord.id': userResult.id }, { $set: doc, $setOnInsert: { minecraft: { uuid: null, username: null }, team: null } }, { upsert: true })
|
||||
await coll.updateOne({ 'discord.id': userResult.id }, { $set: doc, $setOnInsert: { minecraft: { uuid: null, username: null }, teamInvites: [] } }, { upsert: true })
|
||||
|
||||
const token = createToken(tokenResponseData.access_token, tokenResponseData.refresh_token, tokenResponseData.expires_in, userResult.id )
|
||||
|
||||
|
18
web/server/api/team/acceptinvite.js
Normal file
18
web/server/api/team/acceptinvite.js
Normal file
@@ -0,0 +1,18 @@
|
||||
import { ObjectId } from 'mongodb'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const { teamId } = await readBody(event);
|
||||
|
||||
const user = await getAuth(event)
|
||||
|
||||
const teamsColl = db.collection('teams')
|
||||
const usersColl = db.collection('users')
|
||||
|
||||
const team = await teamsColl.findOneAndUpdate({ _id: new ObjectId(teamId) }, { $inc: { count: 1 } })
|
||||
|
||||
if (!team.value) return createError({ statusCode: 500, statusMessage: 'Could not find team'})
|
||||
|
||||
await usersColl.updateOne({ _id: new ObjectId(user._id) }, { $set: { 'team.id': teamId, 'team.admin': false }})
|
||||
|
||||
return team
|
||||
});
|
8
web/server/api/team/all.js
Normal file
8
web/server/api/team/all.js
Normal file
@@ -0,0 +1,8 @@
|
||||
export default defineEventHandler(async () => {
|
||||
const teamsColl = db.collection('teams')
|
||||
|
||||
const cursor = teamsColl.find();
|
||||
const teams = await cursor.toArray()
|
||||
|
||||
return teams
|
||||
});
|
16
web/server/api/team/cancelinvite.js
Normal file
16
web/server/api/team/cancelinvite.js
Normal file
@@ -0,0 +1,16 @@
|
||||
import { ObjectId } from 'mongodb'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const { id } = await readBody(event);
|
||||
|
||||
const user = await getAuth(event)
|
||||
|
||||
const teamsColl = db.collection('teams')
|
||||
const usersColl = db.collection('users')
|
||||
|
||||
const team = await teamsColl.findOne({ _id: new ObjectId(user.team.id) })
|
||||
|
||||
usersColl.updateOne({ _id: new ObjectId(id) }, { $pull: { teamInvites: team._id.toString() } })
|
||||
|
||||
return team
|
||||
});
|
21
web/server/api/team/create.post.js
Normal file
21
web/server/api/team/create.post.js
Normal file
@@ -0,0 +1,21 @@
|
||||
export default defineEventHandler(async (event) => {
|
||||
const { teamName, teamColor } = await readBody(event);
|
||||
|
||||
if (!teamName || !teamColor) return createError({ statusCode: 400, statusMessage: 'teamName and teamColor are required' })
|
||||
if (!isHexColor(teamColor)) return createError({ statusCode: 400, statusMessage: 'Team color is not a valid hex code' })
|
||||
|
||||
const user = await getAuth(event)
|
||||
|
||||
if (user.team) return createError({ statusCode: 400, statusMessage: 'User already is in a team' })
|
||||
|
||||
const teamsColl = db.collection('teams')
|
||||
const usersColl = db.collection('users')
|
||||
|
||||
if (await teamsColl.findOne({ name: teamName })) return createError({ statusCode: 400, statusMessage: 'Team name already exists' })
|
||||
|
||||
const response = await teamsColl.insertOne({ name: teamName, color: teamColor, count: 1 })
|
||||
|
||||
await usersColl.findOneAndUpdate({ 'discord.id': user.discord.id }, { $set: { 'team.id': response.insertedId.toString(), 'team.admin': true } })
|
||||
|
||||
return response;
|
||||
});
|
14
web/server/api/team/demote.js
Normal file
14
web/server/api/team/demote.js
Normal file
@@ -0,0 +1,14 @@
|
||||
import { ObjectId } from 'mongodb'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const { userId } = await readBody(event)
|
||||
|
||||
const user = await getAuth(event)
|
||||
|
||||
if (!user.team.admin) return createError({ statusCode: 403, statusMessage: 'Forbidden' })
|
||||
|
||||
const usersColl = db.collection('users')
|
||||
await usersColl.findOneAndUpdate({ _id: new ObjectId(userId) }, { $set: { 'team.admin': false } });
|
||||
|
||||
return { status: 'Success' }
|
||||
});
|
19
web/server/api/team/edit.js
Normal file
19
web/server/api/team/edit.js
Normal file
@@ -0,0 +1,19 @@
|
||||
import { ObjectId } from 'mongodb'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const { name, color } = await readBody(event);
|
||||
|
||||
if (!isHexColor(color)) return createError({ statusCode: 400, statusMessage: 'Team color is not a valid hex code' })
|
||||
|
||||
const user = await getAuth(event)
|
||||
|
||||
const teamsColl = db.collection('teams')
|
||||
|
||||
const team = await teamsColl.findOne({ _id: new ObjectId(user.team.id) });
|
||||
|
||||
if (team.name !== name && await teamsColl.findOne({ name: name })) return createError({ statusCode: 400, statusMessage: 'Team name already exists' })
|
||||
|
||||
await teamsColl.updateOne({ _id: new ObjectId(user.team.id) }, { $set: { name: name, color: color } })
|
||||
|
||||
return team
|
||||
});
|
10
web/server/api/team/index.js
Normal file
10
web/server/api/team/index.js
Normal file
@@ -0,0 +1,10 @@
|
||||
import { ObjectId } from 'mongodb'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const user = await getAuth(event)
|
||||
|
||||
const teamsColl = db.collection('teams')
|
||||
const team = await teamsColl.findOne({ _id: new ObjectId(user.team.id) })
|
||||
|
||||
return team
|
||||
});
|
20
web/server/api/team/invite.js
Normal file
20
web/server/api/team/invite.js
Normal file
@@ -0,0 +1,20 @@
|
||||
import { ObjectId } from 'mongodb'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const { id } = await readBody(event);
|
||||
|
||||
const user = await getAuth(event)
|
||||
|
||||
const teamsColl = db.collection('teams')
|
||||
const usersColl = db.collection('users')
|
||||
|
||||
const team = await teamsColl.findOne({ _id: new ObjectId(user.team.id) })
|
||||
|
||||
const invitedUser = await usersColl.findOne({ _id: new ObjectId(id)});
|
||||
|
||||
if (invitedUser.team) return createError({ statusCode: 400, statusMessage: 'User already is in a team' })
|
||||
|
||||
usersColl.updateOne({ _id: new ObjectId(id) }, { $push: { teamInvites: team._id.toString() } })
|
||||
|
||||
return team
|
||||
});
|
16
web/server/api/team/leave.js
Normal file
16
web/server/api/team/leave.js
Normal file
@@ -0,0 +1,16 @@
|
||||
import { ObjectId } from 'mongodb'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const user = await getAuth(event)
|
||||
|
||||
const teamsColl = db.collection('teams')
|
||||
const team = await teamsColl.findOneAndUpdate({ _id: new ObjectId(user.team.id) }, { $inc: { count: -1 }})
|
||||
|
||||
const usersColl = db.collection('users')
|
||||
await usersColl.findOneAndUpdate({ _id: new ObjectId(user._id) }, { $unset: { team: "" } })
|
||||
|
||||
if (team.value.count <= 1) {
|
||||
await teamsColl.deleteOne({ _id: new ObjectId(user.team.id )})
|
||||
}
|
||||
return team
|
||||
});
|
17
web/server/api/team/members.js
Normal file
17
web/server/api/team/members.js
Normal file
@@ -0,0 +1,17 @@
|
||||
export default defineEventHandler(async (event) => {
|
||||
const user = await getAuth(event)
|
||||
|
||||
const usersColl = db.collection('users')
|
||||
const cursor = usersColl.find({ 'team.id': user.team.id })
|
||||
|
||||
if((await usersColl.countDocuments({ 'team.id': user.team.id })) === 0) {
|
||||
return createError({ statusCode: 500, statusMessage: 'No users were found' })
|
||||
}
|
||||
|
||||
const users = [];
|
||||
for await (const doc of cursor) {
|
||||
users.push(doc)
|
||||
}
|
||||
|
||||
return users;
|
||||
});
|
14
web/server/api/team/promote.js
Normal file
14
web/server/api/team/promote.js
Normal file
@@ -0,0 +1,14 @@
|
||||
import { ObjectId } from 'mongodb'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const { userId } = await readBody(event)
|
||||
|
||||
const user = await getAuth(event)
|
||||
|
||||
if (!user.team.admin) return createError({ statusCode: 403, statusMessage: 'Forbidden' })
|
||||
|
||||
const usersColl = db.collection('users')
|
||||
await usersColl.findOneAndUpdate({ _id: new ObjectId(userId) },{ $set: { 'team.admin': true } });
|
||||
|
||||
return { status: 'Success' }
|
||||
});
|
7
web/server/api/team/unaffiliated.js
Normal file
7
web/server/api/team/unaffiliated.js
Normal file
@@ -0,0 +1,7 @@
|
||||
export default defineEventHandler(async (event) => {
|
||||
const usersColl = db.collection('users')
|
||||
const cursor = usersColl.find({ team: { $exists: false } })
|
||||
const unaffiliatedUsers = await cursor.toArray()
|
||||
|
||||
return unaffiliatedUsers
|
||||
});
|
4
web/server/utils/check.js
Normal file
4
web/server/utils/check.js
Normal file
@@ -0,0 +1,4 @@
|
||||
export const isHexColor = (str) => {
|
||||
const pattern = /^#([0-9A-F]{3}){1,2}$/i;
|
||||
return pattern.test(str);
|
||||
}
|
Reference in New Issue
Block a user