Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions app/auth/callback/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import { useEffect } from "react"
import { useRouter } from "next/navigation"
import { Loader2 } from "lucide-react"

import { createClient } from "@/lib/supabase/client"
import { webSupabaseClientFactory } from "@ui/web/adapters/supabaseClient"
import { webStorageAdapter } from "@ui/web/adapters/storage"
import { supabaseConfig } from "@ui/web/config"

export default function AuthCallback() {
const router = useRouter()
Expand All @@ -18,7 +20,10 @@ export default function AuthCallback() {
return
}

const supabase = createClient()
const supabase = webSupabaseClientFactory.createClient(
supabaseConfig,
{ storage: webStorageAdapter }
)

// If Supabase already processed the callback (detectSessionInUrl runs internally)
// and we already have a session, just redirect without re-exchanging the code.
Expand Down
2 changes: 1 addition & 1 deletion app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { toast } from "sonner"

import { AuthShell } from "@/components/features/auth/AuthShell"
import { NotesShell } from "@/components/features/notes/NotesShell"
import { useNoteAppController } from "@/hooks/useNoteAppController"
import { useNoteAppController } from "@ui/web/hooks/useNoteAppController"

export default function App() {
const controller = useNoteAppController()
Expand Down
8 changes: 4 additions & 4 deletions components/ImportButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import {
import { useSupabase } from "@/lib/providers/SupabaseProvider"
import { browser } from "@/lib/adapters/browser"

const MAX_FILE_SIZE = 100 * 1024 * 1024 // 100MB
const IMPORT_STATE_KEY = "everfreenote-import-state"

const initialProgress: ImportProgress = {
Expand All @@ -37,9 +36,10 @@ type ImportButtonProps = {
status: ImportStatus,
counts: { successCount: number; errorCount: number }
) => void
maxFileSize?: number
}

export function ImportButton({ onImportComplete }: ImportButtonProps) {
export function ImportButton({ onImportComplete, maxFileSize = 100 * 1024 * 1024 }: ImportButtonProps) {
const [importing, setImporting] = React.useState(false)
const [dialogOpen, setDialogOpen] = React.useState(false)
const [progressDialogOpen, setProgressDialogOpen] = React.useState(false)
Expand Down Expand Up @@ -99,10 +99,10 @@ export function ImportButton({ onImportComplete }: ImportButtonProps) {
setImportResult(null)

// Validate file sizes
const oversizedFiles = files.filter((file) => file.size > MAX_FILE_SIZE)
const oversizedFiles = files.filter((file) => file.size > maxFileSize)
if (oversizedFiles.length > 0) {
const fileNames = oversizedFiles.map((file) => file.name).join(", ")
toast.error(`Files too large (max 100MB): ${fileNames}`)
toast.error(`Files too large (max ${Math.round(maxFileSize / 1024 / 1024)}MB): ${fileNames}`)
setImporting(false)
setProgressDialogOpen(false)
return
Expand Down
6 changes: 2 additions & 4 deletions components/RichTextEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -137,11 +137,10 @@ const MenuBar = ({ editor }: MenuBarProps) => {
</Popover>

<Select
data-cy="font-family-select"
onValueChange={(value) => editor.chain().focus().setFontFamily(value).run()}
defaultValue={fontFamilies[0]}
>
<SelectTrigger className="w-[120px] text-xs h-8">
<SelectTrigger data-cy="font-family-select" className="w-[120px] text-xs h-8">
<SelectValue placeholder="Font Family" />
</SelectTrigger>
<SelectContent>
Expand All @@ -154,11 +153,10 @@ const MenuBar = ({ editor }: MenuBarProps) => {
</Select>

<Select
data-cy="font-size-select"
onValueChange={(value) => editor.chain().focus().setFontSize(`${value}pt`).run()}
defaultValue={fontSizes[1]}
>
<SelectTrigger className="w-[70px] text-xs h-8">
<SelectTrigger data-cy="font-size-select" className="w-[70px] text-xs h-8">
<SelectValue placeholder="Font Size" />
</SelectTrigger>
<SelectContent>
Expand Down
2 changes: 1 addition & 1 deletion components/features/notes/NotesShell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { NoteEditor } from "@/components/features/notes/NoteEditor"
import { NoteView } from "@/components/features/notes/NoteView"
import { EmptyState } from "@/components/features/notes/EmptyState"
import type { Note } from "@/types/domain"
import type { NoteAppController } from "@/hooks/useNoteAppController"
import type { NoteAppController } from "@ui/web/hooks/useNoteAppController"

type NoteRecord = Note & {
content?: string | null
Expand Down
2 changes: 1 addition & 1 deletion components/ui/sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { PanelLeft } from "lucide-react"

import { useIsMobile } from "@/hooks/use-mobile"
import { useIsMobile } from "@ui/web/hooks/use-mobile"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
Expand Down
2 changes: 1 addition & 1 deletion components/ui/toaster.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client"

import { useToast } from "@/hooks/use-toast"
import { useToast } from "@ui/web/hooks/use-toast"
import {
Toast,
ToastClose,
Expand Down
14 changes: 14 additions & 0 deletions core/adapters/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
export interface SupabaseConfig {
url: string
anonKey: string
}

export interface OAuthConfig {
webRedirectUri?: string
mobileRedirectUri?: string
}

export interface CoreConfig {
supabase: SupabaseConfig
oauth?: OAuthConfig
}
3 changes: 3 additions & 0 deletions core/adapters/navigation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export interface NavigationAdapter {
navigate(url: string, options?: { replace?: boolean }): Promise<void> | void
}
7 changes: 7 additions & 0 deletions core/adapters/oauth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export interface OAuthAdapter {
/**
* Starts platform-specific OAuth flow (web redirect or mobile custom tab/deep link).
* The redirectUri should be platform-specific (e.g., https://.../auth/callback for web, everfreenote://auth/callback for mobile).
*/
startOAuth(redirectUri: string): Promise<void>
}
5 changes: 5 additions & 0 deletions core/adapters/storage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export interface StorageAdapter {
getItem(key: string): Promise<string | null>
setItem(key: string, value: string): Promise<void>
removeItem(key: string): Promise<void>
}
13 changes: 13 additions & 0 deletions core/adapters/supabaseClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import type { SupabaseClient } from '@supabase/supabase-js'

import type { StorageAdapter } from './storage'
import type { SupabaseConfig } from './config'

export interface SupabaseClientFactoryDeps {
storage: StorageAdapter
fetch?: typeof fetch
}

export interface SupabaseClientFactory {
createClient(config: SupabaseConfig, deps: SupabaseClientFactoryDeps): SupabaseClient
}
10 changes: 10 additions & 0 deletions core/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export * from './adapters/storage'
export * from './adapters/navigation'
export * from './adapters/oauth'
export * from './adapters/config'
export * from './adapters/supabaseClient'
export * from './services/auth'
export * from './services/notes'
export * from './services/search'
export * from './services/sanitizer'
export * from './utils/search'
24 changes: 24 additions & 0 deletions core/services/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import type { SupabaseClient } from '@supabase/supabase-js'

export class AuthService {
constructor(private supabase: SupabaseClient) {}

async signInWithGoogle(redirectTo: string) {
return this.supabase.auth.signInWithOAuth({
provider: 'google',
options: { redirectTo },
})
}

async signInWithPassword(email: string, password: string) {
return this.supabase.auth.signInWithPassword({ email, password })
}

async signOut() {
return this.supabase.auth.signOut()
}

async getSession() {
return this.supabase.auth.getSession()
}
}
90 changes: 90 additions & 0 deletions core/services/notes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import type { SupabaseClient } from '@supabase/supabase-js'
import type { Tables } from '@/supabase/types'

type Note = Tables<'notes'>

// Sanitize value for PostgREST OR syntax: strip commas to avoid breaking the logic tree
const sanitizeOrValue = (value: string) => value.replace(/,/g, ' ')

export class NoteService {
constructor(private supabase: SupabaseClient) {}

async getNotes(
userId: string,
options: {
page?: number
pageSize?: number
tag?: string | null
searchQuery?: string
} = {}
) {
const { page = 0, pageSize = 50, tag, searchQuery } = options
const start = page * pageSize
const end = start + pageSize - 1

let query = this.supabase
.from('notes')
.select('id, title, description, tags, created_at, updated_at', { count: 'exact' })
.order('updated_at', { ascending: false })
.range(start, end)

if (tag) {
query = query.contains('tags', [tag])
}

if (searchQuery) {
const searchLower = searchQuery.toLowerCase()
const safeSearch = sanitizeOrValue(searchLower)
query = query.or(`title.ilike.%${safeSearch}%,description.ilike.%${safeSearch}%`)
}

const { data, error, count } = await query
if (error) throw error

return {
notes: (data as Note[]) || [],
totalCount: count || 0,
hasMore: !!(data && data.length === pageSize),
nextCursor: data && data.length === pageSize ? page + 1 : undefined,
}
}

async createNote(note: Pick<Note, 'title' | 'description' | 'tags'> & { userId: string }) {
const { data, error } = await this.supabase
.from('notes')
.insert([
{
title: note.title,
description: note.description,
tags: note.tags,
user_id: note.userId,
},
])
.select()
.single()

if (error) throw error
return data
}

async updateNote(id: string, updates: Partial<Pick<Note, 'title' | 'description' | 'tags'>>) {
const { data, error } = await this.supabase
.from('notes')
.update({
...updates,
updated_at: new Date().toISOString(),
})
.eq('id', id)
.select()
.single()

if (error) throw error
return data
}

async deleteNote(id: string) {
const { error } = await this.supabase.from('notes').delete().eq('id', id)
if (error) throw error
return id
}
}
4 changes: 4 additions & 0 deletions core/services/sanitizer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { SanitizationService } from '@/lib/services/sanitizer'

// Re-export wrapper to keep existing implementation; TODO: migrate to core-native implementation if needed.
export { SanitizationService }
Loading