diff --git a/apps/web/app/(app)/dashboard/page.tsx b/apps/web/app/(app)/dashboard/page.tsx new file mode 100644 index 0000000..4efd7cd --- /dev/null +++ b/apps/web/app/(app)/dashboard/page.tsx @@ -0,0 +1,9 @@ +'use client'; + +export default function DashboardPage() { + return ( +
+

Dashboard

+
+ ); +} 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..2697a32 --- /dev/null +++ b/apps/web/app/(auth)/login/page.tsx @@ -0,0 +1,97 @@ +'use client'; + +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'; +import { useState } from 'react'; + +export default function AuthPage() { + const [isLoading, setIsLoading] = useState(false); + + const handleAuth = async () => { + setIsLoading(true); + try { + const response = await fetch('/api/auth/login', { + method: 'POST', + }); + + if (!response.ok) { + throw new Error('Authentication failed'); + } + + const data = await response.json(); + + if (data.url) { + window.location.href = data.url; + } else { + throw new Error('No OAuth URL received'); + } + } catch (error) { + console.error('Authentication error:', error); + // You might want to show an error message to the user here + } finally { + setIsLoading(false); + } + }; + + 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/api/auth/login/route.ts b/apps/web/app/api/auth/login/route.ts new file mode 100644 index 0000000..ea3f480 --- /dev/null +++ b/apps/web/app/api/auth/login/route.ts @@ -0,0 +1,48 @@ +import { logger } from '@ultra-reporter/logger'; +import { createClient } from '@ultra-reporter/supabase/server'; +import { NextResponse } from 'next/server'; + +export async function POST(req: Request) { + try { + const supabase = await createClient(); + const origin = req.headers.get('origin') || 'http://localhost:3000'; + + // Get the URL for Google OAuth sign-in + const { data, error } = await supabase.auth.signInWithOAuth({ + provider: 'google', + options: { + redirectTo: `${origin}/auth/callback`, + queryParams: { + access_type: 'offline', + prompt: 'consent', + }, + scopes: 'email profile', + }, + }); + + if (error) { + logger.error('OAuth initialization failed', { error }); + return NextResponse.json( + { error: 'Failed to start OAuth flow' }, + { status: 500 } + ); + } + + if (!data.url) { + logger.error('No OAuth URL returned'); + return NextResponse.json( + { error: 'Invalid OAuth configuration' }, + { status: 500 } + ); + } + + logger.info('OAuth flow initiated', { url: data.url }); + return NextResponse.json({ url: data.url }, { status: 200 }); + } catch (error) { + logger.error('Unexpected error during OAuth initialization', { error }); + return NextResponse.json( + { error: 'Internal server error' }, + { status: 500 } + ); + } +} diff --git a/apps/web/app/api/auth/sign-up/route.ts b/apps/web/app/api/auth/sign-up/route.ts deleted file mode 100644 index fb1581b..0000000 --- a/apps/web/app/api/auth/sign-up/route.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { logger } from '@ultra-reporter/logger'; -import { createClient } from '@ultra-reporter/supabase/server'; -import { NextResponse } from 'next/server'; - -export async function POST(req: Request) { - const supabase = await createClient(); - - const origin = req.headers.get('origin'); - - const { provider } = await req.json(); - const { error, data } = await supabase.auth.signInWithOAuth({ - provider, - options: { - redirectTo: `${origin}/auth/callback`, - }, - }); - - if (error) { - logger.error('Sign up failed', error); - return new NextResponse('Error while signing up', { - status: 500, - }); - } - - const { - data: { user }, - } = await supabase.auth.getUser(); - - if (!user) { - logger.error('User not found'); - return new NextResponse('Error while signing up', { - status: 500, - }); - } - - logger.info('Sign up successful', data); - return NextResponse.json(user, { - status: 201, - }); -} 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/package.json b/package.json index 7f050f1..1c60a48 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,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.4", + "@types/node": "^22.10.5", "@typescript-eslint/eslint-plugin": "^8.19.0", "@typescript-eslint/parser": "^8.19.0", "@vercel/style-guide": "^6.0.0", diff --git a/packages/supabase/src/middleware.ts b/packages/supabase/src/middleware.ts index 56bfee4..0566e71 100644 --- a/packages/supabase/src/middleware.ts +++ b/packages/supabase/src/middleware.ts @@ -34,12 +34,12 @@ export const updateSession = async (request: NextRequest) => { const user = await supabase.auth.getUser(); - if (request.nextUrl.pathname.startsWith('/protected') && user.error) { - return NextResponse.redirect(new URL('/sign-in', request.url)); + if (request.nextUrl.pathname.startsWith('/dashboard') && user.error) { + return NextResponse.redirect(new URL('/login', request.url)); } - if (request.nextUrl.pathname === '/' && !user.error) { - return NextResponse.redirect(new URL('/protected', request.url)); + if (request.nextUrl.pathname === '/login' && !user.error) { + return NextResponse.redirect(new URL('/dashboard', request.url)); } return response; diff --git a/packages/ui/package.json b/packages/ui/package.json index d394dc8..6b0db4e 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -41,6 +41,7 @@ "@radix-ui/react-separator": "^1.1.1", "@radix-ui/react-slot": "^1.1.1", "@radix-ui/react-switch": "^1.1.2", + "@radix-ui/react-tabs": "^1.1.2", "@radix-ui/react-toast": "^1.2.4", "@radix-ui/react-tooltip": "^1.1.6", "@tanstack/react-table": "^8.20.6", @@ -54,11 +55,13 @@ "react-hook-form": "^7.54.2", "recharts": "^2.15.0", "tailwindcss-animate": "^1.0.7", - "zod": "^3.24.1" + "zod": "^3.24.1", + "zxcvbn": "^4.4.2" }, "devDependencies": { "@types/react": "^19.0.2", "@types/react-dom": "^19.0.2", + "@types/zxcvbn": "^4.4.5", "@ultra-reporter/feature-toggle": "workspace:*", "@ultra-reporter/typescript-config": "workspace:*", "@ultra-reporter/utils": "workspace:*", 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 (