@shadowob/shared 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/__tests__/constants.test.ts +69 -0
- package/__tests__/utils.test.ts +80 -0
- package/package.json +20 -0
- package/src/constants/events.ts +28 -0
- package/src/constants/index.ts +2 -0
- package/src/constants/limits.ts +30 -0
- package/src/index.ts +3 -0
- package/src/types/agent.types.ts +44 -0
- package/src/types/channel.types.ts +24 -0
- package/src/types/index.ts +5 -0
- package/src/types/message.types.ts +89 -0
- package/src/types/server.types.ts +38 -0
- package/src/types/user.types.ts +40 -0
- package/src/utils/index.ts +21 -0
- package/tsconfig.json +8 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { CLIENT_EVENTS, SERVER_EVENTS } from '../src/constants/events'
|
|
3
|
+
import { LIMITS } from '../src/constants/limits'
|
|
4
|
+
|
|
5
|
+
describe('CLIENT_EVENTS', () => {
|
|
6
|
+
it('should have all expected client events', () => {
|
|
7
|
+
expect(CLIENT_EVENTS.CHANNEL_JOIN).toBe('channel:join')
|
|
8
|
+
expect(CLIENT_EVENTS.CHANNEL_LEAVE).toBe('channel:leave')
|
|
9
|
+
expect(CLIENT_EVENTS.MESSAGE_SEND).toBe('message:send')
|
|
10
|
+
expect(CLIENT_EVENTS.MESSAGE_TYPING).toBe('message:typing')
|
|
11
|
+
expect(CLIENT_EVENTS.PRESENCE_UPDATE).toBe('presence:update')
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
it('should be readonly', () => {
|
|
15
|
+
const events = CLIENT_EVENTS
|
|
16
|
+
expect(Object.keys(events)).toHaveLength(5)
|
|
17
|
+
})
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
describe('SERVER_EVENTS', () => {
|
|
21
|
+
it('should have all expected server events', () => {
|
|
22
|
+
expect(SERVER_EVENTS.MESSAGE_NEW).toBe('message:new')
|
|
23
|
+
expect(SERVER_EVENTS.MESSAGE_UPDATE).toBe('message:update')
|
|
24
|
+
expect(SERVER_EVENTS.MESSAGE_DELETE).toBe('message:delete')
|
|
25
|
+
expect(SERVER_EVENTS.MEMBER_TYPING).toBe('member:typing')
|
|
26
|
+
expect(SERVER_EVENTS.MEMBER_JOIN).toBe('member:join')
|
|
27
|
+
expect(SERVER_EVENTS.MEMBER_LEAVE).toBe('member:leave')
|
|
28
|
+
expect(SERVER_EVENTS.PRESENCE_CHANGE).toBe('presence:change')
|
|
29
|
+
expect(SERVER_EVENTS.REACTION_ADD).toBe('reaction:add')
|
|
30
|
+
expect(SERVER_EVENTS.REACTION_REMOVE).toBe('reaction:remove')
|
|
31
|
+
expect(SERVER_EVENTS.NOTIFICATION_NEW).toBe('notification:new')
|
|
32
|
+
expect(SERVER_EVENTS.DM_MESSAGE_NEW).toBe('dm:message:new')
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('should have 11 server events', () => {
|
|
36
|
+
expect(Object.keys(SERVER_EVENTS)).toHaveLength(11)
|
|
37
|
+
})
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
describe('LIMITS', () => {
|
|
41
|
+
it('should define message limits', () => {
|
|
42
|
+
expect(LIMITS.MESSAGE_CONTENT_MAX).toBe(4000)
|
|
43
|
+
expect(LIMITS.MESSAGES_PER_PAGE).toBe(50)
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('should define username limits', () => {
|
|
47
|
+
expect(LIMITS.USERNAME_MIN).toBe(3)
|
|
48
|
+
expect(LIMITS.USERNAME_MAX).toBe(32)
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('should define file upload limit', () => {
|
|
52
|
+
expect(LIMITS.FILE_UPLOAD_MAX_SIZE).toBe(10 * 1024 * 1024)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('should define server/channel limits', () => {
|
|
56
|
+
expect(LIMITS.SERVER_NAME_MAX).toBe(100)
|
|
57
|
+
expect(LIMITS.CHANNEL_NAME_MAX).toBe(100)
|
|
58
|
+
expect(LIMITS.SERVERS_PER_USER_MAX).toBe(100)
|
|
59
|
+
expect(LIMITS.CHANNELS_PER_SERVER_MAX).toBe(200)
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('should define invite code length', () => {
|
|
63
|
+
expect(LIMITS.INVITE_CODE_LENGTH).toBe(8)
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it('should define password min length', () => {
|
|
67
|
+
expect(LIMITS.PASSWORD_MIN).toBe(8)
|
|
68
|
+
})
|
|
69
|
+
})
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { formatDate, generateInviteCode, isValidEmail, slugify } from '../src/utils'
|
|
3
|
+
|
|
4
|
+
describe('generateInviteCode', () => {
|
|
5
|
+
it('should generate a string of length 8', () => {
|
|
6
|
+
const code = generateInviteCode()
|
|
7
|
+
expect(code).toHaveLength(8)
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
it('should only contain alphanumeric characters', () => {
|
|
11
|
+
const code = generateInviteCode()
|
|
12
|
+
expect(code).toMatch(/^[A-Za-z0-9]+$/)
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
it('should generate unique codes', () => {
|
|
16
|
+
const codes = new Set(Array.from({ length: 100 }, () => generateInviteCode()))
|
|
17
|
+
expect(codes.size).toBe(100)
|
|
18
|
+
})
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
describe('formatDate', () => {
|
|
22
|
+
it('should format a Date object to ISO string', () => {
|
|
23
|
+
const date = new Date('2024-01-15T12:30:00Z')
|
|
24
|
+
expect(formatDate(date)).toBe('2024-01-15T12:30:00.000Z')
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('should format a date string to ISO string', () => {
|
|
28
|
+
const result = formatDate('2024-01-15T12:30:00Z')
|
|
29
|
+
expect(result).toBe('2024-01-15T12:30:00.000Z')
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('should handle various date string formats', () => {
|
|
33
|
+
const result = formatDate('2024-01-15')
|
|
34
|
+
expect(result).toContain('2024-01-15')
|
|
35
|
+
})
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
describe('isValidEmail', () => {
|
|
39
|
+
it('should return true for valid emails', () => {
|
|
40
|
+
expect(isValidEmail('user@example.com')).toBe(true)
|
|
41
|
+
expect(isValidEmail('first.last@domain.org')).toBe(true)
|
|
42
|
+
expect(isValidEmail('user+tag@sub.domain.com')).toBe(true)
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('should return false for invalid emails', () => {
|
|
46
|
+
expect(isValidEmail('')).toBe(false)
|
|
47
|
+
expect(isValidEmail('user')).toBe(false)
|
|
48
|
+
expect(isValidEmail('user@')).toBe(false)
|
|
49
|
+
expect(isValidEmail('@domain.com')).toBe(false)
|
|
50
|
+
expect(isValidEmail('user @domain.com')).toBe(false)
|
|
51
|
+
expect(isValidEmail('user@domain')).toBe(false)
|
|
52
|
+
})
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
describe('slugify', () => {
|
|
56
|
+
it('should convert text to lowercase slug', () => {
|
|
57
|
+
expect(slugify('Hello World')).toBe('hello-world')
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('should replace special characters with hyphens', () => {
|
|
61
|
+
expect(slugify('Hello, World!')).toBe('hello-world')
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('should trim leading/trailing hyphens', () => {
|
|
65
|
+
expect(slugify(' Hello World ')).toBe('hello-world')
|
|
66
|
+
expect(slugify('---hello---')).toBe('hello')
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('should handle multiple consecutive special characters', () => {
|
|
70
|
+
expect(slugify('hello...world')).toBe('hello-world')
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('should keep numbers', () => {
|
|
74
|
+
expect(slugify('Version 2.0')).toBe('version-2-0')
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
it('should handle empty string', () => {
|
|
78
|
+
expect(slugify('')).toBe('')
|
|
79
|
+
})
|
|
80
|
+
})
|
package/package.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@shadowob/shared",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "./src/index.ts",
|
|
6
|
+
"types": "./src/index.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./src/index.ts",
|
|
9
|
+
"./types": "./src/types/index.ts",
|
|
10
|
+
"./constants": "./src/constants/index.ts",
|
|
11
|
+
"./utils": "./src/utils/index.ts"
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"nanoid": "^5.1.5"
|
|
15
|
+
},
|
|
16
|
+
"scripts": {
|
|
17
|
+
"test": "vitest run",
|
|
18
|
+
"test:watch": "vitest"
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// ─── Client → Server ───
|
|
2
|
+
|
|
3
|
+
export const CLIENT_EVENTS = {
|
|
4
|
+
CHANNEL_JOIN: 'channel:join',
|
|
5
|
+
CHANNEL_LEAVE: 'channel:leave',
|
|
6
|
+
MESSAGE_SEND: 'message:send',
|
|
7
|
+
MESSAGE_TYPING: 'message:typing',
|
|
8
|
+
PRESENCE_UPDATE: 'presence:update',
|
|
9
|
+
} as const
|
|
10
|
+
|
|
11
|
+
// ─── Server → Client ───
|
|
12
|
+
|
|
13
|
+
export const SERVER_EVENTS = {
|
|
14
|
+
MESSAGE_NEW: 'message:new',
|
|
15
|
+
MESSAGE_UPDATE: 'message:update',
|
|
16
|
+
MESSAGE_DELETE: 'message:delete',
|
|
17
|
+
MEMBER_TYPING: 'member:typing',
|
|
18
|
+
MEMBER_JOIN: 'member:join',
|
|
19
|
+
MEMBER_LEAVE: 'member:leave',
|
|
20
|
+
PRESENCE_CHANGE: 'presence:change',
|
|
21
|
+
REACTION_ADD: 'reaction:add',
|
|
22
|
+
REACTION_REMOVE: 'reaction:remove',
|
|
23
|
+
NOTIFICATION_NEW: 'notification:new',
|
|
24
|
+
DM_MESSAGE_NEW: 'dm:message:new',
|
|
25
|
+
} as const
|
|
26
|
+
|
|
27
|
+
export type ClientEvent = (typeof CLIENT_EVENTS)[keyof typeof CLIENT_EVENTS]
|
|
28
|
+
export type ServerEvent = (typeof SERVER_EVENTS)[keyof typeof SERVER_EVENTS]
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export const LIMITS = {
|
|
2
|
+
/** Max message content length */
|
|
3
|
+
MESSAGE_CONTENT_MAX: 4000,
|
|
4
|
+
/** Max username length */
|
|
5
|
+
USERNAME_MAX: 32,
|
|
6
|
+
/** Min username length */
|
|
7
|
+
USERNAME_MIN: 3,
|
|
8
|
+
/** Max display name length */
|
|
9
|
+
DISPLAY_NAME_MAX: 64,
|
|
10
|
+
/** Max server name length */
|
|
11
|
+
SERVER_NAME_MAX: 100,
|
|
12
|
+
/** Max channel name length */
|
|
13
|
+
CHANNEL_NAME_MAX: 100,
|
|
14
|
+
/** Max thread name length */
|
|
15
|
+
THREAD_NAME_MAX: 100,
|
|
16
|
+
/** Max file upload size (10MB) */
|
|
17
|
+
FILE_UPLOAD_MAX_SIZE: 10 * 1024 * 1024,
|
|
18
|
+
/** Messages per page (cursor pagination) */
|
|
19
|
+
MESSAGES_PER_PAGE: 50,
|
|
20
|
+
/** Max servers per user */
|
|
21
|
+
SERVERS_PER_USER_MAX: 100,
|
|
22
|
+
/** Max channels per server */
|
|
23
|
+
CHANNELS_PER_SERVER_MAX: 200,
|
|
24
|
+
/** Invite code length */
|
|
25
|
+
INVITE_CODE_LENGTH: 8,
|
|
26
|
+
/** Password min length */
|
|
27
|
+
PASSWORD_MIN: 8,
|
|
28
|
+
/** Max reactions per message per user */
|
|
29
|
+
REACTIONS_PER_MESSAGE_MAX: 20,
|
|
30
|
+
} as const
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export type AgentStatus = 'running' | 'stopped' | 'error'
|
|
2
|
+
|
|
3
|
+
export type AgentKernelType = 'claude-code' | 'cursor' | 'mcp-server' | 'custom'
|
|
4
|
+
|
|
5
|
+
export type AgentCapability =
|
|
6
|
+
| 'chat'
|
|
7
|
+
| 'code-gen'
|
|
8
|
+
| 'code-review'
|
|
9
|
+
| 'research'
|
|
10
|
+
| 'tools'
|
|
11
|
+
| 'file-access'
|
|
12
|
+
|
|
13
|
+
export interface Agent {
|
|
14
|
+
id: string
|
|
15
|
+
userId: string
|
|
16
|
+
kernelType: AgentKernelType
|
|
17
|
+
config: Record<string, unknown>
|
|
18
|
+
containerId: string | null
|
|
19
|
+
status: AgentStatus
|
|
20
|
+
ownerId: string
|
|
21
|
+
createdAt: string
|
|
22
|
+
updatedAt: string
|
|
23
|
+
user?: {
|
|
24
|
+
id: string
|
|
25
|
+
username: string
|
|
26
|
+
displayName: string
|
|
27
|
+
avatarUrl: string | null
|
|
28
|
+
}
|
|
29
|
+
capabilities?: AgentCapability[]
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface CreateAgentRequest {
|
|
33
|
+
name: string
|
|
34
|
+
kernelType: AgentKernelType
|
|
35
|
+
config?: Record<string, unknown>
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface AgentInfo {
|
|
39
|
+
id: string
|
|
40
|
+
name: string
|
|
41
|
+
kernelType: AgentKernelType
|
|
42
|
+
status: AgentStatus
|
|
43
|
+
capabilities: AgentCapability[]
|
|
44
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export type ChannelType = 'text' | 'voice' | 'announcement'
|
|
2
|
+
|
|
3
|
+
export interface Channel {
|
|
4
|
+
id: string
|
|
5
|
+
name: string
|
|
6
|
+
type: ChannelType
|
|
7
|
+
serverId: string
|
|
8
|
+
topic: string | null
|
|
9
|
+
position: number
|
|
10
|
+
createdAt: string
|
|
11
|
+
updatedAt: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface CreateChannelRequest {
|
|
15
|
+
name: string
|
|
16
|
+
type?: ChannelType
|
|
17
|
+
topic?: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface UpdateChannelRequest {
|
|
21
|
+
name?: string
|
|
22
|
+
topic?: string
|
|
23
|
+
position?: number
|
|
24
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
export interface Message {
|
|
2
|
+
id: string
|
|
3
|
+
content: string
|
|
4
|
+
channelId: string
|
|
5
|
+
authorId: string
|
|
6
|
+
threadId: string | null
|
|
7
|
+
replyToId: string | null
|
|
8
|
+
isEdited: boolean
|
|
9
|
+
isPinned: boolean
|
|
10
|
+
createdAt: string
|
|
11
|
+
updatedAt: string
|
|
12
|
+
author?: {
|
|
13
|
+
id: string
|
|
14
|
+
username: string
|
|
15
|
+
displayName: string
|
|
16
|
+
avatarUrl: string | null
|
|
17
|
+
isBot: boolean
|
|
18
|
+
}
|
|
19
|
+
attachments?: Attachment[]
|
|
20
|
+
reactions?: ReactionGroup[]
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface Attachment {
|
|
24
|
+
id: string
|
|
25
|
+
messageId: string
|
|
26
|
+
filename: string
|
|
27
|
+
url: string
|
|
28
|
+
contentType: string
|
|
29
|
+
size: number
|
|
30
|
+
width: number | null
|
|
31
|
+
height: number | null
|
|
32
|
+
createdAt: string
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface ReactionGroup {
|
|
36
|
+
emoji: string
|
|
37
|
+
count: number
|
|
38
|
+
userIds: string[]
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface Thread {
|
|
42
|
+
id: string
|
|
43
|
+
name: string
|
|
44
|
+
channelId: string
|
|
45
|
+
parentMessageId: string
|
|
46
|
+
creatorId: string
|
|
47
|
+
isArchived: boolean
|
|
48
|
+
createdAt: string
|
|
49
|
+
updatedAt: string
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface SendMessageRequest {
|
|
53
|
+
content: string
|
|
54
|
+
threadId?: string
|
|
55
|
+
replyToId?: string
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface UpdateMessageRequest {
|
|
59
|
+
content: string
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export type NotificationType = 'mention' | 'reply' | 'dm' | 'system'
|
|
63
|
+
|
|
64
|
+
export interface Notification {
|
|
65
|
+
id: string
|
|
66
|
+
userId: string
|
|
67
|
+
type: NotificationType
|
|
68
|
+
title: string
|
|
69
|
+
body: string | null
|
|
70
|
+
referenceId: string | null
|
|
71
|
+
referenceType: string | null
|
|
72
|
+
isRead: boolean
|
|
73
|
+
createdAt: string
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface DmChannel {
|
|
77
|
+
id: string
|
|
78
|
+
userAId: string
|
|
79
|
+
userBId: string
|
|
80
|
+
lastMessageAt: string | null
|
|
81
|
+
createdAt: string
|
|
82
|
+
otherUser?: {
|
|
83
|
+
id: string
|
|
84
|
+
username: string
|
|
85
|
+
displayName: string
|
|
86
|
+
avatarUrl: string | null
|
|
87
|
+
status: string
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export interface Server {
|
|
2
|
+
id: string
|
|
3
|
+
name: string
|
|
4
|
+
iconUrl: string | null
|
|
5
|
+
ownerId: string
|
|
6
|
+
inviteCode: string
|
|
7
|
+
createdAt: string
|
|
8
|
+
updatedAt: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface CreateServerRequest {
|
|
12
|
+
name: string
|
|
13
|
+
iconUrl?: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface UpdateServerRequest {
|
|
17
|
+
name?: string
|
|
18
|
+
iconUrl?: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type MemberRole = 'owner' | 'admin' | 'member'
|
|
22
|
+
|
|
23
|
+
export interface Member {
|
|
24
|
+
id: string
|
|
25
|
+
userId: string
|
|
26
|
+
serverId: string
|
|
27
|
+
role: MemberRole
|
|
28
|
+
nickname: string | null
|
|
29
|
+
joinedAt: string
|
|
30
|
+
user?: {
|
|
31
|
+
id: string
|
|
32
|
+
username: string
|
|
33
|
+
displayName: string
|
|
34
|
+
avatarUrl: string | null
|
|
35
|
+
status: string
|
|
36
|
+
isBot: boolean
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export type UserStatus = 'online' | 'idle' | 'dnd' | 'offline'
|
|
2
|
+
|
|
3
|
+
export interface User {
|
|
4
|
+
id: string
|
|
5
|
+
email: string
|
|
6
|
+
username: string
|
|
7
|
+
displayName: string
|
|
8
|
+
avatarUrl: string | null
|
|
9
|
+
status: UserStatus
|
|
10
|
+
isBot: boolean
|
|
11
|
+
createdAt: string
|
|
12
|
+
updatedAt: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface UserProfile {
|
|
16
|
+
id: string
|
|
17
|
+
username: string
|
|
18
|
+
displayName: string
|
|
19
|
+
avatarUrl: string | null
|
|
20
|
+
status: UserStatus
|
|
21
|
+
isBot: boolean
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface LoginRequest {
|
|
25
|
+
email: string
|
|
26
|
+
password: string
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface RegisterRequest {
|
|
30
|
+
email: string
|
|
31
|
+
username: string
|
|
32
|
+
displayName: string
|
|
33
|
+
password: string
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface AuthResponse {
|
|
37
|
+
user: User
|
|
38
|
+
accessToken: string
|
|
39
|
+
refreshToken: string
|
|
40
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { customAlphabet } from 'nanoid'
|
|
2
|
+
|
|
3
|
+
const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
|
|
4
|
+
|
|
5
|
+
export const generateInviteCode = customAlphabet(alphabet, 8)
|
|
6
|
+
|
|
7
|
+
export function formatDate(date: string | Date): string {
|
|
8
|
+
const d = typeof date === 'string' ? new Date(date) : date
|
|
9
|
+
return d.toISOString()
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function isValidEmail(email: string): boolean {
|
|
13
|
+
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function slugify(text: string): string {
|
|
17
|
+
return text
|
|
18
|
+
.toLowerCase()
|
|
19
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
20
|
+
.replace(/^-+|-+$/g, '')
|
|
21
|
+
}
|