From 858ac6c22223b54e22145287b5e24ec1790cd7df Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 20 Nov 2024 21:44:07 +0000 Subject: [PATCH 1/6] chore(deps): bump cross-spawn from 7.0.3 to 7.0.6 in /apps/extensions Bumps [cross-spawn](https://github.com/moxystudio/node-cross-spawn) from 7.0.3 to 7.0.6. - [Changelog](https://github.com/moxystudio/node-cross-spawn/blob/master/CHANGELOG.md) - [Commits](https://github.com/moxystudio/node-cross-spawn/compare/v7.0.3...v7.0.6) --- updated-dependencies: - dependency-name: cross-spawn dependency-type: indirect ... Signed-off-by: dependabot[bot] --- apps/extensions/yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/extensions/yarn.lock b/apps/extensions/yarn.lock index aab83a967..3e4478338 100644 --- a/apps/extensions/yarn.lock +++ b/apps/extensions/yarn.lock @@ -2684,9 +2684,9 @@ cosmiconfig@^7.0.1: yaml "^1.10.0" cross-spawn@^7.0.3: - version "7.0.3" - resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" - integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== + version "7.0.6" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f" + integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA== dependencies: path-key "^3.1.0" shebang-command "^2.0.0" From 360c0bb16a10811f26d6dece77c41eb20e54bf85 Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Thu, 21 Nov 2024 14:34:33 +0200 Subject: [PATCH 2/6] feat: add project information in task card --- .../features/task/task-all-status-type.tsx | 28 +++++++++++++++---- 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/apps/web/lib/features/task/task-all-status-type.tsx b/apps/web/lib/features/task/task-all-status-type.tsx index 979871820..673decd92 100644 --- a/apps/web/lib/features/task/task-all-status-type.tsx +++ b/apps/web/lib/features/task/task-all-status-type.tsx @@ -1,9 +1,9 @@ 'use client'; -import { useCustomEmblaCarousel, useDailyPlan, useSyncRef } from '@app/hooks'; -import { ITeamTask, Nullable } from '@app/interfaces'; +import { useCustomEmblaCarousel, useDailyPlan, useOrganizationProjects, useSyncRef } from '@app/hooks'; +import { IProject, ITeamTask, Nullable } from '@app/interfaces'; import { RoundedButton } from 'lib/components'; -import { useEffect, useMemo } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import { TaskStatus, useTaskLabelsValue, @@ -15,6 +15,7 @@ import { clsxm } from '@app/utils'; import { planBadgeContent, planBadgeContPast } from '@app/helpers'; import { CalendarIcon } from '@radix-ui/react-icons'; import { FilterTabs } from '../user-profile-plans'; +import ProjectIcon from '@components/ui/svgs/project-icon'; export function TaskAllStatusTypes({ task, @@ -35,6 +36,8 @@ export function TaskAllStatusTypes({ const taskSizes = useTaskSizesValue(); const taskLabels = useTaskLabelsValue(); const taskStatus = useTaskStatusValue(); + const { getOrganizationProject } = useOrganizationProjects(); + const [project, setProject] = useState(); const { dailyPlan } = useDailyPlan(); @@ -68,6 +71,17 @@ export function TaskAllStatusTypes({ const taskId = task ? planBadgeContPast(dailyPlan.items, task.id) : ''; + useEffect(() => { + const fetchProject = async () => { + if (task?.projectId) { + const res = await getOrganizationProject(task.projectId); + setProject(res?.data); + } + }; + + fetchProject(); + }, [getOrganizationProject, task?.projectId]); + return (
@@ -81,7 +95,6 @@ export function TaskAllStatusTypes({ titleClassName={'text-[0.625rem] font-[500]'} /> )} - {task?.priority && ( )} - {task?.size && !toBlockCard && ( )} - + {project && ( +
+ {project.name} +
+ )} {planBadgeContent(dailyPlan.items, task?.id ?? '', tab) && (
Date: Fri, 22 Nov 2024 00:48:57 +0200 Subject: [PATCH 3/6] [Feat]: Timesheet-Duration-Display (#3352) * feat: add DisplayTimeForTimesheet and TotalTimeDisplay components for formatted timesheet duration * fix: coderabbitai * feat: total duration by date * fix: conflit and display date time * fix: deepscan --- .../hooks/features/useTimelogFilterOptions.ts | 2 +- .../calendar/table-time-sheet.tsx | 23 +++---- apps/web/lib/features/task/task-displays.tsx | 60 ++++++++++++++++++- 3 files changed, 72 insertions(+), 13 deletions(-) diff --git a/apps/web/app/hooks/features/useTimelogFilterOptions.ts b/apps/web/app/hooks/features/useTimelogFilterOptions.ts index ca39217a3..c246038a8 100644 --- a/apps/web/app/hooks/features/useTimelogFilterOptions.ts +++ b/apps/web/app/hooks/features/useTimelogFilterOptions.ts @@ -31,6 +31,6 @@ export function useTimelogFilterOptions() { setStatusState, handleSelectRowTimesheet, selectTimesheet, - setSelectTimesheet + setSelectTimesheet, }; } diff --git a/apps/web/lib/features/integrations/calendar/table-time-sheet.tsx b/apps/web/lib/features/integrations/calendar/table-time-sheet.tsx index 52fb005b5..592c24c89 100644 --- a/apps/web/lib/features/integrations/calendar/table-time-sheet.tsx +++ b/apps/web/lib/features/integrations/calendar/table-time-sheet.tsx @@ -57,9 +57,8 @@ import { import { useTranslations } from 'next-intl'; import { formatDate } from '@/app/helpers'; import { GroupedTimesheet, useTimesheet } from '@/app/hooks/features/useTimesheet'; -import { TaskNameInfoDisplay } from '../../task/task-displays'; +import { DisplayTimeForTimesheet, TaskNameInfoDisplay, TotalDurationByDate, TotalTimeDisplay } from '../../task/task-displays'; import { TimesheetStatus } from '@/app/interfaces'; -import dayjs from 'dayjs'; export const columns: ColumnDef[] = [ { @@ -249,10 +248,11 @@ export function DataTableTimeSheet({ data }: { data?: GroupedTimesheet[] }) { 'h-[48px] flex justify-between items-center w-full', 'bg-[#ffffffcc] dark:bg-dark--theme rounded-md border-1', 'border-gray-400 px-5 text-[#71717A] font-medium' - )} - > + )}> {formatDate(plan.date)} - 64:30h +
@@ -283,8 +283,8 @@ export function DataTableTimeSheet({ data }: { data?: GroupedTimesheet[] }) { variant={'outline'} className="flex items-center gap-x-2 h-[25px] rounded-md bg-[#E4E4E7] dark:bg-gray-800" > - Total - 24:30h + {t('timer.TOTAL_HOURS')} +
@@ -339,10 +339,11 @@ export function DataTableTimeSheet({ data }: { data?: GroupedTimesheet[] }) { {task.timesheet.status}
- - {dayjs(task.timesheet.createdAt).format('HH:mm:ss')} - - + +
))} diff --git a/apps/web/lib/features/task/task-displays.tsx b/apps/web/lib/features/task/task-displays.tsx index 1f37c924a..0bca22f27 100644 --- a/apps/web/lib/features/task/task-displays.tsx +++ b/apps/web/lib/features/task/task-displays.tsx @@ -1,7 +1,10 @@ -import { ITeamTask, Nullable } from '@app/interfaces'; +import { ITeamTask, Nullable, TimesheetLog } from '@app/interfaces'; import { clsxm } from '@app/utils'; import { Tooltip } from 'lib/components'; import { TaskIssueStatus } from './task-issue'; +import { formatDate, secondsToTime } from '@/app/helpers'; +import { ClockIcon } from "@radix-ui/react-icons" +import React from 'react'; type Props = { task: Nullable; @@ -61,3 +64,58 @@ export function TaskNameInfoDisplay({ ); } + +const formatTime = (hours: number, minutes: number) => ( +
+ {String(hours).padStart(2, '0')} + : + {String(minutes).padStart(2, '0')} +
+); + +export const DisplayTimeForTimesheet = ({ duration }: { duration: number }) => { + if (duration < 0) { + console.warn('Negative duration provided to DisplayTimeForTimesheet'); + duration = 0; + } + const { h: hours, m: minute } = secondsToTime(duration || 0); + return ( +
+ +
+ {formatTime(hours, minute)} +
+
+ ) + +} + +export const TotalTimeDisplay = React.memo(({ timesheetLog }: { timesheetLog: TimesheetLog[] }) => { + const totalDuration = Array.isArray(timesheetLog) + ? timesheetLog.reduce((acc, curr) => acc + (curr.timesheet?.duration || 0), 0) + : 0; + const { h: hours, m: minute } = secondsToTime(totalDuration || 0); + return ( +
+ {formatTime(hours, minute)} +
) +}); +TotalTimeDisplay.displayName = 'TotalTimeDisplay'; + + +export const TotalDurationByDate = React.memo( + ({ timesheetLog, createdAt }: { timesheetLog: TimesheetLog[]; createdAt: Date | string }) => { + const targetDateISO = new Date(createdAt).toISOString(); + const filteredLogs = timesheetLog.filter( + (item) => formatDate(item.timesheet.createdAt) === formatDate(targetDateISO)); + const totalDurationInSeconds = filteredLogs.reduce( + (total, log) => total + (log.timesheet?.duration || 0), 0); + const { h: hours, m: minutes } = secondsToTime(totalDurationInSeconds); + return ( +
+ {formatTime(hours, minutes)} +
+ ); + } +); +TotalDurationByDate.displayName = 'TotalDurationByDate'; From 74bdec529858e1644b0c96bb53fd1ec40beb38ed Mon Sep 17 00:00:00 2001 From: Arick Bulakali <85836702+NdekoCode@users.noreply.github.com> Date: Fri, 22 Nov 2024 00:59:00 +0200 Subject: [PATCH 4/6] [Web] Refactor/sidebar (#3361) * refactor(web): [Sidebar] improve sidebar, styles, add form and workspace switter * refactor(web): [Sidebar] fix broken spaces * feat(web): [Subscribe] add subscription user email * fix: spelling errors --- .cspell.json | 7 +- apps/web/.env | 4 + apps/web/.env.sample | 4 + apps/web/app/[locale]/layout.tsx | 2 +- apps/web/app/[locale]/page-component.tsx | 4 +- apps/web/app/api/subscribe/route.ts | 67 + apps/web/components/app-sidebar.tsx | 132 +- apps/web/components/nav-main.tsx | 34 +- apps/web/components/nav-projects.tsx | 158 +- apps/web/components/nav-secondary.tsx | 6 +- .../pages/all-teams/header-tabs.tsx | 70 +- .../web/components/pages/main/header-tabs.tsx | 103 +- .../editor-components/LinkElement.tsx | 2 +- apps/web/components/sidebar-opt-in-form.tsx | 111 ++ apps/web/components/ui/card.tsx | 43 + apps/web/components/ui/form.tsx | 135 ++ apps/web/components/ui/label.tsx | 19 + apps/web/components/ui/sidebar.tsx | 4 +- ...am-switcher.tsx => workspace-switcher.tsx} | 27 +- .../task/daily-plan/views-header-tabs.tsx | 78 +- .../lib/features/task/task-input-kanban.tsx | 864 +++++----- apps/web/lib/features/task/task-input.tsx | 1437 ++++++++--------- apps/web/lib/features/user-nav-menu.tsx | 4 +- apps/web/lib/layout/main-layout.tsx | 2 +- apps/web/next.config.js | 1 + apps/web/package.json | 8 +- apps/web/styles/globals.css | 3 +- apps/web/tailwind.config.js | 344 ++-- yarn.lock | 30 +- 29 files changed, 1997 insertions(+), 1706 deletions(-) create mode 100644 apps/web/app/api/subscribe/route.ts create mode 100644 apps/web/components/sidebar-opt-in-form.tsx create mode 100644 apps/web/components/ui/card.tsx create mode 100644 apps/web/components/ui/form.tsx create mode 100644 apps/web/components/ui/label.tsx rename apps/web/components/{team-switcher.tsx => workspace-switcher.tsx} (76%) diff --git a/.cspell.json b/.cspell.json index 9c47d01cc..b22d44a42 100644 --- a/.cspell.json +++ b/.cspell.json @@ -6,6 +6,7 @@ "words": [ " X", " X ", + "hookform", "accepte", "Accordian", "adipiscing", @@ -24,6 +25,7 @@ "apidemodt", "apidemodts", "apidev", + "apikey", "apisauce", "apistage", "apistagecivo", @@ -92,6 +94,7 @@ "creatoe", "dailyplan", "Darkmode", + "DATACENTER", "datas", "dataToDisplay", "daygrid", @@ -240,6 +243,7 @@ "longpress", "Lorem", "lucide", + "mailchimp", "mainconfig", "mappagination", "mathieudutour", @@ -355,7 +359,6 @@ "tailess", "Tailess", "tailwindcss", - "timesheet-viewMode", "tanstack", "taskid", "taskstatus", @@ -367,6 +370,7 @@ "testid", "timegrid", "Timesheet", + "timesheet-viewMode", "Timesheets", "Timeslot", "tinvitations", @@ -400,6 +404,7 @@ "VERSONS", "vertificalline", "vhidden", + "Waitlist", "WARNING️", "wasabisys", "webm", diff --git a/apps/web/.env b/apps/web/.env index 9180c30c7..b6a667a29 100644 --- a/apps/web/.env +++ b/apps/web/.env @@ -137,3 +137,7 @@ NEXT_PUBLIC_POSTHOG_HOST=https://us.i.posthog.com # Warning: IF TRUE This allows production builds to successfully complete even if # your project has ESLint errors. NEXT_IGNORE_ESLINT_ERROR_ON_BUILD=true + +# Mailchimp +MAILCHIMP_API_KEY= +MAILCHIMP_LIST_ID= diff --git a/apps/web/.env.sample b/apps/web/.env.sample index 62ae03258..1989758f4 100644 --- a/apps/web/.env.sample +++ b/apps/web/.env.sample @@ -87,3 +87,7 @@ MEET_JWT_APP_SECRET= # Warning: IF TRUE This allows production builds to successfully complete even if # your project has ESLint errors. NEXT_IGNORE_ESLINT_ERROR_ON_BUILD=true + +# Mailchimp +MAILCHIMP_API_KEY= +MAILCHIMP_LIST_ID= diff --git a/apps/web/app/[locale]/layout.tsx b/apps/web/app/[locale]/layout.tsx index 8dd5ce0ce..7353f53b4 100644 --- a/apps/web/app/[locale]/layout.tsx +++ b/apps/web/app/[locale]/layout.tsx @@ -1,7 +1,7 @@ /* eslint-disable no-mixed-spaces-and-tabs */ 'use client'; import 'react-loading-skeleton/dist/skeleton.css'; -import '../../styles/globals.css'; +import '@/styles/globals.css'; import clsx from 'clsx'; import { Provider } from 'jotai'; diff --git a/apps/web/app/[locale]/page-component.tsx b/apps/web/app/[locale]/page-component.tsx index 68f697eed..ddb2d6b58 100644 --- a/apps/web/app/[locale]/page-component.tsx +++ b/apps/web/app/[locale]/page-component.tsx @@ -66,9 +66,9 @@ function MainPage() { showTimer={headerSize <= 11.8 && isTrackingEnabled} className="h-full" mainHeaderSlot={ -
+
-
+
diff --git a/apps/web/app/api/subscribe/route.ts b/apps/web/app/api/subscribe/route.ts new file mode 100644 index 000000000..e7a08d50d --- /dev/null +++ b/apps/web/app/api/subscribe/route.ts @@ -0,0 +1,67 @@ +import { NextRequest, NextResponse } from 'next/server'; + +export const POST = async (req: NextRequest) => { + // 1. Destructure the email address from the request body. + const reqData = (await req.json()) as { + email_address: string; + tags: string[]; + captcha?: string; + }; + + if (!reqData.email_address) { + // 2. Throw an error if an email wasn't provided. + return NextResponse.json({ error: 'Email is required' }, { status: 400 }); + } + + if (!reqData.captcha) { + // 2. Display an error if the captcha code wasn't provided. + console.error('ERROR: Please provide required fields', 'STATUS: 400'); + } + + try { + // 3. Fetch the environment variables. + const LIST_ID = process.env.MAILCHIMP_LIST_ID; + const API_KEY = process.env.MAILCHIMP_API_KEY ? process.env.MAILCHIMP_API_KEY : ''; + if (!LIST_ID || !API_KEY) { + throw new Error('Missing Mailchimp environment variables'); + } + // 4. API keys are in the form -us3. + const DATACENTER = API_KEY.split('-')[1]; + const mailchimpData = { + email_address: reqData.email_address, + status: 'subscribed', + tags: reqData.tags ? [...reqData.tags] : ['Ever Teams'] + }; + // 5. Send a POST request to Mailchimp. + const response = await fetch(`https://${DATACENTER}.api.mailchimp.com/3.0/lists/${LIST_ID}/members`, { + body: JSON.stringify(mailchimpData), + headers: { + Authorization: `apikey ${API_KEY}`, + 'Content-Type': 'application/json' + }, + method: 'POST' + }); + console.log(response); + // 6. Swallow any errors from Mailchimp and return a better error message. + if (response.status >= 400) { + const errorResponse = await response.json(); + return NextResponse.json( + { + error: `There was an error subscribing to the newsletter: ${errorResponse.detail}` + }, + { status: 400 } + ); + } + + // 7. If we made it this far, it was a success! 🎉 + return NextResponse.json({ error: '', resp: response }, { status: 201 }); + } catch (error) { + return NextResponse.json( + { + error: (error as Error).message || (error as Error).toString(), + resp: null + }, + { status: 500 } + ); + } +}; diff --git a/apps/web/components/app-sidebar.tsx b/apps/web/components/app-sidebar.tsx index eaf3048e7..8c304672e 100644 --- a/apps/web/components/app-sidebar.tsx +++ b/apps/web/components/app-sidebar.tsx @@ -3,38 +3,36 @@ import { MonitorSmartphone, LayoutDashboard, Heart, - FolderKanban, SquareActivity, - PlusIcon, Files, - X + X, + Command, + AudioWaveform, + GalleryVerticalEnd } from 'lucide-react'; -import { EverTeamsLogo, SymbolAppLogo } from '@/lib/components/svgs'; import { NavMain } from '@/components/nav-main'; import { Sidebar, SidebarContent, SidebarHeader, - SidebarMenu, - SidebarMenuButton, - SidebarMenuItem, SidebarRail, SidebarTrigger, useSidebar, - SidebarMenuSubButton + SidebarMenuSubButton, + SidebarFooter } from '@/components/ui/sidebar'; import Link from 'next/link'; import { cn } from '@/lib/utils'; -import { useOrganizationAndTeamManagers } from '@/app/hooks/features/useOrganizationTeamManagers'; import { useAuthenticateUser, useModal, useOrganizationTeams } from '@/app/hooks'; import { useFavoritesTask } from '@/app/hooks/features/useFavoritesTask'; -import { Button } from '@/lib/components/button'; import { CreateTeamModal, TaskIssueStatus } from '@/lib/features'; import { useTranslations } from 'next-intl'; +import { WorkspacesSwitcher } from './workspace-switcher'; +import { SidebarOptInForm } from './sidebar-opt-in-form'; +import { NavProjects } from './nav-projects'; type AppSidebarProps = React.ComponentProps & { publicTeam: boolean | undefined }; export function AppSidebar({ publicTeam, ...props }: AppSidebarProps) { - const { userManagedTeams } = useOrganizationAndTeamManagers(); const { user } = useAuthenticateUser(); const username = user?.name || user?.firstName || user?.lastName || user?.username; const { isTeamManager } = useOrganizationTeams(); @@ -44,11 +42,57 @@ export function AppSidebar({ publicTeam, ...props }: AppSidebarProps) { const t = useTranslations(); // This is sample data. const data = { - user: { - name: 'evereq', - email: 'evereq@ever.co', - avatar: '/assets/svg/profile.svg' - }, + workspaces: [ + { + name: 'Ever Teams', + logo: ({ className }: { className?: string }) => ( + + + + + + + + + + ), + plan: 'Enterprise' + }, + { + name: 'Ever Gauzy', + logo: AudioWaveform, + plan: 'Startup' + }, + { + name: 'Ever Cloc', + logo: GalleryVerticalEnd, + plan: 'Free' + }, + { + name: 'Ever Rec', + logo: Command, + plan: 'Free' + } + ], navMain: [ { title: t('sidebar.DASHBOARD'), @@ -138,35 +182,6 @@ export function AppSidebar({ publicTeam, ...props }: AppSidebarProps) { } ] }, - ...(userManagedTeams && userManagedTeams.length > 0 - ? [ - { - title: t('sidebar.PROJECTS'), - label: 'projects', - url: '#', - icon: FolderKanban, - items: [ - { - title: t('common.NO_PROJECT'), - label: 'no-project', - url: '#', - component: ( - - - - ) - } - ] - } - ] - : []), { title: t('sidebar.MY_WORKS'), url: '#', @@ -232,7 +247,8 @@ export function AppSidebar({ publicTeam, ...props }: AppSidebarProps) { } ] : []) - ] + ], + projects: [] }; return ( @@ -245,30 +261,20 @@ export function AppSidebar({ publicTeam, ...props }: AppSidebarProps) { - - - - -
- -
- {state === 'expanded' && } - -
-
-
+
+ + + + + diff --git a/apps/web/components/nav-main.tsx b/apps/web/components/nav-main.tsx index de5446bdf..79934f941 100644 --- a/apps/web/components/nav-main.tsx +++ b/apps/web/components/nav-main.tsx @@ -5,6 +5,7 @@ import { cn } from '@/lib/utils'; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'; import { SidebarGroup, + SidebarGroupLabel, SidebarMenu, SidebarMenuAction, SidebarMenuButton, @@ -64,7 +65,10 @@ export function NavMain({ }; return ( - + Platform + {items.map((item, index) => ( @@ -110,11 +114,11 @@ export function NavMain({ ) : ( @@ -142,23 +146,23 @@ export function NavMain({ {item.items?.length ? ( <> - + Toggle - + {item.items.map((subItem, key) => ( - + {subItem?.component || ( handleSubMenuToggle(key)} @@ -167,7 +171,7 @@ export function NavMain({ ) { - const { isMobile, state } = useSidebar(); +}) { + const { isMobile } = useSidebar(); - return ( + const { user } = useAuthenticateUser(); + + const { userManagedTeams } = useOrganizationAndTeamManagers(); + const t = useTranslations(); + return userManagedTeams && userManagedTeams.length > 0 ? ( Projects - - {projects.map((item) => ( - - - - - - {item.name} - - - - - - - - More - - - - - - + {projects && projects.length ? ( + <> + {projects.map((item) => ( + + + + + {item.name} + + + + + + + More + + + - View Project - - - - - - Share Project - - - - - - - Delete Project - - - - + + + View Project + + + + Share Project + + + + + Delete Project + + + + + ))} + + + + More + + + + ) : ( + + + + - ))} - - - - - More - - - + )} - ); + ) : null; } diff --git a/apps/web/components/nav-secondary.tsx b/apps/web/components/nav-secondary.tsx index 3b1bd5394..b4e2aec2b 100644 --- a/apps/web/components/nav-secondary.tsx +++ b/apps/web/components/nav-secondary.tsx @@ -25,10 +25,10 @@ export function NavSecondary({ return ( - + {items.map((item) => ( - - + + - {options.map(({ label, icon: Icon, view: optionView }) => ( - - - - ))} - - ); + return ( + <> + {options.map(({ label, icon: Icon, view: optionView }) => ( + + + + ))} + + ); } diff --git a/apps/web/components/pages/main/header-tabs.tsx b/apps/web/components/pages/main/header-tabs.tsx index 3cb3561e4..ee7fadb5c 100644 --- a/apps/web/components/pages/main/header-tabs.tsx +++ b/apps/web/components/pages/main/header-tabs.tsx @@ -1,69 +1,58 @@ import { clsxm } from '@app/utils'; import { Tooltip } from 'lib/components'; import LinkWrapper from '../kanban/link-wrapper'; -import { - QueueListIcon, - Squares2X2Icon, - TableCellsIcon -} from '@heroicons/react/20/solid'; +import { QueueListIcon, Squares2X2Icon, TableCellsIcon } from '@heroicons/react/20/solid'; import KanbanIcon from '@components/ui/svgs/kanban'; import { IssuesView } from '@app/constants'; import { useAtom } from 'jotai'; import { headerTabs } from '@app/stores/header-tabs'; import { DottedLanguageObjectStringPaths, useTranslations } from 'next-intl'; -const HeaderTabs = ({ - linkAll, - kanban = false -}: { - linkAll: boolean; - kanban?: boolean; -}) => { - const t = useTranslations(); - const options = [ - { label: 'CARDS', icon: QueueListIcon, view: IssuesView.CARDS }, - { label: 'TABLE', icon: TableCellsIcon, view: IssuesView.TABLE }, - { label: 'BLOCKS', icon: Squares2X2Icon, view: IssuesView.BLOCKS }, - { label: 'KANBAN', icon: KanbanIcon, view: IssuesView.KANBAN } - ]; - const links = linkAll - ? ['/', '/', '/', '/kanban'] - : [undefined, undefined, undefined, '/kanban']; - const [view, setView] = useAtom(headerTabs); - const activeView = kanban ? IssuesView.KANBAN : view; - return ( - <> - {options.map(({ label, icon: Icon, view: optionView }, index) => ( - - - - - - ))} - - ); +const HeaderTabs = ({ linkAll, kanban = false }: { linkAll: boolean; kanban?: boolean }) => { + const t = useTranslations(); + const options = [ + { label: 'CARDS', icon: QueueListIcon, view: IssuesView.CARDS }, + { label: 'TABLE', icon: TableCellsIcon, view: IssuesView.TABLE }, + { label: 'BLOCKS', icon: Squares2X2Icon, view: IssuesView.BLOCKS }, + { label: 'KANBAN', icon: KanbanIcon, view: IssuesView.KANBAN } + ]; + const links = linkAll ? ['/', '/', '/', '/kanban'] : [undefined, undefined, undefined, '/kanban']; + const [view, setView] = useAtom(headerTabs); + const activeView = kanban ? IssuesView.KANBAN : view; + return ( + <> + {options.map(({ label, icon: Icon, view: optionView }, index) => ( + + + + + + ))} + + ); }; export default HeaderTabs; diff --git a/apps/web/components/pages/task/description-block/editor-components/LinkElement.tsx b/apps/web/components/pages/task/description-block/editor-components/LinkElement.tsx index 3bc09d970..033e97093 100644 --- a/apps/web/components/pages/task/description-block/editor-components/LinkElement.tsx +++ b/apps/web/components/pages/task/description-block/editor-components/LinkElement.tsx @@ -53,7 +53,7 @@ const LinkElement = ({ attributes, element, children }: any) => { href={href} rel="noreferrer" target="_blank" - className=" text-[#5000B9] dark:text-primary-light truncate max-w-[240px] overflow-hidden whitespace-nowrap mr-0" + className=" text-[#5000B9] dark:text-primary-light truncate max-w-[230px] overflow-hidden whitespace-nowrap mr-0" style={{ textOverflow: 'ellipsis' }} > {element.href} diff --git a/apps/web/components/sidebar-opt-in-form.tsx b/apps/web/components/sidebar-opt-in-form.tsx new file mode 100644 index 000000000..670acd645 --- /dev/null +++ b/apps/web/components/sidebar-opt-in-form.tsx @@ -0,0 +1,111 @@ +import { Button } from '@/components/ui/button'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { SidebarInput, useSidebar } from '@/components/ui/sidebar'; + +import { zodResolver } from '@hookform/resolvers/zod'; +import { useForm } from 'react-hook-form'; +import { z } from 'zod'; +import { Form, FormControl, FormField, FormItem, FormMessage } from '@/components/ui/form'; +import { ToastAction } from './ui/toast'; +import { toast } from './ui/use-toast'; +import { useState } from 'react'; + +export function SidebarOptInForm() { + const { state } = useSidebar(); + const [isLoading, setLoading] = useState(false); + const subscribeFormSchema = z + .object({ + email: z.string().email() + }) + .required(); + const form = useForm>({ + resolver: zodResolver(subscribeFormSchema) + }); + + const subscribe = async () => { + let tags = ['Ever Teams, Ever Teams App', 'Open', 'Cloud']; + setLoading((prev) => true); + const res = await fetch('/api/subscribe', { + body: JSON.stringify({ + email_address: form.getValues('email'), + captcha: '', + tags: tags, + status: 'subscribed' + }), + headers: { + 'Content-Type': 'application/json' + }, + method: 'POST' + }); + const { error } = (await res.json()) as { + error: string; + status: number; + [key: string]: unknown; + }; + + if (error) { + setLoading((prev) => false); + toast({ + title: 'Waiting list registration error', + description: `We have encountered a problem ${error} with your registration to our waiting list for Ever Teams`, + variant: 'destructive' + }); + return; + } + + setLoading(() => false); + toast({ + title: 'Confirmation of registration on waiting list', + description: "Thank you for joining our waiting list! We're delighted you're interested in Ever Teams", + variant: 'default', + className: 'bg-green-50 text-green-600 border-green-500', + action: Undo + }); + }; + + const onSubmit = (data: z.infer) => { + console.log(data); + (async () => await subscribe())(); + }; + + return state == 'expanded' ? ( +
+ + + + Subscribe to our newsletter + + Opt-in to receive updates and news about Ever Teams. + + + + ( + + + + + + + )} + /> + + + +
+ + ) : null; +} diff --git a/apps/web/components/ui/card.tsx b/apps/web/components/ui/card.tsx new file mode 100644 index 000000000..fbb0c3772 --- /dev/null +++ b/apps/web/components/ui/card.tsx @@ -0,0 +1,43 @@ +import * as React from 'react'; + +import { cn } from '@/lib/utils'; + +const Card = React.forwardRef>(({ className, ...props }, ref) => ( +
+)); +Card.displayName = 'Card'; + +const CardHeader = React.forwardRef>( + ({ className, ...props }, ref) => ( +
+ ) +); +CardHeader.displayName = 'CardHeader'; + +const CardTitle = React.forwardRef>( + ({ className, ...props }, ref) => ( +
+ ) +); +CardTitle.displayName = 'CardTitle'; + +const CardDescription = React.forwardRef>( + ({ className, ...props }, ref) => ( +
+ ) +); +CardDescription.displayName = 'CardDescription'; + +const CardContent = React.forwardRef>( + ({ className, ...props }, ref) =>
+); +CardContent.displayName = 'CardContent'; + +const CardFooter = React.forwardRef>( + ({ className, ...props }, ref) => ( +
+ ) +); +CardFooter.displayName = 'CardFooter'; + +export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }; diff --git a/apps/web/components/ui/form.tsx b/apps/web/components/ui/form.tsx new file mode 100644 index 000000000..4bcc587c8 --- /dev/null +++ b/apps/web/components/ui/form.tsx @@ -0,0 +1,135 @@ +import * as React from 'react'; +import * as LabelPrimitive from '@radix-ui/react-label'; +import { Slot } from '@radix-ui/react-slot'; +import { Controller, ControllerProps, FieldPath, FieldValues, FormProvider, useFormContext } from 'react-hook-form'; + +import { cn } from '@/lib/utils'; +import { Label } from 'components/ui/label'; + +const Form = FormProvider; + +type FormFieldContextValue< + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath +> = { + name: TName; +}; + +const FormFieldContext = React.createContext({} as FormFieldContextValue); + +const FormField = < + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath +>({ + ...props +}: ControllerProps) => { + return ( + + + + ); +}; + +const useFormField = () => { + const fieldContext = React.useContext(FormFieldContext); + const itemContext = React.useContext(FormItemContext); + const { getFieldState, formState } = useFormContext(); + + if (!fieldContext) { + throw new Error('useFormField should be used within '); + } + const fieldState = getFieldState(fieldContext.name, formState); + + const { id } = itemContext; + + return { + id, + name: fieldContext.name, + formItemId: `${id}-form-item`, + formDescriptionId: `${id}-form-item-description`, + formMessageId: `${id}-form-item-message`, + ...fieldState + }; +}; + +type FormItemContextValue = { + id: string; +}; + +const FormItemContext = React.createContext({} as FormItemContextValue); + +const FormItem = React.forwardRef>( + ({ className, ...props }, ref) => { + const id = React.useId(); + + return ( + +
+ + ); + } +); +FormItem.displayName = 'FormItem'; + +const FormLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => { + const { error, formItemId } = useFormField(); + + return