deepspace 0.3.9 → 0.3.11
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/dist/cli.js +96 -18
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +160 -1
- package/dist/index.js +283 -126
- package/dist/index.js.map +1 -1
- package/dist/server.d.ts +172 -2
- package/dist/server.js +581 -1
- package/dist/server.js.map +1 -1
- package/dist/worker.d.ts +252 -2
- package/dist/worker.js +588 -1
- package/dist/worker.js.map +1 -1
- package/features/admin-page/feature.json +29 -0
- package/features/admin-page/src/AdminPage.tsx +292 -0
- package/features/admin-page/src/admin-schema.ts +20 -0
- package/features/ai-chat/feature.json +41 -0
- package/features/ai-chat/src/AiChatPage.tsx +692 -0
- package/features/ai-chat/src/ChatPanel.tsx +1098 -0
- package/features/ai-chat/src/ai-chat-schema.ts +11 -0
- package/features/canvas/feature.json +45 -0
- package/features/canvas/src/CanvasEditorPage.tsx +87 -0
- package/features/canvas/src/CanvasListPage.tsx +202 -0
- package/features/canvas/src/canvas-schema.ts +33 -0
- package/features/canvas/src/components/CanvasToolbar.tsx +170 -0
- package/features/canvas/src/components/CanvasView.tsx +369 -0
- package/features/canvas/src/components/ShapeRenderer.tsx +114 -0
- package/features/cron/feature.json +33 -0
- package/features/cron/src/CronLogPage.tsx +100 -0
- package/features/cron/src/cron.ts +25 -0
- package/features/docs/feature.json +96 -0
- package/features/docs/src/DocsEditorPage.tsx +1012 -0
- package/features/docs/src/DocsListPage.tsx +951 -0
- package/features/docs/src/DocsOutlinePanel.tsx +68 -0
- package/features/docs/src/DocsPresence.tsx +138 -0
- package/features/docs/src/InviteDialog.tsx +336 -0
- package/features/docs/src/LibrarySidebar.tsx +464 -0
- package/features/docs/src/docs-favorites.ts +18 -0
- package/features/docs/src/docs-library-types.ts +15 -0
- package/features/docs/src/docs-schema.ts +80 -0
- package/features/docs/src/docs-ui.css +309 -0
- package/features/docs/src/editor/DocEditorSurface.tsx +257 -0
- package/features/docs/src/editor/DocsTiptapToolbar.tsx +560 -0
- package/features/docs/src/editor/useDocEditor.ts +206 -0
- package/features/docs/src/use-yjs-room-with-awareness.ts +285 -0
- package/features/file-attachments/feature.json +42 -0
- package/features/file-attachments/src/AttachmentsPage.tsx +864 -0
- package/features/file-attachments/src/attachments-constants.ts +9 -0
- package/features/file-attachments/src/attachments-schema.ts +45 -0
- package/features/file-attachments/src/useFilePreview.ts +471 -0
- package/features/file-attachments/src/useMimeTypeDetection.ts +138 -0
- package/features/file-manager/feature.json +24 -0
- package/features/file-manager/src/FileManagerPage.tsx +330 -0
- package/features/integration-test/feature.json +18 -0
- package/features/integration-test/src/IntegrationTestPage.tsx +423 -0
- package/features/integration-test/src/integration.spec.ts +42 -0
- package/features/items/feature.json +33 -0
- package/features/items/src/ItemsPage.tsx +325 -0
- package/features/items/src/items-constants.ts +10 -0
- package/features/items/src/items-schema.ts +27 -0
- package/features/kanban/feature.json +18 -0
- package/features/kanban/src/Kanban.tsx +283 -0
- package/features/landing/feature.json +62 -0
- package/features/landing/src/LandingPage.tsx +627 -0
- package/features/landing/src/primitives.tsx +389 -0
- package/features/landing/src/sections/FAQSection.tsx +110 -0
- package/features/landing/src/sections/FeaturesGridSection.tsx +311 -0
- package/features/landing/src/sections/HowItWorksSection.tsx +62 -0
- package/features/landing/src/sections/LogoCloudSection.tsx +58 -0
- package/features/landing/src/sections/ShowcaseSection.tsx +63 -0
- package/features/landing/src/sections/StatsSection.tsx +31 -0
- package/features/landing/src/sections/TeamSection.tsx +86 -0
- package/features/landing/src/sections/TestimonialsSection.tsx +68 -0
- package/features/landing/src/sections/VideoSection.tsx +71 -0
- package/features/leaderboard/feature.json +33 -0
- package/features/leaderboard/src/LeaderboardPage.tsx +372 -0
- package/features/leaderboard/src/leaderboard-constants.ts +21 -0
- package/features/leaderboard/src/leaderboard-schema.ts +36 -0
- package/features/messaging/feature.json +111 -0
- package/features/messaging/src/ChatMultiPage.tsx +300 -0
- package/features/messaging/src/ChatPage.tsx +75 -0
- package/features/messaging/src/components/chat/ChatHeader.tsx +76 -0
- package/features/messaging/src/components/chat/MessageActionSheet.tsx +168 -0
- package/features/messaging/src/components/chat/MessageInput.tsx +65 -0
- package/features/messaging/src/components/chat/MessageItem.tsx +352 -0
- package/features/messaging/src/components/chat/MessageList.tsx +248 -0
- package/features/messaging/src/components/chat/ReactionPicker.tsx +2 -0
- package/features/messaging/src/components/chat/ThreadPanel.tsx +107 -0
- package/features/messaging/src/components/chat/UserProfilePopover.tsx +158 -0
- package/features/messaging/src/components/chat-multi/AddMemberModal.tsx +188 -0
- package/features/messaging/src/components/chat-multi/BrowseGroupsModal.tsx +163 -0
- package/features/messaging/src/components/chat-multi/ChannelListItem.tsx +147 -0
- package/features/messaging/src/components/chat-multi/ChannelSettingsPanel.tsx +344 -0
- package/features/messaging/src/components/chat-multi/ChannelSidebar.tsx +460 -0
- package/features/messaging/src/components/chat-multi/CreateGroupModal.tsx +190 -0
- package/features/messaging/src/components/chat-multi/InvitationList.tsx +149 -0
- package/features/messaging/src/components/chat-multi/NewMessageModal.tsx +159 -0
- package/features/messaging/src/hooks/useChatChannel.ts +64 -0
- package/features/messaging/src/hooks/useLongPress.ts +74 -0
- package/features/messaging/src/hooks/useMultiChannel.ts +142 -0
- package/features/messaging/src/messaging-schema.ts +25 -0
- package/features/presence-test/feature.json +21 -0
- package/features/presence-test/src/PresenceTestPage.tsx +120 -0
- package/features/sidebar/feature.json +19 -0
- package/features/sidebar/src/AppSidebar.tsx +558 -0
- package/features/tasks/feature.json +33 -0
- package/features/tasks/src/TasksPage.tsx +598 -0
- package/features/tasks/src/tasks-constants.ts +43 -0
- package/features/tasks/src/tasks-schema.ts +56 -0
- package/features/testing/feature.json +49 -0
- package/features/testing/src/TestPage.tsx +145 -0
- package/features/testing/src/cpu-burn-action.ts +51 -0
- package/features/testing/src/subrequest-burn-action.ts +53 -0
- package/features/testing/src/test-schema.ts +23 -0
- package/features/topbar/feature.json +18 -0
- package/features/topbar/src/Topbar.tsx +353 -0
- package/features/tree/feature.json +26 -0
- package/features/tree/src/TreeSidebar.tsx +607 -0
- package/features/tree/src/tree-constants.ts +68 -0
- package/features/tree/src/tree-hooks.ts +258 -0
- package/package.json +4 -2
- package/scripts/add-feature.cjs +654 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Landing Section: Video Embed
|
|
3
|
+
*
|
|
4
|
+
* Click-to-play video section with a glassmorphic container.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { useState } from 'react'
|
|
8
|
+
import { Play } from 'lucide-react'
|
|
9
|
+
import { ScrollReveal, PlaceholderImage, SectionHeading, cn, motion } from '../primitives'
|
|
10
|
+
|
|
11
|
+
const VIDEO_URL = ''
|
|
12
|
+
const VIDEO_THUMBNAIL = ''
|
|
13
|
+
const SECTION_TITLE = 'See it in action'
|
|
14
|
+
const SECTION_SUBTITLE = 'Watch a quick walkthrough of how everything works.'
|
|
15
|
+
|
|
16
|
+
export default function VideoSection() {
|
|
17
|
+
const [isPlaying, setIsPlaying] = useState(false)
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<section className="relative py-28 md:py-36">
|
|
21
|
+
<div className="max-w-4xl mx-auto px-6">
|
|
22
|
+
<SectionHeading tag="Demo" title={SECTION_TITLE} subtitle={SECTION_SUBTITLE} />
|
|
23
|
+
|
|
24
|
+
<ScrollReveal>
|
|
25
|
+
<div className="relative rounded-2xl overflow-hidden border border-foreground/[0.1] shadow-2xl landing-gradient-border">
|
|
26
|
+
{isPlaying && VIDEO_URL ? (
|
|
27
|
+
<div className="aspect-video">
|
|
28
|
+
<iframe
|
|
29
|
+
src={VIDEO_URL}
|
|
30
|
+
className="w-full h-full"
|
|
31
|
+
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
|
32
|
+
allowFullScreen
|
|
33
|
+
title="Demo video"
|
|
34
|
+
/>
|
|
35
|
+
</div>
|
|
36
|
+
) : (
|
|
37
|
+
<button
|
|
38
|
+
onClick={() => { if (VIDEO_URL) setIsPlaying(true) }}
|
|
39
|
+
className="relative w-full group"
|
|
40
|
+
aria-label="Play video"
|
|
41
|
+
>
|
|
42
|
+
{VIDEO_THUMBNAIL ? (
|
|
43
|
+
<div className="aspect-video">
|
|
44
|
+
<img src={VIDEO_THUMBNAIL} alt="Video preview" className="w-full h-full object-cover" />
|
|
45
|
+
<div className="absolute inset-0 bg-background/50 group-hover:bg-background/30 transition-colors duration-300" />
|
|
46
|
+
</div>
|
|
47
|
+
) : (
|
|
48
|
+
<PlaceholderImage label="Video Preview" aspectRatio="aspect-video" className="rounded-none border-0" />
|
|
49
|
+
)}
|
|
50
|
+
|
|
51
|
+
<div className="absolute inset-0 flex items-center justify-center">
|
|
52
|
+
<motion.div
|
|
53
|
+
className={cn(
|
|
54
|
+
'w-20 h-20 rounded-full flex items-center justify-center',
|
|
55
|
+
'bg-foreground/[0.1] backdrop-blur-sm border border-foreground/[0.2]',
|
|
56
|
+
'group-hover:bg-foreground/[0.2] group-hover:scale-110',
|
|
57
|
+
'transition-all duration-300',
|
|
58
|
+
)}
|
|
59
|
+
whileHover={{ scale: 1.1 }}
|
|
60
|
+
>
|
|
61
|
+
<Play className="w-8 h-8 text-foreground ml-1" />
|
|
62
|
+
</motion.div>
|
|
63
|
+
</div>
|
|
64
|
+
</button>
|
|
65
|
+
)}
|
|
66
|
+
</div>
|
|
67
|
+
</ScrollReveal>
|
|
68
|
+
</div>
|
|
69
|
+
</section>
|
|
70
|
+
)
|
|
71
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "leaderboard",
|
|
3
|
+
"name": "Leaderboard",
|
|
4
|
+
"category": "data",
|
|
5
|
+
"description": "Score-based leaderboard with categories, rankings, and admin editing",
|
|
6
|
+
"details": "Ranked leaderboard table with score submission, category filtering, medal badges for top 3, and admin score editing. Uses useQuery with ordering for real-time rankings and useMutations for CRUD.",
|
|
7
|
+
"files": [
|
|
8
|
+
{
|
|
9
|
+
"src": "src/LeaderboardPage.tsx",
|
|
10
|
+
"dest": "src/pages/leaderboard.tsx"
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"src": "src/leaderboard-schema.ts",
|
|
14
|
+
"dest": "src/schemas/leaderboard-schema.ts"
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"src": "src/leaderboard-constants.ts",
|
|
18
|
+
"dest": "src/components/leaderboard/leaderboard-constants.ts"
|
|
19
|
+
}
|
|
20
|
+
],
|
|
21
|
+
"schema": {
|
|
22
|
+
"exportName": "leaderboardSchema",
|
|
23
|
+
"importPath": "./schemas/leaderboard-schema",
|
|
24
|
+
"spreadOperator": false
|
|
25
|
+
},
|
|
26
|
+
"route": {
|
|
27
|
+
"path": "/leaderboard",
|
|
28
|
+
"protected": true
|
|
29
|
+
},
|
|
30
|
+
"requires": [],
|
|
31
|
+
"instructions": [],
|
|
32
|
+
"patterns": []
|
|
33
|
+
}
|
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Leaderboard Page
|
|
3
|
+
*
|
|
4
|
+
* Demonstrates:
|
|
5
|
+
* - useQuery for fetching records with ordering
|
|
6
|
+
* - useMutations for CRUD operations
|
|
7
|
+
* - useUser for current user
|
|
8
|
+
* - Role-based UI (admin can edit anyone's score)
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { useState } from 'react'
|
|
12
|
+
import { useUser } from 'deepspace'
|
|
13
|
+
import { useQuery } from 'deepspace'
|
|
14
|
+
import { useMutations } from 'deepspace'
|
|
15
|
+
import { Button, Modal, EmptyState, Badge } from 'deepspace'
|
|
16
|
+
import { ROLES, type Role } from 'deepspace'
|
|
17
|
+
import { LEADERBOARD_CATEGORY, CATEGORY_CONFIG, type LeaderboardCategory } from '../components/leaderboard/leaderboard-constants'
|
|
18
|
+
|
|
19
|
+
// ============================================================================
|
|
20
|
+
// Types
|
|
21
|
+
// ============================================================================
|
|
22
|
+
|
|
23
|
+
interface LeaderboardEntry {
|
|
24
|
+
playerName: string
|
|
25
|
+
score: number
|
|
26
|
+
category: string
|
|
27
|
+
playerId: string
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface LeaderboardPageProps {
|
|
31
|
+
className?: string
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ============================================================================
|
|
35
|
+
// Submit Score Modal
|
|
36
|
+
// ============================================================================
|
|
37
|
+
|
|
38
|
+
interface SubmitScoreModalProps {
|
|
39
|
+
isOpen: boolean
|
|
40
|
+
onClose: () => void
|
|
41
|
+
onSubmit: (playerName: string, score: number, category: string) => void
|
|
42
|
+
userName: string
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function SubmitScoreModal({ isOpen, onClose, onSubmit, userName }: SubmitScoreModalProps) {
|
|
46
|
+
const [playerName, setPlayerName] = useState(userName)
|
|
47
|
+
const [score, setScore] = useState('')
|
|
48
|
+
const [category, setCategory] = useState<string>(LEADERBOARD_CATEGORY.GENERAL)
|
|
49
|
+
|
|
50
|
+
const handleSubmit = () => {
|
|
51
|
+
const parsed = Number(score)
|
|
52
|
+
if (playerName.trim() && !isNaN(parsed)) {
|
|
53
|
+
onSubmit(playerName.trim(), parsed, category)
|
|
54
|
+
setPlayerName(userName)
|
|
55
|
+
setScore('')
|
|
56
|
+
setCategory(LEADERBOARD_CATEGORY.GENERAL)
|
|
57
|
+
onClose()
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<Modal open={isOpen} onClose={onClose} size="sm">
|
|
63
|
+
<Modal.Header onClose={onClose}>
|
|
64
|
+
<Modal.Title>Submit Score</Modal.Title>
|
|
65
|
+
</Modal.Header>
|
|
66
|
+
<Modal.Body>
|
|
67
|
+
<div className="space-y-4">
|
|
68
|
+
<div>
|
|
69
|
+
<label className="block text-sm font-medium text-muted-foreground mb-2">Player Name</label>
|
|
70
|
+
<input
|
|
71
|
+
type="text"
|
|
72
|
+
value={playerName}
|
|
73
|
+
onChange={(e) => setPlayerName(e.target.value)}
|
|
74
|
+
placeholder="Your name"
|
|
75
|
+
className="w-full px-3 py-2 bg-transparent border border-border rounded-lg text-foreground placeholder-muted-foreground focus:outline-none focus:ring-ring"
|
|
76
|
+
autoFocus
|
|
77
|
+
/>
|
|
78
|
+
</div>
|
|
79
|
+
<div>
|
|
80
|
+
<label className="block text-sm font-medium text-muted-foreground mb-2">Score</label>
|
|
81
|
+
<input
|
|
82
|
+
type="number"
|
|
83
|
+
value={score}
|
|
84
|
+
onChange={(e) => setScore(e.target.value)}
|
|
85
|
+
placeholder="Enter your score"
|
|
86
|
+
className="w-full px-3 py-2 bg-transparent border border-border rounded-lg text-foreground placeholder-muted-foreground focus:outline-none focus:ring-ring"
|
|
87
|
+
/>
|
|
88
|
+
</div>
|
|
89
|
+
<div>
|
|
90
|
+
<label className="block text-sm font-medium text-muted-foreground mb-2">Category</label>
|
|
91
|
+
<select
|
|
92
|
+
value={category}
|
|
93
|
+
onChange={(e) => setCategory(e.target.value)}
|
|
94
|
+
className="w-full px-3 py-2 bg-transparent border border-border rounded-lg text-foreground focus:outline-none focus:ring-ring"
|
|
95
|
+
>
|
|
96
|
+
{Object.values(LEADERBOARD_CATEGORY).map(cat => (
|
|
97
|
+
<option key={cat} value={cat}>{CATEGORY_CONFIG[cat].title}</option>
|
|
98
|
+
))}
|
|
99
|
+
</select>
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
</Modal.Body>
|
|
103
|
+
<Modal.Footer>
|
|
104
|
+
<Button variant="secondary" onClick={onClose}>Cancel</Button>
|
|
105
|
+
<Button onClick={handleSubmit} disabled={!playerName.trim() || !score}>Submit</Button>
|
|
106
|
+
</Modal.Footer>
|
|
107
|
+
</Modal>
|
|
108
|
+
)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ============================================================================
|
|
112
|
+
// Edit Score Modal (Admin)
|
|
113
|
+
// ============================================================================
|
|
114
|
+
|
|
115
|
+
interface EditScoreModalProps {
|
|
116
|
+
isOpen: boolean
|
|
117
|
+
onClose: () => void
|
|
118
|
+
onSave: (score: number) => void
|
|
119
|
+
entry: { recordId: string; data: LeaderboardEntry } | null
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function EditScoreModal({ isOpen, onClose, onSave, entry }: EditScoreModalProps) {
|
|
123
|
+
const [score, setScore] = useState('')
|
|
124
|
+
|
|
125
|
+
const handleOpen = () => {
|
|
126
|
+
if (entry) setScore(String(entry.data.score))
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const handleSave = () => {
|
|
130
|
+
const parsed = Number(score)
|
|
131
|
+
if (!isNaN(parsed)) {
|
|
132
|
+
onSave(parsed)
|
|
133
|
+
onClose()
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Reset score when entry changes
|
|
138
|
+
if (isOpen && entry && score === '') handleOpen()
|
|
139
|
+
|
|
140
|
+
return (
|
|
141
|
+
<Modal open={isOpen} onClose={onClose} size="sm">
|
|
142
|
+
<Modal.Header onClose={onClose}>
|
|
143
|
+
<Modal.Title>Edit Score - {entry?.data.playerName}</Modal.Title>
|
|
144
|
+
</Modal.Header>
|
|
145
|
+
<Modal.Body>
|
|
146
|
+
<div>
|
|
147
|
+
<label className="block text-sm font-medium text-muted-foreground mb-2">Score</label>
|
|
148
|
+
<input
|
|
149
|
+
type="number"
|
|
150
|
+
value={score}
|
|
151
|
+
onChange={(e) => setScore(e.target.value)}
|
|
152
|
+
className="w-full px-3 py-2 bg-transparent border border-border rounded-lg text-foreground placeholder-muted-foreground focus:outline-none focus:ring-ring"
|
|
153
|
+
autoFocus
|
|
154
|
+
/>
|
|
155
|
+
</div>
|
|
156
|
+
</Modal.Body>
|
|
157
|
+
<Modal.Footer>
|
|
158
|
+
<Button variant="secondary" onClick={onClose}>Cancel</Button>
|
|
159
|
+
<Button onClick={handleSave} disabled={!score}>Save</Button>
|
|
160
|
+
</Modal.Footer>
|
|
161
|
+
</Modal>
|
|
162
|
+
)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ============================================================================
|
|
166
|
+
// Rank Badge Component
|
|
167
|
+
// ============================================================================
|
|
168
|
+
|
|
169
|
+
function RankBadge({ rank }: { rank: number }) {
|
|
170
|
+
if (rank === 1) {
|
|
171
|
+
return <span className="text-lg" title="1st place">🥇</span>
|
|
172
|
+
}
|
|
173
|
+
if (rank === 2) {
|
|
174
|
+
return <span className="text-lg" title="2nd place">🥈</span>
|
|
175
|
+
}
|
|
176
|
+
if (rank === 3) {
|
|
177
|
+
return <span className="text-lg" title="3rd place">🥉</span>
|
|
178
|
+
}
|
|
179
|
+
return <span className="text-sm font-medium text-muted-foreground">#{rank}</span>
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// ============================================================================
|
|
183
|
+
// Main Page
|
|
184
|
+
// ============================================================================
|
|
185
|
+
|
|
186
|
+
export default function LeaderboardPage({ className }: LeaderboardPageProps) {
|
|
187
|
+
const { user } = useUser()
|
|
188
|
+
const userRole = (user?.role ?? ROLES.VIEWER) as Role
|
|
189
|
+
const isAdmin = userRole === ROLES.ADMIN
|
|
190
|
+
const canSubmit = userRole === ROLES.MEMBER || userRole === ROLES.ADMIN
|
|
191
|
+
|
|
192
|
+
const [showSubmitModal, setShowSubmitModal] = useState(false)
|
|
193
|
+
const [editingEntry, setEditingEntry] = useState<{ recordId: string; data: LeaderboardEntry } | null>(null)
|
|
194
|
+
|
|
195
|
+
// Query all leaderboard entries, sorted by score descending
|
|
196
|
+
const { records: entries, status } = useQuery<LeaderboardEntry>('leaderboard', {
|
|
197
|
+
orderBy: 'score',
|
|
198
|
+
orderDir: 'desc',
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
// Mutations for leaderboard
|
|
202
|
+
const { create, put, remove } = useMutations<LeaderboardEntry>('leaderboard')
|
|
203
|
+
|
|
204
|
+
const handleSubmit = async (playerName: string, score: number, category: string) => {
|
|
205
|
+
await create({
|
|
206
|
+
playerName,
|
|
207
|
+
score,
|
|
208
|
+
category,
|
|
209
|
+
playerId: user!.id,
|
|
210
|
+
})
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const handleEditSave = async (newScore: number) => {
|
|
214
|
+
if (editingEntry) {
|
|
215
|
+
await put(editingEntry.recordId, { ...editingEntry.data, score: newScore })
|
|
216
|
+
setEditingEntry(null)
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const handleDelete = async (recordId: string) => {
|
|
221
|
+
if (confirm('Are you sure you want to delete this entry?')) {
|
|
222
|
+
await remove(recordId)
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const isLoading = status === 'loading'
|
|
227
|
+
|
|
228
|
+
return (
|
|
229
|
+
<div className={`h-full bg-background overflow-y-auto ${className ?? ''}`}>
|
|
230
|
+
{/* Header */}
|
|
231
|
+
<div className="bg-card/60 backdrop-blur-md border-b border-border sticky top-0 z-10">
|
|
232
|
+
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
|
|
233
|
+
<div className="flex items-center justify-between">
|
|
234
|
+
<div>
|
|
235
|
+
<h1 className="text-2xl font-bold text-foreground">Leaderboard</h1>
|
|
236
|
+
<p className="text-muted-foreground mt-1">
|
|
237
|
+
{canSubmit
|
|
238
|
+
? 'Submit your score and compete for the top spot'
|
|
239
|
+
: 'View the rankings below'
|
|
240
|
+
}
|
|
241
|
+
</p>
|
|
242
|
+
</div>
|
|
243
|
+
|
|
244
|
+
{canSubmit && (
|
|
245
|
+
<Button onClick={() => setShowSubmitModal(true)}>
|
|
246
|
+
<svg className="w-4 h-4 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
247
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
|
|
248
|
+
</svg>
|
|
249
|
+
Submit Score
|
|
250
|
+
</Button>
|
|
251
|
+
)}
|
|
252
|
+
</div>
|
|
253
|
+
</div>
|
|
254
|
+
</div>
|
|
255
|
+
|
|
256
|
+
{/* Content */}
|
|
257
|
+
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
|
258
|
+
{isLoading ? (
|
|
259
|
+
<div className="flex items-center justify-center py-16">
|
|
260
|
+
<div className="w-8 h-8 border-2 border-primary/20 border-t-primary rounded-full animate-spin" />
|
|
261
|
+
</div>
|
|
262
|
+
) : entries.length === 0 ? (
|
|
263
|
+
<EmptyState
|
|
264
|
+
title="No scores yet"
|
|
265
|
+
description={canSubmit
|
|
266
|
+
? "Be the first to submit a score!"
|
|
267
|
+
: "No scores have been submitted yet"
|
|
268
|
+
}
|
|
269
|
+
icon={
|
|
270
|
+
<svg className="w-12 h-12" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
271
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M16 8v8m-4-5v5m-4-2v2m-2 4h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
|
272
|
+
</svg>
|
|
273
|
+
}
|
|
274
|
+
/>
|
|
275
|
+
) : (
|
|
276
|
+
<div className="bg-card/60 rounded-xl border border-border overflow-hidden">
|
|
277
|
+
<table className="w-full">
|
|
278
|
+
<thead>
|
|
279
|
+
<tr className="border-b border-border">
|
|
280
|
+
<th className="px-4 py-3 text-left text-xs font-semibold text-muted-foreground uppercase tracking-wider w-16">Rank</th>
|
|
281
|
+
<th className="px-4 py-3 text-left text-xs font-semibold text-muted-foreground uppercase tracking-wider">Player</th>
|
|
282
|
+
<th className="px-4 py-3 text-left text-xs font-semibold text-muted-foreground uppercase tracking-wider">Category</th>
|
|
283
|
+
<th className="px-4 py-3 text-right text-xs font-semibold text-muted-foreground uppercase tracking-wider">Score</th>
|
|
284
|
+
{(canSubmit || isAdmin) && (
|
|
285
|
+
<th className="px-4 py-3 text-right text-xs font-semibold text-muted-foreground uppercase tracking-wider w-24">Actions</th>
|
|
286
|
+
)}
|
|
287
|
+
</tr>
|
|
288
|
+
</thead>
|
|
289
|
+
<tbody className="divide-y divide-border/50">
|
|
290
|
+
{entries.map((entry, index) => {
|
|
291
|
+
const rank = index + 1
|
|
292
|
+
const isOwn = entry.data.playerId === user?.id
|
|
293
|
+
const cat = entry.data.category as LeaderboardCategory
|
|
294
|
+
const catConfig = CATEGORY_CONFIG[cat] ?? CATEGORY_CONFIG[LEADERBOARD_CATEGORY.GENERAL]
|
|
295
|
+
|
|
296
|
+
return (
|
|
297
|
+
<tr
|
|
298
|
+
key={entry.recordId}
|
|
299
|
+
className={`transition-colors ${isOwn ? 'bg-primary/20/10' : 'hover:bg-muted/30'}`}
|
|
300
|
+
>
|
|
301
|
+
<td className="px-4 py-3">
|
|
302
|
+
<RankBadge rank={rank} />
|
|
303
|
+
</td>
|
|
304
|
+
<td className="px-4 py-3">
|
|
305
|
+
<span className={`font-medium ${isOwn ? 'text-primary' : 'text-foreground'}`}>
|
|
306
|
+
{entry.data.playerName}
|
|
307
|
+
</span>
|
|
308
|
+
{isOwn && (
|
|
309
|
+
<span className="ml-2 text-xs text-primary">(you)</span>
|
|
310
|
+
)}
|
|
311
|
+
</td>
|
|
312
|
+
<td className="px-4 py-3">
|
|
313
|
+
<Badge variant={catConfig.color} size="sm">{catConfig.title}</Badge>
|
|
314
|
+
</td>
|
|
315
|
+
<td className="px-4 py-3 text-right font-mono font-semibold text-foreground">
|
|
316
|
+
{entry.data.score.toLocaleString()}
|
|
317
|
+
</td>
|
|
318
|
+
{(canSubmit || isAdmin) && (
|
|
319
|
+
<td className="px-4 py-3 text-right">
|
|
320
|
+
<div className="flex items-center justify-end gap-1">
|
|
321
|
+
{(isOwn || isAdmin) && (
|
|
322
|
+
<button
|
|
323
|
+
onClick={() => setEditingEntry(entry)}
|
|
324
|
+
className="p-1.5 text-muted-foreground hover:text-foreground hover:bg-muted/60 rounded-lg transition-colors"
|
|
325
|
+
title="Edit score"
|
|
326
|
+
>
|
|
327
|
+
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
328
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
|
|
329
|
+
</svg>
|
|
330
|
+
</button>
|
|
331
|
+
)}
|
|
332
|
+
{(isOwn || isAdmin) && (
|
|
333
|
+
<button
|
|
334
|
+
onClick={() => handleDelete(entry.recordId)}
|
|
335
|
+
className="p-1.5 text-muted-foreground hover:text-destructive hover:bg-destructive/20 rounded-lg transition-colors"
|
|
336
|
+
title="Delete"
|
|
337
|
+
>
|
|
338
|
+
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
339
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
|
340
|
+
</svg>
|
|
341
|
+
</button>
|
|
342
|
+
)}
|
|
343
|
+
</div>
|
|
344
|
+
</td>
|
|
345
|
+
)}
|
|
346
|
+
</tr>
|
|
347
|
+
)
|
|
348
|
+
})}
|
|
349
|
+
</tbody>
|
|
350
|
+
</table>
|
|
351
|
+
</div>
|
|
352
|
+
)}
|
|
353
|
+
</div>
|
|
354
|
+
|
|
355
|
+
{/* Submit Modal */}
|
|
356
|
+
<SubmitScoreModal
|
|
357
|
+
isOpen={showSubmitModal}
|
|
358
|
+
onClose={() => setShowSubmitModal(false)}
|
|
359
|
+
onSubmit={handleSubmit}
|
|
360
|
+
userName={user?.name ?? ''}
|
|
361
|
+
/>
|
|
362
|
+
|
|
363
|
+
{/* Edit Modal (admin or own) */}
|
|
364
|
+
<EditScoreModal
|
|
365
|
+
isOpen={!!editingEntry}
|
|
366
|
+
onClose={() => setEditingEntry(null)}
|
|
367
|
+
onSave={handleEditSave}
|
|
368
|
+
entry={editingEntry}
|
|
369
|
+
/>
|
|
370
|
+
</div>
|
|
371
|
+
)
|
|
372
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Leaderboard Feature - Constants
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { BadgeProps } from 'deepspace'
|
|
6
|
+
|
|
7
|
+
export type BadgeVariant = BadgeProps['variant']
|
|
8
|
+
|
|
9
|
+
export const LEADERBOARD_CATEGORY = {
|
|
10
|
+
GENERAL: 'general',
|
|
11
|
+
SPEED: 'speed',
|
|
12
|
+
ACCURACY: 'accuracy',
|
|
13
|
+
} as const
|
|
14
|
+
|
|
15
|
+
export type LeaderboardCategory = typeof LEADERBOARD_CATEGORY[keyof typeof LEADERBOARD_CATEGORY]
|
|
16
|
+
|
|
17
|
+
export const CATEGORY_CONFIG: Record<LeaderboardCategory, { title: string; color: BadgeVariant }> = {
|
|
18
|
+
[LEADERBOARD_CATEGORY.GENERAL]: { title: 'General', color: 'info' },
|
|
19
|
+
[LEADERBOARD_CATEGORY.SPEED]: { title: 'Speed', color: 'warning' },
|
|
20
|
+
[LEADERBOARD_CATEGORY.ACCURACY]: { title: 'Accuracy', color: 'success' },
|
|
21
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Leaderboard Feature - Schema
|
|
3
|
+
*
|
|
4
|
+
* A score-based leaderboard demonstrating:
|
|
5
|
+
* - ownerField for 'own' permission checks
|
|
6
|
+
* - userBound fields that auto-populate with current user ID
|
|
7
|
+
* - Admin override for updating any entry
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { CollectionSchema } from 'deepspace/worker'
|
|
11
|
+
|
|
12
|
+
export const leaderboardSchema: CollectionSchema = {
|
|
13
|
+
name: 'leaderboard',
|
|
14
|
+
columns: [
|
|
15
|
+
{ name: 'playerName', storage: 'text', interpretation: 'plain', required: true },
|
|
16
|
+
{ name: 'score', storage: 'number', interpretation: 'plain', required: true },
|
|
17
|
+
{ name: 'category', storage: 'text', interpretation: 'plain', default: 'general' },
|
|
18
|
+
{ name: 'playerId', storage: 'text', interpretation: 'plain', required: true, userBound: true, immutable: true },
|
|
19
|
+
],
|
|
20
|
+
ownerField: 'playerId',
|
|
21
|
+
permissions: {
|
|
22
|
+
viewer: {
|
|
23
|
+
read: true,
|
|
24
|
+
create: false,
|
|
25
|
+
update: false,
|
|
26
|
+
delete: false,
|
|
27
|
+
},
|
|
28
|
+
member: {
|
|
29
|
+
read: true,
|
|
30
|
+
create: true,
|
|
31
|
+
update: 'own',
|
|
32
|
+
delete: 'own',
|
|
33
|
+
},
|
|
34
|
+
admin: { read: true, create: true, update: true, delete: true },
|
|
35
|
+
},
|
|
36
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "messaging",
|
|
3
|
+
"name": "Messaging",
|
|
4
|
+
"category": "data",
|
|
5
|
+
"description": "Real-time chat with channels, threads, reactions, and multi-channel support",
|
|
6
|
+
"details": "Complete messaging system with single-channel chat (ChatPage) and multi-channel Slack-like interface (ChatMultiPage). Includes message threads, reactions, read receipts, user profile popovers, channel management (create, browse, settings), DMs, invitations, and member management. Uses SDK messaging hooks (useChannels, useMessages, useReadReceipts).",
|
|
7
|
+
"files": [
|
|
8
|
+
{
|
|
9
|
+
"src": "src/messaging-schema.ts",
|
|
10
|
+
"dest": "src/schemas/messaging-schema.ts"
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"src": "src/ChatPage.tsx",
|
|
14
|
+
"dest": "src/pages/chat.tsx"
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"src": "src/ChatMultiPage.tsx",
|
|
18
|
+
"dest": "src/pages/chat-multi.tsx"
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"src": "src/hooks/useChatChannel.ts",
|
|
22
|
+
"dest": "src/components/messaging/hooks/useChatChannel.ts"
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"src": "src/hooks/useLongPress.ts",
|
|
26
|
+
"dest": "src/components/messaging/hooks/useLongPress.ts"
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"src": "src/hooks/useMultiChannel.ts",
|
|
30
|
+
"dest": "src/components/messaging/hooks/useMultiChannel.ts"
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"src": "src/components/chat/ChatHeader.tsx",
|
|
34
|
+
"dest": "src/components/messaging/chat/ChatHeader.tsx"
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"src": "src/components/chat/MessageList.tsx",
|
|
38
|
+
"dest": "src/components/messaging/chat/MessageList.tsx"
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"src": "src/components/chat/MessageInput.tsx",
|
|
42
|
+
"dest": "src/components/messaging/chat/MessageInput.tsx"
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"src": "src/components/chat/MessageItem.tsx",
|
|
46
|
+
"dest": "src/components/messaging/chat/MessageItem.tsx"
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
"src": "src/components/chat/ThreadPanel.tsx",
|
|
50
|
+
"dest": "src/components/messaging/chat/ThreadPanel.tsx"
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
"src": "src/components/chat/UserProfilePopover.tsx",
|
|
54
|
+
"dest": "src/components/messaging/chat/UserProfilePopover.tsx"
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
"src": "src/components/chat/MessageActionSheet.tsx",
|
|
58
|
+
"dest": "src/components/messaging/chat/MessageActionSheet.tsx"
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
"src": "src/components/chat/ReactionPicker.tsx",
|
|
62
|
+
"dest": "src/components/messaging/chat/ReactionPicker.tsx"
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
"src": "src/components/chat-multi/ChannelSidebar.tsx",
|
|
66
|
+
"dest": "src/components/messaging/chat-multi/ChannelSidebar.tsx"
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
"src": "src/components/chat-multi/ChannelListItem.tsx",
|
|
70
|
+
"dest": "src/components/messaging/chat-multi/ChannelListItem.tsx"
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
"src": "src/components/chat-multi/CreateGroupModal.tsx",
|
|
74
|
+
"dest": "src/components/messaging/chat-multi/CreateGroupModal.tsx"
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
"src": "src/components/chat-multi/NewMessageModal.tsx",
|
|
78
|
+
"dest": "src/components/messaging/chat-multi/NewMessageModal.tsx"
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
"src": "src/components/chat-multi/BrowseGroupsModal.tsx",
|
|
82
|
+
"dest": "src/components/messaging/chat-multi/BrowseGroupsModal.tsx"
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
"src": "src/components/chat-multi/ChannelSettingsPanel.tsx",
|
|
86
|
+
"dest": "src/components/messaging/chat-multi/ChannelSettingsPanel.tsx"
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
"src": "src/components/chat-multi/AddMemberModal.tsx",
|
|
90
|
+
"dest": "src/components/messaging/chat-multi/AddMemberModal.tsx"
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
"src": "src/components/chat-multi/InvitationList.tsx",
|
|
94
|
+
"dest": "src/components/messaging/chat-multi/InvitationList.tsx"
|
|
95
|
+
}
|
|
96
|
+
],
|
|
97
|
+
"schema": {
|
|
98
|
+
"exportName": "messagingSchemas",
|
|
99
|
+
"importPath": "./schemas/messaging-schema",
|
|
100
|
+
"spreadOperator": true
|
|
101
|
+
},
|
|
102
|
+
"route": {
|
|
103
|
+
"path": "/chat",
|
|
104
|
+
"protected": true
|
|
105
|
+
},
|
|
106
|
+
"requires": [],
|
|
107
|
+
"instructions": [
|
|
108
|
+
"Optionally add a second route for /chat-multi -> ChatMultiPage"
|
|
109
|
+
],
|
|
110
|
+
"patterns": []
|
|
111
|
+
}
|