fix: conflict

This commit is contained in:
Aslam H
2024-09-28 19:53:32 +07:00
205 changed files with 8806 additions and 2121 deletions

View File

@@ -11,11 +11,11 @@ export function DeepLinkProvider({ children }: DeepLinkProviderProps) {
const eventHandlers: { [key: string]: (event: Event) => void } = {
click: (event: Event) => {
const e = event as MouseEvent
console.log("Click event:", { x: e.clientX, y: e.clientY })
// console.log("Click event:", { x: e.clientX, y: e.clientY })
},
keydown: (event: Event) => {
const e = event as KeyboardEvent
console.log("Keydown event:", { key: e.key, code: e.code })
// console.log("Keydown event:", { key: e.key, code: e.code })
}
}

View File

@@ -2,105 +2,56 @@
import { createJazzReactApp } from "jazz-react"
import { LaAccount } from "@/lib/schema"
import { useClerk } from "@clerk/nextjs"
import { createContext, useMemo, useState } from "react"
import { AuthMethodCtx } from "jazz-react"
import { useAuth, useClerk } from "@clerk/nextjs"
import { useJazzClerkAuth } from "jazz-react-auth-clerk"
import { usePathname } from "next/navigation"
const Jazz = createJazzReactApp({
AccountSchema: LaAccount
})
export const { useAccount, useCoState, useAcceptInvite } = Jazz
export const { useAccount, useAccountOrGuest, useCoState, useAcceptInvite } = Jazz
export function JazzProvider({ children }: { children: React.ReactNode }) {
return <Jazz.Provider peer="wss://mesh.jazz.tools/?key=example@gmail.com">{children}</Jazz.Provider>
function assertPeerUrl(url: string | undefined): asserts url is `wss://${string}` | `ws://${string}` {
if (!url) {
throw new Error("NEXT_PUBLIC_JAZZ_PEER_URL is not defined")
}
if (!url.startsWith("wss://") && !url.startsWith("ws://")) {
throw new Error("NEXT_PUBLIC_JAZZ_PEER_URL must start with wss:// or ws://")
}
}
export const JazzClerkAuthCtx = createContext<{
errors: string[]
}>({
errors: []
})
const rawUrl = process.env.NEXT_PUBLIC_JAZZ_PEER_URL
assertPeerUrl(rawUrl)
const JAZZ_PEER_URL = rawUrl
export function JazzClerkAuth({ children }: { children: React.ReactNode }) {
interface ChildrenProps {
children: React.ReactNode
}
export function JazzAndAuth({ children }: ChildrenProps) {
const pathname = usePathname()
return pathname === "/" ? <JazzGuest>{children}</JazzGuest> : <JazzAuth>{children}</JazzAuth>
}
export function JazzAuth({ children }: ChildrenProps) {
const clerk = useClerk()
const [errors, setErrors] = useState<string[]>([])
const { isLoaded } = useAuth()
const [authMethod] = useJazzClerkAuth(clerk)
const authMethod = useMemo(() => {
return new BrowserClerkAuth(
{
onError: error => {
void clerk.signOut()
setErrors(errors => [...errors, error.toString()])
}
},
clerk
)
}, [clerk])
if (!isLoaded) return null
return (
<JazzClerkAuthCtx.Provider value={{ errors }}>
<AuthMethodCtx.Provider value={authMethod}>{children}</AuthMethodCtx.Provider>
</JazzClerkAuthCtx.Provider>
<Jazz.Provider auth={authMethod || "guest"} peer={JAZZ_PEER_URL}>
{children}
</Jazz.Provider>
)
}
import { Account, AuthMethod, AuthResult, ID } from "jazz-tools"
import type { LoadedClerk } from "@clerk/types"
import { AgentSecret } from "cojson"
export class BrowserClerkAuth implements AuthMethod {
constructor(
public driver: BrowserClerkAuth.Driver,
private readonly clerkClient: LoadedClerk
) {}
async start(): Promise<AuthResult> {
if (this.clerkClient.user) {
const storedCredentials = this.clerkClient.user.unsafeMetadata
if (storedCredentials.jazzAccountID) {
if (!storedCredentials.jazzAccountSecret) {
throw new Error("No secret for existing user")
}
return {
type: "existing",
credentials: {
accountID: storedCredentials.jazzAccountID as ID<Account>,
secret: storedCredentials.jazzAccountSecret as AgentSecret
},
onSuccess: () => {},
onError: (error: string | Error) => {
this.driver.onError(error)
}
}
} else {
return {
type: "new",
creationProps: {
name: this.clerkClient.user.fullName || this.clerkClient.user.username || this.clerkClient.user.id
},
saveCredentials: async (credentials: { accountID: ID<Account>; secret: AgentSecret }) => {
await this.clerkClient.user?.update({
unsafeMetadata: {
jazzAccountID: credentials.accountID,
jazzAccountSecret: credentials.secret
}
})
},
onSuccess: () => {},
onError: (error: string | Error) => {
this.driver.onError(error)
}
}
}
} else {
throw new Error("Not signed in")
}
}
}
export namespace BrowserClerkAuth {
export interface Driver {
onError: (error: string | Error) => void
}
export function JazzGuest({ children }: ChildrenProps) {
return (
<Jazz.Provider auth="guest" peer={JAZZ_PEER_URL}>
{children}
</Jazz.Provider>
)
}

View File

@@ -8,67 +8,81 @@
// open issue about it: https://github.com/gardencmp/jazz/issues/44
// TODO: figure out how to do default values, e.g. `GlobalLink.protocol` should have default value `https` so we don't have to supply it every time in code..
// TODO: can jazz support vector fields? e.g. `GlobalLinkAiSummary.vectorContent`, would be nice to store website content as vector for semantic search
import { CoMap, co, Account, Profile } from "jazz-tools"
import { PersonalPageLists } from "./personal-page"
import { PersonalLinkLists } from "./personal-link"
import { ListOfTopics } from "./master/topic"
import { CoMap, co, Account, Profile } from 'jazz-tools'
import { PersonalPageLists } from './personal-page'
import { PersonalLinkLists } from './personal-link'
import { ListOfTopics } from './master/topic'
import { ListOfTasks } from './tasks'
import { JournalEntryLists } from './journal'
declare module 'jazz-tools' {
interface Profile {
avatarUrl?: string
}
}
export class UserRoot extends CoMap {
name = co.string
username = co.string
avatar = co.optional.string
website = co.optional.string
bio = co.optional.string
is_public = co.optional.boolean
name = co.string
username = co.string
avatar = co.optional.string
website = co.optional.string
bio = co.optional.string
is_public = co.optional.boolean
personalLinks = co.ref(PersonalLinkLists)
personalPages = co.ref(PersonalPageLists)
personalLinks = co.ref(PersonalLinkLists)
personalPages = co.ref(PersonalPageLists)
topicsWantToLearn = co.ref(ListOfTopics)
topicsLearning = co.ref(ListOfTopics)
topicsLearned = co.ref(ListOfTopics)
topicsWantToLearn = co.ref(ListOfTopics)
topicsLearning = co.ref(ListOfTopics)
topicsLearned = co.ref(ListOfTopics)
// TODO: maybe should be in another place?
connectedFolderPath = co.optional.string
tasks = co.ref(ListOfTasks)
journalEntries = co.ref(JournalEntryLists)
// TODO: maybe should be in another place?
connectedFolderPath = co.optional.string
}
export class LaAccount extends Account {
profile = co.ref(Profile)
root = co.ref(UserRoot)
profile = co.ref(Profile)
root = co.ref(UserRoot)
migrate(this: LaAccount, creationProps?: { name: string }) {
// since we dont have a custom AuthProvider yet.
// and still using the DemoAuth. the creationProps will only accept name.
// so just do default profile create provided by jazz-tools
super.migrate(creationProps)
migrate(
this: LaAccount,
creationProps?: { name: string; avatarUrl?: string }
) {
// since we dont have a custom AuthProvider yet.
// and still using the DemoAuth. the creationProps will only accept name.
// so just do default profile create provided by jazz-tools
super.migrate(creationProps)
console.log("In migration", this._refs.root, creationProps)
if (!this._refs.root && creationProps) {
this.root = UserRoot.create(
{
name: creationProps.name,
username: creationProps.name,
avatar: creationProps.avatarUrl || '',
website: '',
bio: '',
is_public: false,
if (!this._refs.root && creationProps) {
this.root = UserRoot.create(
{
name: creationProps.name,
username: creationProps.name,
avatar: "",
website: "",
bio: "",
is_public: false,
connectedFolderPath: '',
connectedFolderPath: "",
personalLinks: PersonalLinkLists.create([], { owner: this }),
personalPages: PersonalPageLists.create([], { owner: this }),
personalLinks: PersonalLinkLists.create([], { owner: this }),
personalPages: PersonalPageLists.create([], { owner: this }),
topicsWantToLearn: ListOfTopics.create([], { owner: this }),
topicsLearning: ListOfTopics.create([], { owner: this }),
topicsLearned: ListOfTopics.create([], { owner: this }),
topicsWantToLearn: ListOfTopics.create([], { owner: this }),
topicsLearning: ListOfTopics.create([], { owner: this }),
topicsLearned: ListOfTopics.create([], { owner: this })
},
{ owner: this }
)
}
}
tasks: ListOfTasks.create([], { owner: this }),
journalEntries: JournalEntryLists.create([], { owner: this })
},
{ owner: this }
)
}
}
}
export * from "./master/topic"
export * from "./personal-link"
export * from "./personal-page"
export * from './master/topic'
export * from './personal-link'
export * from './personal-page'

11
web/lib/schema/journal.ts Normal file
View File

@@ -0,0 +1,11 @@
import { co, CoList, CoMap, Encoders } from "jazz-tools"
export class JournalEntry extends CoMap {
title = co.string
content = co.json()
date = co.encoded(Encoders.Date)
createdAt = co.encoded(Encoders.Date)
updatedAt = co.encoded(Encoders.Date)
}
export class JournalEntryLists extends CoList.Of(co.ref(JournalEntry)) {}

11
web/lib/schema/tasks.ts Normal file
View File

@@ -0,0 +1,11 @@
import { co, CoList, CoMap, Encoders } from "jazz-tools"
export class Task extends CoMap {
title = co.string
description = co.optional.string
status = co.literal("todo", "in_progress", "done")
createdAt = co.encoded(Encoders.Date)
updatedAt = co.encoded(Encoders.Date)
}
export class ListOfTasks extends CoList.Of(co.ref(Task)) {}

View File

@@ -0,0 +1,13 @@
import { currentUser } from "@clerk/nextjs/server"
import { createServerActionProcedure, ZSAError } from "zsa"
export const authedProcedure = createServerActionProcedure()
.handler(async () => {
try {
const clerkUser = await currentUser()
return { clerkUser }
} catch {
throw new ZSAError("NOT_AUTHORIZED", "User not authenticated")
}
})
.createServerAction()

View File

@@ -0,0 +1,45 @@
import React from "react"
import { render } from "@testing-library/react"
import { HTMLLikeElement, renderHTMLLikeElement } from "./htmlLikeElementUtil"
const HTMLLikeRenderer: React.FC<{ content: HTMLLikeElement | string }> = ({ content }) => {
return <>{renderHTMLLikeElement(content)}</>
}
describe("HTML-like Element Utility", () => {
test("HTMLLikeRenderer renders simple string content", () => {
const { getByText } = render(<HTMLLikeRenderer content="Hello, World!" />)
expect(getByText("Hello, World!")).toBeTruthy()
})
test("HTMLLikeRenderer renders HTML-like structure", () => {
const content: HTMLLikeElement = {
tag: "div",
attributes: { className: "test-class" },
children: ["Hello, ", { tag: "strong", children: ["World"] }, "!"]
}
const { container, getByText } = render(<HTMLLikeRenderer content={content} />)
expect(container.firstChild).toHaveProperty("className", "test-class")
const strongElement = getByText("World")
expect(strongElement.tagName.toLowerCase()).toBe("strong")
})
test("HTMLLikeRenderer handles multiple attributes", () => {
const content: HTMLLikeElement = {
tag: "div",
attributes: {
className: "test-class",
id: "test-id",
"data-testid": "custom-element",
style: { color: "red", fontSize: "16px" }
},
children: ["Test Content"]
}
const { getByTestId } = render(<HTMLLikeRenderer content={content} />)
const element = getByTestId("custom-element")
expect(element.className).toBe("test-class")
expect(element.id).toBe("test-id")
expect(element.style.color).toBe("red")
expect(element.style.fontSize).toBe("16px")
})
})

View File

@@ -0,0 +1,21 @@
import React from "react"
export type HTMLAttributes = React.HTMLAttributes<HTMLElement> & {
[key: string]: any
}
export type HTMLLikeElement = {
tag: keyof JSX.IntrinsicElements
attributes?: HTMLAttributes
children?: (HTMLLikeElement | string)[]
}
export const renderHTMLLikeElement = (element: HTMLLikeElement | string): React.ReactNode => {
if (typeof element === "string") {
return element
}
const { tag, attributes = {}, children = [] } = element
return React.createElement(tag, attributes, ...children.map(child => renderHTMLLikeElement(child)))
}

View File

@@ -9,6 +9,52 @@ export const randomId = () => {
return Math.random().toString(36).substring(7)
}
export const toTitleCase = (str: string): string => {
return str
.replace(/([A-Z])/g, " $1")
.replace(/^./, str => str.toUpperCase())
.trim()
}
function escapeRegExp(string: string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
}
export const searchSafeRegExp = (inputValue: string) => {
const escapedChars = inputValue.split("").map(escapeRegExp)
return new RegExp(escapedChars.join(".*"), "i")
}
export function shuffleArray<T>(array: T[]): T[] {
const shuffled = [...array]
for (let i = shuffled.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1))
;[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]]
}
return shuffled
}
const inputs = ["input", "select", "button", "textarea"] // detect if node is a text input element
export function isTextInput(element: Element): boolean {
return !!(
element &&
element.tagName &&
(inputs.indexOf(element.tagName.toLowerCase()) !== -1 ||
element.attributes.getNamedItem("role")?.value === "textbox" ||
element.attributes.getNamedItem("contenteditable")?.value === "true")
)
}
export function calendarFormatDate(date: Date): string {
return date.toLocaleDateString("en-US", {
year: "numeric",
month: "long",
day: "numeric"
})
}
export * from "./urls"
export * from "./slug"
export * from "./keyboard"
export * from "./htmlLikeElementUtil"

View File

@@ -1,47 +1,4 @@
let isMac: boolean | undefined
interface Navigator {
userAgentData?: {
brands: { brand: string; version: string }[]
mobile: boolean
platform: string
getHighEntropyValues: (hints: string[]) => Promise<{
platform: string
platformVersion: string
uaFullVersion: string
}>
}
}
function getPlatform(): string {
const nav = navigator as Navigator
if (nav.userAgentData) {
if (nav.userAgentData.platform) {
return nav.userAgentData.platform
}
nav.userAgentData.getHighEntropyValues(["platform"]).then(highEntropyValues => {
if (highEntropyValues.platform) {
return highEntropyValues.platform
}
})
}
if (typeof navigator.platform === "string") {
return navigator.platform
}
return ""
}
export function isMacOS() {
if (isMac === undefined) {
isMac = getPlatform().toLowerCase().includes("mac")
}
return isMac
}
const SSR = typeof window === "undefined"
interface ShortcutKeyResult {
symbol: string
@@ -51,15 +8,11 @@ interface ShortcutKeyResult {
export function getShortcutKey(key: string): ShortcutKeyResult {
const lowercaseKey = key.toLowerCase()
if (lowercaseKey === "mod") {
return isMacOS() ? { symbol: "⌘", readable: "Command" } : { symbol: "Ctrl", readable: "Control" }
return isMac() ? { symbol: "⌘", readable: "Command" } : { symbol: "Ctrl", readable: "Control" }
} else if (lowercaseKey === "alt") {
return isMacOS() ? { symbol: "⌥", readable: "Option" } : { symbol: "Alt", readable: "Alt" }
return isMac() ? { symbol: "⌥", readable: "Option" } : { symbol: "Alt", readable: "Alt" }
} else if (lowercaseKey === "shift") {
return { symbol: "⇧", readable: "Shift" }
} else if (lowercaseKey === "control") {
return { symbol: "⌃", readable: "Control" }
} else if (lowercaseKey === "windows" && !isMacOS()) {
return { symbol: "Win", readable: "Windows" }
return isMac() ? { symbol: "⇧", readable: "Shift" } : { symbol: "Shift", readable: "Shift" }
} else {
return { symbol: key.toUpperCase(), readable: key }
}
@@ -69,20 +22,38 @@ export function getShortcutKeys(keys: string[]): ShortcutKeyResult[] {
return keys.map(key => getShortcutKey(key))
}
export function getSpecialShortcut(shortcutName: string): ShortcutKeyResult[] {
if (shortcutName === "expandToolbar") {
return isMacOS()
? [getShortcutKey("control"), getShortcutKey("mod"), getShortcutKey("n")]
: [getShortcutKey("mod"), getShortcutKey("windows"), getShortcutKey("n")]
export function isModKey(event: KeyboardEvent | MouseEvent | React.KeyboardEvent) {
return isMac() ? event.metaKey : event.ctrlKey
}
export function isMac(): boolean {
if (SSR) {
return false
}
return []
return window.navigator.platform === "MacIntel"
}
export function formatShortcut(shortcutKeys: ShortcutKeyResult[]): string {
return shortcutKeys.map(key => key.symbol).join("")
export function isWindows(): boolean {
if (SSR) {
return false
}
return window.navigator.platform === "Win32"
}
export function formatReadableShortcut(shortcutKeys: ShortcutKeyResult[]): string {
return shortcutKeys.map(key => key.readable).join(" + ")
let supportsPassive = false
try {
const opts = Object.defineProperty({}, "passive", {
get() {
supportsPassive = true
}
})
// @ts-expect-error ts-migrate(2769) testPassive is not a real event
window.addEventListener("testPassive", null, opts)
// @ts-expect-error ts-migrate(2769) testPassive is not a real event
window.removeEventListener("testPassive", null, opts)
} catch (e) {
// No-op
}
export const supportsPassiveListener = supportsPassive

View File

@@ -0,0 +1,29 @@
import { generateUniqueSlug } from "./slug"
describe("generateUniqueSlug", () => {
it("should generate a slug with the correct format", () => {
const title = "This is a test title"
const slug = generateUniqueSlug(title)
expect(slug).toMatch(/^this-is-a-test-title-[a-f0-9]{8}$/)
})
it("should respect the maxLength parameter", () => {
const title = "This is a very long title that should be truncated"
const maxLength = 30
const slug = generateUniqueSlug(title, maxLength)
expect(slug.length).toBe(maxLength)
})
it("should generate different slugs for the same title", () => {
const title = "Same Title"
const slug1 = generateUniqueSlug(title)
const slug2 = generateUniqueSlug(title)
expect(slug1).not.toBe(slug2)
})
it("should handle empty strings", () => {
const title = ""
const slug = generateUniqueSlug(title)
expect(slug).toMatch(/^-[a-f0-9]{8}$/)
})
})

View File

@@ -1,36 +1,14 @@
import slugify from "slugify"
import crypto from "crypto"
type SlugLikeProperty = string | undefined
export function generateUniqueSlug(title: string, maxLength: number = 60): string {
const baseSlug = slugify(title, {
lower: true,
strict: true
})
const randomSuffix = crypto.randomBytes(4).toString("hex")
interface Data {
[key: string]: any
}
export function generateUniqueSlug(
existingItems: Data[],
title: string,
slugProperty: string = "slug",
maxLength: number = 50
): string {
const baseSlug = slugify(title, { lower: true, strict: true })
let uniqueSlug = baseSlug.slice(0, maxLength)
let num = 1
if (!existingItems || existingItems.length === 0) {
return uniqueSlug
}
const isSlugTaken = (slug: string) =>
existingItems.some(item => {
const itemSlug = item[slugProperty] as SlugLikeProperty
return itemSlug === slug
})
while (isSlugTaken(uniqueSlug)) {
const suffix = `-${num}`
uniqueSlug = `${baseSlug.slice(0, maxLength - suffix.length)}${suffix}`
num++
}
return uniqueSlug
const truncatedSlug = baseSlug.slice(0, Math.min(maxLength, 75) - 9)
return `${truncatedSlug}-${randomSuffix}`
}