diff --git a/apps/web/app/(app)/dashboard/page.tsx b/apps/web/app/(app)/dashboard/page.tsx new file mode 100644 index 0000000..a6de268 --- /dev/null +++ b/apps/web/app/(app)/dashboard/page.tsx @@ -0,0 +1,15 @@ +'use client'; + +import { signOut } from '@/app/utils/actions'; +import { Button } from '@ultra-reporter/ui/components/button'; + +export default function DashboardPage() { + return ( +
+

Dashboard

+ +
+ ); +} diff --git a/apps/web/app/(auth)/auth/callback/route.ts b/apps/web/app/(auth)/auth/callback/route.ts new file mode 100644 index 0000000..bb086ad --- /dev/null +++ b/apps/web/app/(auth)/auth/callback/route.ts @@ -0,0 +1,46 @@ +import { logger } from '@ultra-reporter/logger'; +import { createClient } from '@ultra-reporter/supabase/server'; +import { isPreview } from '@ultra-reporter/utils/constants'; +import { NextResponse } from 'next/server'; + +export async function GET(request: Request) { + const { searchParams, origin } = new URL(request.url); + const code = searchParams.get('code'); + const next = searchParams.get('next') ?? '/dashboard'; + + if (isPreview) { + logger.debug('===================='); + logger.debug(`Route Received URL: ${request.url}`); + logger.debug(`Route Received searchParams: ${searchParams}`); + logger.debug(`Route Received code: ${code}`); + logger.debug(`Route Received next: ${next}`); + logger.debug(`Route Received origin: ${origin}`); + logger.debug('===================='); + } + + if (code) { + const supabase = await createClient(); + const { error } = await supabase.auth.exchangeCodeForSession(code); + if (!error) { + const forwardedHost = request.headers.get('x-forwarded-host'); + const isLocalEnv = process.env.NODE_ENV === 'development'; + if (isPreview) { + logger.debug('===================='); + logger.debug(`Route Received forwardedHost: ${forwardedHost}`); + logger.debug(`Route Received isLocalEnv: ${isLocalEnv}`); + logger.debug('===================='); + } + if (isLocalEnv) { + // we can be sure that there is no load balancer in between, so no need to watch for X-Forwarded-Host + return NextResponse.redirect(`${origin}${next}`); + } else if (forwardedHost) { + return NextResponse.redirect(`https://${forwardedHost}${next}`); + } else { + return NextResponse.redirect(`${origin}${next}`); + } + } + } + + // return the user to an error page with instructions + return NextResponse.redirect(`${origin}/auth/auth-code-error`); +} diff --git a/apps/web/app/(auth)/layout.tsx b/apps/web/app/(auth)/layout.tsx new file mode 100644 index 0000000..49098a5 --- /dev/null +++ b/apps/web/app/(auth)/layout.tsx @@ -0,0 +1,14 @@ +import { NavBar } from '@ultra-reporter/ui/home/nav-bar'; + +export default function AuthLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( + <> + + {children} + + ); +} diff --git a/apps/web/app/(auth)/login/page.tsx b/apps/web/app/(auth)/login/page.tsx new file mode 100644 index 0000000..8188136 --- /dev/null +++ b/apps/web/app/(auth)/login/page.tsx @@ -0,0 +1,65 @@ +'use client'; + +import { signinWithGoogle } from '@/app/utils/actions'; +import { Button } from '@ultra-reporter/ui/components/button'; +import { DemoCarousel } from '@ultra-reporter/ui/components/demo-carousel'; +import { Icons } from '@ultra-reporter/ui/components/icons'; + +export default function AuthPage() { + return ( +
+
+ {/* Demo Carousel Section */} +
+ +
+ + {/* Auth Section */} +
+
+

+ Welcome to Ultra Reporter +

+

+ Sign in to your account or create a new one +

+
+ +
+ + +
+
+ +
+
+ + Secure Authentication + +
+
+ +

+ By continuing, you agree to our{' '} + + Terms of Service + {' '} + and{' '} + + Privacy Policy + +

+
+
+
+
+ ); +} diff --git a/apps/web/app/utils/actions.ts b/apps/web/app/utils/actions.ts new file mode 100644 index 0000000..b2e1f0c --- /dev/null +++ b/apps/web/app/utils/actions.ts @@ -0,0 +1,69 @@ +'use server'; + +import { logger } from '@ultra-reporter/logger'; +import { Provider } from '@ultra-reporter/supabase/client'; +import { createClient } from '@ultra-reporter/supabase/server'; +import { isPreview, isProd } from '@ultra-reporter/utils/constants'; +import { headers } from 'next/headers'; +import { redirect } from 'next/navigation'; + +const getURL = () => { + let url = isProd + ? process?.env?.NEXT_PUBLIC_SITE_URL + : isPreview + ? process?.env?.NEXT_PUBLIC_VERCEL_URL + : 'http://localhost:3000/'; + // Make sure to include `https://` when not localhost. + url = url?.startsWith('http') ? url : `https://${url}`; + // Make sure to include a trailing `/`. + url = url.endsWith('/') ? url : `${url}/`; + return url; +}; + +const signInWith = (provider: Provider) => async () => { + const supabase = await createClient(); + const requestHeaders = await headers(); + const origin = getURL(); + + if (isPreview) { + logger.debug('===================='); + logger.debug(`Actions Received provider: ${provider}`); + logger.debug(`Actions Received origin: ${origin}`); + requestHeaders.forEach((key, value) => { + logger.debug(`Actions Header [${key}]: [${value}]`); + }); + logger.debug('===================='); + } + + const { data, error } = await supabase.auth.signInWithOAuth({ + provider: provider, + options: { + redirectTo: `${origin}auth/callback`, + }, + }); + + if (error) { + logger.error(`Error while signing in with ${provider}: ${error.message}`); + } + if (data?.url) { + if (isPreview) { + logger.debug(`Actions Redirecting to: ${data.url}`); + } + redirect(data.url); + } +}; + +const signinWithGoogle = signInWith('google'); + +const signOut = async () => { + const supabase = await createClient(); + const { error } = await supabase.auth.signOut(); + + if (!error) { + redirect('/'); + } else { + logger.error(`Error while signing out: ${error.message}`); + } +}; + +export { signinWithGoogle, signOut }; diff --git a/apps/web/middleware.ts b/apps/web/middleware.ts new file mode 100644 index 0000000..7647846 --- /dev/null +++ b/apps/web/middleware.ts @@ -0,0 +1,20 @@ +import { updateSession } from '@ultra-reporter/supabase/middleware'; +import { type NextRequest } from 'next/server'; + +export async function middleware(request: NextRequest) { + return await updateSession(request); +} + +export const config = { + matcher: [ + /* + * Match all request paths except: + * - _next/static (static files) + * - _next/image (image optimization files) + * - favicon.ico (favicon file) + * - images - .svg, .png, .jpg, .jpeg, .gif, .webp + * Feel free to modify this pattern to include more paths. + */ + '/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)', + ], +}; diff --git a/apps/web/package.json b/apps/web/package.json index 86a3e2b..9556922 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -14,20 +14,22 @@ "react-dom": "^19.0" }, "dependencies": { - "@next/third-parties": "^15.1.2", - "@tanstack/react-table": "^8.20.6", + "@next/third-parties": "^15.1.7", + "@tanstack/react-table": "^8.21.2", "@ultra-reporter/analytics": "workspace:*", "@ultra-reporter/feature-toggle": "workspace:*", + "@ultra-reporter/logger": "workspace:*", + "@ultra-reporter/supabase": "workspace:*", "@ultra-reporter/ui": "workspace:*", "@ultra-reporter/utils": "workspace:*", - "lucide-react": "^0.469.0", - "next": "15.1.2" + "lucide-react": "^0.475.0", + "next": "15.1.7" }, "devDependencies": { - "@types/react": "^19.0.2", - "@types/react-dom": "^19.0.2", + "@types/react": "^19.0.8", + "@types/react-dom": "^19.0.3", "@ultra-reporter/typescript-config": "workspace:*", - "postcss": "^8.4.49", + "postcss": "^8.5.2", "tailwindcss": "^3.4.17" } } diff --git a/package.json b/package.json index aecb8eb..8316a2a 100644 --- a/package.json +++ b/package.json @@ -25,33 +25,35 @@ "release:prepatch": "pnpm beta prepatch" }, "devDependencies": { - "@eslint/compat": "^1.2.4", - "@next/eslint-plugin-next": "^15.1.2", + "@eslint/compat": "^1.2.6", + "@eslint/js": "^9.20.0", + "@next/eslint-plugin-next": "^15.1.7", "@release-it-plugins/lerna-changelog": "^7.0.0", - "@stylistic/eslint-plugin-js": "^2.12.1", - "@stylistic/eslint-plugin-ts": "^2.12.1", - "@types/node": "^22.10.2", - "@typescript-eslint/eslint-plugin": "^8.18.1", - "@typescript-eslint/parser": "^8.18.1", + "@stylistic/eslint-plugin-js": "^3.1.0", + "@stylistic/eslint-plugin-ts": "^3.1.0", + "@types/node": "^22.13.4", + "@typescript-eslint/eslint-plugin": "^8.24.0", + "@typescript-eslint/parser": "^8.24.0", "@vercel/style-guide": "^6.0.0", - "eslint": "^9.17.0", - "eslint-config-next": "15.1.2", - "eslint-config-prettier": "^9.1.0", - "eslint-config-turbo": "2.3.3", + "eslint": "^9.20.1", + "eslint-config-next": "15.1.7", + "eslint-config-prettier": "^10.0.1", + "eslint-config-turbo": "2.4.2", "eslint-plugin-only-warn": "^1.1.0", - "eslint-plugin-prettier": "^5.2.1", - "globals": "^15.14.0", + "eslint-plugin-prettier": "^5.2.3", + "eslint-plugin-react": "^7.37.4", + "globals": "^15.15.0", "husky": "^9.1.7", "lerna-changelog": "^2.2.0", - "lint-staged": "^15.2.11", - "prettier": "^3.4.2", + "lint-staged": "^15.4.3", + "prettier": "^3.5.1", "prettier-plugin-organize-imports": "^4.1.0", - "prettier-plugin-tailwindcss": "^0.6.9", - "release-it": "^17.10.0", - "release-it-pnpm": "^4.6.3", - "turbo": "^2.3.3", - "typescript": "^5.7.2", - "typescript-eslint": "^8.18.1" + "prettier-plugin-tailwindcss": "^0.6.11", + "release-it": "^18.1.2", + "release-it-pnpm": "^4.6.4", + "turbo": "^2.4.2", + "typescript": "^5.7.3", + "typescript-eslint": "^8.24.0" }, "lint-staged": { "**/*.{ts,tsx}": [ @@ -66,5 +68,5 @@ "engines": { "node": ">=20" }, - "packageManager": "pnpm@9.15.0" + "packageManager": "pnpm@10.4.0" } diff --git a/packages/analytics/package.json b/packages/analytics/package.json index bb8ee34..84c4ba6 100644 --- a/packages/analytics/package.json +++ b/packages/analytics/package.json @@ -13,10 +13,10 @@ "dependencies": { "@openpanel/nextjs": "^1.0.7", "@ultra-reporter/utils": "workspace:*", - "@vercel/functions": "^1.5.2" + "@vercel/functions": "^2.0.0" }, "devDependencies": { - "@types/react": "^19.0.2", + "@types/react": "^19.0.8", "@ultra-reporter/logger": "workspace:*", "@ultra-reporter/typescript-config": "workspace:*" } diff --git a/packages/db/package.json b/packages/db/package.json index 5887152..4a9b36a 100644 --- a/packages/db/package.json +++ b/packages/db/package.json @@ -13,10 +13,10 @@ ".": "./src/index.ts" }, "dependencies": { - "@prisma/client": "^6.1.0" + "@prisma/client": "^6.3.1" }, "devDependencies": { "@ultra-reporter/typescript-config": "workspace:*", - "prisma": "^6.1.0" + "prisma": "^6.3.1" } } diff --git a/packages/db/src/schema/schema.prisma b/packages/db/src/schema/schema.prisma index 988c585..0e2e8b7 100644 --- a/packages/db/src/schema/schema.prisma +++ b/packages/db/src/schema/schema.prisma @@ -4,30 +4,45 @@ generator client { } datasource db { - provider = "postgresql" - url = env("DATABASE_URL") + provider = "postgresql" + url = env("DATABASE_URL") + relationMode = "prisma" } -model User { - id String @id @default(cuid()) - user_name String - email String - provider String +model TestResultData { + id String @id @default(cuid()) + suite_name String + test_name String + class_name String + method_name String + is_config Boolean + tags String[] + parameters String[] + status String + exception_id String + attachment_id String + started_at DateTime + finished_at DateTime + duration Float - created_at DateTime @default(now()) - updated_at DateTime @updatedAt + exception TestException? @relation(fields: [exception_id], references: [id]) - LoginSession LoginSession[] -} + attachment TestLog? @relation(fields: [attachment_id], references: [id]) -model LoginSession { - id String @id @default(cuid()) - user_id String - last_login_at DateTime @default(now()) - created_at DateTime @default(now()) - updated_at DateTime @updatedAt + @@index([exception_id]) + @@index([attachment_id]) +} - user User @relation(fields: [user_id], references: [id]) +model TestLog { + id String @id @default(cuid()) + line String + TestResultData TestResultData[] +} - @@index([user_id]) +model TestException { + id String @id @default(cuid()) + class_name String + message String + stack_trace String[] + TestResultData TestResultData[] } diff --git a/packages/feature-toggle/package.json b/packages/feature-toggle/package.json index d2a5cdb..26e0800 100644 --- a/packages/feature-toggle/package.json +++ b/packages/feature-toggle/package.json @@ -8,11 +8,12 @@ }, "exports": { "./client": "./src/client.ts", - "./provider": "./src/provider.tsx" + "./provider": "./src/provider.tsx", + "./middleware": "./src/middleware.ts" }, "devDependencies": { - "@types/react": "^19.0.2", - "@types/react-dom": "^19.0.2", + "@types/react": "^19.0.8", + "@types/react-dom": "^19.0.3", "@ultra-reporter/typescript-config": "workspace:*" }, "peerDependencies": { @@ -20,6 +21,6 @@ "react-dom": "^19.0" }, "dependencies": { - "flagsmith": "^8.0.1" + "flagsmith": "^9.0.4" } } diff --git a/packages/feature-toggle/src/middleware.ts b/packages/feature-toggle/src/middleware.ts new file mode 100644 index 0000000..0484dd6 --- /dev/null +++ b/packages/feature-toggle/src/middleware.ts @@ -0,0 +1,11 @@ +'use server'; + +import flagsmith from 'flagsmith/next-middleware'; + +export async function getFlag(flagId: string): Promise { + await flagsmith.init({ + environmentID: process.env.NEXT_PUBLIC_FLAGSMITH_ENVIRONMENT_ID, + }); + + return flagsmith.hasFeature(flagId); +} diff --git a/packages/logger/package.json b/packages/logger/package.json index 821d94e..c70d2a0 100644 --- a/packages/logger/package.json +++ b/packages/logger/package.json @@ -8,7 +8,7 @@ "lint": "eslint . --max-warnings 0" }, "dependencies": { - "pino": "^9.5.0" + "pino": "^9.6.0" }, "devDependencies": { "@ultra-reporter/typescript-config": "workspace:*" diff --git a/packages/supabase/package.json b/packages/supabase/package.json new file mode 100644 index 0000000..e92e37d --- /dev/null +++ b/packages/supabase/package.json @@ -0,0 +1,30 @@ +{ + "name": "@ultra-reporter/supabase", + "description": "Ultra Reporter Supabase integration", + "version": "0.6.0", + "private": true, + "scripts": { + "lint": "eslint . --max-warnings 0" + }, + "exports": { + "./client": "./src/client.ts", + "./server": "./src/server.ts", + "./middleware": "./src/middleware.ts" + }, + "devDependencies": { + "@supabase/auth-js": "^2.68.0", + "@types/react": "^19.0.8", + "@types/react-dom": "^19.0.3", + "@ultra-reporter/typescript-config": "workspace:*" + }, + "peerDependencies": { + "react": "^19.0", + "react-dom": "^19.0" + }, + "dependencies": { + "@supabase/ssr": "latest", + "@supabase/supabase-js": "^2.48.1", + "next": "15.1.7", + "@ultra-reporter/feature-toggle": "workspace:*" + } +} diff --git a/packages/supabase/src/client.ts b/packages/supabase/src/client.ts new file mode 100644 index 0000000..9b64bc9 --- /dev/null +++ b/packages/supabase/src/client.ts @@ -0,0 +1,11 @@ +import { createBrowserClient } from '@supabase/ssr'; +import { Provider } from '@supabase/supabase-js'; + +export function createClient() { + return createBrowserClient( + process.env.NEXT_PUBLIC_SUPABASE_URL!, + process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY! + ); +} + +export type { Provider }; diff --git a/packages/supabase/src/middleware.ts b/packages/supabase/src/middleware.ts new file mode 100644 index 0000000..4856d68 --- /dev/null +++ b/packages/supabase/src/middleware.ts @@ -0,0 +1,48 @@ +import { createServerClient } from '@supabase/ssr'; +import { getFlag } from '@ultra-reporter/feature-toggle/middleware'; +import { type NextRequest, NextResponse } from 'next/server'; +const protectedRoutes = ['/dashboard']; + +export const updateSession = async (request: NextRequest) => { + let response = NextResponse.next({ + request, + }); + + const supabase = createServerClient( + process.env.NEXT_PUBLIC_SUPABASE_URL!, + process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, + { + cookies: { + getAll() { + return request.cookies.getAll(); + }, + setAll(cookiesToSet) { + cookiesToSet.forEach(({ name, value }) => + request.cookies.set(name, value) + ); + response = NextResponse.next({ + request, + }); + cookiesToSet.forEach(({ name, value, options }) => + response.cookies.set(name, value, options) + ); + }, + }, + } + ); + + const pathname = request.nextUrl.pathname; + const isProtectedRoute = protectedRoutes.includes(pathname); + const session = await supabase.auth.getUser(); + const signInSupport = await getFlag('sign_in_support'); + + if (!signInSupport && (isProtectedRoute || pathname === '/login')) { + return NextResponse.redirect(new URL('/', request.url)); + } + + if (isProtectedRoute && session.error) { + return NextResponse.redirect(new URL('/login', request.url)); + } + + return response; +}; diff --git a/packages/supabase/src/server.ts b/packages/supabase/src/server.ts new file mode 100644 index 0000000..110c264 --- /dev/null +++ b/packages/supabase/src/server.ts @@ -0,0 +1,30 @@ +import { createServerClient } from '@supabase/ssr'; +import { cookies } from 'next/headers'; + +export const createClient = async () => { + const cookieStore = await cookies(); + + return createServerClient( + process.env.NEXT_PUBLIC_SUPABASE_URL!, + process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, + { + cookies: { + getAll() { + return cookieStore.getAll(); + }, + setAll(cookiesToSet) { + try { + cookiesToSet.forEach(({ name, value, options }) => { + cookieStore.set(name, value, options); + }); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + } catch (error) { + // The `set` method was called from a Server Component. + // This can be ignored if you have middleware refreshing + // user sessions. + } + }, + }, + } + ); +}; diff --git a/packages/supabase/tsconfig.json b/packages/supabase/tsconfig.json new file mode 100644 index 0000000..33aa6d1 --- /dev/null +++ b/packages/supabase/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "@ultra-reporter/typescript-config/nextjs.json", + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["src"], + "exclude": ["node_modules"] +} diff --git a/packages/ui/package.json b/packages/ui/package.json index 9e33bb7..ad38829 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -28,43 +28,46 @@ "react-dom": "^19.0" }, "dependencies": { - "@radix-ui/react-accordion": "^1.2.2", - "@radix-ui/react-avatar": "^1.1.2", - "@radix-ui/react-dialog": "^1.1.4", - "@radix-ui/react-dropdown-menu": "^2.1.4", - "@radix-ui/react-hover-card": "^1.1.4", + "@radix-ui/react-accordion": "^1.2.3", + "@radix-ui/react-avatar": "^1.1.3", + "@radix-ui/react-dialog": "^1.1.6", + "@radix-ui/react-dropdown-menu": "^2.1.6", + "@radix-ui/react-hover-card": "^1.1.6", "@radix-ui/react-icons": "^1.3.2", - "@radix-ui/react-label": "^2.1.1", - "@radix-ui/react-popover": "^1.1.4", - "@radix-ui/react-progress": "^1.1.1", - "@radix-ui/react-select": "^2.1.4", - "@radix-ui/react-separator": "^1.1.1", - "@radix-ui/react-slot": "^1.1.1", - "@radix-ui/react-switch": "^1.1.2", - "@radix-ui/react-toast": "^1.2.4", - "@radix-ui/react-tooltip": "^1.1.6", - "@tanstack/react-table": "^8.20.6", + "@radix-ui/react-label": "^2.1.2", + "@radix-ui/react-popover": "^1.1.6", + "@radix-ui/react-progress": "^1.1.2", + "@radix-ui/react-select": "^2.1.6", + "@radix-ui/react-separator": "^1.1.2", + "@radix-ui/react-slot": "^1.1.2", + "@radix-ui/react-switch": "^1.1.3", + "@radix-ui/react-tabs": "^1.1.3", + "@radix-ui/react-toast": "^1.2.6", + "@radix-ui/react-tooltip": "^1.1.8", + "@tanstack/react-table": "^8.21.2", "class-variance-authority": "^0.7.1", "cmdk": "1.0.4", - "embla-carousel-react": "^8.5.1", - "lucide-react": "^0.469.0", - "next": "15.1.2", + "embla-carousel-react": "^8.5.2", + "lucide-react": "^0.475.0", + "next": "15.1.7", "next-themes": "^0.4.4", "react-code-blocks": "^0.1.6", "react-hook-form": "^7.54.2", - "recharts": "^2.15.0", + "recharts": "^2.15.1", "tailwindcss-animate": "^1.0.7", - "zod": "^3.24.1" + "zod": "^3.24.2", + "zxcvbn": "^4.4.2" }, "devDependencies": { - "@types/react": "^19.0.2", - "@types/react-dom": "^19.0.2", + "@types/react": "^19.0.8", + "@types/react-dom": "^19.0.3", + "@types/zxcvbn": "^4.4.5", "@ultra-reporter/feature-toggle": "workspace:*", "@ultra-reporter/typescript-config": "workspace:*", "@ultra-reporter/utils": "workspace:*", "autoprefixer": "^10.4.20", - "daisyui": "^4.12.22", - "postcss": "^8.4.49", + "daisyui": "^4.12.23", + "postcss": "^8.5.2", "postcss-load-config": "^6.0.1", "tailwindcss": "^3.4.17" } diff --git a/packages/ui/src/components/button.tsx b/packages/ui/src/components/button.tsx index 1db727d..f0df5a2 100644 --- a/packages/ui/src/components/button.tsx +++ b/packages/ui/src/components/button.tsx @@ -1,9 +1,8 @@ import { Slot } from '@radix-ui/react-slot'; +import { cn } from '@ultra-reporter/utils/cn'; import { cva, type VariantProps } from 'class-variance-authority'; import * as React from 'react'; -import { cn } from '@ultra-reporter/utils/cn'; - const buttonVariants = cva( 'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0', { diff --git a/packages/ui/src/components/card.tsx b/packages/ui/src/components/card.tsx index eaa06fb..dfffe12 100644 --- a/packages/ui/src/components/card.tsx +++ b/packages/ui/src/components/card.tsx @@ -1,6 +1,5 @@ -import * as React from 'react'; - import { cn } from '@ultra-reporter/utils/cn'; +import * as React from 'react'; const Card = React.forwardRef< HTMLDivElement, @@ -30,10 +29,10 @@ const CardHeader = React.forwardRef< CardHeader.displayName = 'CardHeader'; const CardTitle = React.forwardRef< - HTMLParagraphElement, - React.HTMLAttributes + HTMLDivElement, + React.HTMLAttributes >(({ className, ...props }, ref) => ( -

+ HTMLDivElement, + React.HTMLAttributes >(({ className, ...props }, ref) => ( -

{ + const timer = setInterval(() => { + setCurrentSlide((prev) => (prev + 1) % demoSlides.length); + }, SLIDE_DURATION); + + return () => clearInterval(timer); + }, []); + + return ( +

+ {demoSlides.map((slide, index) => ( +
+ {slide.alt} +
+

{slide.caption}

+
+
+ ))} + + {/* Slide indicators */} +
+ {demoSlides.map((_, index) => ( +
+
+ ); +} diff --git a/packages/ui/src/components/icons.tsx b/packages/ui/src/components/icons.tsx new file mode 100644 index 0000000..f0b3368 --- /dev/null +++ b/packages/ui/src/components/icons.tsx @@ -0,0 +1,42 @@ +import { + Loader2, + Lock, + LucideProps, + Moon, + SunMedium, + Twitter, +} from 'lucide-react'; + +export const Icons = { + sun: SunMedium, + moon: Moon, + twitter: Twitter, + lock: Lock, + spinner: Loader2, + google: (props: LucideProps) => ( + + + + + + + + ), + gitHub: (props: LucideProps) => ( + + + + ), +}; diff --git a/packages/ui/src/components/input.tsx b/packages/ui/src/components/input.tsx index 926c111..e240e7f 100644 --- a/packages/ui/src/components/input.tsx +++ b/packages/ui/src/components/input.tsx @@ -2,16 +2,13 @@ import * as React from 'react'; import { cn } from '@ultra-reporter/utils/cn'; -export interface InputProps - extends React.InputHTMLAttributes {} - -const Input = React.forwardRef( +const Input = React.forwardRef>( ({ className, type, ...props }, ref) => { return ( , + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +TabsList.displayName = TabsPrimitive.List.displayName; + +const TabsTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +TabsTrigger.displayName = TabsPrimitive.Trigger.displayName; + +const TabsContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +TabsContent.displayName = TabsPrimitive.Content.displayName; + +export { Tabs, TabsContent, TabsList, TabsTrigger }; diff --git a/packages/ui/src/home/hero.tsx b/packages/ui/src/home/hero.tsx index 090846f..76a9914 100644 --- a/packages/ui/src/home/hero.tsx +++ b/packages/ui/src/home/hero.tsx @@ -2,7 +2,9 @@ import { getFlag } from '@ultra-reporter/feature-toggle/provider'; import Image from 'next/image'; +import Link from 'next/link'; import { JSX } from 'react'; +import { Button } from '../components/button'; import { FileUpload } from '../utils/file-upload'; export const Hero = (): JSX.Element => { @@ -23,6 +25,15 @@ export const Hero = (): JSX.Element => { )} + {signInSupport?.enabled && ( +
+ + + +
+ )}
{ + const signInSupport = getFlag('sign_in_support'); + return (