Merge branch 'main' of https://github.com/GamerBoss101/HoyaHax2025
This commit is contained in:
@@ -18,6 +18,7 @@
|
|||||||
"@radix-ui/react-navigation-menu": "^1.2.4",
|
"@radix-ui/react-navigation-menu": "^1.2.4",
|
||||||
"@radix-ui/react-separator": "^1.1.1",
|
"@radix-ui/react-separator": "^1.1.1",
|
||||||
"@radix-ui/react-slot": "^1.1.1",
|
"@radix-ui/react-slot": "^1.1.1",
|
||||||
|
"@radix-ui/react-tabs": "^1.1.2",
|
||||||
"@radix-ui/react-tooltip": "^1.1.7",
|
"@radix-ui/react-tooltip": "^1.1.7",
|
||||||
"@tanstack/react-table": "^8.20.6",
|
"@tanstack/react-table": "^8.20.6",
|
||||||
"@vercel/analytics": "^1.4.1",
|
"@vercel/analytics": "^1.4.1",
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
/*
|
||||||
|
|
||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
@@ -23,3 +25,60 @@ export default function RootLayout({ children }: { children: React.ReactNode })
|
|||||||
</html>
|
</html>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { Metadata } from "next"
|
||||||
|
import { Inter } from "next/font/google"
|
||||||
|
import "./globals.css"
|
||||||
|
import { Mic } from "lucide-react"
|
||||||
|
|
||||||
|
const inter = Inter({ subsets: ["latin"] })
|
||||||
|
|
||||||
|
export default function RootLayout({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<html lang="en">
|
||||||
|
<body className={inter.className}>
|
||||||
|
<div className="flex flex-col min-h-screen">
|
||||||
|
<header className="bg-primary text-primary-foreground shadow-md">
|
||||||
|
<div className="container mx-auto px-4 py-4 flex items-center justify-between">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Mic className="h-6 w-6" />
|
||||||
|
<h1 className="text-2xl font-bold">PostCare</h1>
|
||||||
|
</div>
|
||||||
|
<nav>
|
||||||
|
<ul className="flex space-x-4">
|
||||||
|
<li>
|
||||||
|
<a href="#" className="hover:underline">
|
||||||
|
Home
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="#" className="hover:underline">
|
||||||
|
About
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="#" className="hover:underline">
|
||||||
|
Contact
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<main className="flex-grow">{children}</main>
|
||||||
|
<footer className="bg-muted mt-8">
|
||||||
|
<div className="container mx-auto px-4 py-6 text-center">
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
106
src/app/(web)/page.css
Normal file
106
src/app/(web)/page.css
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
/*
|
||||||
|
|
||||||
|
"use client"
|
||||||
|
|
||||||
|
import { Hero } from "@/components/hero";
|
||||||
|
import { Facts } from "@/components/facts";
|
||||||
|
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
export default function Home() {
|
||||||
|
return (
|
||||||
|
<div className="items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20 font-[family-name:var(--font-geist-sans)]">
|
||||||
|
|
||||||
|
<Hero />
|
||||||
|
<Facts />
|
||||||
|
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', height: '100vh' }}>
|
||||||
|
<Link href="/transcribe">
|
||||||
|
<button>Go to Transcribe Page</button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
:root {
|
||||||
|
--background: 0 0% 100%;
|
||||||
|
--foreground: 222.2 84% 4.9%;
|
||||||
|
|
||||||
|
--muted: 210 40% 96.1%;
|
||||||
|
--muted-foreground: 215.4 16.3% 46.9%;
|
||||||
|
|
||||||
|
--popover: 0 0% 100%;
|
||||||
|
--popover-foreground: 222.2 84% 4.9%;
|
||||||
|
|
||||||
|
--card: 0 0% 100%;
|
||||||
|
--card-foreground: 222.2 84% 4.9%;
|
||||||
|
|
||||||
|
--border: 214.3 31.8% 91.4%;
|
||||||
|
--input: 214.3 31.8% 91.4%;
|
||||||
|
|
||||||
|
--primary: 222.2 47.4% 11.2%;
|
||||||
|
--primary-foreground: 210 40% 98%;
|
||||||
|
|
||||||
|
--secondary: 210 40% 96.1%;
|
||||||
|
--secondary-foreground: 222.2 47.4% 11.2%;
|
||||||
|
|
||||||
|
--accent: 210 40% 96.1%;
|
||||||
|
--accent-foreground: 222.2 47.4% 11.2%;
|
||||||
|
|
||||||
|
--destructive: 0 84.2% 60.2%;
|
||||||
|
--destructive-foreground: 210 40% 98%;
|
||||||
|
|
||||||
|
--ring: 215 20.2% 65.1%;
|
||||||
|
|
||||||
|
--radius: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
--background: 222.2 84% 4.9%;
|
||||||
|
--foreground: 210 40% 98%;
|
||||||
|
|
||||||
|
--muted: 217.2 32.6% 17.5%;
|
||||||
|
--muted-foreground: 215 20.2% 65.1%;
|
||||||
|
|
||||||
|
--popover: 222.2 84% 4.9%;
|
||||||
|
--popover-foreground: 210 40% 98%;
|
||||||
|
|
||||||
|
--card: 222.2 84% 4.9%;
|
||||||
|
--card-foreground: 210 40% 98%;
|
||||||
|
|
||||||
|
--border: 217.2 32.6% 17.5%;
|
||||||
|
--input: 217.2 32.6% 17.5%;
|
||||||
|
|
||||||
|
--primary: 210 40% 98%;
|
||||||
|
--primary-foreground: 222.2 47.4% 11.2%;
|
||||||
|
|
||||||
|
--secondary: 217.2 32.6% 17.5%;
|
||||||
|
--secondary-foreground: 210 40% 98%;
|
||||||
|
|
||||||
|
--accent: 217.2 32.6% 17.5%;
|
||||||
|
--accent-foreground: 210 40% 98%;
|
||||||
|
|
||||||
|
--destructive: 0 62.8% 30.6%;
|
||||||
|
--destructive-foreground: 0 85.7% 97.3%;
|
||||||
|
|
||||||
|
--ring: 217.2 32.6% 17.5%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
* {
|
||||||
|
@apply border-border;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
@apply bg-background text-foreground;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import { Hero } from "@/components/hero";
|
|
||||||
import { Facts } from "@/components/facts";
|
|
||||||
|
|
||||||
import Link from "next/link";
|
|
||||||
|
|
||||||
export default function Home() {
|
|
||||||
return (
|
|
||||||
<div className="items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20 font-[family-name:var(--font-geist-sans)]">
|
|
||||||
|
|
||||||
<Hero />
|
|
||||||
<Facts />
|
|
||||||
|
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', height: '100vh' }}>
|
|
||||||
<Link href="/transcribe">
|
|
||||||
<button>Go to Transcribe Page</button>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
/*
|
||||||
|
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React, { useState, useRef } from "react";
|
import React, { useState, useRef } from "react";
|
||||||
@@ -129,3 +131,208 @@ const AudioTranscriber: React.FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default AudioTranscriber;
|
export default AudioTranscriber;
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
"use client"
|
||||||
|
|
||||||
|
import type React from "react"
|
||||||
|
import { useState, useRef } from "react"
|
||||||
|
import axios from "axios"
|
||||||
|
import { Button } from "@/components/ui/button"
|
||||||
|
import { Input } from "@/components/ui/input"
|
||||||
|
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"
|
||||||
|
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"
|
||||||
|
import { Mic, Upload, StopCircle, FileAudio } from "lucide-react"
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||||||
|
import { Label } from "@/components/ui/label"
|
||||||
|
import { Textarea } from "@/components/ui/textarea"
|
||||||
|
|
||||||
|
const AudioTranscriber: React.FC = () => {
|
||||||
|
const [file, setFile] = useState<File | null>(null)
|
||||||
|
const [transcription, setTranscription] = useState<string | null>(null)
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
const [recording, setRecording] = useState(false)
|
||||||
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
const mediaRecorderRef = useRef<MediaRecorder | null>(null)
|
||||||
|
const audioChunksRef = useRef<Blob[]>([])
|
||||||
|
|
||||||
|
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
if (event.target.files && event.target.files.length > 0) {
|
||||||
|
setFile(event.target.files[0])
|
||||||
|
console.log("File selected:", event.target.files[0].name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleTranscription = async (audioFile: File) => {
|
||||||
|
if (!audioFile) {
|
||||||
|
setError("No audio file to transcribe!")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Starting transcription for:", audioFile.name)
|
||||||
|
|
||||||
|
const formData = new FormData()
|
||||||
|
formData.append("file", audioFile)
|
||||||
|
|
||||||
|
setLoading(true)
|
||||||
|
setError(null)
|
||||||
|
try {
|
||||||
|
const response = await axios.post("http://localhost:8000/transcribe", formData, {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "multipart/form-data",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log("Transcription response:", response.data)
|
||||||
|
|
||||||
|
if (response.data && response.data.transcription) {
|
||||||
|
setTranscription(response.data.transcription)
|
||||||
|
} else {
|
||||||
|
setError("Unexpected response format. Check backend API.")
|
||||||
|
console.error("Invalid response format:", response.data)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error transcribing audio:", error)
|
||||||
|
setError("Failed to transcribe audio. Please try again.")
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const startRecording = async () => {
|
||||||
|
try {
|
||||||
|
const stream = await navigator.mediaDevices.getUserMedia({ audio: true })
|
||||||
|
console.log("Microphone access granted.")
|
||||||
|
|
||||||
|
mediaRecorderRef.current = new MediaRecorder(stream)
|
||||||
|
audioChunksRef.current = []
|
||||||
|
|
||||||
|
mediaRecorderRef.current.ondataavailable = (event) => {
|
||||||
|
if (event.data.size > 0) {
|
||||||
|
console.log("Audio chunk received:", event.data)
|
||||||
|
audioChunksRef.current.push(event.data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mediaRecorderRef.current.onstop = async () => {
|
||||||
|
const audioBlob = new Blob(audioChunksRef.current, { type: "audio/mp3" })
|
||||||
|
const audioFile = new File([audioBlob], "recording.mp3", { type: "audio/mp3" })
|
||||||
|
|
||||||
|
console.log("Recording stopped. Blob created:", audioBlob)
|
||||||
|
|
||||||
|
setFile(audioFile)
|
||||||
|
setTranscription("Processing transcription for recorded audio...")
|
||||||
|
await handleTranscription(audioFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
mediaRecorderRef.current.start()
|
||||||
|
console.log("Recording started.")
|
||||||
|
setRecording(true)
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error starting recording:", error)
|
||||||
|
setError("Failed to start recording. Please check microphone permissions.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const stopRecording = () => {
|
||||||
|
if (mediaRecorderRef.current) {
|
||||||
|
console.log("Stopping recording...")
|
||||||
|
mediaRecorderRef.current.stop()
|
||||||
|
setRecording(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container mx-auto px-4 py-8">
|
||||||
|
<h1 className="text-4xl font-bold text-center mb-8">Audio Transcriber</h1>
|
||||||
|
|
||||||
|
<Card className="mb-8">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Transcribe Audio</CardTitle>
|
||||||
|
<CardDescription>Upload an audio file or record directly to transcribe</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<Tabs defaultValue="upload" className="w-full">
|
||||||
|
<TabsList className="grid w-full grid-cols-2">
|
||||||
|
<TabsTrigger value="upload">Upload Audio</TabsTrigger>
|
||||||
|
<TabsTrigger value="record">Record Audio</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
<TabsContent value="upload">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<Label htmlFor="audio-file">Select an audio file</Label>
|
||||||
|
<Input id="audio-file" type="file" accept="audio/*" onChange={handleFileChange} />
|
||||||
|
<Button
|
||||||
|
onClick={() => file && handleTranscription(file)}
|
||||||
|
disabled={loading || !file}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
{loading ? "Transcribing..." : "Transcribe"}
|
||||||
|
<Upload className="ml-2 h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
<TabsContent value="record">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<Label>Record audio from your microphone</Label>
|
||||||
|
{!recording ? (
|
||||||
|
<Button onClick={startRecording} className="w-full">
|
||||||
|
Start Recording
|
||||||
|
<Mic className="ml-2 h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button onClick={stopRecording} variant="destructive" className="w-full">
|
||||||
|
Stop Recording
|
||||||
|
<StopCircle className="ml-2 h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Transcription Result</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
{loading
|
||||||
|
? "Processing transcription..."
|
||||||
|
: transcription
|
||||||
|
? "Your transcription is ready"
|
||||||
|
: "No transcription available yet"}
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<Textarea
|
||||||
|
value={transcription || ""}
|
||||||
|
readOnly
|
||||||
|
placeholder="Transcription will appear here"
|
||||||
|
className="min-h-[200px]"
|
||||||
|
/>
|
||||||
|
</CardContent>
|
||||||
|
{file && (
|
||||||
|
<CardFooter className="flex justify-between items-center">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<FileAudio className="mr-2 h-4 w-4" />
|
||||||
|
<span className="text-sm text-muted-foreground">{file.name}</span>
|
||||||
|
</div>
|
||||||
|
<Button variant="outline" onClick={() => setFile(null)}>
|
||||||
|
Clear
|
||||||
|
</Button>
|
||||||
|
</CardFooter>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<Alert variant="destructive" className="mt-4">
|
||||||
|
<AlertTitle>Error</AlertTitle>
|
||||||
|
<AlertDescription>{error}</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AudioTranscriber
|
||||||
|
|||||||
59
src/components/ui/alert.tsx
Normal file
59
src/components/ui/alert.tsx
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const alertVariants = cva(
|
||||||
|
"relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: "bg-background text-foreground",
|
||||||
|
destructive:
|
||||||
|
"border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const Alert = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
|
||||||
|
>(({ className, variant, ...props }, ref) => (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
role="alert"
|
||||||
|
className={cn(alertVariants({ variant }), className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
Alert.displayName = "Alert"
|
||||||
|
|
||||||
|
const AlertTitle = React.forwardRef<
|
||||||
|
HTMLParagraphElement,
|
||||||
|
React.HTMLAttributes<HTMLHeadingElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<h5
|
||||||
|
ref={ref}
|
||||||
|
className={cn("mb-1 font-medium leading-none tracking-tight", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
AlertTitle.displayName = "AlertTitle"
|
||||||
|
|
||||||
|
const AlertDescription = React.forwardRef<
|
||||||
|
HTMLParagraphElement,
|
||||||
|
React.HTMLAttributes<HTMLParagraphElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={cn("text-sm [&_p]:leading-relaxed", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
AlertDescription.displayName = "AlertDescription"
|
||||||
|
|
||||||
|
export { Alert, AlertTitle, AlertDescription }
|
||||||
55
src/components/ui/tabs.tsx
Normal file
55
src/components/ui/tabs.tsx
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import * as TabsPrimitive from "@radix-ui/react-tabs"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const Tabs = TabsPrimitive.Root
|
||||||
|
|
||||||
|
const TabsList = React.forwardRef<
|
||||||
|
React.ElementRef<typeof TabsPrimitive.List>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<TabsPrimitive.List
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"inline-flex h-9 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
TabsList.displayName = TabsPrimitive.List.displayName
|
||||||
|
|
||||||
|
const TabsTrigger = React.forwardRef<
|
||||||
|
React.ElementRef<typeof TabsPrimitive.Trigger>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<TabsPrimitive.Trigger
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
|
||||||
|
|
||||||
|
const TabsContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof TabsPrimitive.Content>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<TabsPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
TabsContent.displayName = TabsPrimitive.Content.displayName
|
||||||
|
|
||||||
|
export { Tabs, TabsList, TabsTrigger, TabsContent }
|
||||||
Reference in New Issue
Block a user