Skip to content

Commit 1632989

Browse files
committed
coderabbit issues solved
1 parent 5222228 commit 1632989

18 files changed

+499
-47
lines changed

package-lock.json

Lines changed: 54 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/app/actions/collaborationActions.ts

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"use server"
22

33
import { db } from '@/lib/db';
4-
import { users, document_collaborators } from '@/lib/db/schema';
4+
import { users, document_collaborators, workspace_members } from '@/lib/db/schema';
55
import { eq, and } from 'drizzle-orm';
66
import { revalidatePath } from 'next/cache';
77
import { broadcastNotesUpdated } from '@/lib/realtime'
@@ -98,6 +98,65 @@ export async function getUserById(userId: string) {
9898
}
9999
}
100100

101+
// Search users by prefix (case-insensitive) on email or name. Returns up to 10 results.
102+
export async function searchUsersByPrefix(prefix: string, documentId?: string) {
103+
if (!prefix || prefix.trim().length === 0) return []
104+
const q = prefix.trim().toLowerCase()
105+
try {
106+
const { sql } = await import('drizzle-orm')
107+
const pattern = q + '%'
108+
109+
// If a documentId is provided, try to limit suggestions to users in the same workspace
110+
let workspaceId: string | null = null
111+
let docAuthorId: string | null = null
112+
if (documentId) {
113+
try {
114+
const docRows = await db.select({ workspace_id: documents.workspace_id, author_id: documents.author_id }).from(documents).where(eq(documents.id, documentId)).limit(1)
115+
if (docRows.length > 0) {
116+
// @ts-ignore
117+
workspaceId = docRows[0].workspace_id || null
118+
// @ts-ignore
119+
docAuthorId = docRows[0].author_id || null
120+
}
121+
} catch {}
122+
}
123+
124+
// Build base where clause for prefix matching
125+
const whereClause = sql`(LOWER(${users.email}) LIKE ${pattern}) OR (LOWER(${users.name}) LIKE ${pattern})`
126+
127+
if (workspaceId) {
128+
// Fetch workspace member users that match the prefix
129+
const rows = await db
130+
.select({ id: users.id, name: users.name, email: users.email, image: users.image })
131+
.from(users)
132+
.leftJoin(workspace_members, eq(workspace_members.user_id, users.id))
133+
.where(sql`(${workspace_members.workspace_id} = ${workspaceId}) AND (${whereClause})`)
134+
.limit(10)
135+
136+
// Exclude existing collaborators for the document and the document author
137+
if (documentId) {
138+
const existing = await db.select({ id: document_collaborators.userId }).from(document_collaborators).where(eq(document_collaborators.documentId, documentId))
139+
const excluded = new Set(existing.map((r: any) => r.id))
140+
if (docAuthorId) excluded.add(docAuthorId)
141+
return (rows as any[]).filter(r => !excluded.has(r.id)).slice(0, 10)
142+
}
143+
144+
return rows
145+
}
146+
147+
// Fallback: global search across users when no workspace context
148+
const rows = await db
149+
.select({ id: users.id, name: users.name, email: users.email, image: users.image })
150+
.from(users)
151+
.where(whereClause)
152+
.limit(10)
153+
154+
return rows
155+
} catch {
156+
return []
157+
}
158+
}
159+
101160
// Return the current user's role for a given document
102161
export async function getUserRoleForDocument(documentId: string) {
103162
const session = await getServerSession(topAuthOptions)

src/app/layout.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { ClientProviders } from "@/components/ClientProviders";
44
import ConditionalHeader from "@/components/ConditionalHeader";
55
import ConditionalFooter from "@/components/ConditionalFooter";
66
import { Toaster } from "@/components/ui/toaster";
7+
import BackToTop from '@/components/BackToTop';
78

89
export const metadata: Metadata = {
910
title: "Keynotes",
@@ -27,6 +28,7 @@ export default function RootLayout({
2728
<ConditionalFooter />
2829
</div>
2930
<Toaster />
31+
<BackToTop />
3032
</ClientProviders>
3133
</body>
3234
</html>

src/components/BackToTop.tsx

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
"use client";
2+
3+
import { useEffect, useState } from "react";
4+
import { Button } from "@/components/ui/button";
5+
import { ArrowUp } from "lucide-react";
6+
7+
export default function BackToTop() {
8+
const [visible, setVisible] = useState(false);
9+
10+
useEffect(() => {
11+
const onScroll = () => {
12+
setVisible(window.scrollY > 200);
13+
};
14+
15+
// run on mount
16+
onScroll();
17+
18+
window.addEventListener("scroll", onScroll, { passive: true });
19+
return () => window.removeEventListener("scroll", onScroll);
20+
}, []);
21+
22+
const handleClick = () => {
23+
window.scrollTo({ top: 0, behavior: "smooth" });
24+
};
25+
26+
if (!visible) return null;
27+
28+
return (
29+
<div className="fixed bottom-6 left-6 z-50">
30+
<Button
31+
onClick={handleClick}
32+
className="rounded-full w-12 h-12 shadow-lg"
33+
size="icon"
34+
aria-label="Back to top"
35+
>
36+
<ArrowUp className="h-5 w-5" />
37+
</Button>
38+
</div>
39+
);
40+
}

src/components/ClientProviders.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import { ThemeProvider } from './ThemeProvider'
44
import AuthProvider from './AuthProvider'
5+
import { SplashProvider } from './SplashProvider'
56

67
interface ClientProvidersProps {
78
children: React.ReactNode
@@ -12,7 +13,9 @@ export function ClientProviders({ children }: ClientProvidersProps) {
1213
return (
1314
<ThemeProvider>
1415
<AuthProvider>
15-
{children}
16+
<SplashProvider>
17+
{children}
18+
</SplashProvider>
1619
</AuthProvider>
1720
</ThemeProvider>
1821
)

src/components/ConditionalFooter.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ export default function ConditionalFooter() {
77
const pathname = usePathname()
88

99
// Don't show footer on dashboard or notes pages
10-
if (pathname.startsWith('/dashboard') || pathname.startsWith('/notes')) {
10+
if (pathname && (pathname.startsWith('/dashboard') || pathname.startsWith('/notes'))) {
1111
return null
1212
}
1313

src/components/ConditionalHeader.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ export default function ConditionalHeader() {
77
const pathname = usePathname()
88

99
// Don't show header on notes pages or dashboard
10-
if (pathname.startsWith('/notes') || pathname.startsWith('/dashboard')) {
10+
if (pathname && (pathname.startsWith('/notes') || pathname.startsWith('/dashboard'))) {
1111
return null
1212
}
1313

src/components/Header.tsx

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,12 @@
22

33
import { ThemeToggleButton } from './ThemeToggleButton'
44
import { UserNav } from './UserNav'
5+
import { Sparkles } from 'lucide-react'
6+
import { useSplash } from './SplashProvider'
57

68
export function Header() {
9+
const { enabled, toggle } = useSplash()
10+
711
return (
812
<header className="border-b sticky top-0 z-20 bg-background">
913
<div className="flex items-center justify-between px-6 py-4">
@@ -17,6 +21,26 @@ export function Header() {
1721
</nav>
1822

1923
<div className="flex items-center gap-4">
24+
{/* Sparkle toggle placed beside theme toggle */}
25+
<div className="relative group">
26+
<button
27+
onClick={toggle}
28+
aria-describedby="splash-tooltip"
29+
className={`shrink-0 px-2.5 py-1.5 border-2 rounded-md transition-all duration-200 flex items-center gap-1 ${enabled ? 'border-purple-600 bg-purple-50' : 'border-purple-500 bg-white hover:bg-purple-100'}`}
30+
style={{ minWidth: '56px', minHeight: '34px' }}
31+
aria-pressed={enabled}
32+
>
33+
<Sparkles className="w-5 h-5 text-purple-600" />
34+
</button>
35+
{/* Simple tooltip */}
36+
<span
37+
id="splash-tooltip"
38+
role="status"
39+
className="pointer-events-none absolute left-1/2 -translate-x-1/2 -bottom-8 opacity-0 scale-95 transform rounded-md bg-muted/90 text-sm text-muted-foreground px-2 py-1 transition-all duration-150 group-hover:opacity-100"
40+
>
41+
Toggle splash effect
42+
</span>
43+
</div>
2044
<ThemeToggleButton />
2145
<UserNav />
2246
</div>

src/components/SplashProvider.tsx

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
'use client'
2+
3+
import React, { createContext, useContext, useState } from 'react'
4+
import SplashCursor from './SplashCursor'
5+
6+
type SplashContextType = {
7+
enabled: boolean
8+
toggle: () => void
9+
setEnabled: (v: boolean) => void
10+
}
11+
12+
const SplashContext = createContext<SplashContextType | undefined>(undefined)
13+
14+
export function SplashProvider({ children }: { children: React.ReactNode }) {
15+
const [enabled, setEnabled] = useState(false)
16+
17+
const toggle = () => setEnabled(e => !e)
18+
19+
return (
20+
<SplashContext.Provider value={{ enabled, toggle, setEnabled }}>
21+
{children}
22+
{enabled && <SplashCursor />}
23+
</SplashContext.Provider>
24+
)
25+
}
26+
27+
export function useSplash() {
28+
const ctx = useContext(SplashContext)
29+
if (!ctx) throw new Error('useSplash must be used within a SplashProvider')
30+
return ctx
31+
}
32+
33+
export default SplashProvider

src/components/StarBorder.css

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
.star-border-container {
2+
display: inline-block;
3+
position: relative;
4+
border-radius: 20px;
5+
overflow: hidden;
6+
}
7+
8+
.border-gradient-bottom {
9+
position: absolute;
10+
width: 300%;
11+
height: 50%;
12+
opacity: 0.7;
13+
bottom: -12px;
14+
right: -250%;
15+
border-radius: 50%;
16+
animation: star-movement-bottom linear infinite alternate;
17+
z-index: 0;
18+
}
19+
20+
.border-gradient-top {
21+
position: absolute;
22+
opacity: 0.7;
23+
width: 300%;
24+
height: 50%;
25+
top: -12px;
26+
left: -250%;
27+
border-radius: 50%;
28+
animation: star-movement-top linear infinite alternate;
29+
z-index: 0;
30+
}
31+
32+
.inner-content {
33+
position: relative;
34+
z-index: 1;
35+
display: block;
36+
width: 100%;
37+
/* Let wrapped children control padding, background and border so the host card styles remain intact */
38+
}
39+
40+
@keyframes star-movement-bottom {
41+
0% {
42+
transform: translate(0%, 0%);
43+
opacity: 1;
44+
}
45+
100% {
46+
transform: translate(-100%, 0%);
47+
opacity: 0;
48+
}
49+
}
50+
51+
@keyframes star-movement-top {
52+
0% {
53+
transform: translate(0%, 0%);
54+
opacity: 1;
55+
}
56+
100% {
57+
transform: translate(100%, 0%);
58+
opacity: 0;
59+
}
60+
}

0 commit comments

Comments
 (0)