Initial commit

This commit is contained in:
2024-09-14 23:38:24 -04:00
parent e48ab0aa6e
commit ce046c2062
25 changed files with 2263 additions and 83 deletions

13
.env Normal file
View File

@@ -0,0 +1,13 @@
# When adding additional environment variables, the schema in "/src/env.js"
# should be updated accordingly.
# Drizzle
DATABASE_URL="postgresql://postgres:password@localhost:5432/hristudio"
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_cmVmaW5lZC1kcnVtLTIzLmNsZXJrLmFjY291bnRzLmRldiQ
CLERK_SECRET_KEY=sk_test_3qESERGxZqHpROHzFe7nYxjfqfVhpHWS1UVDQt86v8
NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in
NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up
# Example:
# SERVERVAR="foo"
# NEXT_PUBLIC_CLIENTVAR="bar"

32
.gitignore vendored
View File

@@ -1,5 +1,7 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. # Created by https://www.toptal.com/developers/gitignore/api/nextjs,react
# Edit at https://www.toptal.com/developers/gitignore?templates=nextjs,react
### NextJS ###
# dependencies # dependencies
/node_modules /node_modules
/.pnp /.pnp
@@ -8,15 +10,9 @@
# testing # testing
/coverage /coverage
# database
/prisma/db.sqlite
/prisma/db.sqlite-journal
db.sqlite
# next.js # next.js
/.next/ /.next/
/out/ /out/
next-env.d.ts
# production # production
/build /build
@@ -32,8 +28,6 @@ yarn-error.log*
.pnpm-debug.log* .pnpm-debug.log*
# local env files # local env files
# do not commit any .env files to git, except for the .env.example file. https://create.t3.gg/en/usage/env-variables#using-environment-variables
.env
.env*.local .env*.local
# vercel # vercel
@@ -41,6 +35,22 @@ yarn-error.log*
# typescript # typescript
*.tsbuildinfo *.tsbuildinfo
next-env.d.ts
# idea files ### react ###
.idea .DS_*
*.log
logs
**/*.backup.*
**/*.back.*
node_modules
bower_components
*.sublime*
psd
thumb
sketch
# End of https://www.toptal.com/developers/gitignore/api/nextjs,react

View File

@@ -1,29 +1,80 @@
# Create T3 App # HRIStudio
This is a [T3 Stack](https://create.t3.gg/) project bootstrapped with `create-t3-app`. HRIStudio is a web-based platform designed to streamline the design, execution, and analysis of Wizard-of-Oz (WoZ) experiments in Human-Robot Interaction (HRI) studies. It offers an intuitive interface for experiment creation, real-time control and monitoring during experimental runs, and comprehensive data logging and playback tools for analysis and reproducibility.
## What's next? How do I make an app with this? ## Features
We try to keep this project as simple as possible, so you can start with just the scaffolding we set up for you, and add additional things later when they become necessary. - User-friendly graphical interface for designing WoZ experiments
- Visual programming environment with drag-and-drop functionality
- Real-time control and observation capabilities during live experiment sessions
- Comprehensive data logging and playback tools
- Integration with Robot Operating System (ROS) for various robotic platforms
- Collaborative workflow support with multiple user accounts and data sharing
If you are not familiar with the different technologies used in this project, please refer to the respective docs. If you still are in the wind, please join our [Discord](https://t3.gg/discord) and ask for help. ## System Requirements
- [Next.js](https://nextjs.org) - Node.js (version X.X.X or higher)
- [NextAuth.js](https://next-auth.js.org) - npm (version X.X.X or higher)
- [Prisma](https://prisma.io) - ROS (Robot Operating System)
- [Drizzle](https://orm.drizzle.team)
- [Tailwind CSS](https://tailwindcss.com)
- [tRPC](https://trpc.io)
## Learn More ## Installation
To learn more about the [T3 Stack](https://create.t3.gg/), take a look at the following resources: 1. Clone the repository:
```
git clone https://github.com/your-username/hristudio.git
```
- [Documentation](https://create.t3.gg/) 2. Navigate to the project directory:
- [Learn the T3 Stack](https://create.t3.gg/en/faq#what-learning-resources-are-currently-available) — Check out these awesome tutorials ```
cd hristudio
```
You can check out the [create-t3-app GitHub repository](https://github.com/t3-oss/create-t3-app) — your feedback and contributions are welcome! 3. Install dependencies:
```
npm install
```
## How do I deploy this? 4. Set up environment variables:
Create a `.env.local` file in the root directory and add the necessary environment variables (e.g., database connection string, API keys).
Follow our deployment guides for [Vercel](https://create.t3.gg/en/deployment/vercel), [Netlify](https://create.t3.gg/en/deployment/netlify) and [Docker](https://create.t3.gg/en/deployment/docker) for more information. 5. Run the development server:
```
npm run dev
```
6. Open [http://localhost:3000](http://localhost:3000) in your browser to see the application.
## Project Structure
- `pages/`: Contains the Next.js pages
- `components/`: Reusable React components
- `public/`: Static assets
- `styles/`: CSS styles
- `lib/`: Utility functions and modules
- `api/`: API routes
- `ros/`: ROS interface and related components
## Usage
1. Create a new study in the Dashboard view
2. Design your experiment using the visual programming interface in the Design view
3. Execute the experiment using the Execute view
4. Analyze results and playback recorded data in the Playback view
For detailed usage instructions, please refer to the [User Guide](link-to-user-guide).
## Contributing
We welcome contributions to HRIStudio! Please read our [Contributing Guidelines](link-to-contributing-guidelines) for more information on how to get started.
## License
This project is licensed under the [MIT License](link-to-license-file).
## Contact
For questions or support, please contact [your-email@example.com](mailto:your-email@example.com).
## Acknowledgments
This project is being developed by Sean O'Connor and L. Felipe Perrone at Bucknell University. We would like to thank the robotics and HRI research community for their valuable insights and contributions.

20
components.json Normal file
View File

@@ -0,0 +1,20 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "tailwind.config.ts",
"css": "src/styles/globals.css",
"baseColor": "zinc",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "~/components",
"utils": "~/lib/utils",
"ui": "~/components/ui",
"lib": "~/lib",
"hooks": "~/hooks"
}
}

View File

@@ -14,13 +14,27 @@
"start": "next start" "start": "next start"
}, },
"dependencies": { "dependencies": {
"@clerk/nextjs": "^5.5.2",
"@radix-ui/react-avatar": "^1.1.0",
"@radix-ui/react-dialog": "^1.1.1",
"@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-separator": "^1.1.0",
"@radix-ui/react-slot": "^1.1.0",
"@t3-oss/env-nextjs": "^0.10.1", "@t3-oss/env-nextjs": "^0.10.1",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"cn": "^0.1.1",
"drizzle-orm": "^0.33.0", "drizzle-orm": "^0.33.0",
"geist": "^1.3.0", "geist": "^1.3.0",
"lucide-react": "^0.441.0",
"next": "^14.2.4", "next": "^14.2.4",
"next-themes": "^0.3.0",
"postgres": "^3.4.4", "postgres": "^3.4.4",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-icons": "^5.3.0",
"tailwind-merge": "^2.5.2",
"tailwindcss-animate": "^1.0.7",
"zod": "^3.23.3" "zod": "^3.23.3"
}, },
"devDependencies": { "devDependencies": {

964
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

BIN
public/hristudio_laptop.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

20
src/app/dash/layout.tsx Normal file
View File

@@ -0,0 +1,20 @@
import { type PropsWithChildren } from "react"
import { Sidebar } from "~/components/sidebar"
import { inter } from "../layout"
import "~/styles/globals.css"
export default function RootLayout({ children }: PropsWithChildren) {
return (
<html lang="en">
<body className={`font-sans ${inter.variable}`}>
<div className="flex h-screen">
<Sidebar />
<main className="flex-1 overflow-y-auto">
{children}
</main>
</div>
</body>
</html>
)
}

54
src/app/dash/page.tsx Normal file
View File

@@ -0,0 +1,54 @@
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '~/components/ui/card';
import { Button } from '~/components/ui/button';
const HomePage: React.FC = () => {
return (
<div className="min-h-screen bg-gradient-to-b from-blue-100 to-white pt-14 lg:pt-0">
<div className="container mx-auto px-4 py-16">
<header className="text-center mb-16">
<h1 className="text-5xl font-bold mb-4 text-blue-800">Welcome to the HRIStudio Dashboard!</h1>
<p className="text-xl text-gray-600 max-w-3xl mx-auto">
Manage your Human-Robot Interaction projects and experiments
</p>
</header>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<Card className="bg-white shadow-lg">
<CardHeader>
<CardTitle className="text-2xl font-semibold text-blue-700">Projects</CardTitle>
<CardDescription>Manage your HRI projects</CardDescription>
</CardHeader>
<CardContent>
<p className="mb-4">Create, edit, and analyze your HRI projects.</p>
<Button className="bg-blue-600 hover:bg-blue-700 text-white">View Projects</Button>
</CardContent>
</Card>
<Card className="bg-white shadow-lg">
<CardHeader>
<CardTitle className="text-2xl font-semibold text-blue-700">Experiments</CardTitle>
<CardDescription>Design and run experiments</CardDescription>
</CardHeader>
<CardContent>
<p className="mb-4">Set up, conduct, and analyze HRI experiments.</p>
<Button className="bg-green-600 hover:bg-green-700 text-white">New Experiment</Button>
</CardContent>
</Card>
<Card className="bg-white shadow-lg">
<CardHeader>
<CardTitle className="text-2xl font-semibold text-blue-700">Data Analysis</CardTitle>
<CardDescription>Analyze your research data</CardDescription>
</CardHeader>
<CardContent>
<p className="mb-4">Visualize and interpret your HRI research data.</p>
<Button className="bg-purple-600 hover:bg-purple-700 text-white">Analyze Data</Button>
</CardContent>
</Card>
</div>
</div>
</div>
);
};
export default HomePage;

View File

@@ -1,20 +1,31 @@
import "~/styles/globals.css"; import { ClerkProvider } from '@clerk/nextjs'
import { Inter } from "next/font/google"
import { ThemeProvider } from "next-themes"
import { GeistSans } from "geist/font/sans"; import "~/styles/globals.css"
import { type Metadata } from "next";
export const metadata: Metadata = { export const inter = Inter({
title: "Create T3 App", subsets: ["latin"],
description: "Generated by create-t3-app", display: "swap",
variable: "--font-sans",
})
export const metadata = {
title: "T3 App",
description: "Created with create-t3-app",
icons: [{ rel: "icon", url: "/favicon.ico" }], icons: [{ rel: "icon", url: "/favicon.ico" }],
}; }
export default function RootLayout({ export default function RootLayout({ children }: React.PropsWithChildren) {
children, return (
}: Readonly<{ children: React.ReactNode }>) { <ClerkProvider>
return ( {/* <ThemeProvider attribute="class" defaultTheme="system" enableSystem> */}
<html lang="en" className={`${GeistSans.variable}`}> <html lang="en" className={inter.variable}>
<body>{children}</body> <body className="font-sans">
</html> {children}
); </body>
</html>
{/* </ThemeProvider> */}
</ClerkProvider>
)
} }

View File

@@ -1,37 +1,74 @@
import Link from "next/link"; import Link from 'next/link';
import Image from 'next/image';
import { auth } from "@clerk/nextjs/server";
import { redirect } from "next/navigation";
export default function HomePage() { export default function HomePage() {
const { userId } = auth();
if (userId) {
redirect("/dash");
}
return ( return (
<main className="flex min-h-screen flex-col items-center justify-center bg-gradient-to-b from-[#2e026d] to-[#15162c] text-white"> <div className="min-h-screen bg-gradient-to-b from-blue-100 to-white">
<div className="container flex flex-col items-center justify-center gap-12 px-4 py-16"> <div className="container mx-auto px-4 py-16">
<h1 className="text-5xl font-extrabold tracking-tight text-white sm:text-[5rem]"> <header className="text-center mb-16">
Create <span className="text-[hsl(280,100%,70%)]">T3</span> App <h1 className="text-5xl font-bold mb-4 text-blue-800">Welcome to HRIStudio</h1>
</h1> <p className="text-xl text-gray-600 max-w-3xl mx-auto">
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 md:gap-8"> Empowering Human-Robot Interaction Research and Development
<Link </p>
className="flex max-w-xs flex-col gap-4 rounded-xl bg-white/10 p-4 text-white hover:bg-white/20" </header>
href="https://create.t3.gg/en/usage/first-steps"
target="_blank" <div className="grid md:grid-cols-2 gap-12 items-center mb-16">
> <div>
<h3 className="text-2xl font-bold">First Steps </h3> <h2 className="text-3xl font-semibold mb-4 text-blue-700">About HRIStudio</h2>
<div className="text-lg"> <p className="text-lg text-gray-700 mb-4">
Just the basics - Everything you need to know to set up your HRIStudio is a cutting-edge platform designed to streamline the process of creating,
database and authentication. managing, and analyzing Human-Robot Interaction experiments. Our suite of tools
</div> empowers researchers and developers to push the boundaries of HRI research.
</Link> </p>
<Link <p className="text-lg text-gray-700 mb-4">
className="flex max-w-xs flex-col gap-4 rounded-xl bg-white/10 p-4 text-white hover:bg-white/20" With HRIStudio, you can:
href="https://create.t3.gg/en/introduction" </p>
target="_blank" <ul className="list-disc list-inside text-gray-700 mb-6">
> <li>Design complex interaction scenarios with ease</li>
<h3 className="text-2xl font-bold">Documentation </h3> <li>Collect and analyze data in real-time</li>
<div className="text-lg"> <li>Collaborate seamlessly with team members</li>
Learn more about Create T3 App, the libraries it uses, and how to <li>Visualize results with advanced reporting tools</li>
deploy it. </ul>
</div> </div>
</Link> <div className="relative aspect-video w-full">
<Image
src="/hristudio_laptop.png"
alt="HRIStudio Interface on Laptop"
fill
style={{ objectFit: 'contain' }}
// className="rounded-lg shadow-lg"
/>
</div>
</div> </div>
<div className="text-center mb-12">
<h2 className="text-3xl font-semibold mb-4 text-blue-700">Join the HRI Revolution</h2>
<p className="text-lg text-gray-700 mb-6">
Whether you're a seasoned researcher or just starting in the field of Human-Robot Interaction,
HRIStudio provides the tools and support you need to succeed.
</p>
<div className="space-x-4">
<Link href="/sign-in" className="bg-blue-600 hover:bg-blue-700 text-white font-bold py-3 px-6 rounded-full transition duration-300">
Sign In
</Link>
<Link href="/sign-up" className="bg-green-600 hover:bg-green-700 text-white font-bold py-3 px-6 rounded-full transition duration-300">
Sign Up
</Link>
</div>
</div>
<footer className="text-center text-gray-600">
<p>© 2024 HRIStudio. All rights reserved.</p>
</footer>
</div> </div>
</main> </div>
); );
} }

View File

@@ -0,0 +1,102 @@
"use client"
import { useState } from "react"
import { useSignIn } from "@clerk/nextjs"
import { useRouter } from "next/navigation"
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "~/components/ui/card"
import { Input } from "~/components/ui/input"
import { Button } from "~/components/ui/button"
import { Separator } from "~/components/ui/separator"
import Link from "next/link"
import { FcGoogle } from "react-icons/fc"
import { FaApple } from "react-icons/fa"
export default function SignInPage() {
const { isLoaded, signIn, setActive } = useSignIn()
const [emailAddress, setEmailAddress] = useState("")
const [password, setPassword] = useState("")
const router = useRouter()
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
if (!isLoaded) return
try {
const result = await signIn.create({
identifier: emailAddress,
password,
})
if (result.status === "complete") {
await setActive({ session: result.createdSessionId })
router.push("/dash")
}
} catch (err: any) {
console.error("Error:", err.errors[0].message)
}
}
const signInWith = (strategy: "oauth_google" | "oauth_apple") => {
if (!isLoaded) return
signIn.authenticateWithRedirect({
strategy,
redirectUrl: "/sso-callback",
redirectUrlComplete: "/dash",
})
}
return (
<div className="min-h-screen bg-gradient-to-b from-blue-100 to-white flex items-center justify-center">
<Card className="w-[350px]">
<CardHeader>
<CardTitle>Sign In to HRIStudio</CardTitle>
<CardDescription>Enter your email and password to sign in</CardDescription>
</CardHeader>
<CardContent>
<div className="grid w-full items-center gap-4">
<Button variant="outline" onClick={() => signInWith("oauth_google")}>
<FcGoogle className="mr-2 h-4 w-4" />
Sign in with Google
</Button>
<Button variant="outline" onClick={() => signInWith("oauth_apple")}>
<FaApple className="mr-2 h-4 w-4" />
Sign in with Apple
</Button>
</div>
<Separator className="my-4" />
<form onSubmit={handleSubmit}>
<div className="grid w-full items-center gap-4">
<div className="flex flex-col space-y-1.5">
<Input
id="email"
placeholder="Email"
type="email"
value={emailAddress}
onChange={(e) => setEmailAddress(e.target.value)}
/>
</div>
<div className="flex flex-col space-y-1.5">
<Input
id="password"
placeholder="Password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
</div>
<Button className="w-full" type="submit">Sign In</Button>
</div>
</form>
</CardContent>
<CardFooter className="flex flex-col">
<p className="mt-4 text-sm text-center">
Don't have an account?{" "}
<Link href="/sign-up" className="text-blue-600 hover:underline">
Sign up
</Link>
</p>
</CardFooter>
</Card>
</div>
)
}

View File

@@ -0,0 +1,102 @@
"use client"
import { useState } from "react"
import { useSignUp } from "@clerk/nextjs"
import { useRouter } from "next/navigation"
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "~/components/ui/card"
import { Input } from "~/components/ui/input"
import { Button } from "~/components/ui/button"
import { Separator } from "~/components/ui/separator"
import Link from "next/link"
import { FcGoogle } from "react-icons/fc"
import { FaApple } from "react-icons/fa"
export default function SignUpPage() {
const { isLoaded, signUp, setActive } = useSignUp()
const [emailAddress, setEmailAddress] = useState("")
const [password, setPassword] = useState("")
const router = useRouter()
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
if (!isLoaded) return
try {
const result = await signUp.create({
emailAddress,
password,
})
if (result.status === "complete") {
await setActive({ session: result.createdSessionId })
router.push("/dash")
}
} catch (err: any) {
console.error("Error:", err.errors[0].message)
}
}
const signUpWith = (strategy: "oauth_google" | "oauth_apple") => {
if (!isLoaded) return
signUp.authenticateWithRedirect({
strategy,
redirectUrl: "/sso-callback",
redirectUrlComplete: "/dash",
})
}
return (
<div className="min-h-screen bg-gradient-to-b from-blue-100 to-white flex items-center justify-center">
<Card className="w-[350px]">
<CardHeader>
<CardTitle>Sign Up for HRIStudio</CardTitle>
<CardDescription>Create an account to get started</CardDescription>
</CardHeader>
<CardContent>
<div className="grid w-full items-center gap-4">
<Button variant="outline" onClick={() => signUpWith("oauth_google")}>
<FcGoogle className="mr-2 h-4 w-4" />
Sign up with Google
</Button>
<Button variant="outline" onClick={() => signUpWith("oauth_apple")}>
<FaApple className="mr-2 h-4 w-4" />
Sign up with Apple
</Button>
</div>
<Separator className="my-4" />
<form onSubmit={handleSubmit}>
<div className="grid w-full items-center gap-4">
<div className="flex flex-col space-y-1.5">
<Input
id="email"
placeholder="Email"
type="email"
value={emailAddress}
onChange={(e) => setEmailAddress(e.target.value)}
/>
</div>
<div className="flex flex-col space-y-1.5">
<Input
id="password"
placeholder="Password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
</div>
<Button className="w-full" type="submit">Sign Up</Button>
</div>
</form>
</CardContent>
<CardFooter className="flex flex-col">
<p className="mt-4 text-sm text-center">
Already have an account?{" "}
<Link href="/sign-in" className="text-blue-600 hover:underline">
Sign in
</Link>
</p>
</CardFooter>
</Card>
</div>
)
}

106
src/components/sidebar.tsx Normal file
View File

@@ -0,0 +1,106 @@
"use client"
import { UserButton, useUser } from "@clerk/nextjs"
import {
BarChartIcon,
BeakerIcon,
BotIcon,
FolderIcon,
LayoutDashboard,
Menu,
Settings
} from "lucide-react"
import Link from "next/link"
import { usePathname } from "next/navigation"
import { useState } from "react"
import { Button } from "~/components/ui/button"
import { Sheet, SheetContent, SheetTrigger } from "~/components/ui/sheet"
import { cn } from "~/lib/utils"
const navItems = [
{ name: "Dashboard", href: "/dash", icon: LayoutDashboard },
{ name: "Projects", href: "/projects", icon: FolderIcon },
{ name: "Experiments", href: "/experiments", icon: BeakerIcon },
{ name: "Data Analysis", href: "/analysis", icon: BarChartIcon },
{ name: "Settings", href: "/settings", icon: Settings },
];
export function Sidebar() {
const pathname = usePathname()
const [isOpen, setIsOpen] = useState(false)
const { user } = useUser()
const SidebarContent = () => (
<div className="flex h-full flex-col lg:bg-blue-50">
<nav className="flex-1 overflow-y-auto p-4">
<ul className="space-y-2">
{navItems.map((item) => {
const IconComponent = item.icon;
return (
<li key={item.href}>
<Button
asChild
variant="ghost"
className={cn(
"w-full justify-start text-blue-800 hover:bg-blue-100",
pathname === item.href && "bg-blue-100 font-semibold"
)}
>
<Link href={item.href} onClick={() => setIsOpen(false)}>
<IconComponent className="h-5 w-5 mr-3" />
<span>{item.name}</span>
</Link>
</Button>
</li>
);
})}
</ul>
</nav>
<div className="border-t border-blue-200 p-4">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-4">
<UserButton />
<div>
<p className="text-sm font-medium text-blue-800">{user?.fullName || 'User'}</p>
<p className="text-xs text-blue-600">{user?.primaryEmailAddress?.emailAddress || 'user@example.com'}</p>
</div>
</div>
</div>
</div>
</div>
)
const HRIStudioLogo = () => (
<Link href="/dash" className="flex items-center font-sans text-xl text-blue-800">
<BotIcon className="h-6 w-6 mr-1 text-blue-600" />
<span className="font-extrabold">HRI</span>
<span className="font-normal">Studio</span>
</Link>
)
return (
<>
<div className="lg:hidden fixed top-0 left-0 right-0">
<div className="flex h-14 items-center justify-between border-b px-4 bg-background">
<HRIStudioLogo />
<Sheet open={isOpen} onOpenChange={setIsOpen}>
<SheetTrigger asChild>
<Button variant="ghost" className="h-14 w-14 px-0">
<Menu className="h-6 w-6" />
</Button>
</SheetTrigger>
<SheetContent side="top" className="w-full">
<SidebarContent />
</SheetContent>
</Sheet>
</div>
</div>
<div className="hidden lg:flex lg:w-64 lg:flex-col lg:border-r lg:bg-background">
<div className="flex h-14 items-center border-b px-4">
<HRIStudioLogo />
</div>
<SidebarContent />
</div>
</>
)
}

View File

@@ -0,0 +1,50 @@
"use client"
import * as React from "react"
import * as AvatarPrimitive from "@radix-ui/react-avatar"
import { cn } from "~/lib/utils"
const Avatar = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Root
ref={ref}
className={cn(
"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
className
)}
{...props}
/>
))
Avatar.displayName = AvatarPrimitive.Root.displayName
const AvatarImage = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Image>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Image
ref={ref}
className={cn("aspect-square h-full w-full", className)}
{...props}
/>
))
AvatarImage.displayName = AvatarPrimitive.Image.displayName
const AvatarFallback = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Fallback>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Fallback
ref={ref}
className={cn(
"flex h-full w-full items-center justify-center rounded-full bg-muted",
className
)}
{...props}
/>
))
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
export { Avatar, AvatarImage, AvatarFallback }

View File

@@ -0,0 +1,57 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "~/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center 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",
{
variants: {
variant: {
default:
"bg-primary text-primary-foreground shadow hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
outline:
"border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2",
sm: "h-8 rounded-md px-3 text-xs",
lg: "h-10 rounded-md px-8",
icon: "h-9 w-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
)
}
)
Button.displayName = "Button"
export { Button, buttonVariants }

View File

@@ -0,0 +1,76 @@
import * as React from "react"
import { cn } from "~/lib/utils"
const Card = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
"rounded-xl border bg-card text-card-foreground shadow",
className
)}
{...props}
/>
))
Card.displayName = "Card"
const CardHeader = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex flex-col space-y-1.5 p-6", className)}
{...props}
/>
))
CardHeader.displayName = "CardHeader"
const CardTitle = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h3
ref={ref}
className={cn("font-semibold leading-none tracking-tight text-2xl", className)}
{...props}
/>
))
CardTitle.displayName = "CardTitle"
const CardDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<p
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
CardDescription.displayName = "CardDescription"
const CardContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
))
CardContent.displayName = "CardContent"
const CardFooter = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex items-center p-6 pt-0", className)}
{...props}
/>
))
CardFooter.displayName = "CardFooter"
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }

View File

@@ -0,0 +1,122 @@
"use client"
import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { Cross2Icon } from "@radix-ui/react-icons"
import { cn } from "~/lib/utils"
const Dialog = DialogPrimitive.Root
const DialogTrigger = DialogPrimitive.Trigger
const DialogPortal = DialogPrimitive.Portal
const DialogClose = DialogPrimitive.Close
const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props}
/>
))
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className
)}
{...props}
>
{children}
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<Cross2Icon className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
))
DialogContent.displayName = DialogPrimitive.Content.displayName
const DialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-1.5 text-center sm:text-left",
className
)}
{...props}
/>
)
DialogHeader.displayName = "DialogHeader"
const DialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
)}
{...props}
/>
)
DialogFooter.displayName = "DialogFooter"
const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={cn(
"text-lg font-semibold leading-none tracking-tight",
className
)}
{...props}
/>
))
DialogTitle.displayName = DialogPrimitive.Title.displayName
const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
DialogDescription.displayName = DialogPrimitive.Description.displayName
export {
Dialog,
DialogPortal,
DialogOverlay,
DialogTrigger,
DialogClose,
DialogContent,
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription,
}

View File

@@ -0,0 +1,25 @@
import * as React from "react"
import { cn } from "~/lib/utils"
export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement> {}
const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
className
)}
ref={ref}
{...props}
/>
)
}
)
Input.displayName = "Input"
export { Input }

View File

@@ -0,0 +1,31 @@
"use client"
import * as React from "react"
import * as SeparatorPrimitive from "@radix-ui/react-separator"
import { cn } from "~/lib/utils"
const Separator = React.forwardRef<
React.ElementRef<typeof SeparatorPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
>(
(
{ className, orientation = "horizontal", decorative = true, ...props },
ref
) => (
<SeparatorPrimitive.Root
ref={ref}
decorative={decorative}
orientation={orientation}
className={cn(
"shrink-0 bg-border",
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
className
)}
{...props}
/>
)
)
Separator.displayName = SeparatorPrimitive.Root.displayName
export { Separator }

140
src/components/ui/sheet.tsx Normal file
View File

@@ -0,0 +1,140 @@
"use client"
import * as React from "react"
import * as SheetPrimitive from "@radix-ui/react-dialog"
import { Cross2Icon } from "@radix-ui/react-icons"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "~/lib/utils"
const Sheet = SheetPrimitive.Root
const SheetTrigger = SheetPrimitive.Trigger
const SheetClose = SheetPrimitive.Close
const SheetPortal = SheetPrimitive.Portal
const SheetOverlay = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<SheetPrimitive.Overlay
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props}
ref={ref}
/>
))
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName
const sheetVariants = cva(
"fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500 data-[state=open]:animate-in data-[state=closed]:animate-out",
{
variants: {
side: {
top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
bottom:
"inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
right:
"inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
},
},
defaultVariants: {
side: "right",
},
}
)
interface SheetContentProps
extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
VariantProps<typeof sheetVariants> {}
const SheetContent = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Content>,
SheetContentProps
>(({ side = "right", className, children, ...props }, ref) => (
<SheetPortal>
<SheetOverlay />
<SheetPrimitive.Content
ref={ref}
className={cn(sheetVariants({ side }), className)}
{...props}
>
<SheetPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary">
<Cross2Icon className="h-4 w-4" />
<span className="sr-only">Close</span>
</SheetPrimitive.Close>
{children}
</SheetPrimitive.Content>
</SheetPortal>
))
SheetContent.displayName = SheetPrimitive.Content.displayName
const SheetHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-2 text-center sm:text-left",
className
)}
{...props}
/>
)
SheetHeader.displayName = "SheetHeader"
const SheetFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
)}
{...props}
/>
)
SheetFooter.displayName = "SheetFooter"
const SheetTitle = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title>
>(({ className, ...props }, ref) => (
<SheetPrimitive.Title
ref={ref}
className={cn("text-lg font-semibold text-foreground", className)}
{...props}
/>
))
SheetTitle.displayName = SheetPrimitive.Title.displayName
const SheetDescription = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description>
>(({ className, ...props }, ref) => (
<SheetPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
SheetDescription.displayName = SheetPrimitive.Description.displayName
export {
Sheet,
SheetPortal,
SheetOverlay,
SheetTrigger,
SheetClose,
SheetContent,
SheetHeader,
SheetFooter,
SheetTitle,
SheetDescription,
}

6
src/lib/utils.ts Normal file
View File

@@ -0,0 +1,6 @@
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}

18
src/middleware.ts Normal file
View File

@@ -0,0 +1,18 @@
import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server'
const isPublicRoute = createRouteMatcher(['/sign-in(.*)', '/sign-up(.*)', '/'])
export default clerkMiddleware((auth, request) => {
if (!isPublicRoute(request)) {
auth().protect()
}
})
export const config = {
matcher: [
// Skip Next.js internals and all static files, unless found in search params
'/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)',
// Always run for API routes
'/(api|trpc)(.*)',
],
}

View File

@@ -1,3 +1,105 @@
@tailwind base; @tailwind base;
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 240 10% 3.9%;
--card: 0 0% 100%;
--card-foreground: 240 10% 3.9%;
--popover: 0 0% 100%;
--popover-foreground: 240 10% 3.9%;
--primary: 240 5.9% 10%;
--primary-foreground: 0 0% 98%;
--secondary: 240 4.8% 95.9%;
--secondary-foreground: 240 5.9% 10%;
--muted: 240 4.8% 95.9%;
--muted-foreground: 240 3.8% 46.1%;
--accent: 240 4.8% 95.9%;
--accent-foreground: 240 5.9% 10%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 0 0% 98%;
--border: 240 5.9% 90%;
--input: 240 5.9% 90%;
--ring: 240 10% 3.9%;
--chart-1: 12 76% 61%;
--chart-2: 173 58% 39%;
--chart-3: 197 37% 24%;
--chart-4: 43 74% 66%;
--chart-5: 27 87% 67%;
--radius: 0.5rem
}
.dark {
--background: 240 10% 3.9%;
--foreground: 0 0% 98%;
--card: 240 10% 3.9%;
--card-foreground: 0 0% 98%;
--popover: 240 10% 3.9%;
--popover-foreground: 0 0% 98%;
--primary: 0 0% 98%;
--primary-foreground: 240 5.9% 10%;
--secondary: 240 3.7% 15.9%;
--secondary-foreground: 0 0% 98%;
--muted: 240 3.7% 15.9%;
--muted-foreground: 240 5% 64.9%;
--accent: 240 3.7% 15.9%;
--accent-foreground: 0 0% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 0% 98%;
--border: 240 3.7% 15.9%;
--input: 240 3.7% 15.9%;
--ring: 240 4.9% 83.9%;
--chart-1: 220 70% 50%;
--chart-2: 160 60% 45%;
--chart-3: 30 80% 55%;
--chart-4: 280 65% 60%;
--chart-5: 340 75% 55%
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}
@keyframes bounce {
0%, 20%, 50%, 80%, 100% {
transform: translateY(0);
}
40% {
transform: translateY(-10px);
}
60% {
transform: translateY(-5px);
}
}
.animate-bounce {
animation: bounce 1s infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.spinner {
border: 4px solid rgba(255, 255, 255, 0.3);
border-top: 4px solid #3498db;
border-radius: 50%;
width: 40px;
height: 40px;
animation: spin 1s linear infinite;
}
.fade-in {
animation: fadeIn 0.5s ease-in-out;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}

View File

@@ -2,13 +2,62 @@ import { type Config } from "tailwindcss";
import { fontFamily } from "tailwindcss/defaultTheme"; import { fontFamily } from "tailwindcss/defaultTheme";
export default { export default {
content: ["./src/**/*.tsx"], darkMode: ["class"],
content: ["./src/**/*.tsx"],
theme: { theme: {
extend: { extend: {
fontFamily: { fontFamily: {
sans: ["var(--font-geist-sans)", ...fontFamily.sans], sans: ["var(--font-sans)"],
}, // ... other theme extensions
}, },
borderRadius: {
lg: 'var(--radius)',
md: 'calc(var(--radius) - 2px)',
sm: 'calc(var(--radius) - 4px)'
},
colors: {
background: 'hsl(var(--background))',
foreground: 'hsl(var(--foreground))',
card: {
DEFAULT: 'hsl(var(--card))',
foreground: 'hsl(var(--card-foreground))'
},
popover: {
DEFAULT: 'hsl(var(--popover))',
foreground: 'hsl(var(--popover-foreground))'
},
primary: {
DEFAULT: 'hsl(var(--primary))',
foreground: 'hsl(var(--primary-foreground))'
},
secondary: {
DEFAULT: 'hsl(var(--secondary))',
foreground: 'hsl(var(--secondary-foreground))'
},
muted: {
DEFAULT: 'hsl(var(--muted))',
foreground: 'hsl(var(--muted-foreground))'
},
accent: {
DEFAULT: 'hsl(var(--accent))',
foreground: 'hsl(var(--accent-foreground))'
},
destructive: {
DEFAULT: 'hsl(var(--destructive))',
foreground: 'hsl(var(--destructive-foreground))'
},
border: 'hsl(var(--border))',
input: 'hsl(var(--input))',
ring: 'hsl(var(--ring))',
chart: {
'1': 'hsl(var(--chart-1))',
'2': 'hsl(var(--chart-2))',
'3': 'hsl(var(--chart-3))',
'4': 'hsl(var(--chart-4))',
'5': 'hsl(var(--chart-5))'
}
}
}
}, },
plugins: [], plugins: [require("tailwindcss-animate")],
} satisfies Config; } satisfies Config;