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
Binary file added apps/web/public/latitude-logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
'use server'

import { MembershipsRepository } from '@latitude-data/core/repositories'
import { updateEscalatingIssuesEmailPreference } from '@latitude-data/core/services/memberships/updateEscalatingIssuesEmailPreference'
import { z } from 'zod'

import { authProcedure } from '../procedures'

export const updateEscalatingIssuesEmailPreferenceAction = authProcedure
.inputSchema(z.object({ wantToReceive: z.boolean() }))
.action(
async ({ parsedInput: { wantToReceive }, ctx: { workspace, user } }) => {
const membershipsScope = new MembershipsRepository(workspace.id)
const membership = await membershipsScope
.findByUserId(user.id)
.then((r) => r.unwrap())

return await updateEscalatingIssuesEmailPreference({
membership,
wantToReceive,
userEmail: user.email,
}).then((r) => r.unwrap())
},
)
24 changes: 24 additions & 0 deletions apps/web/src/actions/memberships/updateWeeklyEmailPreference.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
'use server'

import { MembershipsRepository } from '@latitude-data/core/repositories'
import { updateWeeklyEmailPreference } from '@latitude-data/core/services/memberships/updateWeeklyEmailPreference'
import { z } from 'zod'

import { authProcedure } from '../procedures'

export const updateWeeklyEmailPreferenceAction = authProcedure
.inputSchema(z.object({ wantToReceive: z.boolean() }))
.action(
async ({ parsedInput: { wantToReceive }, ctx: { workspace, user } }) => {
const membershipsScope = new MembershipsRepository(workspace.id)
const membership = await membershipsScope
.findByUserId(user.id)
.then((r) => r.unwrap())

return await updateWeeklyEmailPreference({
membership,
wantToReceive,
userEmail: user.email,
}).then((r) => r.unwrap())
},
)
6 changes: 6 additions & 0 deletions apps/web/src/actions/workspaces/switch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@ import { setSession } from '$/services/auth/setSession'
import { authProcedure } from '../procedures'
import { cookies } from 'next/headers'
import { removeSession, Session } from '$/services/auth/removeSession'
import { redirect } from 'next/navigation'

export const switchWorkspaceAction = authProcedure
.inputSchema(
z.object({
workspaceId: z.number(),
redirectTo: z.string().optional(),
}),
)
.action(async ({ parsedInput, ctx }) => {
Expand All @@ -37,5 +39,9 @@ export const switchWorkspaceAction = authProcedure
await cookies(),
)

if (parsedInput.redirectTo) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is used on the normal switch selector and now on the swich redirect for the notifications

return redirect(parsedInput.redirectTo)
}

return workspace
})
2 changes: 1 addition & 1 deletion apps/web/src/app/(admin)/backoffice/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ import { ReactNode } from 'react'

import buildMetatags from '$/app/_lib/buildMetatags'
import { getCurrentUserOrRedirect } from '$/services/auth/getCurrentUser'
import { SessionProvider } from '@latitude-data/web-ui/providers'
import { notFound } from 'next/navigation'

import { BackofficeTabs } from './_components/BackofficeTabs'
import { SessionProvider } from '$/components/Providers/SessionProvider'

export const metadata = buildMetatags({
title: 'Backoffice',
Expand Down
12 changes: 12 additions & 0 deletions apps/web/src/app/(private)/@modal/(.)notifications/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Metadata } from 'next'
import buildMetatags from '$/app/_lib/buildMetatags'
import NotificationsModal from '$/components/Notifications/Modal'

export const metadata: Promise<Metadata> = buildMetatags({
title: 'Notifications',
locationDescription: 'Email Notifications',
})

export default function NotificationsModalPage() {
return <NotificationsModal />
}
3 changes: 3 additions & 0 deletions apps/web/src/app/(private)/@modal/default.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function Default() {
return null
}
6 changes: 5 additions & 1 deletion apps/web/src/app/(private)/dashboard/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { ReactNode } from 'react'
import { Metadata } from 'next'

import { Container } from '@latitude-data/web-ui/atoms/Container'
import { TableBlankSlate } from '@latitude-data/web-ui/molecules/TableBlankSlate'
Expand All @@ -12,15 +13,17 @@ import Link from 'next/link'
import { getActiveProjectsCached } from '../_data-access'
import { ProjectsTable } from './_components/ProjectsTable'

export const metadata = buildMetatags({
export const metadata: Promise<Metadata> = buildMetatags({
title: 'Dashboard',
locationDescription: 'Projects List',
})

export default async function DashboardLayout({
children,
modal,
}: Readonly<{
children: ReactNode
modal: ReactNode
}>) {
const { workspace } = await getCurrentUserOrRedirect()

Expand All @@ -30,6 +33,7 @@ export default async function DashboardLayout({
<Container>
<AppTabs />
{children}
{modal}
<TableWithHeader
title='Projects'
actions={
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
'use client'

import { ROUTES } from '$/services/routes'
import { Text } from '@latitude-data/web-ui/atoms/Text'
import { useOnce } from '$/hooks/useMount'
import { switchWorkspaceAction } from '$/actions/workspaces/switch'
import useLatitudeAction from '$/hooks/useLatitudeAction'
import { useToast } from '@latitude-data/web-ui/atoms/Toast'

/**
* We do this here because next.js page can't change server cookies on the fly
* so we need to have a client component that calls the action to switch workspace
* and then redirect to the notifications page.
*/
export function WorkspaceSwitcherRedirect({
targetWorkspaceId,
}: {
targetWorkspaceId: number
}) {
const { toast } = useToast()
const { execute: switchWorkspace, isPending: isSwitching } =
useLatitudeAction(switchWorkspaceAction)

useOnce(() => {
async function performSwitch() {
const [, error] = await switchWorkspace({
workspaceId: targetWorkspaceId,
redirectTo: ROUTES.dashboard.notifications.root,
})

if (error) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not add this as the onFailure of the action?

toast({
title: 'Failed to switch workspace',
description: error.message,
variant: 'destructive',
})

return
}
}

performSwitch()
})

return (
<div className='flex items-center justify-center min-h-screen'>
<div className='flex flex-col items-center gap-4'>
<Text.H4>
{isSwitching
? 'Switching workspace...'
: 'Redirecting to notifications...'}
</Text.H4>
</div>
</div>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { notFound, redirect } from 'next/navigation'
import { getCurrentUserOrRedirect } from '$/services/auth/getCurrentUser'
import { WorkspaceSwitcherRedirect } from './_components/WorkspaceSwitcherRedirect'
import { ROUTES } from '$/services/routes'
import { WorkspacesRepository } from '@latitude-data/core/repositories'
import { Result } from '@latitude-data/core/lib/Result'

/**
* Handle notifications page by workspace id
*
* 1. If this workspace is valid we find if the user in the current session
* (if any) has a membership in that workspace. If not current session we
* redirect to login page with `redirectTo` param set to this page.
*
* 2. If the user has a membership in that workspace but is not the current
* workspace we render workspace switcher redirect component to switch to that
* so frontend can set the right session in cookies.
*
* 3. If the user has membership to that workspace and it's the current workspace
* we redirect to the default notifications page
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tremenda logica

*
*/
export default async function NotificationsByWorkspacePage({
params,
}: {
params: Promise<{ workspaceId: string }>
}) {
const { user: currentUser, workspace: currentWorkspace } =
await getCurrentUserOrRedirect()

const paramsResolved = await params
const workspaceId = parseInt(paramsResolved.workspaceId, 10)
const workspaceRepo = new WorkspacesRepository(currentUser.id)
const workspaceResult = await workspaceRepo.find(workspaceId)

if (!Result.isOk(workspaceResult)) return notFound()

const workspace = workspaceResult.value
if (workspace.id === currentWorkspace.id) {
return redirect(ROUTES.dashboard.notifications.root)
}

return <WorkspaceSwitcherRedirect targetWorkspaceId={workspace.id} />
}
10 changes: 10 additions & 0 deletions apps/web/src/app/(private)/dashboard/notifications/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { Metadata } from 'next'
import NotificationsModal from '$/components/Notifications/Modal'
import { metadata as modalMetadata } from '../../notifications/page'
import { ROUTES } from '$/services/routes'

export const metadata: Promise<Metadata> = modalMetadata

export default function DashboardNotificationsPage() {
return <NotificationsModal route={ROUTES.dashboard.root} />
}
3 changes: 2 additions & 1 deletion apps/web/src/app/(private)/error.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
import { useEffect } from 'react'

import { captureClientError } from '$/instrumentation-client'
import { ErrorComponent, useSession } from '@latitude-data/web-ui/browser'
import { ErrorComponent } from '@latitude-data/web-ui/browser'
import { useSession } from '$/components/Providers/SessionProvider'

export default function Error({
error,
Expand Down
5 changes: 4 additions & 1 deletion apps/web/src/app/(private)/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@ import { isOnboardingCompleted } from '$/data-access/workspaceOnboarding'
import { getCurrentUserOrRedirect } from '$/services/auth/getCurrentUser'
import { ROUTES } from '$/services/routes'
import { env } from '@latitude-data/env'
import { SessionProvider } from '@latitude-data/web-ui/browser'
import { redirect } from 'next/navigation'

import { CSPostHogProvider, IdentifyUser } from '../providers'
import { PaywallModalProvider } from './providers/PaywallModalProvider'
import { SessionProvider } from '$/components/Providers/SessionProvider'

export const metadata = buildMetatags({
title: 'Home',
Expand All @@ -25,8 +25,10 @@ export const metadata = buildMetatags({

export default async function PrivateLayout({
children,
modal,
}: Readonly<{
children: ReactNode
modal: ReactNode
}>) {
const { workspace, user, subscriptionPlan } = await getCurrentUserOrRedirect()

Expand Down Expand Up @@ -67,6 +69,7 @@ export default async function PrivateLayout({
isCloud={isCloud}
>
{children}
{modal}
</AppLayout>
</PaywallModalProvider>
</LatitudeWebsocketsProvider>
Expand Down
18 changes: 18 additions & 0 deletions apps/web/src/app/(private)/notifications/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Metadata } from 'next'
import buildMetatags from '$/app/_lib/buildMetatags'

import Notifications from '$/components/Notifications'
import { FocusLayout } from '$/components/layouts'

export const metadata: Promise<Metadata> = buildMetatags({
title: 'Notifications',
locationDescription: 'Email Notifications',
})

export default function NotificationsPage() {
return (
<FocusLayout>
<Notifications />
</FocusLayout>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ function MiniHistogramBar({
}
>
{!isPlaceholder && (
<div className='flex flex-col'>
<div className='flex flex-col w-3'>
<div className='flex justify-between'>
<Text.H6M color={color}>Issues</Text.H6M>
<Text.H6M color={color}>{item.count}</Text.H6M>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ export function IssuesDetailPanel({

return (
<DetailsPanel bordered ref={ref}>
<div className='relative w-full overflow-hidden'>
<div className='relative w-full overflow-hidden custom-scrollbar'>
Copy link
Contributor Author

@andresgutgon andresgutgon Nov 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fix sliding horizontal overflow ⚠️ (I'll do in another PR)

{/* === SLIDING PANEL WRAPPER === */}
<div
className={cn(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ export function StatusBadges({ issue }: { issue: SerializedIssue }) {
if (statuses.length === 0) return null

return (
<>
<div className='flex flex-row gap-x-2'>
{statuses.map((status, index) => {
if (!status.tooltip) {
return (
Expand Down Expand Up @@ -111,6 +111,6 @@ export function StatusBadges({ issue }: { issue: SerializedIssue }) {
</Tooltip>
)
})}
</>
</div>
)
}
2 changes: 1 addition & 1 deletion apps/web/src/app/(public)/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { ReactNode } from 'react'

import { MaybeSessionProvider } from '@latitude-data/web-ui/browser'
import { getDataFromSession } from '$/data-access'
import { MaybeSessionProvider } from '$/components/Providers/MaybeSessionProvider'

/**
* This layout is here only to add providers.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { ROUTES } from '$/services/routes'
import { Button } from '@latitude-data/web-ui/atoms/Button'
import { Modal } from '@latitude-data/web-ui/atoms/Modal'
import { ButtonProps } from '@latitude-data/web-ui/atoms/Button'
import { useMaybeSession } from '@latitude-data/web-ui/providers'
import { useMaybeSession } from '$/components/Providers/MaybeSessionProvider'
import { MouseEvent, useCallback, useState } from 'react'
import { PublishedDocument } from '@latitude-data/core/schema/models/types/PublishedDocument'

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Avatar } from '@latitude-data/web-ui/atoms/Avatar'
import { Icon } from '@latitude-data/web-ui/atoms/Icons'
import { Text } from '@latitude-data/web-ui/atoms/Text'
import { TripleThemeToggle } from '@latitude-data/web-ui/molecules/TrippleThemeToggle'
import { useMaybeSession } from '@latitude-data/web-ui/providers'
import { useMaybeSession } from '$/components/Providers/MaybeSessionProvider'
import { AppHeaderWrapper } from '$/components/layouts/AppLayout/Header'
import { ROUTES } from '$/services/routes'
import Link from 'next/link'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
import { authHandler } from '$/middlewares/authHandler'
import { errorHandler } from '$/middlewares/errorHandler'
import { NextRequest, NextResponse } from 'next/server'
import { DiffValue } from '@latitude-data/core/constants'
import { DiffValue } from '@latitude-data/constants'

import { Workspace } from '@latitude-data/core/schema/models/types/Workspace'
import { DocumentVersion } from '@latitude-data/core/schema/models/types/DocumentVersion'
Expand Down
Loading
Loading