diff --git a/.changeset/tricky-mugs-learn.md b/.changeset/tricky-mugs-learn.md new file mode 100644 index 00000000..08dfde98 --- /dev/null +++ b/.changeset/tricky-mugs-learn.md @@ -0,0 +1,6 @@ +--- +'@snipcode/front': minor +'@snipcode/web': minor +--- + +Use shadcn ui for web design page diff --git a/.eslintrc.json b/.eslintrc.json index afa848f6..970a07a7 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -47,6 +47,14 @@ "index", // <- index imports "unknown" // <- unknown ], + "pathGroups": [ + { + "pattern": "@snipcode/**", + "group": "external", + "position": "after" + } + ], + "pathGroupsExcludedImportTypes": ["builtin"], "newlines-between": "always", "alphabetize": { "order": "asc", diff --git a/apps/backend/src/app.module.ts b/apps/backend/src/app.module.ts index 76524cc5..5d4dfc28 100644 --- a/apps/backend/src/app.module.ts +++ b/apps/backend/src/app.module.ts @@ -7,6 +7,7 @@ import { Logger, Module } from '@nestjs/common'; import { ConfigModule, ConfigService } from '@nestjs/config'; import { GraphQLModule } from '@nestjs/graphql'; import { ServeStaticModule } from '@nestjs/serve-static'; + import { DomainModule } from '@snipcode/domain'; import { EnvironmentVariables, validate } from './configs/environment'; diff --git a/apps/backend/src/configs/auth.guard.ts b/apps/backend/src/configs/auth.guard.ts index 293e7bed..5651db5e 100644 --- a/apps/backend/src/configs/auth.guard.ts +++ b/apps/backend/src/configs/auth.guard.ts @@ -1,5 +1,6 @@ import { CanActivate, ExecutionContext, Injectable, UnauthorizedException, createParamDecorator } from '@nestjs/common'; import { GqlExecutionContext } from '@nestjs/graphql'; + import { SessionService } from '@snipcode/domain'; import { errors } from '@snipcode/utils'; diff --git a/apps/backend/src/configs/exception.filter.ts b/apps/backend/src/configs/exception.filter.ts index 484bce3c..bc0b6e39 100644 --- a/apps/backend/src/configs/exception.filter.ts +++ b/apps/backend/src/configs/exception.filter.ts @@ -8,10 +8,11 @@ import { PrismaClientUnknownRequestError, PrismaClientValidationError, } from '@prisma/client/runtime/library'; -import { isAppError } from '@snipcode/utils'; import { Response } from 'express'; import { GraphQLError } from 'graphql'; +import { isAppError } from '@snipcode/utils'; + import { INTERNAL_SERVER_ERROR } from '../utils/constants'; type PrismaError = diff --git a/apps/backend/src/features/app/app.service.spec.ts b/apps/backend/src/features/app/app.service.spec.ts index d1b70839..eb809b8a 100644 --- a/apps/backend/src/features/app/app.service.spec.ts +++ b/apps/backend/src/features/app/app.service.spec.ts @@ -1,8 +1,9 @@ import { ConfigService } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; -import { PrismaService, RoleService, UserService } from '@snipcode/domain'; import { mock } from 'jest-mock-extended'; +import { PrismaService, RoleService, UserService } from '@snipcode/domain'; + import { AppService } from './app.service'; const prismaServiceMock = mock(); diff --git a/apps/backend/src/features/app/app.service.ts b/apps/backend/src/features/app/app.service.ts index 8888b536..184872de 100644 --- a/apps/backend/src/features/app/app.service.ts +++ b/apps/backend/src/features/app/app.service.ts @@ -1,5 +1,6 @@ import { Injectable, OnModuleInit } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; + import { RoleService, UserService } from '@snipcode/domain'; import { EnvironmentVariables } from '../../configs/environment'; diff --git a/apps/backend/src/features/auth/graphql/auth.integration.spec.ts b/apps/backend/src/features/auth/graphql/auth.integration.spec.ts index 93d6fdcd..f945768d 100644 --- a/apps/backend/src/features/auth/graphql/auth.integration.spec.ts +++ b/apps/backend/src/features/auth/graphql/auth.integration.spec.ts @@ -1,6 +1,7 @@ +import request from 'supertest'; + import { SessionService } from '@snipcode/domain'; import { isValidUUIDV4 } from '@snipcode/utils'; -import request from 'supertest'; import { TestHelper } from '../../../utils/tests/helpers'; import { TestServer, startTestServer } from '../../../utils/tests/server'; diff --git a/apps/backend/src/features/auth/graphql/auth.resolvers.ts b/apps/backend/src/features/auth/graphql/auth.resolvers.ts index 2d2f1cfb..d56c5bd3 100644 --- a/apps/backend/src/features/auth/graphql/auth.resolvers.ts +++ b/apps/backend/src/features/auth/graphql/auth.resolvers.ts @@ -1,6 +1,7 @@ import { UseGuards } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { Args, Context, Mutation, Parent, Query, ResolveField, Resolver } from '@nestjs/graphql'; + import { CreateSessionInput, CreateUserInput, diff --git a/apps/backend/src/features/auth/rest/auth.controller.ts b/apps/backend/src/features/auth/rest/auth.controller.ts index 4c1de5fa..955ba0b6 100644 --- a/apps/backend/src/features/auth/rest/auth.controller.ts +++ b/apps/backend/src/features/auth/rest/auth.controller.ts @@ -1,5 +1,7 @@ import { Controller, Get, Logger, Query, Res } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; +import { Response } from 'express'; + import { CreateSessionInput, CreateUserRootFolderInput, @@ -9,7 +11,6 @@ import { UserService, } from '@snipcode/domain'; import { addDayToDate, errors } from '@snipcode/utils'; -import { Response } from 'express'; import { EnvironmentVariables } from '../../../configs/environment'; import { AUTH_SUCCESS_URL } from '../../../utils/constants'; diff --git a/apps/backend/src/features/auth/rest/auth.provider.integration.spec.ts b/apps/backend/src/features/auth/rest/auth.provider.integration.spec.ts index f79a013b..c5724976 100644 --- a/apps/backend/src/features/auth/rest/auth.provider.integration.spec.ts +++ b/apps/backend/src/features/auth/rest/auth.provider.integration.spec.ts @@ -1,10 +1,11 @@ import * as url from 'node:url'; -import { SessionService } from '@snipcode/domain'; import { HttpResponse, http } from 'msw'; import { setupServer } from 'msw/node'; import request from 'supertest'; +import { SessionService } from '@snipcode/domain'; + import { TestHelper } from '../../../utils/tests/helpers'; import { TestServer, startTestServer } from '../../../utils/tests/server'; import { GitHubUserResponse } from '../types'; diff --git a/apps/backend/src/features/auth/services/github.service.test.ts b/apps/backend/src/features/auth/services/github.service.test.ts index 14aedc45..ba6b6245 100644 --- a/apps/backend/src/features/auth/services/github.service.test.ts +++ b/apps/backend/src/features/auth/services/github.service.test.ts @@ -1,9 +1,10 @@ import { ConfigService } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; -import { User } from '@snipcode/domain'; import { HttpResponse, http } from 'msw'; import { setupServer } from 'msw/node'; +import { User } from '@snipcode/domain'; + import { GithubService } from './github.service'; import { GitHubUserResponse } from '../types'; diff --git a/apps/backend/src/features/auth/services/github.service.ts b/apps/backend/src/features/auth/services/github.service.ts index 8621cea6..4eab0d21 100644 --- a/apps/backend/src/features/auth/services/github.service.ts +++ b/apps/backend/src/features/auth/services/github.service.ts @@ -1,8 +1,9 @@ import { Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; +import axios, { AxiosInstance, AxiosRequestConfig } from 'axios'; + import { CreateUserInput, UpdateUserInput, User } from '@snipcode/domain'; import { AppError } from '@snipcode/utils'; -import axios, { AxiosInstance, AxiosRequestConfig } from 'axios'; import { EnvironmentVariables } from '../../../configs/environment'; import { GitHubUserResponse } from '../types'; diff --git a/apps/backend/src/features/folders/graphql/folder.resolvers.ts b/apps/backend/src/features/folders/graphql/folder.resolvers.ts index b33f78e6..377fed5d 100644 --- a/apps/backend/src/features/folders/graphql/folder.resolvers.ts +++ b/apps/backend/src/features/folders/graphql/folder.resolvers.ts @@ -1,5 +1,6 @@ import { UseGuards } from '@nestjs/common'; import { Args, Mutation, Parent, Query, ResolveField, Resolver } from '@nestjs/graphql'; + import { CreateFolderInput, Folder, diff --git a/apps/backend/src/features/snippets/graphql/snippet.resolvers.ts b/apps/backend/src/features/snippets/graphql/snippet.resolvers.ts index 9f765a7a..30266ba3 100644 --- a/apps/backend/src/features/snippets/graphql/snippet.resolvers.ts +++ b/apps/backend/src/features/snippets/graphql/snippet.resolvers.ts @@ -1,5 +1,6 @@ import { UseGuards } from '@nestjs/common'; import { Args, Mutation, Parent, Query, ResolveField, Resolver } from '@nestjs/graphql'; + import { CreateSnippetInput, DeleteSnippetInput, diff --git a/apps/backend/src/features/snippets/rest/snippet.controller.ts b/apps/backend/src/features/snippets/rest/snippet.controller.ts index e810edfd..c0bbffc8 100644 --- a/apps/backend/src/features/snippets/rest/snippet.controller.ts +++ b/apps/backend/src/features/snippets/rest/snippet.controller.ts @@ -1,5 +1,6 @@ import { Controller, Get, Param } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; + import { SnippetService } from '@snipcode/domain'; import { OEmbedResult, generateOembedMetadata } from '@snipcode/embed'; diff --git a/apps/backend/src/features/users/graphql/user.resolvers.ts b/apps/backend/src/features/users/graphql/user.resolvers.ts index d4d24394..d7a827a4 100644 --- a/apps/backend/src/features/users/graphql/user.resolvers.ts +++ b/apps/backend/src/features/users/graphql/user.resolvers.ts @@ -1,5 +1,6 @@ import { ConfigService } from '@nestjs/config'; import { Args, Mutation, Resolver } from '@nestjs/graphql'; + import { NewsletterService } from '@snipcode/domain'; import { EnvironmentVariables } from '../../../configs/environment'; diff --git a/apps/backend/src/utils/graphql/date-scalar.ts b/apps/backend/src/utils/graphql/date-scalar.ts index d5bfacb8..58138e1c 100644 --- a/apps/backend/src/utils/graphql/date-scalar.ts +++ b/apps/backend/src/utils/graphql/date-scalar.ts @@ -1,8 +1,9 @@ import { CustomScalar, Scalar } from '@nestjs/graphql'; -import { errors } from '@snipcode/utils'; import { Kind, ValueNode } from 'graphql'; import { GraphQLError } from 'graphql'; +import { errors } from '@snipcode/utils'; + import { DATE_REGEX } from '../constants'; @Scalar('Date') diff --git a/apps/backend/src/utils/tests/helpers.ts b/apps/backend/src/utils/tests/helpers.ts index ca845f0a..8fca154e 100644 --- a/apps/backend/src/utils/tests/helpers.ts +++ b/apps/backend/src/utils/tests/helpers.ts @@ -1,8 +1,9 @@ import { INestApplication } from '@nestjs/common'; import { randEmail, randFullName, randPassword, randWord } from '@ngneat/falso'; +import request from 'supertest'; + import { PrismaService, RoleName } from '@snipcode/domain'; import { generateJwtToken } from '@snipcode/utils'; -import request from 'supertest'; type CreateUserInputArgs = { email: string; diff --git a/apps/web/.eslintrc.js b/apps/web/.eslintrc.js index 0a96bd95..099f6b00 100644 --- a/apps/web/.eslintrc.js +++ b/apps/web/.eslintrc.js @@ -17,6 +17,7 @@ module.exports = { 'sentry.edge.config.js', '.eslintrc.js', 'vitest.config.ts', + 'tailwind.config.ts', ], parserOptions: { ecmaVersion: 2023, @@ -27,9 +28,11 @@ module.exports = { rules: { camelcase: 'off', 'import/prefer-default-export': 'off', - 'react/jsx-filename-extension': 'off', 'react/jsx-props-no-spreading': 'off', - 'react/no-unused-prop-types': 'off', + 'react/boolean-prop-naming': 'error', + 'react/jsx-no-leaked-render': 'error', + 'react/no-unused-prop-types': 'error', + 'react/destructuring-assignment': 'error', 'react/require-default-props': 'off', 'import/extensions': 'off', quotes: 'off', diff --git a/apps/web/components.json b/apps/web/components.json new file mode 100644 index 00000000..45adf9ef --- /dev/null +++ b/apps/web/components.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "default", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "tailwind.config.ts", + "css": "src/styles/tailwind.css", + "baseColor": "slate", + "cssVariables": false, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils" + } +} \ No newline at end of file diff --git a/apps/web/package.json b/apps/web/package.json index 0bd3dde1..9f40132a 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -13,12 +13,14 @@ "dependencies": { "@apollo/client": "3.10.6", "@apollo/experimental-nextjs-app-support": "0.11.2", - "@headlessui/react": "2.1.0", "@hookform/resolvers": "3.6.0", + "@radix-ui/react-slot": "1.1.0", "@sentry/nextjs": "8.11.0", "@snipcode/front": "workspace:*", "@snipcode/utils": "workspace:*", + "class-variance-authority": "0.7.0", "classnames": "2.5.1", + "clsx": "2.1.1", "graphql": "16.9.0", "next": "14.2.4", "next-seo": "6.5.0", @@ -26,6 +28,8 @@ "react-cookie": "7.1.4", "react-dom": "18.3.1", "react-hook-form": "7.52.0", + "tailwind-merge": "2.4.0", + "tailwindcss-animate": "1.0.7", "yup": "1.4.0" }, "devDependencies": { diff --git a/apps/web/src/app/(protected)/app/browse/container.tsx b/apps/web/src/app/(protected)/app/browse/container.tsx index 72488b7a..f278836d 100644 --- a/apps/web/src/app/(protected)/app/browse/container.tsx +++ b/apps/web/src/app/(protected)/app/browse/container.tsx @@ -1,12 +1,13 @@ 'use client'; -import { Button } from '@snipcode/front/forms/button'; +import { useState } from 'react'; + +import { Button } from '@snipcode/front/components/ui/button'; import { SelectInput } from '@snipcode/front/forms/select-input'; -import { ChevronDoubleLeftIcon, ChevronDoubleRightIcon, SearchIcon } from '@snipcode/front/icons'; +import { ChevronsLeftIcon, ChevronsRightIcon, SearchIcon } from '@snipcode/front/icons'; import { usePublicSnippets } from '@snipcode/front/services'; -import { SelectOption } from '@snipcode/front/typings/components'; -import { PublicSnippetItem, PublicSnippetResult } from '@snipcode/front/typings/queries'; -import { useState } from 'react'; +import { SelectOption } from '@snipcode/front/types/components'; +import { PublicSnippetItem, PublicSnippetResult } from '@snipcode/front/types/queries'; import { PublicSnippet } from '@/components/snippets/public-snippet'; import { usePaginationToken } from '@/hooks/use-pagination-token'; @@ -124,23 +125,13 @@ export const BrowseContainer = ({ data }: Props) => { ))}
- -
diff --git a/apps/web/src/app/(protected)/app/browse/lib/fetch-snippets.ts b/apps/web/src/app/(protected)/app/browse/lib/fetch-snippets.ts index 26b94218..a6d5beef 100644 --- a/apps/web/src/app/(protected)/app/browse/lib/fetch-snippets.ts +++ b/apps/web/src/app/(protected)/app/browse/lib/fetch-snippets.ts @@ -1,8 +1,9 @@ +import { cookies } from 'next/headers'; + import { findPublicSnippetsQuery } from '@snipcode/front/graphql'; import { PublicSnippetsQuery } from '@snipcode/front/graphql/generated'; +import { SNIPPET_ITEM_PER_PAGE } from '@snipcode/front/lib/constants'; import { formatPublicSnippetsResult } from '@snipcode/front/services'; -import { SNIPPET_ITEM_PER_PAGE } from '@snipcode/front/utils/constants'; -import { cookies } from 'next/headers'; import { getApolloClient } from '@/lib/apollo/server'; import { AUTH_COOKIE_NAME } from '@/lib/constants'; diff --git a/apps/web/src/app/(protected)/app/folders/[id]/container.tsx b/apps/web/src/app/(protected)/app/folders/[id]/container.tsx index 6db305d9..ac264340 100644 --- a/apps/web/src/app/(protected)/app/folders/[id]/container.tsx +++ b/apps/web/src/app/(protected)/app/folders/[id]/container.tsx @@ -1,10 +1,12 @@ 'use client'; +import { useParams } from 'next/navigation'; + import { Directory } from '@snipcode/front/components/directory'; import { useFindFolder } from '@snipcode/front/services'; -import { useParams } from 'next/navigation'; import { useFolderDirectory } from '@/hooks/use-folder-directory'; +import { EMBEDDABLE_HOST_URL, SHAREABLE_HOST_URL } from '@/lib/constants'; export const ViewFolderContainer = () => { const queryParams = useParams<{ id: string }>(); @@ -18,16 +20,18 @@ export const ViewFolderContainer = () => { return (
- {isFolderFound && ( + {isFolderFound ? ( - )} + ) : null}
); }; diff --git a/apps/web/src/app/(protected)/app/home/container.tsx b/apps/web/src/app/(protected)/app/home/container.tsx index 02b32cbe..d3174ce3 100644 --- a/apps/web/src/app/(protected)/app/home/container.tsx +++ b/apps/web/src/app/(protected)/app/home/container.tsx @@ -4,6 +4,7 @@ import { Directory } from '@snipcode/front/components/directory'; import { useAuthenticatedUser } from '@snipcode/front/services'; import { useFolderDirectory } from '@/hooks/use-folder-directory'; +import { EMBEDDABLE_HOST_URL, SHAREABLE_HOST_URL } from '@/lib/constants'; export const HomeContainer = () => { const { data: user } = useAuthenticatedUser(); @@ -12,8 +13,10 @@ export const HomeContainer = () => { return (
{ return (
- {isSnippetFound && ( + {isSnippetFound ? (
{
- )} + ) : null}
); }; diff --git a/apps/web/src/app/(protected)/layout.tsx b/apps/web/src/app/(protected)/layout.tsx index dfaa0efe..c7f5b4d2 100644 --- a/apps/web/src/app/(protected)/layout.tsx +++ b/apps/web/src/app/(protected)/layout.tsx @@ -1,12 +1,20 @@ -import { ToastProvider } from '@snipcode/front/components/toast/provider'; +import '@/styles/globals.css'; + +import { Inter as FontSans } from 'next/font/google'; import React, { PropsWithChildren } from 'react'; +import { Toaster } from '@snipcode/front/components/ui/toaster'; + import { ApolloWrapper } from '@/lib/apollo/client'; import { generatePageMetadata } from '@/lib/seo'; +import { cn } from '@/lib/utils'; import { AuthenticatedLayout } from './layout/content'; -import '@/styles/globals.css'; +const fontSans = FontSans({ + subsets: ['latin'], + variable: '--font-sans', +}); export const metadata = generatePageMetadata({ noIndex: true, @@ -15,12 +23,11 @@ export const metadata = generatePageMetadata({ const AppLayout = ({ children }: PropsWithChildren) => { return ( - +
- - {children} - + + {children}
diff --git a/apps/web/src/app/(protected)/layout/content.tsx b/apps/web/src/app/(protected)/layout/content.tsx index c73c86ea..1eb4c7fd 100644 --- a/apps/web/src/app/(protected)/layout/content.tsx +++ b/apps/web/src/app/(protected)/layout/content.tsx @@ -1,13 +1,13 @@ 'use client'; -import { useAuthenticatedUser } from '@snipcode/front/services'; import { ReactNode } from 'react'; +import { useAuthenticatedUser } from '@snipcode/front/services'; + +import { Header } from '@/app/(protected)/layout/header'; import { Loader } from '@/components/common/loader'; import { Redirect } from '@/components/common/redirect'; -import { Header } from './header'; - type Props = { children?: ReactNode; }; @@ -28,7 +28,7 @@ export const AuthenticatedLayout = ({ children }: Props) => { } return ( -
+
{children}
diff --git a/apps/web/src/app/(protected)/layout/header.tsx b/apps/web/src/app/(protected)/layout/header.tsx index f2c0f808..78458feb 100644 --- a/apps/web/src/app/(protected)/layout/header.tsx +++ b/apps/web/src/app/(protected)/layout/header.tsx @@ -1,15 +1,24 @@ -'use client'; +import Link from 'next/link'; +import { usePathname } from 'next/navigation'; -import { Disclosure, Menu, Transition } from '@snipcode/front'; -import { Link } from '@snipcode/front/components/link'; -import { UserAvatar } from '@snipcode/front/components/user-avatar'; -import { LogoIcon, LogoLightIcon, MenuIcon, XIcon } from '@snipcode/front/icons'; +import { Avatar, AvatarFallback, AvatarImage } from '@snipcode/front/components/ui/avatar'; +import { Button } from '@snipcode/front/components/ui/button'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from '@snipcode/front/components/ui/dropdown-menu'; +import { LogoIcon, LogoLightIcon } from '@snipcode/front/icons'; +import { classNames } from '@snipcode/front/lib/classnames'; +import { COLORS } from '@snipcode/front/lib/constants'; import { useLogoutUser } from '@snipcode/front/services'; -import { classNames } from '@snipcode/front/utils/classnames'; -import { usePathname } from 'next/navigation'; -import { Fragment } from 'react'; import { useAuth } from '@/hooks/authentication/use-auth'; +import { generateInitials } from '@/lib/utils'; const navigation = [ { current: true, href: '/app/home', name: 'Home' }, @@ -27,6 +36,11 @@ export const Header = () => { const { deleteToken, redirectToHome, user } = useAuth(); const pathname = usePathname(); + const initials = generateInitials(user?.name); + + const charIndex = initials.charCodeAt(0) - 65; + const colorIndex = charIndex % COLORS.length; + const logout = async () => { await logoutUserMutation({ onCompleted: async () => { @@ -37,131 +51,66 @@ export const Header = () => { }; return ( - - {({ open }) => ( - <> -
-
-
-
- - -
-
- {navigation.map((item) => ( - - {item.name} - - ))} -
-
-
- -
- - Open user menu - - -
- - - - Profile - - - - - - -
-
-
- {/* Mobile menu button */} - - Open main menu - {open ? ( - -
-
+
+
+
+
+ +
- - -
- {navigation.map((item) => ( - - {item.name} - - ))} -
-
-
-
- -
-
-
{user?.name}
-
{user?.email}
-
-
-
- - Profile - - + +
+ + + + + + +
+

{user?.name}

+

@{user?.username}

+
+
+ + + + + Profile + + + + + Sign out - -
-
- - - )} - + + + +
+
+
+
); }; diff --git a/apps/web/src/app/(public)/layout.tsx b/apps/web/src/app/(public)/layout.tsx index 01293878..5d824af7 100644 --- a/apps/web/src/app/(public)/layout.tsx +++ b/apps/web/src/app/(public)/layout.tsx @@ -1,19 +1,26 @@ +import '@/styles/globals.css'; + +import { Inter as FontSans } from 'next/font/google'; import React, { PropsWithChildren } from 'react'; import { ApolloWrapper } from '@/lib/apollo/client'; import { generatePageMetadata } from '@/lib/seo'; +import { cn } from '@/lib/utils'; import { PublicFooter } from './layout/footer'; import { PublicHeader } from './layout/header'; -import '@/styles/globals.css'; +const fontSans = FontSans({ + subsets: ['latin'], + variable: '--font-sans', +}); export const metadata = generatePageMetadata(); const RootLayout = ({ children }: PropsWithChildren) => { return ( - +
diff --git a/apps/web/src/app/(public)/layout/header.tsx b/apps/web/src/app/(public)/layout/header.tsx index 44dd5a2f..702df054 100644 --- a/apps/web/src/app/(public)/layout/header.tsx +++ b/apps/web/src/app/(public)/layout/header.tsx @@ -1,9 +1,10 @@ 'use client'; -import { LogoIcon } from '@snipcode/front/icons'; import Link from 'next/link'; import { MouseEvent, useState } from 'react'; +import { LogoIcon } from '@snipcode/front/icons'; + import { useAuth } from '@/hooks/authentication/use-auth'; // TODO - Refactor to server component @@ -61,7 +62,7 @@ const PublicHeader = () => { )} - {isExpanded && ( + {isExpanded ? ( - )} + ) : null}
@@ -110,7 +111,6 @@ const PublicHeader = () => { title="Sign in" onClick={redirectToSignInIfAuthenticated} > - {/* eslint-disable-next-line jsx-a11y/anchor-is-valid */} Sign in @@ -120,13 +120,12 @@ const PublicHeader = () => { title="Get started" onClick={redirectToSignupIfAuthenticated} > - {/* eslint-disable-next-line jsx-a11y/anchor-is-valid */} Get started
- {isExpanded && ( + {isExpanded ? ( - )} + ) : null}
); diff --git a/apps/web/src/app/(public)/page.test.tsx b/apps/web/src/app/(public)/page.test.tsx index f78a7727..dbc5e7dc 100644 --- a/apps/web/src/app/(public)/page.test.tsx +++ b/apps/web/src/app/(public)/page.test.tsx @@ -1,8 +1,9 @@ import { MockedProvider } from '@apollo/client/testing'; -import { authenticatedUserQuery } from '@snipcode/front/graphql/users/queries/authenticated-user'; import { render, screen } from '@testing-library/react'; import React from 'react'; +import { authenticatedUserQuery } from '@snipcode/front/graphql/users/queries/authenticated-user'; + import Home from './page'; const mocks = [ diff --git a/apps/web/src/app/(public)/signin/container.tsx b/apps/web/src/app/(public)/signin/container.tsx index ac510e77..e2d39c5e 100644 --- a/apps/web/src/app/(public)/signin/container.tsx +++ b/apps/web/src/app/(public)/signin/container.tsx @@ -1,16 +1,17 @@ 'use client'; import { yupResolver } from '@hookform/resolvers/yup'; -import { Alert } from '@snipcode/front/components/alert'; -import { Button } from '@snipcode/front/forms/button'; -import { TextInput } from '@snipcode/front/forms/text-input'; -import { GithubIcon, GoogleIcon } from '@snipcode/front/icons'; -import { useLoginUser } from '@snipcode/front/services'; import Link from 'next/link'; import { useState } from 'react'; import { FormProvider, useForm } from 'react-hook-form'; import * as yup from 'yup'; +import { Alert } from '@snipcode/front/components/alert'; +import { Button } from '@snipcode/front/components/ui/button'; +import { TextInput } from '@snipcode/front/forms/text-input'; +import { GithubIcon, GoogleIcon, LoaderIcon } from '@snipcode/front/icons'; +import { useLoginUser } from '@snipcode/front/services'; + import { useAuth } from '@/hooks/authentication/use-auth'; import { FORM_ERRORS } from '@/lib/constants'; @@ -65,15 +66,15 @@ export const SignInContainer = () => {
-

Sign in for Snipcode

+

Sign in to Snipcode

-
- - @@ -84,13 +85,14 @@ export const SignInContainer = () => {
- {loginError && } + {loginError ? : null} - @@ -99,7 +101,7 @@ export const SignInContainer = () => {

Don't have an account?{' '} - Create an account now + Sign up now

diff --git a/apps/web/src/app/(public)/signup/container.tsx b/apps/web/src/app/(public)/signup/container.tsx index 3af9480a..05bab592 100644 --- a/apps/web/src/app/(public)/signup/container.tsx +++ b/apps/web/src/app/(public)/signup/container.tsx @@ -1,17 +1,18 @@ 'use client'; import { yupResolver } from '@hookform/resolvers/yup'; -import { Alert } from '@snipcode/front/components/alert'; -import { Link } from '@snipcode/front/components/link'; -import { Button } from '@snipcode/front/forms/button'; -import { TextInput } from '@snipcode/front/forms/text-input'; -import { GithubIcon, GoogleIcon } from '@snipcode/front/icons'; -import { useSignupUser } from '@snipcode/front/services'; import { useRouter } from 'next/navigation'; import { useState } from 'react'; import { FormProvider, useForm } from 'react-hook-form'; import * as yup from 'yup'; +import { Alert } from '@snipcode/front/components/alert'; +import { Link } from '@snipcode/front/components/link'; +import { Button } from '@snipcode/front/components/ui/button'; +import { TextInput } from '@snipcode/front/forms/text-input'; +import { GithubIcon, GoogleIcon, LoaderIcon } from '@snipcode/front/icons'; +import { useSignupUser } from '@snipcode/front/services'; + import { FORM_ERRORS } from '@/lib/constants'; const MIN_PASSWORD_LENGTH = 8; @@ -41,8 +42,6 @@ export const SignupContainer = () => { }); const handleSignup = async (values: FormValues) => { - console.log(values); - setSignupError(null); await signupUser({ @@ -77,15 +76,15 @@ export const SignupContainer = () => {
-

Sign up for Snipcode

+

Sign up to Snipcode

-
- - @@ -96,7 +95,7 @@ export const SignupContainer = () => {
- {signupError && } + {signupError ? : null} @@ -116,7 +115,8 @@ export const SignupContainer = () => { type="password" /> - diff --git a/apps/web/src/components/common/page-not-found.tsx b/apps/web/src/components/common/page-not-found.tsx index fdc9271f..8c94c158 100644 --- a/apps/web/src/components/common/page-not-found.tsx +++ b/apps/web/src/components/common/page-not-found.tsx @@ -1,6 +1,7 @@ -import { LogoIcon } from '@snipcode/front/icons'; import Link from 'next/link'; +import { LogoIcon } from '@snipcode/front/icons'; + export const PageNotFound = () => { return (
diff --git a/apps/web/src/components/home/feature-section.tsx b/apps/web/src/components/home/feature-section.tsx index 297fbef1..a99d4468 100644 --- a/apps/web/src/components/home/feature-section.tsx +++ b/apps/web/src/components/home/feature-section.tsx @@ -1,46 +1,46 @@ import { - CollectionIcon, - DocumentSearchIcon, - EmbedIcon, - ExtensionIcon, - ImportIcon, - ShareIcon, + CloudDownloadIcon, + FileSearch2Icon, + GalleryVerticalEndIcon, + InboxIcon, + Share2Icon, + SlidersVerticalIcon, } from '@snipcode/front/icons'; const features = [ { description: 'Organize related code snippets into folders the same way you manage your file on the computer.', - icon: , + icon: , id: 'organize-snippets', title: 'Organize your snippets', }, { description: 'Quickly find a code snippet in your whole directory and access it.', - icon: , + icon: , id: 'find-snippets', title: 'Find your snippets', }, { description: 'You can easily import all your code snippets from GitHub Gist to keep them all in one place.', - icon: , + icon: , id: 'import-snippets', title: 'Import from GitHub Gist', }, { description: 'Share your code snippets with other developers. Give them the ability to interact and improve.', - icon: , + icon: , id: 'share-snippets', title: 'Share your snippets', }, { description: 'For content creators, you can embed your snippet on a blog post or a post on social networks.', - icon: , + icon: , id: 'embed-snippets', title: 'Embed your snippets', }, { description: 'Easily capture and save code snippets while you are browsing on the web.', - icon: , + icon: , id: 'browser-extensions', title: 'Browser extensions', }, @@ -60,7 +60,7 @@ export const FeatureSection = () => {
{features.map((feature) => (
-
{feature.icon}
+
{feature.icon}

{feature.title}

{feature.description}

diff --git a/apps/web/src/components/home/newsletter/newsletter-alert.tsx b/apps/web/src/components/home/newsletter/newsletter-alert.tsx index fe9b2b4c..3af7ecc8 100644 --- a/apps/web/src/components/home/newsletter/newsletter-alert.tsx +++ b/apps/web/src/components/home/newsletter/newsletter-alert.tsx @@ -1,7 +1,16 @@ -import { Dialog, Transition } from '@headlessui/react'; -import { CheckIcon, CrossIcon } from '@snipcode/front/icons'; import classNames from 'classnames'; -import React, { Fragment, useState } from 'react'; +import React from 'react'; + +import { + AlertDialog, + AlertDialogAction, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from '@snipcode/front/components/ui/alert-dialog'; +import { CircleCheckIcon, CircleXIcon } from '@snipcode/front/icons'; type Props = { handleClose: () => void; @@ -17,7 +26,7 @@ type Content = { const alertContentMap: Record = { failure: { description: An error occurred while performing the subscription, - icon: , + icon: , title: 'Subscription failure', }, success: { @@ -28,14 +37,12 @@ const alertContentMap: Record = { You will receive updates on the application progress. ), - icon: , + icon: , title: 'Subscription successful', }, }; export const NewsletterAlert = ({ handleClose, state = 'failure' }: Props) => { - const [isOpen, setIsOpen] = useState(true); - const titleClasses = classNames('flex flex-col items-center text-2xl mb-6', { 'text-green-600': state === 'success', 'text-red-600': state === 'failure', @@ -45,59 +52,27 @@ export const NewsletterAlert = ({ handleClose, state = 'failure' }: Props) => { const closeModal = () => { handleClose(); - setIsOpen(false); }; return ( - - - -
- - -
-
- - -
-
- {content.icon} -

{content.title}

-
- -

{content.description}

-
- -
- -
-
-
-
-
-
-
+ + + + + {content.icon} + {content.title} + + {content.description} + + + + Got it! + + + + ); }; diff --git a/apps/web/src/components/home/newsletter/newsletter-form.test.tsx b/apps/web/src/components/home/newsletter/newsletter-form.test.tsx index e1b827db..9479aadd 100644 --- a/apps/web/src/components/home/newsletter/newsletter-form.test.tsx +++ b/apps/web/src/components/home/newsletter/newsletter-form.test.tsx @@ -1,9 +1,10 @@ import { MockedProvider } from '@apollo/client/testing'; -import { subscribeNewsletterMutation } from '@snipcode/front/graphql'; import { fireEvent, render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import React, { act } from 'react'; +import { subscribeNewsletterMutation } from '@snipcode/front/graphql'; + import { NewsletterForm } from './newsletter-form'; describe('Newsletter Form', () => { @@ -42,7 +43,7 @@ describe('Newsletter Form', () => { }); await waitFor(() => { - const dialog = screen.getByRole('dialog'); + const dialog = screen.getByRole('alertdialog'); expect(dialog).toBeInTheDocument(); @@ -85,7 +86,7 @@ describe('Newsletter Form', () => { }); await waitFor(() => { - const dialog = screen.getByRole('dialog'); + const dialog = screen.getByRole('alertdialog'); expect(dialog).toBeInTheDocument(); diff --git a/apps/web/src/components/home/newsletter/newsletter-form.tsx b/apps/web/src/components/home/newsletter/newsletter-form.tsx index 6c77ffb8..fb66ade5 100644 --- a/apps/web/src/components/home/newsletter/newsletter-form.tsx +++ b/apps/web/src/components/home/newsletter/newsletter-form.tsx @@ -1,9 +1,10 @@ 'use client'; -import { SpinnerIcon } from '@snipcode/front/icons'; -import { useSubscribeToNewsletter } from '@snipcode/front/services'; import { useState } from 'react'; +import { LoaderIcon } from '@snipcode/front/icons'; +import { useSubscribeToNewsletter } from '@snipcode/front/services'; + import { useBooleanState } from '@/hooks/use-boolean-state'; import { REGEX_EMAIL } from '@/lib/constants'; @@ -41,7 +42,7 @@ export const NewsletterForm = () => { return (
- {isAlertOpened && } + {isAlertOpened ? : null} { className="inline-flex items-center justify-center w-full px-8 py-4 text-base font-bold text-white transition-all duration-200 bg-gray-900 border border-transparent sm:w-auto sm:py-3 hover:bg-opacity-90 rounded-xl" onClick={handleSubscribe} > - {isLoading && } + {isLoading ? : null} Get updates
diff --git a/apps/web/src/components/snippets/public-snippet.tsx b/apps/web/src/components/snippets/public-snippet.tsx index b658f544..c05738ec 100644 --- a/apps/web/src/components/snippets/public-snippet.tsx +++ b/apps/web/src/components/snippets/public-snippet.tsx @@ -1,6 +1,6 @@ import { Link } from '@snipcode/front/components/link'; import { UserAvatar } from '@snipcode/front/components/user-avatar'; -import { PublicSnippetResult } from '@snipcode/front/typings/queries'; +import { PublicSnippetResult } from '@snipcode/front/types/queries'; type Props = { snippet: PublicSnippetResult['items'][number]; @@ -12,7 +12,7 @@ export const PublicSnippet = ({ snippet }: Props) => { const htmlCode = snippet.content; return ( -
+
diff --git a/apps/web/src/hooks/authentication/use-auth.ts b/apps/web/src/hooks/authentication/use-auth.ts index 3f5731d7..5f4d7c72 100644 --- a/apps/web/src/hooks/authentication/use-auth.ts +++ b/apps/web/src/hooks/authentication/use-auth.ts @@ -1,9 +1,10 @@ import { useApolloClient } from '@apollo/client'; -import { useAuthenticatedUser } from '@snipcode/front/services'; -import { addDayToDate } from '@snipcode/utils'; import { useRouter } from 'next/navigation'; import { useCookies } from 'react-cookie'; +import { useAuthenticatedUser } from '@snipcode/front/services'; +import { addDayToDate } from '@snipcode/utils'; + import { AUTH_COOKIE_NAME } from '@/lib/constants'; export const useAuth = () => { diff --git a/apps/web/src/hooks/use-folder-directory.ts b/apps/web/src/hooks/use-folder-directory.ts index 7d856bb7..46ac9b34 100644 --- a/apps/web/src/hooks/use-folder-directory.ts +++ b/apps/web/src/hooks/use-folder-directory.ts @@ -1,6 +1,7 @@ -import { useAuthenticatedUser, useLazyListDirectory } from '@snipcode/front/services'; import { useRouter } from 'next/navigation'; +import { useAuthenticatedUser, useLazyListDirectory } from '@snipcode/front/services'; + export const useFolderDirectory = () => { const router = useRouter(); const { data: user } = useAuthenticatedUser(); diff --git a/apps/web/src/lib/constants.ts b/apps/web/src/lib/constants.ts index 7538296e..fa8a5214 100644 --- a/apps/web/src/lib/constants.ts +++ b/apps/web/src/lib/constants.ts @@ -14,3 +14,5 @@ export const FORM_ERRORS = { export const BAD_LOGIN_MESSAGE = 'The email or password is invalid.'; export const APP_URL = process.env.NEXT_PUBLIC_APP_URL; +export const SHAREABLE_HOST_URL = 'https://snipcode.dev'; +export const EMBEDDABLE_HOST_URL = 'https://api.snipcode.dev'; diff --git a/apps/web/src/lib/utils.ts b/apps/web/src/lib/utils.ts new file mode 100644 index 00000000..c8e82455 --- /dev/null +++ b/apps/web/src/lib/utils.ts @@ -0,0 +1,23 @@ +import { type ClassValue, clsx } from 'clsx'; +import { twMerge } from 'tailwind-merge'; + +export const cn = (...inputs: ClassValue[]) => { + return twMerge(clsx(inputs)); +}; + +export const generateInitials = (name?: string | null) => { + if (!name) { + return 'JD'; + } + + const nameArray = name.trim().split(' '); + + const [firstName, secondName] = nameArray; + + const initials = [firstName, secondName] + .filter(Boolean) + .map((name) => name.charAt(0)) + .join(''); + + return initials.toUpperCase(); +}; diff --git a/apps/web/tailwind.config.js b/apps/web/tailwind.config.js deleted file mode 100644 index 25234a6d..00000000 --- a/apps/web/tailwind.config.js +++ /dev/null @@ -1,29 +0,0 @@ -const defaultTheme = require('tailwindcss/defaultTheme'); - -module.exports = { - content: [ - './src/app/**/*.{js,ts,jsx,tsx}', - './src/components/**/*.{js,ts,jsx,tsx}', - '../../packages/front/**/*.{js,ts,jsx,tsx}', - ], - darkMode: 'class', - theme: { - extend: { - animation: { - 'spin-fast': 'spin .5s linear infinite', - }, - maxHeight: { - 0: '0', - xl: '36rem', - }, - fontFamily: { - sans: ['Inter', ...defaultTheme.fontFamily.sans], - }, - }, - screens: { - xs: { max: '639px' }, - ...defaultTheme.screens, - }, - }, - plugins: [require('@tailwindcss/forms')], -}; diff --git a/apps/web/tailwind.config.ts b/apps/web/tailwind.config.ts new file mode 100644 index 00000000..93221946 --- /dev/null +++ b/apps/web/tailwind.config.ts @@ -0,0 +1,52 @@ +import type { Config } from 'tailwindcss'; +import defaultTheme from 'tailwindcss/defaultTheme'; + +const config = { + content: [ + './src/app/**/*.{js,ts,jsx,tsx}', + './src/components/**/*.{js,ts,jsx,tsx}', + '../../packages/front/**/*.{js,ts,jsx,tsx}', + ], + darkMode: ['class'], + prefix: '', + theme: { + container: { + center: true, + padding: '2rem', + screens: { + '2xl': '1400px', + }, + }, + extend: { + keyframes: { + 'accordion-down': { + from: { height: '0' }, + to: { height: 'var(--radix-accordion-content-height)' }, + }, + 'accordion-up': { + from: { height: 'var(--radix-accordion-content-height)' }, + to: { height: '0' }, + }, + }, + animation: { + 'accordion-down': 'accordion-down 0.2s ease-out', + 'accordion-up': 'accordion-up 0.2s ease-out', + 'spin-fast': 'spin .5s linear infinite', + }, + maxHeight: { + 0: '0', + xl: '36rem', + }, + fontFamily: { + sans: ['var(--font-sans)', ...defaultTheme.fontFamily.sans], + }, + }, + screens: { + xs: { max: '639px' }, + ...defaultTheme.screens, + }, + }, + plugins: [require('tailwindcss-animate'), require('@tailwindcss/forms')], +} satisfies Config; + +export default config; diff --git a/packages/domain/.gitignore b/packages/domain/.gitignore index bcd7cae8..c456d557 100644 --- a/packages/domain/.gitignore +++ b/packages/domain/.gitignore @@ -3,3 +3,4 @@ dist .env .env.local coverage +dbdata diff --git a/packages/domain/package.json b/packages/domain/package.json index a7a673de..6b467c20 100644 --- a/packages/domain/package.json +++ b/packages/domain/package.json @@ -12,7 +12,7 @@ "clean": "rm -rf .turbo dist coverage", "lint": "eslint --fix", "env": "dotenv -e .env.local", - "db:local": "docker run -d --rm -e MYSQL_ROOT_PASSWORD=secret -e MYSQL_DATABASE=snipcode --name snipcode-local-db -p 3311:3306 mysql:8.0.39", + "db:local": "docker run -d --rm -e MYSQL_ROOT_PASSWORD=secret -e MYSQL_DATABASE=snipcode -v ./dbdata/snipcode:/var/lib/mysql --name snipcode-local-db -p 3311:3306 mysql:8.0.39", "db:local:stop": "docker kill snipcode-local-db && docker container prune -f", "db:generate": "yarn env -- prisma generate", "db:migrate": "yarn env -- prisma migrate dev", diff --git a/packages/embed/src/renderer/content/html-generator.ts b/packages/embed/src/renderer/content/html-generator.ts index 76195fe0..557b2d28 100644 --- a/packages/embed/src/renderer/content/html-generator.ts +++ b/packages/embed/src/renderer/content/html-generator.ts @@ -1,6 +1,7 @@ -import { Snippet } from '@snipcode/domain'; import { Lang } from 'shiki'; +import { Snippet } from '@snipcode/domain'; + import { generateLineHighlightOptions, parseHTMLSnippetCode } from './utils'; import { Shiki } from '../types'; diff --git a/packages/embed/src/server/index.ts b/packages/embed/src/server/index.ts index f1d042fd..647dddce 100644 --- a/packages/embed/src/server/index.ts +++ b/packages/embed/src/server/index.ts @@ -1,10 +1,11 @@ import http from 'http'; import path from 'path'; -import { dbClient } from '@snipcode/domain'; import dotenv from 'dotenv'; import express from 'express'; +import { dbClient } from '@snipcode/domain'; + import { renderSnippetToHtml } from '../renderer'; dotenv.config({ override: true }); diff --git a/packages/front/.eslintrc.js b/packages/front/.eslintrc.js index ae010f6f..d8837d6d 100644 --- a/packages/front/.eslintrc.js +++ b/packages/front/.eslintrc.js @@ -6,7 +6,8 @@ module.exports = { "tailwind.config.js", "jest.config.ts", "generated.ts", - '.eslintrc.js' + '.eslintrc.js', + 'tailwind.config.ts' ], "parserOptions": { "ecmaVersion": 2023, diff --git a/packages/front/components.json b/packages/front/components.json new file mode 100644 index 00000000..ac9fbc41 --- /dev/null +++ b/packages/front/components.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "default", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "tailwind.config.ts", + "css": "styles/globals.css", + "baseColor": "slate", + "cssVariables": false, + "prefix": "" + }, + "aliases": { + "components": "packages/front/components", + "utils": "lib/utils" + } +} diff --git a/packages/front/components/alert.tsx b/packages/front/components/alert.tsx index 4d9b0c60..f1c8d1b7 100644 --- a/packages/front/components/alert.tsx +++ b/packages/front/components/alert.tsx @@ -1,34 +1,26 @@ -import { classNames } from '../utils/classnames'; +import { AlertCircle } from 'lucide-react'; -type AlertType = 'success' | 'error' | 'info'; +import { AlertDescription, Alert as AlertRoot, AlertTitle } from './ui/alert'; + +type AlertType = 'destructive' | 'info' | 'success'; type Props = { message: string; - title?: string; type?: AlertType; }; -const alertTypeMapToString: Record = { - error: 'Error', +const alertTypeMap: Record = { + destructive: 'Error', info: 'Info', success: 'Success', }; -const alertStyleMap: Record = { - error: 'bg-red-200 border-red-600 text-red-600', - info: 'bg-blue-200 border-blue-600 text-blue-600', - success: 'bg-green-200 border-green-600 text-green-600', -}; - -const Alert = ({ message, title, type = 'info' }: Props) => { - const classes = classNames('border-l-4 p-4 mb-8', alertStyleMap[type]); - +export const Alert = ({ message, type = 'info' }: Props) => { return ( -
-

{title ?? alertTypeMapToString[type]}

-

{message}

-
+ + + {alertTypeMap[type]} + {message} + ); }; - -export { Alert }; diff --git a/packages/front/components/dialog/confirm-dialog.tsx b/packages/front/components/dialog/confirm-dialog.tsx index 3967b93c..98a75407 100644 --- a/packages/front/components/dialog/confirm-dialog.tsx +++ b/packages/front/components/dialog/confirm-dialog.tsx @@ -1,7 +1,15 @@ -import { Description, Dialog, DialogPanel, DialogTitle } from '@headlessui/react'; -import { useRef } from 'react'; +import { Loader2 } from 'lucide-react'; -import { Button } from '../../forms/button'; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from '../ui/alert-dialog'; type Props = { cancelText?: string; @@ -22,45 +30,26 @@ const ConfirmDialog = ({ onConfirmButtonClick, open, }: Props) => { - const cancelButtonRef = useRef(null); - return ( - - -
- Confirm Action - {messageText} - -
- - -
-
-
-
+ + + + Confirm Action + {messageText} + + + {cancelText} + + {isLoading && } + {confirmText} + + + + ); }; diff --git a/packages/front/components/directory/breadcrumb.tsx b/packages/front/components/directory/breadcrumb.tsx index 5efd3a1a..5e2a08a2 100644 --- a/packages/front/components/directory/breadcrumb.tsx +++ b/packages/front/components/directory/breadcrumb.tsx @@ -1,7 +1,7 @@ -import { ChevronRightIcon, HomeIcon } from '@heroicons/react/20/solid'; +import { ChevronRightIcon, HomeIcon } from 'lucide-react'; -import { FilePath } from '../../typings/components'; -import { classNames } from '../../utils/classnames'; +import { classNames } from '../../lib/classnames'; +import { FilePath } from '../../types/components'; type Props = { current: string; @@ -37,7 +37,7 @@ const BreadcrumbItem = ({ isCurrent, isHome, label }: BreadcrumbItemProps) => { ); }; -const BreadCrumb = ({ current, onPathClick, paths, rootFolderId }: Props) => { +export const BreadCrumb = ({ current, onPathClick, paths, rootFolderId }: Props) => { const onItemClick = async (folderId: string, path?: string) => { if (!path || current === folderId) { return; @@ -50,7 +50,7 @@ const BreadCrumb = ({ current, onPathClick, paths, rootFolderId }: Props) => { ); }; - -export { BreadCrumb }; diff --git a/packages/front/components/directory/folders/empty.tsx b/packages/front/components/directory/folders/empty.tsx deleted file mode 100644 index 53c24825..00000000 --- a/packages/front/components/directory/folders/empty.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import { PlusIcon } from '@heroicons/react/20/solid'; - -import { Button } from '../../../forms/button'; - -type Props = { - handleCreateFolder: () => void; - handleCreateSnippet: () => void; -}; - -const EmptyFolder = ({ handleCreateFolder, handleCreateSnippet }: Props) => { - return ( -
- -

No Files

-

Get started by adding a folder or snippet.

-
- - -
-
- ); -}; - -export { EmptyFolder }; diff --git a/packages/front/components/directory/folders/folder-empty.tsx b/packages/front/components/directory/folders/folder-empty.tsx new file mode 100644 index 00000000..5998b71c --- /dev/null +++ b/packages/front/components/directory/folders/folder-empty.tsx @@ -0,0 +1,28 @@ +import { FolderPlusIcon, PlusIcon } from 'lucide-react'; + +import { Button } from '../../ui/button'; + +type Props = { + handleCreateFolder: () => void; + handleCreateSnippet: () => void; +}; + +export const EmptyFolder = ({ handleCreateFolder, handleCreateSnippet }: Props) => { + return ( +
+ +

No Files

+

Get started by adding a folder or snippet.

+
+ + +
+
+ ); +}; diff --git a/packages/front/components/directory/folders/folder-form.tsx b/packages/front/components/directory/folders/folder-form.tsx new file mode 100644 index 00000000..2e39abb9 --- /dev/null +++ b/packages/front/components/directory/folders/folder-form.tsx @@ -0,0 +1,123 @@ +'use client'; + +import { yupResolver } from '@hookform/resolvers/yup'; +import { Loader2 } from 'lucide-react'; +import { FormProvider, useForm } from 'react-hook-form'; +import * as yup from 'yup'; + +import { TextInput } from '../../../forms/text-input'; +import { useToast } from '../../../hooks/use-toast'; +import { FOLDER_NAME_REGEX, FORM_ERRORS } from '../../../lib/constants'; +import { useCreateFolder } from '../../../services/folders/create-folder'; +import { useUpdateFolder } from '../../../services/folders/update-folder'; +import { FolderItem } from '../../../types/components'; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from '../../ui/alert-dialog'; + +type Props = { + closeModal: () => void; + currentFolder: FolderItem | null; + parentFolderId: string; +}; + +const MIN_NAME_LENGTH = 1; +const MAX_NAME_LENGTH = 100; +const formSchema = yup.object().shape({ + name: yup + .string() + .required(FORM_ERRORS.fieldRequired) + .min(MIN_NAME_LENGTH, FORM_ERRORS.minCharacters(MIN_NAME_LENGTH)) + .max(MAX_NAME_LENGTH, FORM_ERRORS.minCharacters(MAX_NAME_LENGTH)) + .matches(FOLDER_NAME_REGEX, { excludeEmptyString: false, message: FORM_ERRORS.folderNameInvalid }), +}); + +type FormValues = { name: string }; + +export const FolderFormContainer = ({ closeModal, currentFolder, parentFolderId }: Props) => { + const { toastError, toastSuccess } = useToast(); + const { createFolder, isLoading: isCreateFolderLoading } = useCreateFolder(); + const { isLoading: isUpdateFolderLoading, updateFolder } = useUpdateFolder(parentFolderId); + + const isLoading = isCreateFolderLoading || isUpdateFolderLoading; + + const formMethods = useForm({ + defaultValues: { + name: currentFolder?.name, + }, + resolver: yupResolver(formSchema), + }); + + const submitCreateFolder = async (values: FormValues) => { + await createFolder({ + input: { + name: values.name, + parentId: parentFolderId, + }, + onError: (message) => { + toastError(`Failed to create: ${message}`); + }, + onSuccess: () => { + toastSuccess('Folder created!'); + closeModal(); + }, + }); + }; + + const submitUpdateFolder = async (values: FormValues) => { + if (!currentFolder || currentFolder.name === values.name) { + return; + } + + await updateFolder({ + id: currentFolder.id, + input: { + name: values.name, + }, + onError: (message) => { + toastError(`Failed to update: ${message}`); + }, + onSuccess: () => { + toastSuccess('Folder updated!'); + + closeModal(); + }, + }); + }; + + const handleSubmit = async (values: FormValues) => { + if (currentFolder) { + return submitUpdateFolder(values); + } + + return submitCreateFolder(values); + }; + + return ( + + + + {currentFolder ? 'Rename folder' : 'Create a new folder'} + + + + + + + Cancel + + {isLoading && } + {currentFolder ? 'Update' : 'Create'} + + + + + ); +}; diff --git a/packages/front/components/directory/folders/folder.tsx b/packages/front/components/directory/folders/folder.tsx index 4a09039d..0f1f2484 100644 --- a/packages/front/components/directory/folders/folder.tsx +++ b/packages/front/components/directory/folders/folder.tsx @@ -1,8 +1,8 @@ -import { FolderIcon, FolderOpenIcon, PencilIcon, TrashIcon } from '@heroicons/react/20/solid'; +import { FolderIcon, FolderOpenIcon, PencilIcon, TrashIcon } from 'lucide-react'; -import { useHover } from '../../../hooks/use-hover'; -import { FolderItem, MenuItemAction } from '../../../typings/components'; -import { displayItemLabel, truncate } from '../../../utils/text'; +// import { useHover } from '../../../hooks/use-hover'; +import { displayItemLabel, truncate } from '../../../lib/text'; +import { FolderItem, MenuItemAction } from '../../../types/components'; import { DotMenu } from '../../menus/dot-menu'; type Props = { @@ -14,8 +14,8 @@ type Props = { const FOLDER_NAME_MAX_LENGTH = 20; -const Folder = ({ item, onDeleteClick, onNavigate, onRenameClick }: Props) => { - const [hoverRef, isHovered] = useHover(); +export const Folder = ({ item, onDeleteClick, onNavigate, onRenameClick }: Props) => { + /*const [hoverRef, isHovered] = useHover();*/ const handleDoubleClick = () => { onNavigate(item.id); @@ -23,17 +23,17 @@ const Folder = ({ item, onDeleteClick, onNavigate, onRenameClick }: Props) => { const menuActions: MenuItemAction[] = [ { - icon:
@@ -205,7 +216,7 @@ const Directory = ({
{isNewFolderOpened && ( - ); }; - -export { Directory }; diff --git a/packages/front/components/directory/snippets/form/create-snippet.tsx b/packages/front/components/directory/snippets/form/create-snippet.tsx index 6f3bf840..76002ed1 100644 --- a/packages/front/components/directory/snippets/form/create-snippet.tsx +++ b/packages/front/components/directory/snippets/form/create-snippet.tsx @@ -1,17 +1,25 @@ -import { Dialog, Transition } from '@headlessui/react'; import { yupResolver } from '@hookform/resolvers/yup'; -import { Fragment, useRef } from 'react'; +import { Loader2 } from 'lucide-react'; import { FormProvider, useForm } from 'react-hook-form'; import { SnippetTextEditor } from './editor'; import { SnippetFormValues, formSchema } from './form-schema'; import { generateSnippetLanguageOptions } from './utils'; -import { Button } from '../../../../forms/button'; -import { useCodeHighlighter } from '../../../../hooks'; +import { useCodeHighlighter } from '../../../../hooks/use-code-highlighter'; +import { useToast } from '../../../../hooks/use-toast'; +import { CODE_HIGHLIGHT_OPTIONS, THEME_OPTIONS } from '../../../../lib/constants'; +import { extractLanguageFromName, lineHighlightToString } from '../../../../lib/snippets'; import { useCreateSnippet } from '../../../../services/snippets/create-snippet'; -import { CODE_HIGHLIGHT_OPTIONS, THEME_OPTIONS } from '../../../../utils/constants'; -import { extractLanguageFromName, lineHighlightToString } from '../../../../utils/snippets'; -import { useToast } from '../../../toast/provider'; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from '../../../ui/alert-dialog'; type Props = { closeModal: () => void; @@ -20,7 +28,6 @@ type Props = { }; const CreateSnippetContainer = ({ closeModal, folderId, open }: Props) => { - const cancelButtonRef = useRef(null); const { highlighter } = useCodeHighlighter(); const { toastError, toastSuccess } = useToast(); const { createSnippet, isLoading } = useCreateSnippet(); @@ -66,78 +73,39 @@ const CreateSnippetContainer = ({ closeModal, folderId, open }: Props) => { visibility: values.isPrivate ? 'private' : 'public', }, onError: (message) => { - toastError({ message: `Failed to create: ${message}` }); + toastError(`Failed to create: ${message}`); }, onSuccess: () => { - toastSuccess({ message: 'Snippet created!' }); + toastSuccess('Snippet created!'); handleCloseModal(); }, }); }; return ( - - - -
- - -
-
- - -
-
- - Create a new snippet - -
- - - -
-
-
-
- - -
-
-
-
-
-
-
+ + + + Create a new snippet + + + + + + + Cancel + + {isLoading && } + Create + + + + ); }; diff --git a/packages/front/components/directory/snippets/form/editor/hooks/use-editor.ts b/packages/front/components/directory/snippets/form/editor/hooks/use-editor.ts index 49f2d46c..d0b44269 100644 --- a/packages/front/components/directory/snippets/form/editor/hooks/use-editor.ts +++ b/packages/front/components/directory/snippets/form/editor/hooks/use-editor.ts @@ -1,7 +1,7 @@ import { useState } from 'react'; import { BUNDLED_LANGUAGES } from 'shiki'; -import { HighLightOption, HighlightSnippetArgs, TextSelection } from '../../../../../../typings/snippet-form'; +import { HighLightOption, HighlightSnippetArgs, TextSelection } from '../../../../../../types/snippet-form'; const languageNames = BUNDLED_LANGUAGES.map((language) => [language.id].concat(language.aliases ?? [])).flat(); @@ -60,7 +60,7 @@ export const useEditor = () => { const getLanguageFromExtension = (fileName?: string) => { const DEFAULT_LANGUAGE = 'txt'; - if (!fileName || !fileName.includes('.')) { + if (!fileName?.includes('.')) { return DEFAULT_LANGUAGE; } diff --git a/packages/front/components/directory/snippets/form/editor/hooks/use-form-editor.ts b/packages/front/components/directory/snippets/form/editor/hooks/use-form-editor.ts index 44399a1c..c57ad85c 100644 --- a/packages/front/components/directory/snippets/form/editor/hooks/use-form-editor.ts +++ b/packages/front/components/directory/snippets/form/editor/hooks/use-form-editor.ts @@ -3,8 +3,8 @@ import { useFormContext } from 'react-hook-form'; import { Highlighter } from 'shiki'; import { useEditor } from './use-editor'; -import { EditorFormValues } from '../../../../../../typings/snippet-form'; -import { CODE_HIGHLIGHT_OPTIONS } from '../../../../../../utils/constants'; +import { CODE_HIGHLIGHT_OPTIONS } from '../../../../../../lib/constants'; +import { EditorFormValues } from '../../../../../../types/snippet-form'; export const useFormEditor = () => { const { setValue, watch } = useFormContext(); @@ -23,7 +23,6 @@ export const useFormEditor = () => { const language = watch('language'); const lineHighlight = watch('lineHighlight'); const codeHighlight = watch('codeHighlight'); - const isSnippetPrivate = watch('isPrivate'); const codeLanguage = language?.id ?? getLanguageFromExtension(name); @@ -70,7 +69,6 @@ export const useFormEditor = () => { code, handleEditorSelect, highlightSnippet, - isSnippetPrivate, language, name, onHighlight, diff --git a/packages/front/components/directory/snippets/form/editor/index.tsx b/packages/front/components/directory/snippets/form/editor/index.tsx index 8460714f..83e1f230 100644 --- a/packages/front/components/directory/snippets/form/editor/index.tsx +++ b/packages/front/components/directory/snippets/form/editor/index.tsx @@ -2,13 +2,14 @@ import { Controller, useFormContext } from 'react-hook-form'; import Editor from 'react-simple-code-editor'; import { Highlighter } from 'shiki'; +import { useFormEditor } from './hooks/use-form-editor'; import { SelectInput } from '../../../../../forms/select-input'; -import { SwitchInput } from '../../../../../forms/switch-input'; import { TextInput } from '../../../../../forms/text-input'; -import { SelectOption } from '../../../../../typings/components'; -import { EditorFormValues } from '../../../../../typings/snippet-form'; -import { THEME_BACKGROUND_COLOR_MAP } from '../../../../../utils/constants'; -import { useFormEditor } from './hooks/use-form-editor'; +import { THEME_BACKGROUND_COLOR_MAP } from '../../../../../lib/constants'; +import { SelectOption } from '../../../../../types/components'; +import { EditorFormValues } from '../../../../../types/snippet-form'; +import { Label } from '../../../../ui/label'; +import { Switch } from '../../../../ui/switch'; type Props = { codeHighlightOptions: SelectOption[]; @@ -17,9 +18,9 @@ type Props = { themeOptions: SelectOption[]; }; -const SnippetTextEditor = ({ codeHighlightOptions, highlighter, languageOptions, themeOptions }: Props) => { +export const SnippetTextEditor = ({ codeHighlightOptions, highlighter, languageOptions, themeOptions }: Props) => { const { control, setValue } = useFormContext(); - const { code, handleEditorSelect, isSnippetPrivate, onHighlight, theme } = useFormEditor(); + const { code, handleEditorSelect, onHighlight, theme } = useFormEditor(); const handleCodeHighlight = (codeToHighlight: string) => { const highlightedCode = onHighlight(highlighter)(codeToHighlight); @@ -36,7 +37,12 @@ const SnippetTextEditor = ({ codeHighlightOptions, highlighter, languageOptions, name="isPrivate" control={control} render={({ field }) => ( - +
+ + +
)} /> ( - + )} /> } + render={({ field }) => ( + + )} /> } + render={({ field }) => ( + + )} />
- {/* @ts-ignore */} setValue('code', code)} @@ -98,5 +107,3 @@ const SnippetTextEditor = ({ codeHighlightOptions, highlighter, languageOptions,
); }; - -export { SnippetTextEditor }; diff --git a/packages/front/components/directory/snippets/form/form-schema.ts b/packages/front/components/directory/snippets/form/form-schema.ts index deaec505..345052a1 100644 --- a/packages/front/components/directory/snippets/form/form-schema.ts +++ b/packages/front/components/directory/snippets/form/form-schema.ts @@ -1,7 +1,7 @@ import * as yup from 'yup'; -import { EditorFormValues } from '../../../../typings/snippet-form'; -import { FORM_ERRORS } from '../../../../utils/constants'; +import { FORM_ERRORS } from '../../../../lib/constants'; +import { EditorFormValues } from '../../../../types/snippet-form'; const MIN_NAME_LENGTH = 1; const MAX_NAME_LENGTH = 100; diff --git a/packages/front/components/directory/snippets/form/utils.ts b/packages/front/components/directory/snippets/form/utils.ts index f7c65729..28ac849b 100644 --- a/packages/front/components/directory/snippets/form/utils.ts +++ b/packages/front/components/directory/snippets/form/utils.ts @@ -1,7 +1,7 @@ import { capitalize } from 'lodash'; import { BUNDLED_LANGUAGES } from 'shiki'; -import { SelectOption } from '../../../../typings/components'; +import { SelectOption } from '../../../../types/components'; export const generateSnippetLanguageOptions = (): SelectOption[] => { return BUNDLED_LANGUAGES.map((language) => ({ diff --git a/packages/front/components/directory/snippets/form/view-snippet.tsx b/packages/front/components/directory/snippets/form/view-snippet.tsx index f25c9c5b..79db0600 100644 --- a/packages/front/components/directory/snippets/form/view-snippet.tsx +++ b/packages/front/components/directory/snippets/form/view-snippet.tsx @@ -1,17 +1,18 @@ import { yupResolver } from '@hookform/resolvers/yup'; +import { Loader2 } from 'lucide-react'; import { FormProvider, useForm } from 'react-hook-form'; import { SnippetTextEditor } from './editor'; import { SnippetFormValues, formSchema } from './form-schema'; import { generateSnippetLanguageOptions } from './utils'; -import { Button } from '../../../../forms/button'; -import { useCodeHighlighter } from '../../../../hooks'; +import { useCodeHighlighter } from '../../../../hooks/use-code-highlighter'; +import { useToast } from '../../../../hooks/use-toast'; +import { CODE_HIGHLIGHT_OPTIONS, THEME_OPTIONS } from '../../../../lib/constants'; +import { extractLanguageFromName, lineHighlightToString } from '../../../../lib/snippets'; import { useUpdateSnippet } from '../../../../services/snippets/update-snippet'; -import { SelectOption } from '../../../../typings/components'; -import { SnippetItem } from '../../../../typings/queries'; -import { CODE_HIGHLIGHT_OPTIONS, THEME_OPTIONS } from '../../../../utils/constants'; -import { extractLanguageFromName, lineHighlightToString } from '../../../../utils/snippets'; -import { useToast } from '../../../toast/provider'; +import { SelectOption } from '../../../../types/components'; +import { SnippetItem } from '../../../../types/queries'; +import { Button } from '../../../ui/button'; type Props = { snippet: SnippetItem; @@ -27,7 +28,7 @@ const selectLanguageOptionValue = (options: SelectOption[], language: string) => return options.find((option) => option.id === language); }; -const ViewSnippet = ({ snippet }: Props) => { +export const ViewSnippet = ({ snippet }: Props) => { const { highlighter } = useCodeHighlighter(); const { toastError, toastSuccess } = useToast(); @@ -63,10 +64,10 @@ const ViewSnippet = ({ snippet }: Props) => { visibility: values.isPrivate ? 'private' : 'public', }, onError: (message) => { - toastError({ message: `Failed to update: ${message}` }); + toastError(`Failed to update: ${message}`); }, onSuccess: () => { - toastSuccess({ message: 'Snippet updated!' }); + toastSuccess('Snippet updated!'); }, }); }; @@ -81,12 +82,8 @@ const ViewSnippet = ({ snippet }: Props) => { themeOptions={THEME_OPTIONS} />
-
@@ -94,5 +91,3 @@ const ViewSnippet = ({ snippet }: Props) => {
); }; - -export { ViewSnippet }; diff --git a/packages/front/components/directory/snippets/snippet.tsx b/packages/front/components/directory/snippets/snippet.tsx index 74bf561a..c1f51e8c 100644 --- a/packages/front/components/directory/snippets/snippet.tsx +++ b/packages/front/components/directory/snippets/snippet.tsx @@ -1,43 +1,34 @@ -import { - ClipboardIcon, - CodeBracketIcon, - DocumentIcon, - PencilIcon, - ShareIcon, - TrashIcon, -} from '@heroicons/react/20/solid'; +import { ClipboardIcon, CodeXmlIcon, FileIcon, PencilIcon, ShareIcon, TrashIcon } from 'lucide-react'; import React from 'react'; import { useCopyToClipboard } from '../../../hooks/use-copy-to-clipboard'; -import { useHover } from '../../../hooks/use-hover'; -import { MenuItemAction, SnippetItem } from '../../../typings/components'; -import { COLORS } from '../../../utils/constants'; -import { generateEmbeddableLink, generateShareableLink } from '../../../utils/snippets'; -import { truncate } from '../../../utils/text'; +// import { useHover } from '../../../hooks/use-hover'; +import { truncate } from '../../../lib/text'; +import { MenuItemAction, SnippetItem } from '../../../types/components'; import { DotMenu } from '../../menus/dot-menu'; type Props = { + embeddableHostUrl: string; item: SnippetItem; onClick: (snippet: SnippetItem) => void; onDeleteClick: (snippet: SnippetItem) => void; + shareableHostUrl: string; }; const FILE_NAME_MAX_LENGTH = 30; -const Snippet = ({ item, onClick, onDeleteClick }: Props) => { - const [hoverRef, isHovered] = useHover(); +export const Snippet = ({ embeddableHostUrl, item, onClick, onDeleteClick, shareableHostUrl }: Props) => { + // const [hoverRef, isHovered] = useHover(); const [, copyToClipboard] = useCopyToClipboard(); - const charIndex = item.language.charCodeAt(0) - 65; - const colorIndex = charIndex % COLORS.length; const handleCopyShareableLink = async () => { - const link = generateShareableLink(item.id); + const link = `${shareableHostUrl}/snippets/${item.id}`; await copyToClipboard(link); }; const handleEmbeddableLink = async () => { - const link = generateEmbeddableLink(item.id); + const link = `${embeddableHostUrl}/${item.id}`; await copyToClipboard(link); }; @@ -50,27 +41,27 @@ const Snippet = ({ item, onClick, onDeleteClick }: Props) => { const menuActions: MenuItemAction[] = [ { - icon: