diff --git a/apps/admin/package.json b/apps/admin/package.json index b40fc70..e22a181 100644 --- a/apps/admin/package.json +++ b/apps/admin/package.json @@ -3,7 +3,7 @@ "version": "0.1.0", "private": true, "scripts": { - "dev": "next dev", + "dev": "next dev --port 3001", "build": "next build", "start": "next start", "lint": "next lint" diff --git a/apps/application/package.json b/apps/application/package.json index bff1bd4..2617038 100644 --- a/apps/application/package.json +++ b/apps/application/package.json @@ -3,7 +3,7 @@ "version": "0.1.0", "private": true, "scripts": { - "dev": " next dev", + "dev": " next dev --port 3002", "build": "next build", "start": "next start", "lint": "next lint", diff --git a/apps/auth/app/(auth)/layout.tsx b/apps/auth/app/(auth)/layout.tsx new file mode 100644 index 0000000..5b2a7f8 --- /dev/null +++ b/apps/auth/app/(auth)/layout.tsx @@ -0,0 +1,16 @@ +import { Metadata } from "next"; +import { ReactNode } from "react"; + +export const metadata: Metadata = { + title: "Login | Saroh", + description: + "Login to saroh. A platform for managing your portfolios, marketing websites and more.", +}; + +export default function AuthLayout({ children }: { children: ReactNode }) { + return ( +
+ {children} +
+ ); +} diff --git a/apps/auth/app/(auth)/login/github-login-button.tsx b/apps/auth/app/(auth)/login/github-login-button.tsx new file mode 100644 index 0000000..e292f74 --- /dev/null +++ b/apps/auth/app/(auth)/login/github-login-button.tsx @@ -0,0 +1,54 @@ +"use client"; + +import { signIn } from "next-auth/react"; +import { useSearchParams } from "next/navigation"; +import { useEffect, useState } from "react"; +import LoadingDots from "./loading-dots"; +// import { toast } from "sonner"; + +export default function GithubLoginButton() { + const [loading, setLoading] = useState(false); + + // Get error message added by next/auth in URL. + const searchParams = useSearchParams(); + const error = searchParams?.get("error"); + + useEffect(() => { + const errorMessage = Array.isArray(error) ? error.pop() : error; + errorMessage; + // && toast.error(errorMessage); + }, [error]); + + return ( + + ); +} diff --git a/apps/auth/app/(auth)/login/google-login-button.tsx b/apps/auth/app/(auth)/login/google-login-button.tsx new file mode 100644 index 0000000..064de2d --- /dev/null +++ b/apps/auth/app/(auth)/login/google-login-button.tsx @@ -0,0 +1,47 @@ +"use client"; + +import { signIn } from "next-auth/react"; +import { useSearchParams } from "next/navigation"; +import { useEffect, useState } from "react"; +import { SiGoogle } from "react-icons/si"; +import LoadingDots from "./loading-dots"; +// import { toast } from "sonner"; +export default function GoogleLoginButton() { + const [loading, setLoading] = useState(false); + + // Get error message added by next/auth in URL. + const searchParams = useSearchParams(); + const error = searchParams?.get("error"); + + useEffect(() => { + const errorMessage = Array.isArray(error) ? error.pop() : error; + errorMessage; + // && toast.error(errorMessage); + }, [error]); + + return ( + + ); +} diff --git a/apps/auth/app/(auth)/login/loading-dots.module.css b/apps/auth/app/(auth)/login/loading-dots.module.css new file mode 100644 index 0000000..3b63902 --- /dev/null +++ b/apps/auth/app/(auth)/login/loading-dots.module.css @@ -0,0 +1,40 @@ +.loading { + display: inline-flex; + align-items: center; +} + +.loading .spacer { + margin-right: 2px; +} + +.loading span { + animation-name: blink; + animation-duration: 1.4s; + animation-iteration-count: infinite; + animation-fill-mode: both; + width: 5px; + height: 5px; + border-radius: 50%; + display: inline-block; + margin: 0 1px; +} + +.loading span:nth-of-type(2) { + animation-delay: 0.2s; +} + +.loading span:nth-of-type(3) { + animation-delay: 0.4s; +} + +@keyframes blink { + 0% { + opacity: 0.2; + } + 20% { + opacity: 1; + } + 100% { + opacity: 0.2; + } +} diff --git a/apps/auth/app/(auth)/login/loading-dots.tsx b/apps/auth/app/(auth)/login/loading-dots.tsx new file mode 100644 index 0000000..b3ab26b --- /dev/null +++ b/apps/auth/app/(auth)/login/loading-dots.tsx @@ -0,0 +1,17 @@ +import styles from "./loading-dots.module.css"; + +interface LoadingDotsProps { + color?: string; +} + +const LoadingDots = ({ color = "#000" }: LoadingDotsProps) => { + return ( + + + + + + ); +}; + +export default LoadingDots; diff --git a/apps/auth/app/(auth)/login/page.tsx b/apps/auth/app/(auth)/login/page.tsx new file mode 100644 index 0000000..5d6f658 --- /dev/null +++ b/apps/auth/app/(auth)/login/page.tsx @@ -0,0 +1,27 @@ +"use client"; +import { LoginForm } from "@/components/auth/login-form"; + +export default function LoginPage() { + return ; + // return ( + //
+ //

+ // Saroh + //

+ //

+ // Build your storefronts, portfolio and blog websites with + // saroh.io.
+ //

+ + //
+ // + //
+ //
+ // ); +} diff --git a/apps/auth/app/api/auth/[...nextauth]/route.ts b/apps/auth/app/api/auth/[...nextauth]/route.ts new file mode 100644 index 0000000..73228a0 --- /dev/null +++ b/apps/auth/app/api/auth/[...nextauth]/route.ts @@ -0,0 +1,2 @@ +import { handlers } from "@/lib/auth"; +export const { GET, POST } = handlers; diff --git a/apps/auth/app/apps/page.tsx b/apps/auth/app/apps/page.tsx new file mode 100644 index 0000000..f3c108d --- /dev/null +++ b/apps/auth/app/apps/page.tsx @@ -0,0 +1,12 @@ +import Link from "next/link"; + +export default function AppsListPage() { + return ( +
+ Select the app to open +
+ Dashboard +
+
+ ); +} diff --git a/apps/auth/app/layout.tsx b/apps/auth/app/layout.tsx index 2918f93..a914cc0 100644 --- a/apps/auth/app/layout.tsx +++ b/apps/auth/app/layout.tsx @@ -1,12 +1,14 @@ +import "@saroh/ui/globals.css"; import type { Metadata } from "next"; import { Inter } from "next/font/google"; -import "./globals.css"; + +import Providers from "./providers"; const inter = Inter({ subsets: ["latin"] }); export const metadata: Metadata = { - title: "Create Next App", - description: "Generated by create next app", + title: "Saroh Auth", + description: "login to your saroh account.", }; export default function RootLayout({ @@ -16,7 +18,9 @@ export default function RootLayout({ }>) { return ( - {children} + + {children} + ); } diff --git a/apps/auth/app/providers.tsx b/apps/auth/app/providers.tsx new file mode 100644 index 0000000..20db51f --- /dev/null +++ b/apps/auth/app/providers.tsx @@ -0,0 +1,7 @@ +"use client"; +import { SessionProvider } from "next-auth/react"; +import React from "react"; + +export default function Providers({ children }: { children: React.ReactNode }) { + return {children}; +} diff --git a/apps/auth/components/auth/login-form.tsx b/apps/auth/components/auth/login-form.tsx new file mode 100644 index 0000000..e296f09 --- /dev/null +++ b/apps/auth/components/auth/login-form.tsx @@ -0,0 +1,67 @@ +"use client"; +import { Button } from "@saroh/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@saroh/ui/card"; +import { Input } from "@saroh/ui/input"; +import { Label } from "@saroh/ui/label"; +import { signIn } from "next-auth/react"; +import Link from "next/link"; + +export function LoginForm() { + return ( + + + Login + + Enter your email below to login to your account + + + +
+
+ + +
+
+
+ + + Forgot your password? + +
+ +
+ + +
+
+ Don't have an account?{" "} + + Sign up + +
+
+
+ ); +} diff --git a/apps/auth/lib/auth.config.ts b/apps/auth/lib/auth.config.ts new file mode 100644 index 0000000..28d7d06 --- /dev/null +++ b/apps/auth/lib/auth.config.ts @@ -0,0 +1,4 @@ +import type { NextAuthConfig } from "next-auth"; +import GitHub from "next-auth/providers/github"; + +export default { providers: [GitHub] } satisfies NextAuthConfig; diff --git a/apps/auth/lib/auth.ts b/apps/auth/lib/auth.ts new file mode 100644 index 0000000..f9fd029 --- /dev/null +++ b/apps/auth/lib/auth.ts @@ -0,0 +1,12 @@ +import prisma from "@/lib/prisma"; +import { PrismaAdapter } from "@auth/prisma-adapter"; +import NextAuth from "next-auth"; +import authConfig from "./auth.config"; +const VERCEL_DEPLOYMENT = !!process.env.VERCEL_URL; + +export const { auth, handlers, signIn, signOut } = NextAuth({ + // providers: [GitHub, Google], + adapter: PrismaAdapter(prisma), + session: { strategy: "jwt" }, + ...authConfig, +}); diff --git a/apps/auth/lib/prisma.ts b/apps/auth/lib/prisma.ts new file mode 100644 index 0000000..25a9564 --- /dev/null +++ b/apps/auth/lib/prisma.ts @@ -0,0 +1,11 @@ +// import { PrismaClient } from "@prisma/client"; +import { prisma } from "@saroh/database"; +// declare global { +// var prisma: PrismaClient | undefined; +// } + +// const prisma = global.prisma || new PrismaClient(); + +// if (process.env.NODE_ENV === "development") global.prisma = prisma; + +export default prisma; diff --git a/apps/auth/middleware.ts b/apps/auth/middleware.ts new file mode 100644 index 0000000..80b5ff6 --- /dev/null +++ b/apps/auth/middleware.ts @@ -0,0 +1,52 @@ +import NextAuth from "next-auth"; +import { NextResponse } from "next/server"; +import authConfig from "./lib/auth.config"; + +const { auth } = NextAuth(authConfig); +export default auth(async function (req) { + // Your custom middleware logic goes here + const { nextUrl } = req; + const isLoggedIn = !!req.auth; + const protectedRoutes = ["/", "/apps"]; + console.log(nextUrl.pathname); + const path = nextUrl.pathname; + + const authRoutes = ["/login"]; + const isProtectedRoute = protectedRoutes.includes(nextUrl.pathname); + const isAuthRoute = authRoutes.includes(nextUrl.pathname); + if (isAuthRoute) { + if (isLoggedIn) { + return NextResponse.redirect(new URL("/apps", nextUrl)); + } + return; + } + if (isProtectedRoute) { + if (isLoggedIn) { + if (path === "/apps") { + return; + } + return NextResponse.redirect(new URL("/apps", nextUrl)); + } + if (!isLoggedIn) { + return NextResponse.redirect(new URL("/login", nextUrl)); + } + return; + } + + if (!isLoggedIn) { + return NextResponse.redirect(new URL("/login", nextUrl)); + } + return; +}); +export const config = { + matcher: [ + /* + * Match all paths except for: + * 1. /api routes + * 2. /_next (Next.js internals) + * 3. /_static (inside /public) + * 4. all root files inside /public (e.g. /favicon.ico) + */ + "/((?!api/|_next/|_static/|_vercel|[\\w-]+\\.\\w+).*)", + ], +}; diff --git a/apps/auth/next-auth.d.ts b/apps/auth/next-auth.d.ts new file mode 100644 index 0000000..072cca5 --- /dev/null +++ b/apps/auth/next-auth.d.ts @@ -0,0 +1,13 @@ +import { type DefaultSession } from "next-auth"; + +export type ExtendedUser = DefaultSession["user"] & { + // role: UserRole; + isTwoFactorEnabled: boolean; + isOAuth: boolean; +}; + +declare module "next-auth" { + interface Session { + user: ExtendedUser; + } +} diff --git a/apps/auth/next.config.mjs b/apps/auth/next.config.mjs index 4678774..5c91d05 100644 --- a/apps/auth/next.config.mjs +++ b/apps/auth/next.config.mjs @@ -1,4 +1,13 @@ /** @type {import('next').NextConfig} */ -const nextConfig = {}; +import { PrismaPlugin } from '@prisma/nextjs-monorepo-workaround-plugin'; +const nextConfig = { + webpack: (config, { isServer }) => { + if (isServer) { + config.plugins = [...config.plugins, new PrismaPlugin()]; + } + + return config; + }, +}; export default nextConfig; diff --git a/apps/auth/package.json b/apps/auth/package.json index 034446d..babb865 100644 --- a/apps/auth/package.json +++ b/apps/auth/package.json @@ -1,26 +1,36 @@ { - "name": "auth", - "version": "0.1.0", - "private": true, - "scripts": { - "dev": "next dev", - "build": "next build", - "start": "next start", - "lint": "next lint" - }, - "dependencies": { - "react": "^18", - "react-dom": "^18", - "next": "14.2.3" - }, - "devDependencies": { - "typescript": "^5", - "@types/node": "^20", - "@types/react": "^18", - "@types/react-dom": "^18", - "postcss": "^8", - "tailwindcss": "^3.4.1", - "eslint": "^8", - "eslint-config-next": "14.2.3" - } + "name": "auth", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev && prisma generate", + "build": "next build", + "start": "next start", + "lint": "next lint", + "postinstall": "prisma generate" + }, + "dependencies": { + "@auth/prisma-adapter": "^2.2.0", + "@prisma/nextjs-monorepo-workaround-plugin": "^5.14.0", + "next": "14.2.3", + "next-auth": "5.0.0-beta.19", + "react": "^18", + "react-dom": "^18", + "react-icons": "^5.2.1", + "@saroh/ui": "workspace:*" + }, + "devDependencies": { + "@saroh/database": "workspace:*", + "@saroh/tailwind-config": "workspace:*", + "@types/node": "^20", + "@types/react": "^18", + "@types/react-dom": "^18", + "autoprefixer": "^10.4.19", + "prisma": "^5.14.0", + "eslint": "^8", + "eslint-config-next": "14.2.3", + "postcss": "^8", + "tailwindcss": "^3.4.1", + "typescript": "^5" + } } diff --git a/apps/auth/postcss.config.mjs b/apps/auth/postcss.config.mjs index 1a69fd2..88ac031 100644 --- a/apps/auth/postcss.config.mjs +++ b/apps/auth/postcss.config.mjs @@ -1,8 +1,9 @@ /** @type {import('postcss-load-config').Config} */ const config = { - plugins: { - tailwindcss: {}, - }, + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, }; export default config; diff --git a/apps/auth/tailwind.config.ts b/apps/auth/tailwind.config.ts index 86359dc..d0a055b 100644 --- a/apps/auth/tailwind.config.ts +++ b/apps/auth/tailwind.config.ts @@ -1,20 +1 @@ -import type { Config } from "tailwindcss"; - -const config: Config = { - content: [ - "./pages/**/*.{js,ts,jsx,tsx,mdx}", - "./components/**/*.{js,ts,jsx,tsx,mdx}", - "./app/**/*.{js,ts,jsx,tsx,mdx}", - ], - theme: { - extend: { - backgroundImage: { - "gradient-radial": "radial-gradient(var(--tw-gradient-stops))", - "gradient-conic": - "conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))", - }, - }, - }, - plugins: [], -}; -export default config; +export * from "@saroh/tailwind-config/tailwind.config"; diff --git a/apps/auth/tsconfig.json b/apps/auth/tsconfig.json index e7ff90f..1fce257 100644 --- a/apps/auth/tsconfig.json +++ b/apps/auth/tsconfig.json @@ -1,26 +1,26 @@ { - "compilerOptions": { - "lib": ["dom", "dom.iterable", "esnext"], - "allowJs": true, - "skipLibCheck": true, - "strict": true, - "noEmit": true, - "esModuleInterop": true, - "module": "esnext", - "moduleResolution": "bundler", - "resolveJsonModule": true, - "isolatedModules": true, - "jsx": "preserve", - "incremental": true, - "plugins": [ - { - "name": "next" - } - ], - "paths": { - "@/*": ["./*"] - } - }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], - "exclude": ["node_modules"] + "compilerOptions": { + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": ["./*"] + } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"] } diff --git a/apps/chatbot/app/layout.tsx b/apps/chatbot/app/layout.tsx index 2918f93..3970456 100644 --- a/apps/chatbot/app/layout.tsx +++ b/apps/chatbot/app/layout.tsx @@ -5,7 +5,7 @@ import "./globals.css"; const inter = Inter({ subsets: ["latin"] }); export const metadata: Metadata = { - title: "Create Next App", + title: "Saroh chatbot", description: "Generated by create next app", }; diff --git a/apps/chatbot/package.json b/apps/chatbot/package.json index 815a899..1c0ea01 100644 --- a/apps/chatbot/package.json +++ b/apps/chatbot/package.json @@ -3,7 +3,7 @@ "version": "0.1.0", "private": true, "scripts": { - "dev": "next dev", + "dev": "next dev --port 3003", "build": "next build", "start": "next start", "lint": "next lint" diff --git a/apps/dashboard/app/[teamId]/[projectId]/page.tsx b/apps/dashboard/app/[teamId]/[projectId]/page.tsx new file mode 100644 index 0000000..326bbbc --- /dev/null +++ b/apps/dashboard/app/[teamId]/[projectId]/page.tsx @@ -0,0 +1,3 @@ +export default function ProjectHomePage() { + return
ProjectHomePage
; +} diff --git a/apps/dashboard/app/[teamId]/[projectId]/settings/page.tsx b/apps/dashboard/app/[teamId]/[projectId]/settings/page.tsx new file mode 100644 index 0000000..531035b --- /dev/null +++ b/apps/dashboard/app/[teamId]/[projectId]/settings/page.tsx @@ -0,0 +1,3 @@ +export default function ProjectSettingsPage() { + return
ProjectSettingsPage
; +} diff --git a/apps/dashboard/app/[teamId]/layout.tsx b/apps/dashboard/app/[teamId]/layout.tsx new file mode 100644 index 0000000..e31d7fb --- /dev/null +++ b/apps/dashboard/app/[teamId]/layout.tsx @@ -0,0 +1,7 @@ +export default function TeamRootLayout({ + children, +}: { + children: React.ReactNode; +}) { + return
{children}
; +} diff --git a/apps/dashboard/app/[teamId]/page.tsx b/apps/dashboard/app/[teamId]/page.tsx new file mode 100644 index 0000000..a02b958 --- /dev/null +++ b/apps/dashboard/app/[teamId]/page.tsx @@ -0,0 +1,10 @@ +import Dashboard from "@/components/dashboard"; + +export default function TeamHomePage() { + return ( +
+ {" "} + +
+ ); +} diff --git a/apps/dashboard/app/[teamId]/settings/account/page.tsx b/apps/dashboard/app/[teamId]/settings/account/page.tsx new file mode 100644 index 0000000..ecacd29 --- /dev/null +++ b/apps/dashboard/app/[teamId]/settings/account/page.tsx @@ -0,0 +1,18 @@ +import { AccountForm } from "@/components/settings/account/account-form"; +import { Separator } from "@saroh/ui/separator"; + +export default function SettingsAccountPage() { + return ( +
+
+

Account

+

+ Update your account settings. Set your preferred language + and timezone. +

+
+ + +
+ ); +} diff --git a/apps/dashboard/app/[teamId]/settings/appearance/page.tsx b/apps/dashboard/app/[teamId]/settings/appearance/page.tsx new file mode 100644 index 0000000..9598fd9 --- /dev/null +++ b/apps/dashboard/app/[teamId]/settings/appearance/page.tsx @@ -0,0 +1,18 @@ +import { AppearanceForm } from "@/components/settings/appearance/appearance-form"; +import { Separator } from "@saroh/ui/separator"; + +export default function SettingsAppearancePage() { + return ( +
+
+

Appearance

+

+ Customize the appearance of the app. Automatically switch + between day and night themes. +

+
+ + +
+ ); +} diff --git a/apps/dashboard/app/[teamId]/settings/display/page.tsx b/apps/dashboard/app/[teamId]/settings/display/page.tsx new file mode 100644 index 0000000..61c7943 --- /dev/null +++ b/apps/dashboard/app/[teamId]/settings/display/page.tsx @@ -0,0 +1,18 @@ +import { DisplayForm } from "@/components/settings/display/display-form"; +import { Separator } from "@saroh/ui/separator"; + +export default function SettingsDisplayPage() { + return ( +
+
+

Display

+

+ Turn items on or off to control what's displayed in the + app. +

+
+ + +
+ ); +} diff --git a/apps/dashboard/app/[teamId]/settings/layout.tsx b/apps/dashboard/app/[teamId]/settings/layout.tsx new file mode 100644 index 0000000..305640b --- /dev/null +++ b/apps/dashboard/app/[teamId]/settings/layout.tsx @@ -0,0 +1,95 @@ +import { Metadata } from "next"; +import Image from "next/image"; + +import { MainNav } from "@/components/dashboard/main-nav"; +import { Search } from "@/components/dashboard/search"; +import TeamSwitcher from "@/components/dashboard/team-switcher"; +import { UserNav } from "@/components/dashboard/user-nav"; +import { SidebarNav } from "@/components/settings/sidebar-nav"; +import { Separator } from "@saroh/ui/separator"; + +export const metadata: Metadata = { + title: "Forms", + description: "Advanced form example using react-hook-form and Zod.", +}; + +const sidebarNavItems = [ + { + title: "Profile", + href: "/settings", + }, + { + title: "Account", + href: "/settings/account", + }, + { + title: "Appearance", + href: "/settings/appearance", + }, + { + title: "Notifications", + href: "/settings/notifications", + }, + { + title: "Display", + href: "/settings/display", + }, +]; + +interface SettingsLayoutProps { + children: React.ReactNode; +} + +export default function SettingsLayout({ children }: SettingsLayoutProps) { + return ( + <> +
+ Forms + Forms +
+
+
+
+ + +
+ + +
+
+
+ +
+
+

+ Settings +

+

+ Manage your account settings and set e-mail + preferences. +

+
+ +
+ +
{children}
+
+
+
+ + ); +} diff --git a/apps/dashboard/app/[teamId]/settings/notifications/page.tsx b/apps/dashboard/app/[teamId]/settings/notifications/page.tsx new file mode 100644 index 0000000..33ee628 --- /dev/null +++ b/apps/dashboard/app/[teamId]/settings/notifications/page.tsx @@ -0,0 +1,17 @@ +import { NotificationsForm } from "@/components/settings/notifications/notifications-form"; +import { Separator } from "@saroh/ui/separator"; + +export default function SettingsNotificationsPage() { + return ( +
+
+

Notifications

+

+ Configure how you receive notifications. +

+
+ + +
+ ); +} diff --git a/apps/dashboard/app/[teamId]/settings/page.tsx b/apps/dashboard/app/[teamId]/settings/page.tsx new file mode 100644 index 0000000..9495476 --- /dev/null +++ b/apps/dashboard/app/[teamId]/settings/page.tsx @@ -0,0 +1,17 @@ +import { ProfileForm } from "@/components/settings/profile-form"; +import { Separator } from "@saroh/ui/separator"; + +export default function SettingsProfilePage() { + return ( +
+
+

Profile

+

+ This is how others will see you on the site. +

+
+ + +
+ ); +} diff --git a/apps/dashboard/app/api/auth/[...nextauth]/route.ts b/apps/dashboard/app/api/auth/[...nextauth]/route.ts new file mode 100644 index 0000000..73228a0 --- /dev/null +++ b/apps/dashboard/app/api/auth/[...nextauth]/route.ts @@ -0,0 +1,2 @@ +import { handlers } from "@/lib/auth"; +export const { GET, POST } = handlers; diff --git a/apps/dashboard/app/layout.tsx b/apps/dashboard/app/layout.tsx index e401839..34a8e40 100644 --- a/apps/dashboard/app/layout.tsx +++ b/apps/dashboard/app/layout.tsx @@ -1,12 +1,12 @@ import "@saroh/ui/globals.css"; import type { Metadata } from "next"; import { Inter } from "next/font/google"; - +import Providers from "./providers"; const inter = Inter({ subsets: ["latin"] }); export const metadata: Metadata = { - title: "Create Next App", - description: "Generated by create next app", + title: "Build your store with Saroh", + description: "Build ecommerce store with saroh", }; export default function RootLayout({ @@ -16,7 +16,9 @@ export default function RootLayout({ }>) { return ( - {children} + + {children} + ); } diff --git a/apps/dashboard/app/page.tsx b/apps/dashboard/app/page.tsx index 71906ba..d6171df 100644 --- a/apps/dashboard/app/page.tsx +++ b/apps/dashboard/app/page.tsx @@ -1,9 +1,18 @@ -import { Button } from "@saroh/ui/components/ui/button"; - -export default function Home() { +import { auth } from "@/lib/auth"; +export default async function Home() { + const session = await auth(); return (
- + {/* + + + CN + +
{session?.user?.email}
+ */}
); } diff --git a/apps/dashboard/app/products/[id]/page.tsx b/apps/dashboard/app/products/[id]/page.tsx new file mode 100644 index 0000000..774e7b8 --- /dev/null +++ b/apps/dashboard/app/products/[id]/page.tsx @@ -0,0 +1,3 @@ +export default function ProductsDetailsPage() { + return
page
; +} diff --git a/apps/dashboard/app/products/page.tsx b/apps/dashboard/app/products/page.tsx new file mode 100644 index 0000000..e824f60 --- /dev/null +++ b/apps/dashboard/app/products/page.tsx @@ -0,0 +1,750 @@ +import { + File, + Home, + LineChart, + ListFilter, + MoreHorizontal, + Package, + Package2, + PanelLeft, + PlusCircle, + Search, + Settings, + ShoppingCart, + Users2, +} from "lucide-react"; +import Image from "next/image"; +import Link from "next/link"; + +import { Badge } from "@saroh/ui/badge"; +import { + Breadcrumb, + BreadcrumbItem, + BreadcrumbLink, + BreadcrumbList, + BreadcrumbPage, + BreadcrumbSeparator, +} from "@saroh/ui/breadcrumb"; +import { Button } from "@saroh/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from "@saroh/ui/card"; +import { + DropdownMenu, + DropdownMenuCheckboxItem, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@saroh/ui/dropdown-menu"; +import { Input } from "@saroh/ui/input"; +import { Sheet, SheetContent, SheetTrigger } from "@saroh/ui/sheet"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@saroh/ui/table"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@saroh/ui/tabs"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@saroh/ui/tooltip"; + +export default function ProductsPage() { + return ( +
+ +
+
+ + + + + + + + + + + + + Dashboard + + + + + + Products + + + + + All Products + + + +
+ + +
+ + + + + + My Account + + Settings + Support + + Logout + + +
+
+ +
+ + All + Active + Draft + + Archived + + +
+ + + + + + + Filter by + + + + Active + + + Draft + + + Archived + + + + + +
+
+ + + + Products + + Manage your products and view their + sales performance. + + + + + + + + + Image + + + Name + Status + + Price + + + Total Sales + + + Created at + + + + Actions + + + + + + + + + + + Laser Lemonade Machine + + + + Draft + + + + $499.99 + + + 25 + + + 2023-07-12 10:42 AM + + + + + + + + + Actions + + + Edit + + + Delete + + + + + + + + + + + Hypernova Headphones + + + + Active + + + + $129.99 + + + 100 + + + 2023-10-18 03:21 PM + + + + + + + + + Actions + + + Edit + + + Delete + + + + + + + + + + + AeroGlow Desk Lamp + + + + Active + + + + $39.99 + + + 50 + + + 2023-11-29 08:15 AM + + + + + + + + + Actions + + + Edit + + + Delete + + + + + + + + + + + TechTonic Energy Drink + + + + Draft + + + + $2.99 + + + 0 + + + 2023-12-25 11:59 PM + + + + + + + + + Actions + + + Edit + + + Delete + + + + + + + + + + + Gamer Gear Pro Controller + + + + Active + + + + $59.99 + + + 75 + + + 2024-01-01 12:00 AM + + + + + + + + + Actions + + + Edit + + + Delete + + + + + + + + + + + Luminous VR Headset + + + + Active + + + + $199.99 + + + 30 + + + 2024-02-14 02:14 PM + + + + + + + + + Actions + + + Edit + + + Delete + + + + + + +
+
+ +
+ Showing 1-10 of{" "} + 32 products +
+
+
+
+
+
+
+
+ ); +} diff --git a/apps/dashboard/app/providers.tsx b/apps/dashboard/app/providers.tsx new file mode 100644 index 0000000..20db51f --- /dev/null +++ b/apps/dashboard/app/providers.tsx @@ -0,0 +1,7 @@ +"use client"; +import { SessionProvider } from "next-auth/react"; +import React from "react"; + +export default function Providers({ children }: { children: React.ReactNode }) { + return {children}; +} diff --git a/apps/dashboard/components/dashboard/calendar-date-range-picker.tsx b/apps/dashboard/components/dashboard/calendar-date-range-picker.tsx new file mode 100644 index 0000000..f515fee --- /dev/null +++ b/apps/dashboard/components/dashboard/calendar-date-range-picker.tsx @@ -0,0 +1,61 @@ +"use client"; + +import { addDays, format } from "date-fns"; +import { Calendar as CalendarIcon } from "lucide-react"; +import * as React from "react"; +import { DateRange } from "react-day-picker"; + +import { Button } from "@saroh/ui/button"; +import { Calendar } from "@saroh/ui/calendar"; +import { cn } from "@saroh/ui/lib/utils"; +import { Popover, PopoverContent, PopoverTrigger } from "@saroh/ui/popover"; + +export function CalendarDateRangePicker({ + className, +}: React.HTMLAttributes) { + const [date, setDate] = React.useState({ + from: new Date(2022, 0, 20), + to: addDays(new Date(2022, 0, 20), 20), + }); + + return ( +
+ + + + + + + + +
+ ); +} diff --git a/apps/dashboard/components/dashboard/index.tsx b/apps/dashboard/components/dashboard/index.tsx new file mode 100644 index 0000000..7d4eaea --- /dev/null +++ b/apps/dashboard/components/dashboard/index.tsx @@ -0,0 +1,220 @@ +import Image from "next/image"; + +import { Button } from "@saroh/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@saroh/ui/card"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@saroh/ui/tabs"; +import { CalendarDateRangePicker } from "./calendar-date-range-picker"; +import { MainNav } from "./main-nav"; +import { Overview } from "./overview"; +import { RecentSales } from "./recent-sales"; +import { Search } from "./search"; +import TeamSwitcher from "./team-switcher"; +import { UserNav } from "./user-nav"; + +export default function Dashboard() { + return ( + <> +
+ Dashboard + Dashboard +
+
+
+
+ + +
+ + +
+
+
+
+
+

+ Dashboard +

+
+ + +
+
+ + + Overview + + Analytics + + + Reports + + + Notifications + + + +
+ + + + Total Revenue + + + + + + +
+ $45,231.89 +
+

+ +20.1% from last month +

+
+
+ + + + Subscriptions + + + + + + + + +
+ +2350 +
+

+ +180.1% from last month +

+
+
+ + + + Sales + + + + + + + +
+ +12,234 +
+

+ +19% from last month +

+
+
+ + + + Active Now + + + + + + +
+ +573 +
+

+ +201 since last hour +

+
+
+
+
+ + + Overview + + + + + + + + Recent Sales + + You made 265 sales this month. + + + + + + +
+
+
+
+
+ + ); +} diff --git a/apps/dashboard/components/dashboard/main-nav.tsx b/apps/dashboard/components/dashboard/main-nav.tsx new file mode 100644 index 0000000..d513243 --- /dev/null +++ b/apps/dashboard/components/dashboard/main-nav.tsx @@ -0,0 +1,46 @@ +"use client"; +import { cn } from "@saroh/ui/lib/utils"; +import Link from "next/link"; +import { useParams } from "next/navigation"; + +export function MainNav({ + className, + ...props +}: React.HTMLAttributes) { + const params = useParams(); + const domain = params.teamId as string; + return ( + + ); +} diff --git a/apps/dashboard/components/dashboard/overview.tsx b/apps/dashboard/components/dashboard/overview.tsx new file mode 100644 index 0000000..389187f --- /dev/null +++ b/apps/dashboard/components/dashboard/overview.tsx @@ -0,0 +1,83 @@ +"use client"; + +import { Bar, BarChart, ResponsiveContainer, XAxis, YAxis } from "recharts"; + +const data = [ + { + name: "Jan", + total: Math.floor(Math.random() * 5000) + 1000, + }, + { + name: "Feb", + total: Math.floor(Math.random() * 5000) + 1000, + }, + { + name: "Mar", + total: Math.floor(Math.random() * 5000) + 1000, + }, + { + name: "Apr", + total: Math.floor(Math.random() * 5000) + 1000, + }, + { + name: "May", + total: Math.floor(Math.random() * 5000) + 1000, + }, + { + name: "Jun", + total: Math.floor(Math.random() * 5000) + 1000, + }, + { + name: "Jul", + total: Math.floor(Math.random() * 5000) + 1000, + }, + { + name: "Aug", + total: Math.floor(Math.random() * 5000) + 1000, + }, + { + name: "Sep", + total: Math.floor(Math.random() * 5000) + 1000, + }, + { + name: "Oct", + total: Math.floor(Math.random() * 5000) + 1000, + }, + { + name: "Nov", + total: Math.floor(Math.random() * 5000) + 1000, + }, + { + name: "Dec", + total: Math.floor(Math.random() * 5000) + 1000, + }, +]; + +export function Overview() { + return ( + + + + `$${value}`} + /> + + + + ); +} diff --git a/apps/dashboard/components/dashboard/recent-sales.tsx b/apps/dashboard/components/dashboard/recent-sales.tsx new file mode 100644 index 0000000..d939bfd --- /dev/null +++ b/apps/dashboard/components/dashboard/recent-sales.tsx @@ -0,0 +1,83 @@ +import { Avatar, AvatarFallback, AvatarImage } from "@saroh/ui/avatar"; + +export function RecentSales() { + return ( +
+
+ + + OM + +
+

+ Olivia Martin +

+

+ olivia.martin@email.com +

+
+
+$1,999.00
+
+
+ + + JL + +
+

+ Jackson Lee +

+

+ jackson.lee@email.com +

+
+
+$39.00
+
+
+ + + IN + +
+

+ Isabella Nguyen +

+

+ isabella.nguyen@email.com +

+
+
+$299.00
+
+
+ + + WK + +
+

+ William Kim +

+

+ will@email.com +

+
+
+$99.00
+
+
+ + + SD + +
+

+ Sofia Davis +

+

+ sofia.davis@email.com +

+
+
+$39.00
+
+
+ ); +} diff --git a/apps/dashboard/components/dashboard/search.tsx b/apps/dashboard/components/dashboard/search.tsx new file mode 100644 index 0000000..772a73e --- /dev/null +++ b/apps/dashboard/components/dashboard/search.tsx @@ -0,0 +1,13 @@ +import { Input } from "@saroh/ui/input"; + +export function Search() { + return ( +
+ +
+ ); +} diff --git a/apps/dashboard/components/dashboard/team-switcher.tsx b/apps/dashboard/components/dashboard/team-switcher.tsx new file mode 100644 index 0000000..1f23a52 --- /dev/null +++ b/apps/dashboard/components/dashboard/team-switcher.tsx @@ -0,0 +1,224 @@ +"use client"; + +import { + CaretSortIcon, + CheckIcon, + PlusCircledIcon, +} from "@radix-ui/react-icons"; +import * as React from "react"; + +import { Avatar, AvatarFallback, AvatarImage } from "@saroh/ui/avatar"; +import { Button } from "@saroh/ui/button"; +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, + CommandSeparator, +} from "@saroh/ui/command"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@saroh/ui/dialog"; +import { Input } from "@saroh/ui/input"; +import { Label } from "@saroh/ui/label"; +import { cn } from "@saroh/ui/lib/utils"; +import { Popover, PopoverContent, PopoverTrigger } from "@saroh/ui/popover"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@saroh/ui/select"; + +const groups = [ + { + label: "Personal Account", + teams: [ + { + label: "Alicia Koch", + value: "personal", + }, + ], + }, + { + label: "Teams", + teams: [ + { + label: "Acme Inc.", + value: "acme-inc", + }, + { + label: "Monsters Inc.", + value: "monsters", + }, + ], + }, +]; + +type Team = (typeof groups)[number]["teams"][number]; + +type PopoverTriggerProps = React.ComponentPropsWithoutRef< + typeof PopoverTrigger +>; + +interface TeamSwitcherProps extends PopoverTriggerProps {} + +export default function TeamSwitcher({ className }: TeamSwitcherProps) { + const [open, setOpen] = React.useState(false); + const [showNewTeamDialog, setShowNewTeamDialog] = React.useState(false); + const [selectedTeam, setSelectedTeam] = React.useState( + groups[0].teams[0], + ); + + return ( + + + + + + + + + + No team found. + {groups.map((group) => ( + + {group.teams.map((team) => ( + { + setSelectedTeam(team); + setOpen(false); + }} + className="text-sm" + > + + + + SC + + + {team.label} + + + ))} + + ))} + + + + + + { + setOpen(false); + setShowNewTeamDialog(true); + }} + > + + Create Team + + + + + + + + + + Create team + + Add a new team to manage products and customers. + + +
+
+
+ + +
+
+ + +
+
+
+ + + + +
+
+ ); +} diff --git a/apps/dashboard/components/dashboard/user-nav.tsx b/apps/dashboard/components/dashboard/user-nav.tsx new file mode 100644 index 0000000..71d4343 --- /dev/null +++ b/apps/dashboard/components/dashboard/user-nav.tsx @@ -0,0 +1,84 @@ +"use client"; +import useCurrentUser from "@/lib/hooks/use-current-user"; +import { Avatar, AvatarFallback, AvatarImage } from "@saroh/ui/avatar"; +import { Button } from "@saroh/ui/button"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuTrigger, +} from "@saroh/ui/dropdown-menu"; +import { Skeleton } from "@saroh/ui/skeleton"; +import { signOut } from "next-auth/react"; + +export function UserNav() { + const { user, isLoading } = useCurrentUser(); + + if (isLoading) + return ( +
+ +
+ ); + if (!user) return null; + + return ( + + + + + + +
+

+ {user.name} +

+

+ {user.email} +

+
+
+ + + + Profile + ⇧⌘P + + + Billing + ⌘B + + + Settings + ⌘S + + New Team + + + { + await signOut(); + }} + > + Log out + ⇧⌘Q + +
+
+ ); +} diff --git a/apps/dashboard/components/logout-button.tsx b/apps/dashboard/components/logout-button.tsx new file mode 100644 index 0000000..b27579a --- /dev/null +++ b/apps/dashboard/components/logout-button.tsx @@ -0,0 +1,6 @@ +"use client"; +import { Button } from "@saroh/ui/button"; +import { signOut } from "next-auth/react"; +export default function LogoutButton() { + return ; +} diff --git a/apps/dashboard/components/settings/account/account-form.tsx b/apps/dashboard/components/settings/account/account-form.tsx new file mode 100644 index 0000000..e0d7649 --- /dev/null +++ b/apps/dashboard/components/settings/account/account-form.tsx @@ -0,0 +1,233 @@ +"use client"; + +import { zodResolver } from "@hookform/resolvers/zod"; +import { CalendarIcon, CaretSortIcon, CheckIcon } from "@radix-ui/react-icons"; +import { format } from "date-fns"; +import { useForm } from "react-hook-form"; +import { z } from "zod"; + +import { Button } from "@saroh/ui/button"; +import { Calendar } from "@saroh/ui/calendar"; +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, +} from "@saroh/ui/command"; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@saroh/ui/form"; +import { Input } from "@saroh/ui/input"; +import { cn } from "@saroh/ui/lib/utils"; +import { Popover, PopoverContent, PopoverTrigger } from "@saroh/ui/popover"; +// import { toast } from "@saroh/ui/use-toast"; + +const languages = [ + { label: "English", value: "en" }, + { label: "French", value: "fr" }, + { label: "German", value: "de" }, + { label: "Spanish", value: "es" }, + { label: "Portuguese", value: "pt" }, + { label: "Russian", value: "ru" }, + { label: "Japanese", value: "ja" }, + { label: "Korean", value: "ko" }, + { label: "Chinese", value: "zh" }, +] as const; + +const accountFormSchema = z.object({ + name: z + .string() + .min(2, { + message: "Name must be at least 2 characters.", + }) + .max(30, { + message: "Name must not be longer than 30 characters.", + }), + dob: z.date({ + required_error: "A date of birth is required.", + }), + language: z.string({ + required_error: "Please select a language.", + }), +}); + +type AccountFormValues = z.infer; + +// This can come from your database or API. +const defaultValues: Partial = { + // name: "Your name", + // dob: new Date("2023-01-23"), +}; + +export function AccountForm() { + const form = useForm({ + resolver: zodResolver(accountFormSchema), + defaultValues, + }); + + function onSubmit(data: AccountFormValues) { + // toast({ + // title: "You submitted the following values:", + // description: ( + //
+        //             
+        //                 {JSON.stringify(data, null, 2)}
+        //             
+        //         
+ // ), + // }); + } + + return ( +
+ + ( + + Name + + + + + This is the name that will be displayed on your + profile and in emails. + + + + )} + /> + ( + + Date of birth + + + + + + + + + date > new Date() || + date < new Date("1900-01-01") + } + initialFocus + /> + + + + Your date of birth is used to calculate your + age. + + + + )} + /> + ( + + Language + + + + + + + + + + + No language found. + + + {languages.map((language) => ( + { + form.setValue( + "language", + language.value, + ); + }} + > + + {language.label} + + ))} + + + + + + This is the language that will be used in the + dashboard. + + + + )} + /> + + + + ); +} diff --git a/apps/dashboard/components/settings/appearance/appearance-form.tsx b/apps/dashboard/components/settings/appearance/appearance-form.tsx new file mode 100644 index 0000000..7b8227c --- /dev/null +++ b/apps/dashboard/components/settings/appearance/appearance-form.tsx @@ -0,0 +1,174 @@ +"use client"; + +import { zodResolver } from "@hookform/resolvers/zod"; +import { ChevronDownIcon } from "@radix-ui/react-icons"; +import { useForm } from "react-hook-form"; +import { z } from "zod"; + +import { Button, buttonVariants } from "@saroh/ui/button"; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@saroh/ui/form"; +import { cn } from "@saroh/ui/lib/utils"; +import { RadioGroup, RadioGroupItem } from "@saroh/ui/radio-group"; +// import { toast } from "@saroh/ui/use-toast"; + +const appearanceFormSchema = z.object({ + theme: z.enum(["light", "dark"], { + required_error: "Please select a theme.", + }), + font: z.enum(["inter", "manrope", "system"], { + invalid_type_error: "Select a font", + required_error: "Please select a font.", + }), +}); + +type AppearanceFormValues = z.infer; + +// This can come from your database or API. +const defaultValues: Partial = { + theme: "light", +}; + +export function AppearanceForm() { + const form = useForm({ + resolver: zodResolver(appearanceFormSchema), + defaultValues, + }); + + function onSubmit(data: AppearanceFormValues) { + // toast({ + // title: "You submitted the following values:", + // description: ( + //
+        //             
+        //                 {JSON.stringify(data, null, 2)}
+        //             
+        //         
+ // ), + // }); + } + + return ( +
+ + ( + + Font +
+ + + + +
+ + Set the font you want to use in the dashboard. + + +
+ )} + /> + ( + + Theme + + Select the theme for the dashboard. + + + + + + + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + Light + + + + + + + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + Dark + + + + + + )} + /> + + + + + ); +} diff --git a/apps/dashboard/components/settings/display/display-form.tsx b/apps/dashboard/components/settings/display/display-form.tsx new file mode 100644 index 0000000..c162cdf --- /dev/null +++ b/apps/dashboard/components/settings/display/display-form.tsx @@ -0,0 +1,150 @@ +"use client"; + +import { zodResolver } from "@hookform/resolvers/zod"; +import { useForm } from "react-hook-form"; +import { z } from "zod"; + +import { Button } from "@saroh/ui/button"; +import { Checkbox } from "@saroh/ui/checkbox"; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@saroh/ui/form"; +// import { toast } from "@saroh/ui/use-toast"; + +const items = [ + { + id: "recents", + label: "Recents", + }, + { + id: "home", + label: "Home", + }, + { + id: "applications", + label: "Applications", + }, + { + id: "desktop", + label: "Desktop", + }, + { + id: "downloads", + label: "Downloads", + }, + { + id: "documents", + label: "Documents", + }, +] as const; + +const displayFormSchema = z.object({ + items: z.array(z.string()).refine((value) => value.some((item) => item), { + message: "You have to select at least one item.", + }), +}); + +type DisplayFormValues = z.infer; + +// This can come from your database or API. +const defaultValues: Partial = { + items: ["recents", "home"], +}; + +export function DisplayForm() { + const form = useForm({ + resolver: zodResolver(displayFormSchema), + defaultValues, + }); + + function onSubmit(data: DisplayFormValues) { + // toast({ + // title: "You submitted the following values:", + // description: ( + //
+        //             
+        //                 {JSON.stringify(data, null, 2)}
+        //             
+        //         
+ // ), + // }); + } + + return ( +
+ + ( + +
+ + Sidebar + + + Select the items you want to display in the + sidebar. + +
+ {items.map((item) => ( + { + return ( + + + { + return checked + ? field.onChange( + [ + ...field.value, + item.id, + ], + ) + : field.onChange( + field.value?.filter( + ( + value, + ) => + value !== + item.id, + ), + ); + }} + /> + + + {item.label} + + + ); + }} + /> + ))} + +
+ )} + /> + + + + ); +} diff --git a/apps/dashboard/components/settings/notifications/notifications-form.tsx b/apps/dashboard/components/settings/notifications/notifications-form.tsx new file mode 100644 index 0000000..d47e8f0 --- /dev/null +++ b/apps/dashboard/components/settings/notifications/notifications-form.tsx @@ -0,0 +1,240 @@ +"use client"; + +import { zodResolver } from "@hookform/resolvers/zod"; +import Link from "next/link"; +import { useForm } from "react-hook-form"; +import { z } from "zod"; + +import { Button } from "@saroh/ui/button"; +import { Checkbox } from "@saroh/ui/checkbox"; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@saroh/ui/form"; +import { RadioGroup, RadioGroupItem } from "@saroh/ui/radio-group"; +import { Switch } from "@saroh/ui/switch"; +// import { toast } from "@saroh/ui/use-toast"; + +const notificationsFormSchema = z.object({ + type: z.enum(["all", "mentions", "none"], { + required_error: "You need to select a notification type.", + }), + mobile: z.boolean().default(false).optional(), + communication_emails: z.boolean().default(false).optional(), + social_emails: z.boolean().default(false).optional(), + marketing_emails: z.boolean().default(false).optional(), + security_emails: z.boolean(), +}); + +type NotificationsFormValues = z.infer; + +// This can come from your database or API. +const defaultValues: Partial = { + communication_emails: false, + marketing_emails: false, + social_emails: true, + security_emails: true, +}; + +export function NotificationsForm() { + const form = useForm({ + resolver: zodResolver(notificationsFormSchema), + defaultValues, + }); + + function onSubmit(data: NotificationsFormValues) { + // toast({ + // title: "You submitted the following values:", + // description: ( + //
+        //             
+        //                 {JSON.stringify(data, null, 2)}
+        //             
+        //         
+ // ), + // }); + } + + return ( +
+ + ( + + Notify me about... + + + + + + + + All new messages + + + + + + + + Direct messages and mentions + + + + + + + + Nothing + + + + + + + )} + /> +
+

+ Email Notifications +

+
+ ( + +
+ + Communication emails + + + Receive emails about your account + activity. + +
+ + + +
+ )} + /> + ( + +
+ + Marketing emails + + + Receive emails about new products, + features, and more. + +
+ + + +
+ )} + /> + ( + +
+ + Social emails + + + Receive emails for friend requests, + follows, and more. + +
+ + + +
+ )} + /> + ( + +
+ + Security emails + + + Receive emails about your account + activity and security. + +
+ + + +
+ )} + /> +
+
+ ( + + + + +
+ + Use different settings for my mobile devices + + + You can manage your mobile notifications in + the{" "} + + mobile settings + {" "} + page. + +
+
+ )} + /> + + + + ); +} diff --git a/apps/dashboard/components/settings/profile-form/index.tsx b/apps/dashboard/components/settings/profile-form/index.tsx new file mode 100644 index 0000000..8c33e61 --- /dev/null +++ b/apps/dashboard/components/settings/profile-form/index.tsx @@ -0,0 +1,211 @@ +"use client"; + +import { zodResolver } from "@hookform/resolvers/zod"; +import Link from "next/link"; +import { useFieldArray, useForm } from "react-hook-form"; +import { z } from "zod"; + +import { Button } from "@saroh/ui/button"; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@saroh/ui/form"; +import { Input } from "@saroh/ui/input"; +import { cn } from "@saroh/ui/lib/utils"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@saroh/ui/select"; +import { Textarea } from "@saroh/ui/textarea"; +// import { toast } from "@saroh/ui/"; + +const profileFormSchema = z.object({ + username: z + .string() + .min(2, { + message: "Username must be at least 2 characters.", + }) + .max(30, { + message: "Username must not be longer than 30 characters.", + }), + email: z + .string({ + required_error: "Please select an email to display.", + }) + .email(), + bio: z.string().max(160).min(4), + urls: z + .array( + z.object({ + value: z.string().url({ message: "Please enter a valid URL." }), + }), + ) + .optional(), +}); + +type ProfileFormValues = z.infer; + +// This can come from your database or API. +const defaultValues: Partial = { + bio: "I own a computer.", + urls: [ + { value: "https://shadcn.com" }, + { value: "http://twitter.com/shadcn" }, + ], +}; + +export function ProfileForm() { + const form = useForm({ + resolver: zodResolver(profileFormSchema), + defaultValues, + mode: "onChange", + }); + + const { fields, append } = useFieldArray({ + name: "urls", + control: form.control, + }); + + function onSubmit(data: ProfileFormValues) { + // toast({ + // title: "You submitted the following values:", + // description: ( + //
+        //             
+        //                 {JSON.stringify(data, null, 2)}
+        //             
+        //         
+ // ), + // }); + } + + return ( +
+ + ( + + Username + + + + + This is your public display name. It can be your + real name or a pseudonym. You can only change + this once every 30 days. + + + + )} + /> + ( + + Email + + + You can manage verified email addresses in your{" "} + + email settings + + . + + + + )} + /> + ( + + Bio + +