Repository: joschan21/image-alt-generator
Branch: main
Commit: 12dcac422023
Files: 71
Total size: 126.6 KB
Directory structure:
gitextract_prl9_fiq/
├── .editorconfig
├── .eslintignore
├── .eslintrc.json
├── .gitignore
├── .prettierignore
├── .vscode/
│ └── settings.json
├── README.md
├── next-env.d.ts
├── next.config.mjs
├── package.json
├── postcss.config.js
├── prettier.config.js
├── src/
│ ├── app/
│ │ ├── head.tsx
│ │ ├── layout.tsx
│ │ ├── page.tsx
│ │ ├── privacy-policy/
│ │ │ ├── head.tsx
│ │ │ └── page.tsx
│ │ └── terms/
│ │ ├── head.tsx
│ │ └── page.tsx
│ ├── components/
│ │ ├── icons.tsx
│ │ ├── main-nav.tsx
│ │ ├── site-header.tsx
│ │ └── ui/
│ │ ├── accordion.tsx
│ │ ├── alert-dialog.tsx
│ │ ├── aspect-ratio.tsx
│ │ ├── avatar.tsx
│ │ ├── button.tsx
│ │ ├── checkbox.tsx
│ │ ├── collapsible.tsx
│ │ ├── context-menu.tsx
│ │ ├── dialog.tsx
│ │ ├── dropdown-menu.tsx
│ │ ├── file-input.tsx
│ │ ├── hover-card.tsx
│ │ ├── image-upload.tsx
│ │ ├── input.tsx
│ │ ├── label.tsx
│ │ ├── menubar.tsx
│ │ ├── navigation-menu.tsx
│ │ ├── popover.tsx
│ │ ├── progress.tsx
│ │ ├── radio-group.tsx
│ │ ├── scroll-area.tsx
│ │ ├── select.tsx
│ │ ├── separator.tsx
│ │ ├── slider.tsx
│ │ ├── spinner.tsx
│ │ ├── switch.tsx
│ │ ├── tabs.tsx
│ │ ├── textarea.tsx
│ │ ├── toast.tsx
│ │ ├── toaster.tsx
│ │ └── tooltip.tsx
│ ├── config/
│ │ ├── image.ts
│ │ ├── s3.ts
│ │ └── site.ts
│ ├── hooks/
│ │ ├── use-s3-upload.ts
│ │ ├── use-toast.ts
│ │ └── use-upload-file.ts
│ ├── lib/
│ │ ├── api-middlewares/
│ │ │ └── with-methods.ts
│ │ ├── exceptions.ts
│ │ ├── s3.ts
│ │ ├── utils.ts
│ │ └── validations/
│ │ └── s3.ts
│ ├── pages/
│ │ └── api/
│ │ └── image/
│ │ ├── presign.ts
│ │ └── process.ts
│ ├── styles/
│ │ └── globals.css
│ └── types/
│ ├── api/
│ │ └── image.ts
│ └── nav.ts
├── tailwind.config.js
└── tsconfig.json
================================================
FILE CONTENTS
================================================
================================================
FILE: .editorconfig
================================================
# editorconfig.org
root = true
[*]
charset = utf-8
end_of_line = lf
indent_size = 2
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true
================================================
FILE: .eslintignore
================================================
dist/*
.cache
public
node_modules
*.esm.js
================================================
FILE: .eslintrc.json
================================================
{
"$schema": "https://json.schemastore.org/eslintrc",
"root": true,
"extends": [
"next/core-web-vitals",
"prettier",
"plugin:tailwindcss/recommended"
],
"plugins": ["tailwindcss"],
"rules": {
"tailwindcss/classnames-order": "off",
"@next/next/no-html-link-for-pages": "off",
"react/jsx-key": "off",
"tailwindcss/no-custom-classname": "off",
"react-hooks/exhaustive-deps": "off"
},
"settings": {
"tailwindcss": {
"callees": ["cn"]
}
}
}
================================================
FILE: .gitignore
================================================
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
node_modules
.pnp
.pnp.js
# testing
coverage
# next.js
.next/
out/
build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# local env files
.env.local
.env.development.local
.env.test.local
.env.production.local
# turbo
.turbo
.contentlayer
.env
================================================
FILE: .prettierignore
================================================
cache
.cache
package.json
package-lock.json
public
CHANGELOG.md
.yarn
================================================
FILE: .vscode/settings.json
================================================
{
"typescript.tsdk": "node_modules\\typescript\\lib",
"typescript.enablePromptUseWorkspaceTsdk": true
}
================================================
FILE: README.md
================================================
# Next alt generator
A Next.js 13 project for generating image alt tags automatically and in bulk.
## Features
- Radix UI Primitives
- Tailwind CSS
- Fonts with `@next/font`
- Icons from [Lucide](https://lucide.dev)
- Dark mode with `next-themes`
- Automatic import sorting with `@ianvs/prettier-plugin-sort-imports`
## Tailwind CSS Features
- Class merging with `taiwind-merge`
- Animation with `tailwindcss-animate`
- Conditional classes with `clsx`
- Variants with `class-variance-authority`
- Automatic class sorting with `eslint-plugin-tailwindcss`
## Import Sort
The starter comes with `@ianvs/prettier-plugin-sort-imports` for automatically sort your imports.
### Input
```tsx
import * as React from "react"
import Link from "next/link"
import { siteConfig } from "@/config/site"
import { buttonVariants } from "@/components/ui/button"
import "@/styles/globals.css"
import { twMerge } from "tailwind-merge"
import { NavItem } from "@/types/nav"
import { cn } from "@/lib/utils"
```
### Output
```tsx
import * as React from "react"
// React is always first.
import Link from "next/link"
// Followed by next modules.
import { twMerge } from "tailwind-merge"
// Followed by third-party modules
// Space
import "@/styles/globals.css"
// styles
import { NavItem } from "@/types/nav"
// types
import { siteConfig } from "@/config/site"
// config
import { cn } from "@/lib/utils"
// lib
import { buttonVariants } from "@/components/ui/button"
// components
```
### Class Merging
The `cn` util handles conditional classes and class merging.
### Input
```ts
cn("px-2 bg-slate-100 py-2 bg-slate-200")
// Outputs `p-2 bg-slate-200`
```
## License & Credits
Licensed under the [MIT license](https://opensource.org/license/mit/).
Boilerplate project template made by [shadcn](https://github.com/shadcn/next-template)
================================================
FILE: next-env.d.ts
================================================
/// <reference types="next" />
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.
================================================
FILE: next.config.mjs
================================================
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
experimental: {
appDir: true,
fontLoaders: [
{
loader: "@next/font/google",
options: { subsets: ["latin"] },
},
],
},
images: {
domains: ["image-to-alt.s3.eu-central-1.amazonaws.com"],
},
}
export default nextConfig
================================================
FILE: package.json
================================================
{
"name": "next-template",
"version": "0.0.1",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint",
"preview": "next build && next start"
},
"dependencies": {
"@next/font": "^13.1.6",
"@radix-ui/react-accessible-icon": "^1.0.1",
"@radix-ui/react-accordion": "^1.1.0",
"@radix-ui/react-alert-dialog": "^1.0.2",
"@radix-ui/react-aspect-ratio": "^1.0.1",
"@radix-ui/react-avatar": "^1.0.1",
"@radix-ui/react-checkbox": "^1.0.1",
"@radix-ui/react-collapsible": "^1.0.1",
"@radix-ui/react-context-menu": "^2.1.1",
"@radix-ui/react-dialog": "^1.0.2",
"@radix-ui/react-dropdown-menu": "^2.0.1",
"@radix-ui/react-hover-card": "^1.0.3",
"@radix-ui/react-label": "^2.0.0",
"@radix-ui/react-menubar": "^1.0.0",
"@radix-ui/react-navigation-menu": "^1.1.1",
"@radix-ui/react-popover": "^1.0.2",
"@radix-ui/react-progress": "^1.0.1",
"@radix-ui/react-radio-group": "^1.1.0",
"@radix-ui/react-scroll-area": "^1.0.2",
"@radix-ui/react-select": "^1.2.0",
"@radix-ui/react-separator": "^1.0.1",
"@radix-ui/react-slider": "^1.1.0",
"@radix-ui/react-slot": "^1.0.1",
"@radix-ui/react-switch": "^1.0.1",
"@radix-ui/react-tabs": "^1.0.2",
"@radix-ui/react-toast": "^1.1.2",
"@radix-ui/react-toggle-group": "^1.0.1",
"@radix-ui/react-tooltip": "^1.0.3",
"aws-sdk": "^2.1318.0",
"axios": "^1.3.3",
"class-variance-authority": "^0.4.0",
"clsx": "^1.2.1",
"lucide-react": "0.105.0-alpha.4",
"nanoid": "^4.0.1",
"next": "^13.1.6",
"next-themes": "^0.2.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"sharp": "^0.31.3",
"tailwind-merge": "^1.8.0",
"tailwindcss-animate": "^1.0.5",
"zod": "^3.20.6"
},
"devDependencies": {
"@ianvs/prettier-plugin-sort-imports": "^3.7.1",
"@types/node": "^17.0.12",
"@types/react": "^18.0.22",
"@types/react-dom": "^18.0.7",
"autoprefixer": "^10.4.13",
"eslint": "^8.31.0",
"eslint-config-next": "13.0.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-react": "^7.31.11",
"eslint-plugin-tailwindcss": "^3.8.0",
"postcss": "^8.4.14",
"prettier": "^2.7.1",
"tailwindcss": "^3.1.7",
"typescript": "^4.5.3"
}
}
================================================
FILE: postcss.config.js
================================================
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
================================================
FILE: prettier.config.js
================================================
/** @type {import('prettier').Config} */
module.exports = {
endOfLine: "lf",
semi: false,
singleQuote: true,
tabWidth: 2,
trailingComma: "es5",
importOrder: [
"^(react/(.*)$)|^(react$)",
"^(next/(.*)$)|^(next$)",
"<THIRD_PARTY_MODULES>",
"",
"^types$",
"^@/types/(.*)$",
"^@/config/(.*)$",
"^@/lib/(.*)$",
"^@/components/(.*)$",
"^@/styles/(.*)$",
"^[./]",
],
importOrderSeparation: false,
importOrderSortSpecifiers: true,
importOrderBuiltinModulesToTop: true,
importOrderParserPlugins: ["typescript", "jsx", "decorators-legacy"],
importOrderMergeDuplicateImports: true,
importOrderCombineTypeAndValueImports: true,
plugins: ["@ianvs/prettier-plugin-sort-imports"],
}
================================================
FILE: src/app/head.tsx
================================================
import { FC } from 'react'
const head: FC = () => {
return <title>ImageToAlt - Generate alt tags from images</title>
}
export default head
================================================
FILE: src/app/layout.tsx
================================================
import { Inter as FontSans } from '@next/font/google'
import '@/styles/globals.css'
import { Toaster } from '@/ui/toaster'
import { cn } from '@/lib/utils'
import { SiteHeader } from '../components/site-header'
import { TooltipProvider } from '../components/ui/tooltip'
const fontSans = FontSans({
subsets: ['latin'],
variable: '--font-inter',
})
interface RootLayoutProps {
children: React.ReactNode
}
export default function RootLayout({ children }: RootLayoutProps) {
return (
<>
<html
lang="en"
className={cn(
'dark bg-white font-sans text-slate-900 antialiased',
fontSans.variable
)}
>
<body className="min-h-screen bg-white font-sans text-slate-900 antialiased dark:bg-slate-900 dark:text-slate-50">
<Toaster />
<SiteHeader />
<TooltipProvider>
<main>{children}</main>
</TooltipProvider>
</body>
</html>
</>
)
}
================================================
FILE: src/app/page.tsx
================================================
'use client'
import { FC } from 'react'
import { Button, buttonVariants } from '@/ui/button'
import { FileInput } from '@/ui/file-input'
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from '@/components/ui/tooltip'
export const metadata = {
title: 'ImageToAlt - Home',
}
const page: FC = () => {
return (
<section className="container grid items-center gap-6 pt-6 pb-8 md:py-10">
<div className="flex max-w-[980px] flex-col items-start gap-2">
<h1 className="text-3xl font-extrabold leading-tight tracking-tighter sm:text-3xl md:text-5xl lg:text-6xl">
Easily create alt-descriptions <br className="hidden sm:inline" />
for your images.
</h1>
<p className="max-w-[700px] text-lg text-slate-700 dark:text-slate-400 sm:text-xl">
Bulk-generate SEO-optimized alt-descriptions that you can copy and
paste into your app. Free & open-source.
</p>
</div>
<div className="flex gap-4">
<FileInput />
</div>
<Tooltip>
<TooltipTrigger asChild>
<div className="w-fit">
<Button
disabled
className={buttonVariants({ size: 'lg', className: 'w-fit' })}
>
Download as CSV
</Button>
</div>
</TooltipTrigger>
<TooltipContent>
<p>Available soon</p>
</TooltipContent>
</Tooltip>
{/* Legal disclaimers */}
<div className="flex flex-col gap-4 mt-12">
<p className="text-slate-400 text-sm">
All images are used solely for alt-generation and are automatically
deleted after 24h.
</p>
<div className="flex items-center gap-4">
<Button
href="/terms"
className={buttonVariants({ variant: 'link', size: 'sm' })}
>
Terms
</Button>
<Button
href="/privacy-policy"
className={buttonVariants({ variant: 'link', size: 'sm' })}
>
Privacy Policy
</Button>
</div>
</div>
</section>
)
}
export default page
================================================
FILE: src/app/privacy-policy/head.tsx
================================================
import { FC } from 'react'
const head: FC = () => {
return <title>Privacy Policy - ImageToAlt</title>
}
export default head
================================================
FILE: src/app/privacy-policy/page.tsx
================================================
import { FC } from 'react'
import Link from 'next/link'
const page: FC = () => {
return (
<div className="text-slate-300">
<div className="max-w-4xl mx-auto py-8 px-4 sm:px-6 lg:px-8">
<h1 className="text-2xl font-bold mb-4 text-slate-100">
ImageToAlt Privacy Policy
</h1>
<p className="mb-4">
ImageToAlt is a free online service that provides a simple way to
generate an alt tag (a description of what is visible on an image,
determined by a machine learning algorithm) from an image. In order to
provide this service, we need to collect and store images on our
servers.
</p>
<h2 className="text-xl font-bold mb-2">Information We Collect</h2>
<p className="mb-4">
When you use the App, we automatically collect certain information
about your device, including information about your web browser, IP
address, time zone, and some of the cookies that are installed on your
device. We refer to this automatically-collected information as
"Device Information".
</p>
<p className="mb-4">
We collect Device Information using the following technologies:
</p>
<ul className="list-disc ml-8 mb-4">
<li>
Cookies: Cookies are data files that are placed on your device or
computer and often include an anonymous unique identifier.
</li>
<li>
Log files: Log files track actions occurring on the App, and collect
data including your IP address, browser type, Internet service
provider, referring/exit pages, and date/time stamps.
</li>
</ul>
<p className="mb-4">
When you upload an image to the App, we collect the image itself. We
use the image to generate an alt tag and store the alt tag. The image
is then automatically deleted after 24 hours.
</p>
<h2 className="text-xl font-bold mb-2">How We Use Your Information</h2>
<p className="mb-4">
We use the images you upload to our servers solely for the purpose of
generating an alt tag and serving it back to you. We do not use your
images for any other purpose. To provide this service, we use a
machine learning algorithm provided by Replicate, Inc. You can read
more about how Replicate, Inc. uses your data{' '}
<Link
className="underline text-blue-400"
href="https://replicate.com/terms"
>
here
</Link>
. We require all third-party providers to have adequate technical and
organizational measures in place to ensure the security of user data.
We do not share user data with any other third parties.
</p>
<h2 className="text-xl font-bold mb-2">
How We Protect Your Information
</h2>
<p className="mb-4">
We take the security of your information very seriously. All images
uploaded to our servers are stored in a secure location in Germany. We
do not share your images with any third parties. We also automatically
delete all images from our servers after 24 hours.
</p>
<h2 className="text-xl font-bold mb-2">Use of Cookies</h2>
<p className="mb-4">
We do not use any cookies to track user behavior. We only use session
cookies to manage your session on our website.
</p>
<h2 className="text-xl font-bold mb-2">
Changes to Our Privacy Policy
</h2>
<p className="mb-4">
We reserve the right to make changes to this Privacy Policy at any
time. Any changes will be posted on this page, so please check back
periodically for updates.
</p>
<h2 className="text-xl font-bold mb-2">Contact Us</h2>
<p className="mb-4">
If you have any questions about this Privacy Policy, please contact us
at admin@wordful.ai.
</p>
</div>
</div>
)
}
export default page
================================================
FILE: src/app/terms/head.tsx
================================================
import { FC } from 'react'
const head: FC = () => {
return <title>Terms and Conditions - ImageToAlt</title>
}
export default head
================================================
FILE: src/app/terms/page.tsx
================================================
import { FC } from 'react'
import Head from 'next/head'
const page: FC = () => {
return (
<div className="text-slate-300">
<Head>
<title>ImageToAlt - Terms and Conditions</title>
</Head>
<div className="max-w-4xl mx-auto py-8 px-4 sm:px-6 lg:px-8">
<h1 className="text-2xl font-bold mb-4">
ImageToAlt Terms and Conditions
</h1>
<p className="mb-4">
These terms and conditions ("Terms") apply to your use of
the ImageToAlt app ("App") and the alt tag generation
service ("Service") provided by ImageToAlt ("we"
or "us"). By using the App or the Service, you agree to be
bound by these Terms.
</p>
<h2 className="text-xl font-bold mb-2">
Use of the App and the Service
</h2>
<p className="mb-4">
The App and the Service are provided for informational purposes only.
You may use the App and the Service at your own risk, and we shall not
be liable for any damages or harm that may arise from your use of the
App or the Service.
</p>
<h2 className="text-xl font-bold mb-2">Intellectual Property</h2>
<p className="mb-4">
The App and the Service, including any content or materials made
available through the App or the Service, are protected by copyright
and other intellectual property laws. You may not copy, modify,
distribute, sell, or lease any part of the App or the Service without
our prior written consent.
</p>
<h2 className="text-xl font-bold mb-2">Disclaimer of Liability</h2>
<p className="mb-4">
The App and the Service are provided "as is" and without warranty of
any kind. We make no representations or warranties of any kind,
express or implied, about the completeness, accuracy, reliability,
suitability or availability with respect to the App or the Service or
the information, products, services, or related graphics contained in
the App or the Service for any purpose. To the fullest extent
permitted by law, we disclaim any and all warranties, express or
implied, including, but not limited to, implied warranties of
merchantability and fitness for a particular purpose.
</p>
<p className="mb-4">
In no event shall ImageToAlt be liable for any direct, indirect,
incidental, consequential, special or exemplary damages, including,
but not limited to, damages for loss of profits, goodwill, use, data
or other intangible losses resulting from the use of or inability to
use the App or the Service.
</p>
<h2 className="text-xl font-bold mb-2">Indemnification</h2>
<p className="mb-4">
You agree to indemnify and hold ImageToAlt, its affiliates, officers,
agents, and other partners and employees, harmless from any loss,
liability, claim or demand, including reasonable attorneys' fees, made
by any third party due to or arising out of your use of the App or the
Service.
</p>
<h2 className="text-xl font-bold mb-2">Termination</h2>
<p className="mb-4">
We may terminate your access to the App and the Service at any time,
without cause or notice.
</p>
<h2 className="text-xl font-bold mb-2">Governing Law</h2>
<p className="mb-4">
These Terms and your use of the App and the Service shall be governed
by and construed in accordance with the laws of Germany, without
giving effect to any principles of conflicts of law.
</p>
<h2 className="text-xl font-bold mb-2">Changes to these Terms</h2>
<p className="mb-4">
We reserve the right to modify these Terms at any time. If we make
changes to these Terms, we will post the revised Terms on the App and
update the "Last Updated" date at the top of these Terms. By
continuing to use the App and the Service after the revised Terms
become effective, you agree to be bound by the revised Terms.
</p>
<h2 className="text-xl font-bold mb-2">Contact Us</h2>
<p className="mb-4">
If you have any questions about these Terms or the App or the Service,
please contact us at admin@wordful.ai.
</p>
<p className="text-sm">Last Updated: Feb 20th, 2023</p>
</div>
</div>
)
}
export default page
================================================
FILE: src/components/icons.tsx
================================================
import {
Laptop,
LucideProps,
Moon,
SunMedium,
type Icon as LucideIcon,
} from 'lucide-react'
export type Icon = LucideIcon
export const Icons = {
sun: SunMedium,
moon: Moon,
laptop: Laptop,
youtube: (props: LucideProps) => (
<svg
{...props}
viewBox="0 -77 512.00213 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="m501.453125 56.09375c-5.902344-21.933594-23.195313-39.222656-45.125-45.128906-40.066406-10.964844-200.332031-10.964844-200.332031-10.964844s-160.261719 0-200.328125 10.546875c-21.507813 5.902344-39.222657 23.617187-45.125 45.546875-10.542969 40.0625-10.542969 123.148438-10.542969 123.148438s0 83.503906 10.542969 123.148437c5.90625 21.929687 23.195312 39.222656 45.128906 45.128906 40.484375 10.964844 200.328125 10.964844 200.328125 10.964844s160.261719 0 200.328125-10.546875c21.933594-5.902344 39.222656-23.195312 45.128906-45.125 10.542969-40.066406 10.542969-123.148438 10.542969-123.148438s.421875-83.507812-10.546875-123.570312zm0 0"
fill="#94a3b8"
/>
<path
d="m204.96875 256 133.269531-76.757812-133.269531-76.757813zm0 0"
fill="#fff"
/>
</svg>
),
logo: (props: LucideProps) => (
<svg {...props} viewBox="0 0 512 512">
<path
style={{ fill: '#EBF0FA' }}
d="M446.575,512H65.425C29.35,512,0,482.65,0,446.575V65.425C0,29.35,29.35,0,65.425,0h381.15
C482.65,0,512,29.35,512,65.425v381.149C512,482.65,482.65,512,446.575,512z"
/>
<path
style={{ fill: '#DCE1EB' }}
d="M446.575,0H256.004v512h190.571C482.65,512,512,482.65,512,446.575V65.425
C512,29.35,482.65,0,446.575,0z"
/>
<path
style={{ fill: '#AAC85A' }}
d="M410.155,191.597c-0.025-0.032-0.052-0.065-0.078-0.098c-7.117-8.764-17.666-14.122-28.942-14.701
c-11.268-0.57-22.317,3.672-30.295,11.662l-138.007,138.22l-51.59-42.839c-14.959-12.422-36.563-12.293-51.373,0.308
c-0.031,0.027-0.063,0.054-0.094,0.081L0,379.215v67.358c0,36.076,29.35,65.425,65.425,65.425h381.15
c36.076,0,65.425-29.349,65.425-65.425V319.154L410.155,191.597z"
/>
<path
style={{ fill: '#6EAA50' }}
d="M410.077,191.5c-7.117-8.764-17.666-14.122-28.942-14.701c-11.268-0.57-22.317,3.672-30.295,11.662
l-94.835,94.981V512h190.571C482.65,512,512,482.651,512,446.575V319.154L410.155,191.597
C410.129,191.565,410.103,191.532,410.077,191.5z"
/>
<path
style={{ fill: '#AAC85A' }}
d="M161.174,208.421c-40.095,0-72.713-32.619-72.713-72.713s32.619-72.713,72.713-72.713
s72.713,32.62,72.713,72.713S201.269,208.421,161.174,208.421z"
/>
</svg>
),
gitHub: (props: LucideProps) => (
<svg viewBox="0 0 438.549 438.549" {...props}>
<path
fill="currentColor"
d="M409.132 114.573c-19.608-33.596-46.205-60.194-79.798-79.8-33.598-19.607-70.277-29.408-110.063-29.408-39.781 0-76.472 9.804-110.063 29.408-33.596 19.605-60.192 46.204-79.8 79.8C9.803 148.168 0 184.854 0 224.63c0 47.78 13.94 90.745 41.827 128.906 27.884 38.164 63.906 64.572 108.063 79.227 5.14.954 8.945.283 11.419-1.996 2.475-2.282 3.711-5.14 3.711-8.562 0-.571-.049-5.708-.144-15.417a2549.81 2549.81 0 01-.144-25.406l-6.567 1.136c-4.187.767-9.469 1.092-15.846 1-6.374-.089-12.991-.757-19.842-1.999-6.854-1.231-13.229-4.086-19.13-8.559-5.898-4.473-10.085-10.328-12.56-17.556l-2.855-6.57c-1.903-4.374-4.899-9.233-8.992-14.559-4.093-5.331-8.232-8.945-12.419-10.848l-1.999-1.431c-1.332-.951-2.568-2.098-3.711-3.429-1.142-1.331-1.997-2.663-2.568-3.997-.572-1.335-.098-2.43 1.427-3.289 1.525-.859 4.281-1.276 8.28-1.276l5.708.853c3.807.763 8.516 3.042 14.133 6.851 5.614 3.806 10.229 8.754 13.846 14.842 4.38 7.806 9.657 13.754 15.846 17.847 6.184 4.093 12.419 6.136 18.699 6.136 6.28 0 11.704-.476 16.274-1.423 4.565-.952 8.848-2.383 12.847-4.285 1.713-12.758 6.377-22.559 13.988-29.41-10.848-1.14-20.601-2.857-29.264-5.14-8.658-2.286-17.605-5.996-26.835-11.14-9.235-5.137-16.896-11.516-22.985-19.126-6.09-7.614-11.088-17.61-14.987-29.979-3.901-12.374-5.852-26.648-5.852-42.826 0-23.035 7.52-42.637 22.557-58.817-7.044-17.318-6.379-36.732 1.997-58.24 5.52-1.715 13.706-.428 24.554 3.853 10.85 4.283 18.794 7.952 23.84 10.994 5.046 3.041 9.089 5.618 12.135 7.708 17.705-4.947 35.976-7.421 54.818-7.421s37.117 2.474 54.823 7.421l10.849-6.849c7.419-4.57 16.18-8.758 26.262-12.565 10.088-3.805 17.802-4.853 23.134-3.138 8.562 21.509 9.325 40.922 2.279 58.24 15.036 16.18 22.559 35.787 22.559 58.817 0 16.178-1.958 30.497-5.853 42.966-3.9 12.471-8.941 22.457-15.125 29.979-6.191 7.521-13.901 13.85-23.131 18.986-9.232 5.14-18.182 8.85-26.84 11.136-8.662 2.286-18.415 4.004-29.263 5.146 9.894 8.562 14.842 22.077 14.842 40.539v60.237c0 3.422 1.19 6.279 3.572 8.562 2.379 2.279 6.136 2.95 11.276 1.995 44.163-14.653 80.185-41.062 108.068-79.226 27.88-38.161 41.825-81.126 41.825-128.906-.01-39.771-9.818-76.454-29.414-110.049z"
></path>
</svg>
),
plus: (props: LucideProps) => (
<svg {...props} viewBox="0 0 512 512">
<path d="m256 512c-141.164062 0-256-114.835938-256-256s114.835938-256 256-256 256 114.835938 256 256-114.835938 256-256 256zm0-480c-123.519531 0-224 100.480469-224 224s100.480469 224 224 224 224-100.480469 224-224-100.480469-224-224-224zm0 0" />
<path d="m368 272h-224c-8.832031 0-16-7.167969-16-16s7.167969-16 16-16h224c8.832031 0 16 7.167969 16 16s-7.167969 16-16 16zm0 0" />
<path d="m256 384c-8.832031 0-16-7.167969-16-16v-224c0-8.832031 7.167969-16 16-16s16 7.167969 16 16v224c0 8.832031-7.167969 16-16 16zm0 0" />
</svg>
),
redx: (props: LucideProps) => (
<svg {...props} viewBox="0 0 455.111 455.111">
<circle
style={{ fill: '#E24C4B' }}
cx="227.556"
cy="227.556"
r="227.556"
/>
<path
style={{ fill: '#D1403F' }}
d="M455.111,227.556c0,125.156-102.4,227.556-227.556,227.556c-72.533,0-136.533-32.711-177.778-85.333
c38.4,31.289,88.178,49.778,142.222,49.778c125.156,0,227.556-102.4,227.556-227.556c0-54.044-18.489-103.822-49.778-142.222
C422.4,91.022,455.111,155.022,455.111,227.556z"
/>
<path
style={{ fill: '#FFFFFF' }}
d="M331.378,331.378c-8.533,8.533-22.756,8.533-31.289,0l-72.533-72.533l-72.533,72.533
c-8.533,8.533-22.756,8.533-31.289,0c-8.533-8.533-8.533-22.756,0-31.289l72.533-72.533l-72.533-72.533
c-8.533-8.533-8.533-22.756,0-31.289c8.533-8.533,22.756-8.533,31.289,0l72.533,72.533l72.533-72.533
c8.533-8.533,22.756-8.533,31.289,0c8.533,8.533,8.533,22.756,0,31.289l-72.533,72.533l72.533,72.533
C339.911,308.622,339.911,322.844,331.378,331.378z"
/>
</svg>
),
}
================================================
FILE: src/components/main-nav.tsx
================================================
import Link from 'next/link'
import { NavItem } from '@/src/types/nav'
import { siteConfig } from '@/config/site'
import { cn } from '@/lib/utils'
import { Icons } from '@/components/icons'
import { Button } from '@/components/ui/button'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
interface MainNavProps {
items?: NavItem[]
}
export function MainNav({ items }: MainNavProps) {
return (
<div className="flex gap-6 md:gap-10">
<Link href="/" className="hidden items-center space-x-2 md:flex">
<Icons.logo className="h-6 w-6" />
<span className="hidden font-bold sm:inline-block">
{siteConfig.name}
</span>
</Link>
{items?.length ? (
<nav className="hidden gap-6 md:flex">
{items?.map(
(item, index) =>
item.href && (
<Link
key={index}
href={item.href}
className={cn(
'flex items-center text-lg font-semibold text-slate-600 hover:text-slate-900 dark:text-slate-100 sm:text-sm',
item.disabled && 'cursor-not-allowed opacity-80'
)}
>
{item.title}
</Link>
)
)}
</nav>
) : null}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
className="-ml-4 text-base hover:bg-transparent focus:ring-0 md:hidden"
>
<Icons.logo className="mr-2 h-4 w-4" />{' '}
<span className="font-bold">Menu</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
align="start"
sideOffset={24}
className="w-[300px] overflow-scroll"
>
<DropdownMenuLabel>
<Link href="/" className="flex items-center">
<Icons.logo className="mr-2 h-4 w-4" /> {siteConfig.name}
</Link>
</DropdownMenuLabel>
<DropdownMenuSeparator />
{items?.map(
(item, index) =>
item.href && (
<DropdownMenuItem key={index} asChild>
<Link href={item.href}>{item.title}</Link>
</DropdownMenuItem>
)
)}
</DropdownMenuContent>
</DropdownMenu>
</div>
)
}
================================================
FILE: src/components/site-header.tsx
================================================
import Link from 'next/link'
import { siteConfig } from '@/config/site'
import { Icons } from '@/components/icons'
import { MainNav } from '@/components/main-nav'
import { buttonVariants } from '@/components/ui/button'
export function SiteHeader() {
return (
<header className="sticky top-0 z-40 w-full border-b border-b-slate-200 bg-white dark:border-b-slate-700 dark:bg-slate-900">
<div className="container flex h-16 items-center space-x-4 sm:justify-between sm:space-x-0">
<MainNav items={siteConfig.mainNav} />
<div className="flex flex-1 items-center justify-end space-x-4">
<nav className="flex items-center space-x-1">
<Link
href={siteConfig.links.github}
target="_blank"
rel="noreferrer"
>
<div
className={buttonVariants({
size: 'sm',
variant: 'ghost',
className: 'text-slate-700 dark:text-slate-400',
})}
>
<Icons.gitHub className="h-5 w-5" />
<span className="sr-only">GitHub</span>
</div>
</Link>
<Link
href={siteConfig.links.youtube}
target="_blank"
rel="noreferrer"
>
<div
className={buttonVariants({
size: 'sm',
variant: 'ghost',
className: '',
})}
>
<Icons.youtube className="h-6 w-6" />
<span className="sr-only">YouTube</span>
</div>
</Link>
</nav>
</div>
</div>
</header>
)
}
================================================
FILE: src/components/ui/accordion.tsx
================================================
'use client'
import * as React from 'react'
import * as AccordionPrimitive from '@radix-ui/react-accordion'
import { ChevronDown } from 'lucide-react'
import { cn } from '@/lib/utils'
const Accordion = AccordionPrimitive.Root
const AccordionItem = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
>(({ className, ...props }, ref) => (
<AccordionPrimitive.Item
ref={ref}
className={cn(
'border-b border-b-slate-200 dark:border-b-slate-700',
className
)}
{...props}
/>
))
AccordionItem.displayName = 'AccordionItem'
const AccordionTrigger = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Header className="flex">
<AccordionPrimitive.Trigger
ref={ref}
className={cn(
'flex flex-1 items-center justify-between py-4 font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180',
className
)}
{...props}
>
{children}
<ChevronDown className="h-4 w-4 transition-transform duration-200" />
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
))
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
const AccordionContent = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Content
ref={ref}
className={cn(
'overflow-hidden text-sm transition-all data-[state=open]:animate-accordion-down data-[state=closed]:animate-accordion-up',
className
)}
{...props}
>
<div className="pt-0 pb-4">{children}</div>
</AccordionPrimitive.Content>
))
AccordionContent.displayName = AccordionPrimitive.Content.displayName
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
================================================
FILE: src/components/ui/alert-dialog.tsx
================================================
'use client'
import * as React from 'react'
import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog'
import { cn } from '@/lib/utils'
const AlertDialog = AlertDialogPrimitive.Root
const AlertDialogTrigger = AlertDialogPrimitive.Trigger
const AlertDialogPortal = ({
className,
children,
...props
}: AlertDialogPrimitive.AlertDialogPortalProps) => (
<AlertDialogPrimitive.Portal className={cn(className)} {...props}>
<div className="fixed inset-0 z-50 flex items-end justify-center sm:items-center">
{children}
</div>
</AlertDialogPrimitive.Portal>
)
AlertDialogPortal.displayName = AlertDialogPrimitive.Portal.displayName
const AlertDialogOverlay = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
>(({ className, children, ...props }, ref) => (
<AlertDialogPrimitive.Overlay
className={cn(
'fixed inset-0 z-50 bg-black/50 backdrop-blur-sm transition-opacity animate-in fade-in',
className
)}
{...props}
ref={ref}
/>
))
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
const AlertDialogContent = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
>(({ className, ...props }, ref) => (
<AlertDialogPortal>
<AlertDialogOverlay />
<AlertDialogPrimitive.Content
ref={ref}
className={cn(
'fixed z-50 grid w-full max-w-lg scale-100 gap-4 bg-white p-6 opacity-100 animate-in fade-in-90 slide-in-from-bottom-10 sm:rounded-lg sm:zoom-in-90 sm:slide-in-from-bottom-0 md:w-full',
'dark:bg-slate-900',
className
)}
{...props}
/>
</AlertDialogPortal>
))
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
const AlertDialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
'flex flex-col space-y-2 text-center sm:text-left',
className
)}
{...props}
/>
)
AlertDialogHeader.displayName = 'AlertDialogHeader'
const AlertDialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
'flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2',
className
)}
{...props}
/>
)
AlertDialogFooter.displayName = 'AlertDialogFooter'
const AlertDialogTitle = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Title
ref={ref}
className={cn(
'text-lg font-semibold text-slate-900',
'dark:text-slate-50',
className
)}
{...props}
/>
))
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
const AlertDialogDescription = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Description
ref={ref}
className={cn('text-sm text-slate-500', 'dark:text-slate-400', className)}
{...props}
/>
))
AlertDialogDescription.displayName =
AlertDialogPrimitive.Description.displayName
const AlertDialogAction = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Action>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Action
ref={ref}
className={cn(
'inline-flex h-10 items-center justify-center rounded-md bg-slate-900 py-2 px-4 text-sm font-semibold text-white transition-colors hover:bg-slate-700 focus:outline-none focus:ring-2 focus:ring-slate-400 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-slate-100 dark:text-slate-900 dark:hover:bg-slate-200 dark:focus:ring-slate-400 dark:focus:ring-offset-slate-900',
className
)}
{...props}
/>
))
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
const AlertDialogCancel = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Cancel
ref={ref}
className={cn(
'mt-2 inline-flex h-10 items-center justify-center rounded-md border border-slate-200 bg-transparent py-2 px-4 text-sm font-semibold text-slate-900 transition-colors hover:bg-slate-100 focus:outline-none focus:ring-2 focus:ring-slate-400 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:border-slate-700 dark:text-slate-100 dark:hover:bg-slate-700 dark:focus:ring-slate-400 dark:focus:ring-offset-slate-900 sm:mt-0',
className
)}
{...props}
/>
))
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
export {
AlertDialog,
AlertDialogTrigger,
AlertDialogContent,
AlertDialogHeader,
AlertDialogFooter,
AlertDialogTitle,
AlertDialogDescription,
AlertDialogAction,
AlertDialogCancel,
}
================================================
FILE: src/components/ui/aspect-ratio.tsx
================================================
'use client'
import * as AspectRatioPrimitive from '@radix-ui/react-aspect-ratio'
const AspectRatio = AspectRatioPrimitive.Root
export { AspectRatio }
================================================
FILE: src/components/ui/avatar.tsx
================================================
'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-slate-100 dark:bg-slate-700',
className
)}
{...props}
/>
))
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
export { Avatar, AvatarImage, AvatarFallback }
================================================
FILE: src/components/ui/button.tsx
================================================
import * as React from 'react'
import Link from 'next/link'
import { VariantProps, cva } from 'class-variance-authority'
import { cn } from '@/lib/utils'
const buttonVariants = cva(
'inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-slate-400 focus:ring-offset-2 dark:hover:bg-slate-800 dark:hover:text-slate-100 disabled:opacity-50 dark:focus:ring-slate-400 disabled:pointer-events-none dark:focus:ring-offset-slate-900 data-[state=open]:bg-slate-100 dark:data-[state=open]:bg-slate-800',
{
variants: {
variant: {
default:
'bg-slate-900 text-white hover:bg-slate-700 dark:bg-slate-50 dark:text-slate-900',
destructive:
'bg-red-500 text-white hover:bg-red-600 dark:hover:bg-red-600',
outline:
'bg-transparent border border-slate-200 hover:bg-slate-100 dark:border-slate-700 dark:text-slate-100',
subtle:
'bg-slate-100 text-slate-900 hover:bg-slate-200 dark:bg-slate-700 dark:text-slate-100',
ghost:
'bg-transparent dark:bg-transparent hover:bg-slate-100 dark:hover:bg-slate-800 dark:text-slate-100 dark:hover:text-slate-100 data-[state=open]:bg-transparent dark:data-[state=open]:bg-transparent',
link: 'bg-transparent dark:bg-transparent underline-offset-4 hover:underline text-slate-900 dark:text-slate-300 hover:bg-transparent dark:hover:bg-transparent',
},
size: {
default: 'h-10 py-2 px-4',
sm: 'h-9 px-2 rounded-md',
lg: 'h-11 px-8 rounded-md',
},
},
defaultVariants: {
variant: 'default',
size: 'default',
},
}
)
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
href?: string
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, children, href, variant, size, ...props }, ref) => {
if (href) {
return (
<Link
href={href}
className={cn(buttonVariants({ variant, size, className }))}
>
{children}
</Link>
)
}
return (
<button
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
>
{children}
</button>
)
}
)
Button.displayName = 'Button'
export { Button, buttonVariants }
================================================
FILE: src/components/ui/checkbox.tsx
================================================
'use client'
import * as React from 'react'
import * as CheckboxPrimitive from '@radix-ui/react-checkbox'
import { Check } from 'lucide-react'
import { cn } from '@/lib/utils'
const Checkbox = React.forwardRef<
React.ElementRef<typeof CheckboxPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
>(({ className, ...props }, ref) => (
<CheckboxPrimitive.Root
ref={ref}
className={cn(
'peer h-4 w-4 shrink-0 rounded-sm border border-slate-300 focus:outline-none focus:ring-2 focus:ring-slate-400 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:border-slate-700 dark:text-slate-50 dark:focus:ring-slate-400 dark:focus:ring-offset-slate-900',
className
)}
{...props}
>
<CheckboxPrimitive.Indicator
className={cn('flex items-center justify-center')}
>
<Check className="h-4 w-4" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
))
Checkbox.displayName = CheckboxPrimitive.Root.displayName
export { Checkbox }
================================================
FILE: src/components/ui/collapsible.tsx
================================================
'use client'
import * as CollapsiblePrimitive from '@radix-ui/react-collapsible'
const Collapsible = CollapsiblePrimitive.Root
const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger
const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent
export { Collapsible, CollapsibleTrigger, CollapsibleContent }
================================================
FILE: src/components/ui/context-menu.tsx
================================================
'use client'
import * as React from 'react'
import * as ContextMenuPrimitive from '@radix-ui/react-context-menu'
import { Check, ChevronRight, Circle } from 'lucide-react'
import { cn } from '@/lib/utils'
const ContextMenu = ContextMenuPrimitive.Root
const ContextMenuTrigger = ContextMenuPrimitive.Trigger
const ContextMenuGroup = ContextMenuPrimitive.Group
const ContextMenuPortal = ContextMenuPrimitive.Portal
const ContextMenuSub = ContextMenuPrimitive.Sub
const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup
const ContextMenuSubTrigger = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubTrigger> & {
inset?: boolean
}
>(({ className, inset, children, ...props }, ref) => (
<ContextMenuPrimitive.SubTrigger
ref={ref}
className={cn(
'flex cursor-default select-none items-center rounded-sm py-1.5 px-2 text-sm font-medium outline-none focus:bg-slate-100 data-[state=open]:bg-slate-100 dark:focus:bg-slate-700 dark:data-[state=open]:bg-slate-700',
inset && 'pl-8',
className
)}
{...props}
>
{children}
<ChevronRight className="ml-auto h-4 w-4" />
</ContextMenuPrimitive.SubTrigger>
))
ContextMenuSubTrigger.displayName = ContextMenuPrimitive.SubTrigger.displayName
const ContextMenuSubContent = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubContent>
>(({ className, ...props }, ref) => (
<ContextMenuPrimitive.SubContent
ref={ref}
className={cn(
'z-50 min-w-[8rem] overflow-hidden rounded-md border border-slate-100 bg-white p-1 shadow-md animate-in slide-in-from-left-1 dark:border-slate-700 dark:bg-slate-800',
className
)}
{...props}
/>
))
ContextMenuSubContent.displayName = ContextMenuPrimitive.SubContent.displayName
const ContextMenuContent = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Content>
>(({ className, ...props }, ref) => (
<ContextMenuPrimitive.Portal>
<ContextMenuPrimitive.Content
ref={ref}
className={cn(
'z-50 min-w-[8rem] overflow-hidden rounded-md border border-slate-100 bg-white p-1 text-slate-700 shadow-md animate-in fade-in-80 dark:border-slate-800 dark:bg-slate-800 dark:text-slate-400',
className
)}
{...props}
/>
</ContextMenuPrimitive.Portal>
))
ContextMenuContent.displayName = ContextMenuPrimitive.Content.displayName
const ContextMenuItem = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Item> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<ContextMenuPrimitive.Item
ref={ref}
className={cn(
'relative flex cursor-default select-none items-center rounded-sm py-1.5 px-2 text-sm font-medium outline-none focus:bg-slate-100 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 dark:focus:bg-slate-700',
inset && 'pl-8',
className
)}
{...props}
/>
))
ContextMenuItem.displayName = ContextMenuPrimitive.Item.displayName
const ContextMenuCheckboxItem = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => (
<ContextMenuPrimitive.CheckboxItem
ref={ref}
className={cn(
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm font-medium outline-none focus:bg-slate-100 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 dark:focus:bg-slate-700',
className
)}
checked={checked}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<ContextMenuPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</ContextMenuPrimitive.ItemIndicator>
</span>
{children}
</ContextMenuPrimitive.CheckboxItem>
))
ContextMenuCheckboxItem.displayName =
ContextMenuPrimitive.CheckboxItem.displayName
const ContextMenuRadioItem = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => (
<ContextMenuPrimitive.RadioItem
ref={ref}
className={cn(
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm font-medium outline-none focus:bg-slate-100 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 dark:focus:bg-slate-700',
className
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<ContextMenuPrimitive.ItemIndicator>
<Circle className="h-2 w-2 fill-current" />
</ContextMenuPrimitive.ItemIndicator>
</span>
{children}
</ContextMenuPrimitive.RadioItem>
))
ContextMenuRadioItem.displayName = ContextMenuPrimitive.RadioItem.displayName
const ContextMenuLabel = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Label> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<ContextMenuPrimitive.Label
ref={ref}
className={cn(
'px-2 py-1.5 text-sm font-semibold text-slate-900 dark:text-slate-300',
inset && 'pl-8',
className
)}
{...props}
/>
))
ContextMenuLabel.displayName = ContextMenuPrimitive.Label.displayName
const ContextMenuSeparator = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Separator>
>(({ className, ...props }, ref) => (
<ContextMenuPrimitive.Separator
ref={ref}
className={cn('-mx-1 my-1 h-px bg-slate-100 dark:bg-slate-700', className)}
{...props}
/>
))
ContextMenuSeparator.displayName = ContextMenuPrimitive.Separator.displayName
const ContextMenuShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn(
'ml-auto text-xs tracking-widest text-slate-500',
className
)}
{...props}
/>
)
}
ContextMenuShortcut.displayName = 'ContextMenuShortcut'
export {
ContextMenu,
ContextMenuTrigger,
ContextMenuContent,
ContextMenuItem,
ContextMenuCheckboxItem,
ContextMenuRadioItem,
ContextMenuLabel,
ContextMenuSeparator,
ContextMenuShortcut,
ContextMenuGroup,
ContextMenuPortal,
ContextMenuSub,
ContextMenuSubContent,
ContextMenuSubTrigger,
ContextMenuRadioGroup,
}
================================================
FILE: src/components/ui/dialog.tsx
================================================
'use client'
import * as React from 'react'
import * as DialogPrimitive from '@radix-ui/react-dialog'
import { X } from 'lucide-react'
import { cn } from '@/lib/utils'
const Dialog = DialogPrimitive.Root
const DialogTrigger = DialogPrimitive.Trigger
const DialogPortal = ({
className,
children,
...props
}: DialogPrimitive.DialogPortalProps) => (
<DialogPrimitive.Portal className={cn(className)} {...props}>
<div className="fixed inset-0 z-50 flex items-start justify-center sm:items-center">
{children}
</div>
</DialogPrimitive.Portal>
)
DialogPortal.displayName = DialogPrimitive.Portal.displayName
const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, children, ...props }, ref) => (
<DialogPrimitive.Overlay
className={cn(
'fixed inset-0 z-50 bg-black/50 backdrop-blur-sm transition-opacity animate-in fade-in',
className
)}
{...props}
ref={ref}
/>
))
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 z-50 grid w-full scale-100 gap-4 bg-white p-6 opacity-100 animate-in fade-in-90 slide-in-from-bottom-10 sm:max-w-lg sm:rounded-lg sm:zoom-in-90 sm:slide-in-from-bottom-0',
'dark:bg-slate-900',
className
)}
{...props}
>
{children}
<DialogPrimitive.Close className="absolute top-4 right-4 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-slate-400 focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-slate-100 dark:focus:ring-slate-400 dark:focus:ring-offset-slate-900 dark:data-[state=open]:bg-slate-800">
<X 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-2 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 text-slate-900',
'dark:text-slate-50',
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-slate-500', 'dark:text-slate-400', className)}
{...props}
/>
))
DialogDescription.displayName = DialogPrimitive.Description.displayName
export {
Dialog,
DialogTrigger,
DialogContent,
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription,
}
================================================
FILE: src/components/ui/dropdown-menu.tsx
================================================
'use client'
import * as React from 'react'
import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'
import { Check, ChevronRight, Circle } from 'lucide-react'
import { cn } from '@/lib/utils'
const DropdownMenu = DropdownMenuPrimitive.Root
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
const DropdownMenuGroup = DropdownMenuPrimitive.Group
const DropdownMenuPortal = DropdownMenuPrimitive.Portal
const DropdownMenuSub = DropdownMenuPrimitive.Sub
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
const DropdownMenuSubTrigger = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean
}
>(({ className, inset, children, ...props }, ref) => (
<DropdownMenuPrimitive.SubTrigger
ref={ref}
className={cn(
'flex cursor-default select-none items-center rounded-sm py-1.5 px-2 text-sm font-medium outline-none focus:bg-slate-100 data-[state=open]:bg-slate-100 dark:focus:bg-slate-700 dark:data-[state=open]:bg-slate-700',
inset && 'pl-8',
className
)}
{...props}
>
{children}
<ChevronRight className="ml-auto h-4 w-4" />
</DropdownMenuPrimitive.SubTrigger>
))
DropdownMenuSubTrigger.displayName =
DropdownMenuPrimitive.SubTrigger.displayName
const DropdownMenuSubContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.SubContent
ref={ref}
className={cn(
'z-50 min-w-[8rem] overflow-hidden rounded-md border border-slate-100 bg-white p-1 shadow-md animate-in slide-in-from-left-1 dark:border-slate-700 dark:bg-slate-800',
className
)}
{...props}
/>
))
DropdownMenuSubContent.displayName =
DropdownMenuPrimitive.SubContent.displayName
const DropdownMenuContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
'z-50 min-w-[8rem] overflow-hidden rounded-md border border-slate-100 bg-white p-1 text-slate-700 shadow-md animate-in data-[side=bottom]:slide-in-from-top-2 data-[side=top]:slide-in-from-bottom-2 dark:border-slate-800 dark:bg-slate-800 dark:text-slate-400',
className
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
))
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
const DropdownMenuItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Item
ref={ref}
className={cn(
'relative flex cursor-default select-none items-center rounded-sm py-1.5 px-2 text-sm font-medium outline-none focus:bg-slate-100 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 dark:focus:bg-slate-700',
inset && 'pl-8',
className
)}
{...props}
/>
))
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
const DropdownMenuCheckboxItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => (
<DropdownMenuPrimitive.CheckboxItem
ref={ref}
className={cn(
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm font-medium outline-none focus:bg-slate-100 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 dark:focus:bg-slate-700',
className
)}
checked={checked}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
))
DropdownMenuCheckboxItem.displayName =
DropdownMenuPrimitive.CheckboxItem.displayName
const DropdownMenuRadioItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => (
<DropdownMenuPrimitive.RadioItem
ref={ref}
className={cn(
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm font-medium outline-none focus:bg-slate-100 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 dark:focus:bg-slate-700',
className
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<Circle className="h-2 w-2 fill-current" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
))
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
const DropdownMenuLabel = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Label
ref={ref}
className={cn(
'px-2 py-1.5 text-sm font-semibold text-slate-900 dark:text-slate-300',
inset && 'pl-8',
className
)}
{...props}
/>
))
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
const DropdownMenuSeparator = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.Separator
ref={ref}
className={cn('-mx-1 my-1 h-px bg-slate-100 dark:bg-slate-700', className)}
{...props}
/>
))
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
const DropdownMenuShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn(
'ml-auto text-xs tracking-widest text-slate-500',
className
)}
{...props}
/>
)
}
DropdownMenuShortcut.displayName = 'DropdownMenuShortcut'
export {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuGroup,
DropdownMenuPortal,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuRadioGroup,
}
================================================
FILE: src/components/ui/file-input.tsx
================================================
'use client'
import {
forwardRef,
useReducer,
useState,
type ChangeEvent,
type DragEvent,
} from 'react'
import { useS3Upload } from '@/src/hooks/use-s3-upload'
import { useToast } from '@/src/hooks/use-toast'
import ImageUpload from '@/ui/image-upload'
import { MAX_FILE_SIZE } from '@/config/image'
import { cn, validateFileType } from '@/lib/utils'
import { Icons } from '../icons'
interface FileWithUrl {
name: string
getUrl: string
size: number
error?: boolean | undefined
}
// Reducer action(s)
const addFilesToInput = () => ({
type: 'ADD_FILES_TO_INPUT' as const,
payload: [] as FileWithUrl[],
})
type Action = ReturnType<typeof addFilesToInput>
type State = FileWithUrl[]
export interface InputProps
extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'type'> {}
const FileInput = forwardRef<HTMLInputElement, InputProps>(
({ className, ...props }, ref) => {
const { toast } = useToast()
const { s3Upload } = useS3Upload()
const [dragActive, setDragActive] = useState<boolean>(false)
const [input, dispatch] = useReducer((state: State, action: Action) => {
switch (action.type) {
case 'ADD_FILES_TO_INPUT': {
// do not allow more than 5 files to be uploaded at once
if (state.length + action.payload.length > 10) {
toast({
title: 'Too many files',
description:
'You can only upload a maximum of 5 files at a time.',
})
return state
}
return [...state, ...action.payload]
}
// You could extend this, for example to allow removing files
}
}, [])
const noInput = input.length === 0
// handle drag events
const handleDrag = (e: DragEvent<HTMLFormElement | HTMLDivElement>) => {
e.preventDefault()
e.stopPropagation()
if (e.type === 'dragenter' || e.type === 'dragover') {
setDragActive(true)
} else if (e.type === 'dragleave') {
setDragActive(false)
}
}
// triggers when file is selected with click
const handleChange = async (e: ChangeEvent<HTMLInputElement>) => {
e.preventDefault()
try {
if (e.target.files && e.target.files[0]) {
// at least one file has been selected
// validate file type
const valid = validateFileType(e.target.files[0])
if (!valid) {
toast({
title: 'Invalid file type',
description: 'Please upload a valid file type.',
})
return
}
const { getUrl, error } = await s3Upload(e.target.files[0])
if (!getUrl || error) throw new Error('Error uploading file')
const { name, size } = e.target.files[0]
addFilesToState([{ name, getUrl, size }])
}
} catch (error) {
// already handled
}
}
const addFilesToState = (files: FileWithUrl[]) => {
dispatch({ type: 'ADD_FILES_TO_INPUT', payload: files })
}
// triggers when file is dropped
const handleDrop = async (e: DragEvent<HTMLDivElement>) => {
e.preventDefault()
e.stopPropagation()
// validate file type
if (e.dataTransfer.files && e.dataTransfer.files[0]) {
const files = Array.from(e.dataTransfer.files)
const validFiles = files.filter((file) => validateFileType(file))
if (files.length !== validFiles.length) {
toast({
title: 'Invalid file type',
description: 'Only image files are allowed.',
})
}
try {
const filesWithUrl = await Promise.all(
validFiles.map(async (file) => {
const { name, size } = file
const { getUrl, error } = await s3Upload(file)
if (!getUrl || error) return { name, size, getUrl: '', error }
return { name, size, getUrl }
})
)
setDragActive(false)
// at least one file has been selected
addFilesToState(filesWithUrl)
e.dataTransfer.clearData()
} catch (error) {
// already handled
}
}
}
return (
<form
onSubmit={(e) => e.preventDefault()}
onDragEnter={handleDrag}
className="flex h-full items-center w-full lg:w-2/3 justify-start"
>
<label
htmlFor="dropzone-file"
className={cn(
'group relative h-full flex flex-col items-center justify-center w-full aspect-video border-2 border-slate-300 border-dashed rounded-lg dark:border-gray-600 transition',
{ 'dark:border-slate-400 dark:bg-slate-800': dragActive },
{ 'h-fit aspect-auto': !noInput },
{ 'items-start justify-start': !noInput },
{ 'dark:hover:border-gray-500 dark:hover:bg-slate-800': noInput }
)}
>
<div
className={cn(
'relative w-full h-full flex flex-col items-center justify-center',
{ 'items-start': !noInput }
)}
>
{noInput ? (
<>
<div
className="absolute inset-0 cursor-pointer"
onDragEnter={handleDrag}
onDragLeave={handleDrag}
onDragOver={handleDrag}
onDrop={handleDrop}
/>
<svg
aria-hidden="true"
className="w-10 h-10 mb-3 text-gray-400"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"
></path>
</svg>
<p className="mb-2 text-sm text-gray-500 dark:text-gray-400">
<span className="font-semibold">Click to upload</span> or drag
and drop
</p>
<p className="text-xs text-gray-500 dark:text-gray-400">
up to 5 images, {(MAX_FILE_SIZE / 1000000).toFixed(0)}MB per
file
</p>
<input
{...props}
ref={ref}
multiple
onChange={handleChange}
accept="image/jpeg, image/jpg, image/png"
id="dropzone-file"
type="file"
className="hidden"
/>
</>
) : (
<div className="flex flex-col w-full h-full">
<div className="overflow-x-auto sm:-mx-6 lg:-mx-8">
<div className="align-middle inline-block min-w-full sm:px-6 lg:px-8">
<div className="shadow overflow-hidden sm:rounded-lg">
<table className="min-w-full divide-y dark:divide-slate-600">
<thead className="bg-slate-800">
<tr>
<th
scope="col"
className="px-6 py-3 text-left text-xs font-medium dark:text-slate-300 uppercase tracking-wider"
>
Preview
</th>
<th
scope="col"
className="px-6 py-3 text-left text-xs font-medium dark:text-slate-300 uppercase tracking-wider"
>
Name
</th>
<th
scope="col"
className="px-6 py-3 text-left text-xs font-medium dark:text-slate-300 uppercase tracking-wider"
>
Size
</th>
<th
scope="col"
className="px-6 py-3 text-left text-xs font-medium dark:text-slate-300 uppercase tracking-wider"
>
Status
</th>
</tr>
</thead>
<tbody className="relative divide-y dark:divide-slate-600">
{input.map((file, index) => (
<ImageUpload
key={index}
error={file.error}
getUrl={file.getUrl}
name={file.name}
size={file.size}
/>
))}
</tbody>
</table>
<label
htmlFor="dropzone-file-images-present"
className="relative cursor-pointer group hover:border-gray-500 hover:dark:bg-slate-800 transition flex justify-center py-4 border-t border-slate-600"
>
<Icons.plus className="group-hover:fill-slate-400 transition stroke-1 w-12 h-12 fill-slate-500" />
<input
{...props}
ref={ref}
multiple
onChange={handleChange}
accept="image/jpeg, image/jpg, image/png"
type="file"
id="dropzone-file-images-present"
className="relative z-20 hidden"
/>
<div
className="absolute inset-0"
onDragEnter={handleDrag}
onDragLeave={handleDrag}
onDragOver={handleDrag}
onDrop={handleDrop}
/>
</label>
</div>
</div>
</div>
</div>
)}
</div>
</label>
</form>
)
}
)
FileInput.displayName = 'FileInput'
export { FileInput }
================================================
FILE: src/components/ui/hover-card.tsx
================================================
'use client'
import * as React from 'react'
import * as HoverCardPrimitive from '@radix-ui/react-hover-card'
import { cn } from '@/lib/utils'
const HoverCard = HoverCardPrimitive.Root
const HoverCardTrigger = HoverCardPrimitive.Trigger
const HoverCardContent = React.forwardRef<
React.ElementRef<typeof HoverCardPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof HoverCardPrimitive.Content>
>(({ className, align = 'center', sideOffset = 4, ...props }, ref) => (
<HoverCardPrimitive.Content
ref={ref}
align={align}
sideOffset={sideOffset}
className={cn(
'z-50 w-64 rounded-md border border-slate-100 bg-white p-4 shadow-md outline-none animate-in zoom-in-90 dark:border-slate-800 dark:bg-slate-800',
className
)}
{...props}
/>
))
HoverCardContent.displayName = HoverCardPrimitive.Content.displayName
export { HoverCard, HoverCardTrigger, HoverCardContent }
================================================
FILE: src/components/ui/image-upload.tsx
================================================
'use client'
import { forwardRef } from 'react'
import Image from 'next/image'
import { useUploadFile } from '@/src/hooks/use-upload-file'
import { ImageResponseData } from '@/src/types/api/image'
import { Progress } from '@/ui/progress'
import { Loader2 } from 'lucide-react'
import { cn } from '@/lib/utils'
import { Icons } from '@/components/icons'
interface ImageUploadProps extends React.HTMLAttributes<HTMLTableRowElement> {
name: string
size: number
getUrl: string
error?: boolean | undefined
}
const ImageUpload = forwardRef<HTMLTableRowElement, ImageUploadProps>(
({ getUrl, error, name, size, className, ...props }, ref) => {
const {
data,
progress,
isLoading,
error: processingError,
} = useUploadFile<ImageResponseData>('/api/image/process', getUrl, {
disabled: error,
})
return (
<tr ref={ref} {...props} className={cn('', className)}>
<td className="px-6 py-4 whitespace-nowrap text-sm dark:text-slate-400">
<div className="relative flex h-12 w-20">
{error ? (
<div className="flex w-full justify-center items-center">
<Icons.redx className="h-6 w-6" />
</div>
) : (
<Image
style={{ objectFit: 'contain' }}
src={getUrl}
fill
alt={name}
/>
)}
</div>
</td>
<td className="px-6 py-4 truncate whitespace-normal text-sm font-medium dark:text-slate-400 ">
<div className="">
<p
className={cn('dark:text-slate-300', {
'dark:text-red-500': error,
})}
>
{name}
</p>
{data ? (
<p>{data.alt}</p>
) : isLoading ? (
<Loader2 className="mt-1 w-4 h-4 animate-spin" />
) : null}
</div>
</td>
<td
className={cn(
'px-6 py-4 whitespace-nowrap text-sm dark:text-slate-400',
{
'dark:text-red-500': error,
}
)}
>
{(size / 1000).toFixed(0)} KB
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm dark:text-slate-400 ">
<Progress
className={cn('w-full h-2')}
value={progress}
isError={error || processingError}
/>
</td>
</tr>
)
}
)
ImageUpload.displayName = 'ImageUpload'
export default ImageUpload
================================================
FILE: src/components/ui/input.tsx
================================================
import * as React from 'react'
import { cn } from '@/lib/utils'
export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement> {}
const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, ...props }, ref) => {
return (
<input
className={cn(
'flex h-10 w-full rounded-md border border-slate-300 bg-transparent py-2 px-3 text-sm placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:ring-slate-400 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:border-slate-700 dark:text-slate-50 dark:focus:ring-slate-400 dark:focus:ring-offset-slate-900',
className
)}
ref={ref}
{...props}
/>
)
}
)
Input.displayName = 'Input'
export { Input }
================================================
FILE: src/components/ui/label.tsx
================================================
'use client'
import * as React from 'react'
import * as LabelPrimitive from '@radix-ui/react-label'
import { cn } from '@/lib/utils'
const Label = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
>(({ className, ...props }, ref) => (
<LabelPrimitive.Root
ref={ref}
className={cn(
'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70',
className
)}
{...props}
/>
))
Label.displayName = LabelPrimitive.Root.displayName
export { Label }
================================================
FILE: src/components/ui/menubar.tsx
================================================
'use client'
import * as React from 'react'
import * as MenubarPrimitive from '@radix-ui/react-menubar'
import { Check, ChevronRight, Circle } from 'lucide-react'
import { cn } from '@/lib/utils'
const MenubarMenu = MenubarPrimitive.Menu
const MenubarGroup = MenubarPrimitive.Group
const MenubarPortal = MenubarPrimitive.Portal
const MenubarSub = MenubarPrimitive.Sub
const MenubarRadioGroup = MenubarPrimitive.RadioGroup
const Menubar = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Root>
>(({ className, ...props }, ref) => (
<MenubarPrimitive.Root
ref={ref}
className={cn(
'flex h-10 items-center space-x-1 rounded-md border border-slate-300 bg-white p-1 dark:border-slate-700 dark:bg-slate-800',
className
)}
{...props}
/>
))
Menubar.displayName = MenubarPrimitive.Root.displayName
const MenubarTrigger = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Trigger>
>(({ className, ...props }, ref) => (
<MenubarPrimitive.Trigger
ref={ref}
className={cn(
'flex cursor-default select-none items-center rounded-[0.2rem] py-1.5 px-3 text-sm font-medium outline-none focus:bg-slate-100 data-[state=open]:bg-slate-100 dark:focus:bg-slate-700 dark:data-[state=open]:bg-slate-700',
className
)}
{...props}
/>
))
MenubarTrigger.displayName = MenubarPrimitive.Trigger.displayName
const MenubarSubTrigger = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.SubTrigger> & {
inset?: boolean
}
>(({ className, inset, children, ...props }, ref) => (
<MenubarPrimitive.SubTrigger
ref={ref}
className={cn(
'flex cursor-default select-none items-center rounded-sm py-1.5 px-2 text-sm font-medium outline-none focus:bg-slate-100 data-[state=open]:bg-slate-100 dark:focus:bg-slate-700 dark:data-[state=open]:bg-slate-700',
inset && 'pl-8',
className
)}
{...props}
>
{children}
<ChevronRight className="ml-auto h-4 w-4" />
</MenubarPrimitive.SubTrigger>
))
MenubarSubTrigger.displayName = MenubarPrimitive.SubTrigger.displayName
const MenubarSubContent = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.SubContent>
>(({ className, ...props }, ref) => (
<MenubarPrimitive.SubContent
ref={ref}
className={cn(
'z-50 min-w-[8rem] overflow-hidden rounded-md border border-slate-100 bg-white p-1 shadow-md animate-in slide-in-from-left-1 dark:border-slate-700 dark:bg-slate-800',
className
)}
{...props}
/>
))
MenubarSubContent.displayName = MenubarPrimitive.SubContent.displayName
const MenubarContent = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Content>
>(
(
{ className, align = 'start', alignOffset = -4, sideOffset = 8, ...props },
ref
) => (
<MenubarPrimitive.Portal>
<MenubarPrimitive.Content
ref={ref}
align={align}
alignOffset={alignOffset}
sideOffset={sideOffset}
className={cn(
'z-50 min-w-[12rem] overflow-hidden rounded-md border border-slate-100 bg-white p-1 text-slate-700 shadow-md animate-in slide-in-from-top-1 dark:border-slate-800 dark:bg-slate-800 dark:text-slate-400',
className
)}
{...props}
/>
</MenubarPrimitive.Portal>
)
)
MenubarContent.displayName = MenubarPrimitive.Content.displayName
const MenubarItem = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Item> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<MenubarPrimitive.Item
ref={ref}
className={cn(
'relative flex cursor-default select-none items-center rounded-sm py-1.5 px-2 text-sm font-medium outline-none focus:bg-slate-100 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 dark:focus:bg-slate-700',
inset && 'pl-8',
className
)}
{...props}
/>
))
MenubarItem.displayName = MenubarPrimitive.Item.displayName
const MenubarCheckboxItem = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => (
<MenubarPrimitive.CheckboxItem
ref={ref}
className={cn(
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm font-medium outline-none focus:bg-slate-100 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 dark:focus:bg-slate-700',
className
)}
checked={checked}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<MenubarPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</MenubarPrimitive.ItemIndicator>
</span>
{children}
</MenubarPrimitive.CheckboxItem>
))
MenubarCheckboxItem.displayName = MenubarPrimitive.CheckboxItem.displayName
const MenubarRadioItem = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => (
<MenubarPrimitive.RadioItem
ref={ref}
className={cn(
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm font-medium outline-none focus:bg-slate-100 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 dark:focus:bg-slate-700',
className
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<MenubarPrimitive.ItemIndicator>
<Circle className="h-2 w-2 fill-current" />
</MenubarPrimitive.ItemIndicator>
</span>
{children}
</MenubarPrimitive.RadioItem>
))
MenubarRadioItem.displayName = MenubarPrimitive.RadioItem.displayName
const MenubarLabel = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Label> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<MenubarPrimitive.Label
ref={ref}
className={cn(
'px-2 py-1.5 text-sm font-semibold text-slate-900 dark:text-slate-300',
inset && 'pl-8',
className
)}
{...props}
/>
))
MenubarLabel.displayName = MenubarPrimitive.Label.displayName
const MenubarSeparator = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Separator>
>(({ className, ...props }, ref) => (
<MenubarPrimitive.Separator
ref={ref}
className={cn('-mx-1 my-1 h-px bg-slate-100 dark:bg-slate-700', className)}
{...props}
/>
))
MenubarSeparator.displayName = MenubarPrimitive.Separator.displayName
const MenubarShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn(
'ml-auto text-xs tracking-widest text-slate-500',
className
)}
{...props}
/>
)
}
MenubarShortcut.displayname = 'MenubarShortcut'
export {
Menubar,
MenubarMenu,
MenubarTrigger,
MenubarContent,
MenubarItem,
MenubarSeparator,
MenubarLabel,
MenubarCheckboxItem,
MenubarRadioGroup,
MenubarRadioItem,
MenubarPortal,
MenubarSubContent,
MenubarSubTrigger,
MenubarGroup,
MenubarSub,
MenubarShortcut,
}
================================================
FILE: src/components/ui/navigation-menu.tsx
================================================
import * as React from 'react'
import * as NavigationMenuPrimitive from '@radix-ui/react-navigation-menu'
import { cva } from 'class-variance-authority'
import { ChevronDown } from 'lucide-react'
import { cn } from '@/lib/utils'
const NavigationMenu = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Root>
>(({ className, children, ...props }, ref) => (
<NavigationMenuPrimitive.Root
ref={ref}
className={cn(
'relative z-10 flex flex-1 items-center justify-center',
className
)}
{...props}
>
{children}
<NavigationMenuViewport />
</NavigationMenuPrimitive.Root>
))
NavigationMenu.displayName = NavigationMenuPrimitive.Root.displayName
const NavigationMenuList = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.List>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.List>
>(({ className, ...props }, ref) => (
<NavigationMenuPrimitive.List
ref={ref}
className={cn(
'group flex flex-1 list-none items-center justify-center',
className
)}
{...props}
/>
))
NavigationMenuList.displayName = NavigationMenuPrimitive.List.displayName
const NavigationMenuItem = NavigationMenuPrimitive.Item
const navigationMenuTriggerStyle = cva(
'inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus:outline-none focus:bg-slate-100 disabled:opacity-50 dark:focus:bg-slate-800 disabled:pointer-events-none bg-transparent hover:bg-slate-100 dark:hover:bg-slate-800 dark:text-slate-100 dark:hover:text-slate-100 data-[state=open]:bg-slate-50 dark:data-[state=open]:bg-slate-800 h-10 py-2 px-4 group'
)
const NavigationMenuTrigger = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<NavigationMenuPrimitive.Trigger
ref={ref}
className={cn(navigationMenuTriggerStyle(), 'group', className)}
{...props}
>
{children}{' '}
<ChevronDown
className="relative top-[1px] ml-1 h-3 w-3 transition duration-200 group-data-[state=open]:rotate-180"
aria-hidden="true"
/>
</NavigationMenuPrimitive.Trigger>
))
NavigationMenuTrigger.displayName = NavigationMenuPrimitive.Trigger.displayName
const NavigationMenuContent = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Content>
>(({ className, ...props }, ref) => (
<NavigationMenuPrimitive.Content
ref={ref}
className={cn(
'absolute top-0 left-0 w-full data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=to-start]:slide-out-to-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=from-end]:slide-in-from-right-52 md:w-auto',
className
)}
{...props}
/>
))
NavigationMenuContent.displayName = NavigationMenuPrimitive.Content.displayName
const NavigationMenuLink = NavigationMenuPrimitive.Link
const NavigationMenuViewport = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Viewport>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Viewport>
>(({ className, ...props }, ref) => (
<div className={cn('absolute left-0 top-full flex justify-center')}>
<NavigationMenuPrimitive.Viewport
className={cn(
'origin-top-center relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border border-slate-200 bg-white shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=open]:zoom-in-90 data-[state=closed]:zoom-out-95 dark:border-slate-700 dark:bg-slate-800 md:w-[var(--radix-navigation-menu-viewport-width)]',
className
)}
ref={ref}
{...props}
/>
</div>
))
NavigationMenuViewport.displayName =
NavigationMenuPrimitive.Viewport.displayName
const NavigationMenuIndicator = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Indicator>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Indicator>
>(({ className, ...props }, ref) => (
<NavigationMenuPrimitive.Indicator
ref={ref}
className={cn(
'top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=visible]:fade-in data-[state=hidden]:fade-out',
className
)}
{...props}
>
<div className="relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm bg-slate-200 shadow-md dark:bg-slate-800" />
</NavigationMenuPrimitive.Indicator>
))
NavigationMenuIndicator.displayName =
NavigationMenuPrimitive.Indicator.displayName
export {
navigationMenuTriggerStyle,
NavigationMenu,
NavigationMenuList,
NavigationMenuItem,
NavigationMenuContent,
NavigationMenuTrigger,
NavigationMenuLink,
NavigationMenuIndicator,
NavigationMenuViewport,
}
================================================
FILE: src/components/ui/popover.tsx
================================================
'use client'
import * as React from 'react'
import * as PopoverPrimitive from '@radix-ui/react-popover'
import { cn } from '@/lib/utils'
const Popover = PopoverPrimitive.Root
const PopoverTrigger = PopoverPrimitive.Trigger
const PopoverContent = React.forwardRef<
React.ElementRef<typeof PopoverPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
>(({ className, align = 'center', sideOffset = 4, ...props }, ref) => (
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
ref={ref}
align={align}
sideOffset={sideOffset}
className={cn(
'z-50 w-72 rounded-md border border-slate-100 bg-white p-4 shadow-md outline-none animate-in data-[side=bottom]:slide-in-from-top-2 data-[side=top]:slide-in-from-bottom-2 dark:border-slate-800 dark:bg-slate-800',
className
)}
{...props}
/>
</PopoverPrimitive.Portal>
))
PopoverContent.displayName = PopoverPrimitive.Content.displayName
export { Popover, PopoverTrigger, PopoverContent }
================================================
FILE: src/components/ui/progress.tsx
================================================
'use client'
import * as React from 'react'
import * as ProgressPrimitive from '@radix-ui/react-progress'
import { cva } from 'class-variance-authority'
import { cn } from '@/lib/utils'
interface ProgressProps
extends React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root> {
isSuccess?: boolean
isError?: boolean
}
const indicatorVariants = cva('h-full w-full flex-1 transition-all', {
variants: {
variant: {
default: 'bg-slate-900 dark:bg-slate-400',
isError: 'bg-red-500 dark:bg-red-500',
isSuccess: 'bg-green-500 dark:bg-green-400',
},
},
})
const rootVariants = cva('relative h-4 w-full overflow-hidden rounded-full', {
variants: {
variant: {
default: 'bg-slate-200 dark:bg-slate-800',
isError: 'bg-red-200 dark:bg-red-800',
isSuccess: 'bg-green-200 dark:bg-green-800',
},
},
})
const Progress = React.forwardRef<
React.ElementRef<typeof ProgressPrimitive.Root>,
ProgressProps
>(({ className, value, isError, isSuccess, ...props }, ref) => {
const variant = isError ? 'isError' : 'default'
return (
<ProgressPrimitive.Root
ref={ref}
className={cn(rootVariants({ variant, className }))}
{...props}
>
<ProgressPrimitive.Indicator
className={cn(indicatorVariants({ variant }))}
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
/>
</ProgressPrimitive.Root>
)
})
Progress.displayName = ProgressPrimitive.Root.displayName
export { Progress }
================================================
FILE: src/components/ui/radio-group.tsx
================================================
'use client'
import * as React from 'react'
import * as RadioGroupPrimitive from '@radix-ui/react-radio-group'
import { Circle } from 'lucide-react'
import { cn } from '@/lib/utils'
const RadioGroup = React.forwardRef<
React.ElementRef<typeof RadioGroupPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>
>(({ className, ...props }, ref) => {
return (
<RadioGroupPrimitive.Root
className={cn('grid gap-2', className)}
{...props}
ref={ref}
/>
)
})
RadioGroup.displayName = RadioGroupPrimitive.Root.displayName
const RadioGroupItem = React.forwardRef<
React.ElementRef<typeof RadioGroupPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>
>(({ className, children, ...props }, ref) => {
return (
<RadioGroupPrimitive.Item
ref={ref}
className={cn(
'text:fill-slate-50 h-4 w-4 rounded-full border border-slate-300 text-slate-900 hover:border-slate-400 focus:outline-none focus:ring-2 focus:ring-slate-400 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:border-slate-700 dark:text-slate-100 dark:hover:text-slate-900 dark:focus:ring-slate-400 dark:focus:ring-offset-slate-900',
className
)}
{...props}
>
<RadioGroupPrimitive.Indicator className="flex items-center justify-center">
<Circle className="h-2.5 w-2.5 fill-slate-900 dark:fill-slate-50" />
</RadioGroupPrimitive.Indicator>
</RadioGroupPrimitive.Item>
)
})
RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName
export { RadioGroup, RadioGroupItem }
================================================
FILE: src/components/ui/scroll-area.tsx
================================================
'use client'
import * as React from 'react'
import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area'
import { cn } from '@/lib/utils'
const ScrollArea = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
>(({ className, children, ...props }, ref) => (
<ScrollAreaPrimitive.Root
ref={ref}
className={cn('relative overflow-hidden', className)}
{...props}
>
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
{children}
</ScrollAreaPrimitive.Viewport>
<ScrollBar />
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
))
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
const ScrollBar = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
>(({ className, orientation = 'vertical', ...props }, ref) => (
<ScrollAreaPrimitive.ScrollAreaScrollbar
ref={ref}
orientation={orientation}
className={cn(
'flex touch-none select-none transition-colors',
orientation === 'vertical' &&
'h-full w-2.5 border-l border-l-transparent p-[1px]',
orientation === 'horizontal' &&
'h-2.5 border-t border-t-transparent p-[1px]',
className
)}
{...props}
>
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-slate-300 dark:bg-slate-700" />
</ScrollAreaPrimitive.ScrollAreaScrollbar>
))
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
export { ScrollArea, ScrollBar }
================================================
FILE: src/components/ui/select.tsx
================================================
'use client'
import * as React from 'react'
import * as SelectPrimitive from '@radix-ui/react-select'
import { Check, ChevronDown } from 'lucide-react'
import { cn } from '@/lib/utils'
const Select = SelectPrimitive.Root
const SelectGroup = SelectPrimitive.Group
const SelectValue = SelectPrimitive.Value
const SelectTrigger = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Trigger
ref={ref}
className={cn(
'flex h-10 w-full items-center justify-between rounded-md border border-slate-300 bg-transparent py-2 px-3 text-sm placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:ring-slate-400 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:border-slate-700 dark:text-slate-50 dark:focus:ring-slate-400 dark:focus:ring-offset-slate-900',
className
)}
{...props}
>
{children}
<ChevronDown className="h-4 w-4 opacity-50" />
</SelectPrimitive.Trigger>
))
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
const SelectContent = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
ref={ref}
className={cn(
'relative z-50 min-w-[8rem] overflow-hidden rounded-md border border-slate-100 bg-white text-slate-700 shadow-md animate-in fade-in-80 dark:border-slate-800 dark:bg-slate-800 dark:text-slate-400',
className
)}
{...props}
>
<SelectPrimitive.Viewport className="p-1">
{children}
</SelectPrimitive.Viewport>
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
))
SelectContent.displayName = SelectPrimitive.Content.displayName
const SelectLabel = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Label
ref={ref}
className={cn(
'py-1.5 pr-2 pl-8 text-sm font-semibold text-slate-900 dark:text-slate-300',
className
)}
{...props}
/>
))
SelectLabel.displayName = SelectPrimitive.Label.displayName
const SelectItem = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Item
ref={ref}
className={cn(
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pr-2 pl-8 text-sm font-medium outline-none focus:bg-slate-100 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 dark:focus:bg-slate-700',
className
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
))
SelectItem.displayName = SelectPrimitive.Item.displayName
const SelectSeparator = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Separator
ref={ref}
className={cn('-mx-1 my-1 h-px bg-slate-100 dark:bg-slate-700', className)}
{...props}
/>
))
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
export {
Select,
SelectGroup,
SelectValue,
SelectTrigger,
SelectContent,
SelectLabel,
SelectItem,
SelectSeparator,
}
================================================
FILE: src/components/ui/separator.tsx
================================================
'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(
'bg-slate-200 dark:bg-slate-700',
orientation === 'horizontal' ? 'h-[1px] w-full' : 'h-full w-[1px]',
className
)}
{...props}
/>
)
)
Separator.displayName = SeparatorPrimitive.Root.displayName
export { Separator }
================================================
FILE: src/components/ui/slider.tsx
================================================
'use client'
import * as React from 'react'
import * as SliderPrimitive from '@radix-ui/react-slider'
import { cn } from '@/lib/utils'
const Slider = React.forwardRef<
React.ElementRef<typeof SliderPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root>
>(({ className, value, ...props }, ref) => (
<SliderPrimitive.Root
ref={ref}
className={cn(
'relative flex w-full touch-none select-none items-center',
className
)}
{...props}
>
<SliderPrimitive.Track className="relative h-2 w-full grow overflow-hidden rounded-full bg-slate-200 dark:bg-slate-800">
<SliderPrimitive.Range className="absolute h-full bg-slate-900 dark:bg-slate-400" />
</SliderPrimitive.Track>
<SliderPrimitive.Thumb className="block h-5 w-5 rounded-full border-2 border-slate-900 bg-white transition-colors focus:outline-none focus:ring-2 focus:ring-slate-400 focus:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 dark:border-slate-100 dark:bg-slate-400 dark:focus:ring-slate-400 dark:focus:ring-offset-slate-900" />
</SliderPrimitive.Root>
))
Slider.displayName = SliderPrimitive.Root.displayName
export { Slider }
================================================
FILE: src/components/ui/spinner.tsx
================================================
import type { FC, HTMLAttributes } from 'react'
import { Loader2 } from 'lucide-react'
import { cn } from '@/lib/utils'
interface SpinnerProps extends HTMLAttributes<HTMLDivElement> {}
const Spinner: FC<SpinnerProps> = ({ className, ...props }) => {
return (
<Loader2
{...props}
className={cn('mr-2 h-4 w-4 animate-spin', className)}
/>
)
}
export default Spinner
================================================
FILE: src/components/ui/switch.tsx
================================================
'use client'
import * as React from 'react'
import * as SwitchPrimitives from '@radix-ui/react-switch'
import { cn } from '@/lib/utils'
const Switch = React.forwardRef<
React.ElementRef<typeof SwitchPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
>(({ className, ...props }, ref) => (
<SwitchPrimitives.Root
className={cn(
'peer inline-flex h-[24px] w-[44px] shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors focus:outline-none focus:ring-2 focus:ring-slate-400 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=unchecked]:bg-slate-200 data-[state=checked]:bg-slate-900 dark:focus:ring-slate-400 dark:focus:ring-offset-slate-900 dark:data-[state=unchecked]:bg-slate-700 dark:data-[state=checked]:bg-slate-400',
className
)}
{...props}
ref={ref}
>
<SwitchPrimitives.Thumb
className={cn(
'pointer-events-none block h-5 w-5 rounded-full bg-white shadow-lg ring-0 transition-transform data-[state=unchecked]:translate-x-0 data-[state=checked]:translate-x-5'
)}
/>
</SwitchPrimitives.Root>
))
Switch.displayName = SwitchPrimitives.Root.displayName
export { Switch }
================================================
FILE: src/components/ui/tabs.tsx
================================================
'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 items-center justify-center rounded-md bg-slate-100 p-1 dark:bg-slate-800',
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
className={cn(
'inline-flex min-w-[100px] items-center justify-center rounded-[0.185rem] px-3 py-1.5 text-sm font-medium text-slate-700 transition-all disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-white data-[state=active]:text-slate-900 data-[state=active]:shadow-sm dark:text-slate-200 dark:data-[state=active]:bg-slate-900',
className
)}
{...props}
ref={ref}
/>
))
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
const TabsContent = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Content
className={cn(
'mt-2 rounded-md border border-slate-200 p-6 dark:border-slate-700',
className
)}
{...props}
ref={ref}
/>
))
TabsContent.displayName = TabsPrimitive.Content.displayName
export { Tabs, TabsList, TabsTrigger, TabsContent }
================================================
FILE: src/components/ui/textarea.tsx
================================================
import * as React from 'react'
import { cn } from '@/lib/utils'
export interface TextareaProps
extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
({ className, ...props }, ref) => {
return (
<textarea
className={cn(
'flex h-20 w-full rounded-md border border-slate-300 bg-transparent py-2 px-3 text-sm placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:ring-slate-400 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:border-slate-700 dark:text-slate-50 dark:focus:ring-slate-400 dark:focus:ring-offset-slate-900',
className
)}
ref={ref}
{...props}
/>
)
}
)
Textarea.displayName = 'Textarea'
export { Textarea }
================================================
FILE: src/components/ui/toast.tsx
================================================
import * as React from 'react'
import * as ToastPrimitives from '@radix-ui/react-toast'
import { VariantProps, cva } from 'class-variance-authority'
import { X } from 'lucide-react'
import { cn } from '@/lib/utils'
const ToastProvider = ToastPrimitives.Provider
const ToastViewport = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Viewport>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Viewport
ref={ref}
className={cn(
'fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:top-auto sm:bottom-0 sm:right-0 sm:flex-col md:max-w-[420px]',
className
)}
{...props}
/>
))
ToastViewport.displayName = ToastPrimitives.Viewport.displayName
const toastVariants = cva(
'data-[swipe=move]:transition-none grow-1 group relative pointer-events-auto flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full mt-4 data-[state=closed]:slide-out-to-right-full dark:border-slate-700 last:mt-0 sm:last:mt-4',
{
variants: {
variant: {
default:
'bg-white border-slate-200 dark:bg-slate-800 dark:border-slate-700',
destructive:
'group destructive bg-red-600 text-white border-red-600 dark:border-red-600',
},
},
defaultVariants: {
variant: 'default',
},
}
)
const Toast = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &
VariantProps<typeof toastVariants>
>(({ className, variant, ...props }, ref) => {
return (
<ToastPrimitives.Root
ref={ref}
className={cn(toastVariants({ variant }), className)}
{...props}
/>
)
})
Toast.displayName = ToastPrimitives.Root.displayName
const ToastAction = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Action>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Action
ref={ref}
className={cn(
'inline-flex h-8 shrink-0 items-center justify-center rounded-md border border-slate-200 bg-transparent px-3 text-sm font-medium transition-colors hover:bg-slate-100 focus:outline-none focus:ring-2 focus:ring-slate-400 focus:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-red-100 group-[.destructive]:hover:border-slate-50 group-[.destructive]:hover:bg-red-100 group-[.destructive]:hover:text-red-600 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600 dark:border-slate-700 dark:text-slate-100 dark:hover:bg-slate-700 dark:hover:text-slate-100 dark:focus:ring-slate-400 dark:focus:ring-offset-slate-900 dark:data-[state=open]:bg-slate-800',
className
)}
{...props}
/>
))
ToastAction.displayName = ToastPrimitives.Action.displayName
const ToastClose = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Close>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Close
ref={ref}
className={cn(
'absolute top-2 right-2 rounded-md p-1 text-slate-500 opacity-0 transition-opacity hover:text-slate-900 focus:opacity-100 focus:outline-none focus:ring-2 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600 dark:hover:text-slate-50',
className
)}
toast-close=""
{...props}
>
<X className="h-4 w-4" />
</ToastPrimitives.Close>
))
ToastClose.displayName = ToastPrimitives.Close.displayName
const ToastTitle = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Title>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Title
ref={ref}
className={cn('text-sm font-semibold', className)}
{...props}
/>
))
ToastTitle.displayName = ToastPrimitives.Title.displayName
const ToastDescription = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Description>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Description
ref={ref}
className={cn('text-sm opacity-90', className)}
{...props}
/>
))
ToastDescription.displayName = ToastPrimitives.Description.displayName
type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>
type ToastActionElement = React.ReactElement<typeof ToastAction>
export {
type ToastProps,
type ToastActionElement,
ToastProvider,
ToastViewport,
Toast,
ToastTitle,
ToastDescription,
ToastClose,
ToastAction,
}
================================================
FILE: src/components/ui/toaster.tsx
================================================
'use client'
import { useToast } from '@/hooks/use-toast'
import {
Toast,
ToastClose,
ToastDescription,
ToastProvider,
ToastTitle,
ToastViewport,
} from '@/ui/toast'
export function Toaster() {
const { toasts } = useToast()
return (
<ToastProvider>
{toasts.map(function ({ id, title, description, action, ...props }) {
return (
<Toast key={id} {...props}>
<div className="grid gap-1">
{title && <ToastTitle>{title}</ToastTitle>}
{description && (
<ToastDescription>{description}</ToastDescription>
)}
</div>
{action}
<ToastClose />
</Toast>
)
})}
<ToastViewport />
</ToastProvider>
)
}
================================================
FILE: src/components/ui/tooltip.tsx
================================================
'use client'
import * as React from 'react'
import * as TooltipPrimitive from '@radix-ui/react-tooltip'
import { cn } from '@/lib/utils'
const TooltipProvider = TooltipPrimitive.Provider
const Tooltip = ({ ...props }) => <TooltipPrimitive.Root {...props} />
Tooltip.displayName = TooltipPrimitive.Tooltip.displayName
const TooltipTrigger = TooltipPrimitive.Trigger
const TooltipContent = React.forwardRef<
React.ElementRef<typeof TooltipPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<TooltipPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
'z-50 overflow-hidden rounded-md border border-slate-100 bg-white px-3 py-1.5 text-sm text-slate-700 shadow-md animate-in fade-in-50 data-[side=bottom]:slide-in-from-top-1 data-[side=top]:slide-in-from-bottom-1 data-[side=left]:slide-in-from-right-1 data-[side=right]:slide-in-from-left-1 dark:border-slate-800 dark:bg-slate-800 dark:text-slate-400',
className
)}
{...props}
/>
))
TooltipContent.displayName = TooltipPrimitive.Content.displayName
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
================================================
FILE: src/config/image.ts
================================================
export const MAX_FILE_SIZE = 5000000 // 5MB
================================================
FILE: src/config/s3.ts
================================================
export const ALLOWED_FILE_TYPES = ['image/png', 'image/jpeg']
export const S3_BUCKET_NAME = 'image-to-alt'
================================================
FILE: src/config/site.ts
================================================
import { NavItem } from '@/src/types/nav'
interface SiteConfig {
name: string
description: string
mainNav: NavItem[]
links: {
youtube: string
github: string
}
}
export const siteConfig: SiteConfig = {
name: 'ImageToAlt',
description: 'Easily create alt-descriptions for your images.',
mainNav: [
{
title: 'Home',
href: '/',
},
],
links: {
github: 'https://github.com/joschan21/image-alt-generator',
youtube: 'https://www.youtube.com/@joshtriedcoding',
},
}
================================================
FILE: src/hooks/use-s3-upload.ts
================================================
import { toast } from '@/hooks/use-toast'
import { MAX_FILE_SIZE } from '@/config/image'
import { FileTooLargeError } from '@/lib/exceptions'
import { s3ResponseSchema } from '@/lib/validations/s3'
interface UseS3UploadReturn {
s3Upload: (file: File) => Promise<{ getUrl: string | null; error: boolean }>
}
const uploadFile = async (file: File) => {
try {
const res = await fetch('/api/image/presign', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
fileType: file.type,
}),
})
const data = await res.json()
const { fields, getUrl, postUrl } = s3ResponseSchema.parse(data)
const outboundToS3 = {
...fields,
'Content-Type': file.type,
file,
}
const formData = new FormData()
Object.entries(outboundToS3).forEach(([key, value]) => {
formData.append(key, value)
})
try {
// Upload to S3
await fetch(postUrl, {
method: 'POST',
body: formData,
})
} catch (error) {
throw new FileTooLargeError()
}
return { getUrl }
} catch (error) {
if (error instanceof FileTooLargeError) {
throw new FileTooLargeError()
}
throw new Error('Internal Server Error')
}
}
export const useS3Upload = (): UseS3UploadReturn => {
const s3Upload = async (file: File) => {
try {
if (file.size > MAX_FILE_SIZE) throw new FileTooLargeError()
// Single file upload
const singleFile = file as File
const { getUrl } = await uploadFile(singleFile)
return { getUrl, error: false }
} catch (error) {
if (error instanceof FileTooLargeError) {
toast({
title: 'Image Too Large',
description: error.message,
})
return { getUrl: null, error: true }
}
toast({
title: 'Internal Server Error',
description: 'There was an error uploading your image.',
})
return { getUrl: null, error: true }
}
}
return { s3Upload }
}
================================================
FILE: src/hooks/use-toast.ts
================================================
// Inspired by react-hot-toast library
import * as React from 'react'
import { ToastActionElement, type ToastProps } from '@/ui/toast'
const TOAST_LIMIT = 1
const TOAST_REMOVE_DELAY = 1000
type ToasterToast = ToastProps & {
id: string
title?: React.ReactNode
description?: React.ReactNode
action?: ToastActionElement
}
const actionTypes = {
ADD_TOAST: 'ADD_TOAST',
UPDATE_TOAST: 'UPDATE_TOAST',
DISMISS_TOAST: 'DISMISS_TOAST',
REMOVE_TOAST: 'REMOVE_TOAST',
} as const
let count = 0
function genId() {
count = (count + 1) % Number.MAX_VALUE
return count.toString()
}
type ActionType = typeof actionTypes
type Action =
| {
type: ActionType['ADD_TOAST']
toast: ToasterToast
}
| {
type: ActionType['UPDATE_TOAST']
toast: Partial<ToasterToast>
}
| {
type: ActionType['DISMISS_TOAST']
toastId?: ToasterToast['id']
}
| {
type: ActionType['REMOVE_TOAST']
toastId?: ToasterToast['id']
}
interface State {
toasts: ToasterToast[]
}
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>()
const addToRemoveQueue = (toastId: string) => {
if (toastTimeouts.has(toastId)) {
return
}
const timeout = setTimeout(() => {
toastTimeouts.delete(toastId)
dispatch({
type: 'REMOVE_TOAST',
toastId: toastId,
})
}, TOAST_REMOVE_DELAY)
toastTimeouts.set(toastId, timeout)
}
export const reducer = (state: State, action: Action): State => {
switch (action.type) {
case 'ADD_TOAST':
return {
...state,
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
}
case 'UPDATE_TOAST':
return {
...state,
toasts: state.toasts.map((t) =>
t.id === action.toast.id ? { ...t, ...action.toast } : t
),
}
case 'DISMISS_TOAST':
const { toastId } = action
// ! Side effects ! - This could be extracted into a dismissToast() action,
// but I'll keep it here for simplicity
if (toastId) {
addToRemoveQueue(toastId)
} else {
state.toasts.forEach((toast) => {
addToRemoveQueue(toast.id)
})
}
return {
...state,
toasts: state.toasts.map((t) =>
t.id === toastId || toastId === undefined
? {
...t,
open: false,
}
: t
),
}
case 'REMOVE_TOAST':
if (action.toastId === undefined) {
return {
...state,
toasts: [],
}
}
return {
...state,
toasts: state.toasts.filter((t) => t.id !== action.toastId),
}
}
}
const listeners: Array<(state: State) => void> = []
let memoryState: State = { toasts: [] }
function dispatch(action: Action) {
memoryState = reducer(memoryState, action)
listeners.forEach((listener) => {
listener(memoryState)
})
}
interface Toast extends Omit<ToasterToast, 'id'> {}
function toast({ ...props }: Toast) {
const id = genId()
const update = (props: ToasterToast) =>
dispatch({
type: 'UPDATE_TOAST',
toast: { ...props, id },
})
const dismiss = () => dispatch({ type: 'DISMISS_TOAST', toastId: id })
dispatch({
type: 'ADD_TOAST',
toast: {
...props,
id,
open: true,
onOpenChange: (open) => {
if (!open) dismiss()
},
},
})
return {
id: id,
dismiss,
update,
}
}
function useToast() {
const [state, setState] = React.useState<State>(memoryState)
React.useEffect(() => {
listeners.push(setState)
return () => {
const index = listeners.indexOf(setState)
if (index > -1) {
listeners.splice(index, 1)
}
}
}, [state])
return {
...state,
toast,
dismiss: (toastId?: string) => dispatch({ type: 'DISMISS_TOAST', toastId }),
}
}
export { useToast, toast }
================================================
FILE: src/hooks/use-upload-file.ts
================================================
import { useEffect, useReducer, useRef } from 'react'
import axios from 'axios'
import { useToast } from './use-toast'
interface State<T> {
data?: T
isLoading: boolean
progress?: number
error?: boolean
}
// discriminated union type
type Action<T> =
| { type: 'loading' }
| { type: 'fetched'; payload: T }
| { type: 'error'; payload: boolean }
| { type: 'progress'; payload: number }
type Options = {
disabled: boolean | undefined
}
export const useUploadFile = <T = unknown>(
url: string,
resourceUrl: string,
options: Options
) => {
const { toast } = useToast()
const { disabled } = options
// Used to prevent state update if the component is unmounted
const cancelRequest = useRef<boolean>(false)
const initialState: State<T> = {
error: undefined,
isLoading: false,
progress: undefined,
data: undefined,
}
// Keep state logic separated
const fetchReducer = (state: State<T>, action: Action<T>): State<T> => {
switch (action.type) {
case 'loading':
return { ...state, isLoading: true }
case 'fetched':
return { ...state, data: action.payload, isLoading: false }
case 'error':
return { ...state, error: action.payload, isLoading: false }
case 'progress':
return { ...state, progress: action.payload }
default:
return state
}
}
const [state, dispatch] = useReducer(fetchReducer, initialState)
useEffect(() => {
// Do nothing if the url is not given
if (!url || disabled) return
cancelRequest.current = false
const fetchData = async () => {
dispatch({ type: 'loading' })
try {
const res = await axios.post(url, resourceUrl, {
headers: {
'Content-Type': 'application/json',
},
onUploadProgress: (progressEvent) => {
const percentCompleted = Math.round(
(progressEvent.loaded * 100) / progressEvent.total!
)
dispatch({ type: 'progress', payload: percentCompleted })
},
})
const data = res.data as T
if (cancelRequest.current) return
dispatch({ type: 'fetched', payload: data })
} catch (error) {
if (cancelRequest.current) return
dispatch({ type: 'error', payload: true })
toast({
title: 'Something went wrong.',
description: 'Please try again later.',
})
}
}
void fetchData()
// Use the cleanup function for avoiding a possible...
// ...state update after the component was unmounted
return () => {
cancelRequest.current = true
}
}, [url])
return state
}
================================================
FILE: src/lib/api-middlewares/with-methods.ts
================================================
import type { NextApiHandler, NextApiRequest, NextApiResponse } from 'next'
export function withMethods(methods: string[], handler: NextApiHandler) {
return async function (req: NextApiRequest, res: NextApiResponse) {
if (!req.method || !methods.includes(req.method)) {
return res.status(405).end()
}
return handler(req, res)
}
}
================================================
FILE: src/lib/exceptions.ts
================================================
import { MAX_FILE_SIZE } from '../config/image'
export class FileTooLargeError extends Error {
constructor(
message = `Images cannot be larger than ${MAX_FILE_SIZE / 1000000}MB.`
) {
super(message)
}
}
================================================
FILE: src/lib/s3.ts
================================================
import S3 from 'aws-sdk/clients/s3'
export const s3 = new S3({
apiVersion: '2006-03-01',
accessKeyId: process.env.S3_ACCESS_KEY_ID,
secretAccessKey: process.env.S3_SECRET_ACCESS_KEY,
region: process.env.S3_REGION,
signatureVersion: 'v4',
})
================================================
FILE: src/lib/utils.ts
================================================
import { ClassValue, clsx } from 'clsx'
import { twMerge } from 'tailwind-merge'
import { ALLOWED_FILE_TYPES } from '../config/s3'
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
export function validateFileType(file: File) {
return ALLOWED_FILE_TYPES.includes(file.type)
}
================================================
FILE: src/lib/validations/s3.ts
================================================
import { ALLOWED_FILE_TYPES } from '@/src/config/s3'
import { z } from 'zod'
export const fileTypeSchema = z.string().refine(
(value) => {
return ALLOWED_FILE_TYPES.includes(value)
},
{
message: 'Invalid file type',
}
)
/**
* fields: provided by AWS to authenticate request
* key: file name in s3 bucket
* getUrl: url to view the file from s3 bucket
* postUrl: url that allows post request to s3 bucket
*/
export const s3ResponseSchema = z.object({
getUrl: z.string(),
postUrl: z.string(),
fields: z.object({
Policy: z.string(),
'X-Amz-Algorithm': z.string(),
'X-Amz-Credential': z.string(),
'X-Amz-Date': z.string(),
'X-Amz-Signature': z.string(),
bucket: z.string(),
key: z.string(),
}),
})
================================================
FILE: src/pages/api/image/presign.ts
================================================
import { NextApiRequest, NextApiResponse } from 'next'
import { MAX_FILE_SIZE } from '@/src/config/image'
import { S3_BUCKET_NAME } from '@/src/config/s3'
import { withMethods } from '@/src/lib/api-middlewares/with-methods'
import { s3 } from '@/src/lib/s3'
import { fileTypeSchema } from '@/src/lib/validations/s3'
import { PresignResponseData } from '@/src/types/api/image'
import { nanoid } from 'nanoid'
import { z } from 'zod'
const handler = async (
req: NextApiRequest,
res: NextApiResponse<PresignResponseData>
) => {
const reqFileType = req.body.fileType
const fileId = nanoid()
try {
// validate file extension, will throw if invalid
const fileType = fileTypeSchema.parse(reqFileType)
const fileExtension = fileType.split('/')[1]
const key = `${fileId}.${fileExtension}`
// Create a presigned POST request to upload the file to S3
const { url: postUrl, fields } = (await new Promise((resolve, reject) => {
s3.createPresignedPost(
{
Bucket: S3_BUCKET_NAME,
Fields: { key },
Expires: 60,
Conditions: [
['content-length-range', 0, MAX_FILE_SIZE],
['starts-with', '$Content-Type', 'image/'],
],
},
(err, signed) => {
if (err) return reject(err)
resolve(signed)
}
)
})) as { url: string; fields: any }
const getUrl = await s3.getSignedUrlPromise('getObject', {
Bucket: S3_BUCKET_NAME,
Key: key,
})
return res.status(200).json({ postUrl, getUrl, fields })
} catch (error) {
if (error instanceof z.ZodError) {
return res.status(415).json(error.issues)
}
if (error instanceof Error) {
return res.status(500).json({ error: error.message })
}
return res.status(500).json({ error: 'Something went wrong' })
}
}
export default withMethods(['POST'], handler)
================================================
FILE: src/pages/api/image/process.ts
================================================
import { NextApiRequest, NextApiResponse } from 'next'
import { ImageResponseData } from '@/src/types/api/image'
import { withMethods } from '@/lib/api-middlewares/with-methods'
type PartialReplicateResponse = {
urls: {
get: string
cancel: string
}
}
export const config = {
api: {
bodyParser: {
sizeLimit: '4mb',
},
},
}
async function handler(
req: NextApiRequest,
res: NextApiResponse<ImageResponseData>
) {
if (!req.body)
return res
.status(400)
.json({ success: false, alt: '', message: 'No image data' })
const imageBase64 = req.body
try {
const startResponse = await fetch(
'https://api.replicate.com/v1/predictions',
{
method: 'POST',
headers: {
Authorization: `Token ${process.env.REPLICATE_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
// Pinned to a specific version of Stable Diffusion
// See https://replicate.com/stability-ai/stable-diffussion/versions
version:
'9a34a6339872a03f45236f114321fb51fc7aa8269d38ae0ce5334969981e4cd8',
input: {
model: 'conceptual-captions',
use_beam_search: false,
image: imageBase64,
},
}),
}
)
let jsonStartResponse =
(await startResponse.json()) as PartialReplicateResponse
let endpointUrl = jsonStartResponse.urls.get
// GET request to get the status of alt text generation process & return the result when it's ready
let altText: string | null = null
while (!altText) {
// Poll in 1s intervals until the alt text is ready
let finalResponse = await fetch(endpointUrl, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
Authorization: 'Token ' + process.env.REPLICATE_API_KEY,
},
})
let jsonFinalResponse = await finalResponse.json()
if (jsonFinalResponse.status === 'succeeded') {
altText = jsonFinalResponse.output as string
res.status(200).json({
success: true,
alt: altText,
message: 'Alt text generated successfully',
})
break
} else if (jsonFinalResponse.status === 'failed') {
res.status(503).json({
success: false,
alt: '',
message: 'Image could not be processed',
})
break
} else {
await new Promise((resolve) => setTimeout(resolve, 1000))
}
}
} catch (error) {
res
.status(500)
.json({ success: false, alt: '', message: 'Internal server error' })
}
}
export default withMethods(['POST'], handler)
================================================
FILE: src/styles/globals.css
================================================
@tailwind base;
@tailwind components;
@tailwind utilities;
================================================
FILE: src/types/api/image.ts
================================================
import { s3ResponseSchema } from '@/src/lib/validations/s3'
import { ZodIssue, z } from 'zod'
export type ImageResponseData = {
success: boolean
alt: string
message: string
}
export type PresignResponseData =
| z.infer<typeof s3ResponseSchema>
| ZodIssue[]
| { error: string }
================================================
FILE: src/types/nav.ts
================================================
export interface NavItem {
title: string
href?: string
disabled?: boolean
external?: boolean
}
================================================
FILE: tailwind.config.js
================================================
const { fontFamily } = require("tailwindcss/defaultTheme")
/** @type {import('tailwindcss').Config} */
module.exports = {
darkMode: ["class"],
content: [
"./app/**/*.{js,ts,jsx,tsx}",
"./pages/**/*.{js,ts,jsx,tsx}",
"./components/**/*.{js,ts,jsx,tsx}",
// Or if using `src` directory:
"./src/**/*.{js,ts,jsx,tsx}",
],
theme: {
container: {
center: true,
padding: "1.5rem",
screens: {
"2xl": "1360px",
},
},
extend: {
fontFamily: {
sans: ["var(--font-inter)", ...fontFamily.sans],
},
keyframes: {
"accordion-down": {
from: { height: 0 },
to: { height: "var(--radix-accordion-content-height)" },
},
"accordion-up": {
from: { height: "var(--radix-accordion-content-height)" },
to: { height: 0 },
},
},
animation: {
"accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out",
},
},
},
plugins: [require("tailwindcss-animate")],
}
================================================
FILE: tsconfig.json
================================================
{
"compilerOptions": {
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"incremental": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"baseUrl": ".",
"paths": {
"@/*": ["./*"],
"@/ui/*": ["./src/components/ui/*"],
"@/hooks/*": ["./src/hooks/*"],
"@/config/*": ["./src/config/*"],
"@/styles/*": ["./src/styles/*"],
"@/lib/*": ["./src/lib/*"],
"@/components/*": ["./src/components/*"]
},
"plugins": [
{
"name": "next"
}
]
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}
gitextract_prl9_fiq/ ├── .editorconfig ├── .eslintignore ├── .eslintrc.json ├── .gitignore ├── .prettierignore ├── .vscode/ │ └── settings.json ├── README.md ├── next-env.d.ts ├── next.config.mjs ├── package.json ├── postcss.config.js ├── prettier.config.js ├── src/ │ ├── app/ │ │ ├── head.tsx │ │ ├── layout.tsx │ │ ├── page.tsx │ │ ├── privacy-policy/ │ │ │ ├── head.tsx │ │ │ └── page.tsx │ │ └── terms/ │ │ ├── head.tsx │ │ └── page.tsx │ ├── components/ │ │ ├── icons.tsx │ │ ├── main-nav.tsx │ │ ├── site-header.tsx │ │ └── ui/ │ │ ├── accordion.tsx │ │ ├── alert-dialog.tsx │ │ ├── aspect-ratio.tsx │ │ ├── avatar.tsx │ │ ├── button.tsx │ │ ├── checkbox.tsx │ │ ├── collapsible.tsx │ │ ├── context-menu.tsx │ │ ├── dialog.tsx │ │ ├── dropdown-menu.tsx │ │ ├── file-input.tsx │ │ ├── hover-card.tsx │ │ ├── image-upload.tsx │ │ ├── input.tsx │ │ ├── label.tsx │ │ ├── menubar.tsx │ │ ├── navigation-menu.tsx │ │ ├── popover.tsx │ │ ├── progress.tsx │ │ ├── radio-group.tsx │ │ ├── scroll-area.tsx │ │ ├── select.tsx │ │ ├── separator.tsx │ │ ├── slider.tsx │ │ ├── spinner.tsx │ │ ├── switch.tsx │ │ ├── tabs.tsx │ │ ├── textarea.tsx │ │ ├── toast.tsx │ │ ├── toaster.tsx │ │ └── tooltip.tsx │ ├── config/ │ │ ├── image.ts │ │ ├── s3.ts │ │ └── site.ts │ ├── hooks/ │ │ ├── use-s3-upload.ts │ │ ├── use-toast.ts │ │ └── use-upload-file.ts │ ├── lib/ │ │ ├── api-middlewares/ │ │ │ └── with-methods.ts │ │ ├── exceptions.ts │ │ ├── s3.ts │ │ ├── utils.ts │ │ └── validations/ │ │ └── s3.ts │ ├── pages/ │ │ └── api/ │ │ └── image/ │ │ ├── presign.ts │ │ └── process.ts │ ├── styles/ │ │ └── globals.css │ └── types/ │ ├── api/ │ │ └── image.ts │ └── nav.ts ├── tailwind.config.js └── tsconfig.json
SYMBOL INDEX (48 symbols across 25 files)
FILE: src/app/layout.tsx
type RootLayoutProps (line 15) | interface RootLayoutProps {
function RootLayout (line 19) | function RootLayout({ children }: RootLayoutProps) {
FILE: src/components/icons.tsx
type Icon (line 9) | type Icon = LucideIcon
FILE: src/components/main-nav.tsx
type MainNavProps (line 17) | interface MainNavProps {
function MainNav (line 21) | function MainNav({ items }: MainNavProps) {
FILE: src/components/site-header.tsx
function SiteHeader (line 8) | function SiteHeader() {
FILE: src/components/ui/button.tsx
type ButtonProps (line 37) | interface ButtonProps
FILE: src/components/ui/file-input.tsx
type FileWithUrl (line 18) | interface FileWithUrl {
type Action (line 31) | type Action = ReturnType<typeof addFilesToInput>
type State (line 32) | type State = FileWithUrl[]
type InputProps (line 34) | interface InputProps
FILE: src/components/ui/image-upload.tsx
type ImageUploadProps (line 13) | interface ImageUploadProps extends React.HTMLAttributes<HTMLTableRowElem...
FILE: src/components/ui/input.tsx
type InputProps (line 5) | interface InputProps
FILE: src/components/ui/progress.tsx
type ProgressProps (line 9) | interface ProgressProps
FILE: src/components/ui/spinner.tsx
type SpinnerProps (line 6) | interface SpinnerProps extends HTMLAttributes<HTMLDivElement> {}
FILE: src/components/ui/textarea.tsx
type TextareaProps (line 5) | interface TextareaProps
FILE: src/components/ui/toast.tsx
type ToastProps (line 114) | type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>
type ToastActionElement (line 116) | type ToastActionElement = React.ReactElement<typeof ToastAction>
FILE: src/components/ui/toaster.tsx
function Toaster (line 13) | function Toaster() {
FILE: src/config/image.ts
constant MAX_FILE_SIZE (line 1) | const MAX_FILE_SIZE = 5000000 // 5MB
FILE: src/config/s3.ts
constant ALLOWED_FILE_TYPES (line 1) | const ALLOWED_FILE_TYPES = ['image/png', 'image/jpeg']
constant S3_BUCKET_NAME (line 2) | const S3_BUCKET_NAME = 'image-to-alt'
FILE: src/config/site.ts
type SiteConfig (line 3) | interface SiteConfig {
FILE: src/hooks/use-s3-upload.ts
type UseS3UploadReturn (line 7) | interface UseS3UploadReturn {
FILE: src/hooks/use-toast.ts
constant TOAST_LIMIT (line 5) | const TOAST_LIMIT = 1
constant TOAST_REMOVE_DELAY (line 6) | const TOAST_REMOVE_DELAY = 1000
type ToasterToast (line 8) | type ToasterToast = ToastProps & {
function genId (line 24) | function genId() {
type ActionType (line 29) | type ActionType = typeof actionTypes
type Action (line 31) | type Action =
type State (line 49) | interface State {
function dispatch (line 129) | function dispatch(action: Action) {
type Toast (line 136) | interface Toast extends Omit<ToasterToast, 'id'> {}
function toast (line 138) | function toast({ ...props }: Toast) {
function useToast (line 167) | function useToast() {
FILE: src/hooks/use-upload-file.ts
type State (line 6) | interface State<T> {
type Action (line 14) | type Action<T> =
type Options (line 20) | type Options = {
FILE: src/lib/api-middlewares/with-methods.ts
function withMethods (line 3) | function withMethods(methods: string[], handler: NextApiHandler) {
FILE: src/lib/exceptions.ts
class FileTooLargeError (line 3) | class FileTooLargeError extends Error {
method constructor (line 4) | constructor(
FILE: src/lib/utils.ts
function cn (line 6) | function cn(...inputs: ClassValue[]) {
function validateFileType (line 10) | function validateFileType(file: File) {
FILE: src/pages/api/image/process.ts
type PartialReplicateResponse (line 6) | type PartialReplicateResponse = {
function handler (line 21) | async function handler(
FILE: src/types/api/image.ts
type ImageResponseData (line 4) | type ImageResponseData = {
type PresignResponseData (line 10) | type PresignResponseData =
FILE: src/types/nav.ts
type NavItem (line 1) | interface NavItem {
Condensed preview — 71 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (138K chars).
[
{
"path": ".editorconfig",
"chars": 166,
"preview": "# editorconfig.org\nroot = true\n\n[*]\ncharset = utf-8\nend_of_line = lf\nindent_size = 2\nindent_style = space\ninsert_final_n"
},
{
"path": ".eslintignore",
"chars": 43,
"preview": "dist/*\n.cache\npublic\nnode_modules\n*.esm.js\n"
},
{
"path": ".eslintrc.json",
"chars": 500,
"preview": "{\n \"$schema\": \"https://json.schemastore.org/eslintrc\",\n \"root\": true,\n \"extends\": [\n \"next/core-web-vitals\",\n \""
},
{
"path": ".gitignore",
"chars": 400,
"preview": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\nnode_modules\n.pnp\n"
},
{
"path": ".prettierignore",
"chars": 70,
"preview": "cache\n.cache\npackage.json\npackage-lock.json\npublic\nCHANGELOG.md\n.yarn\n"
},
{
"path": ".vscode/settings.json",
"chars": 107,
"preview": "{\n \"typescript.tsdk\": \"node_modules\\\\typescript\\\\lib\",\n \"typescript.enablePromptUseWorkspaceTsdk\": true\n}"
},
{
"path": "README.md",
"chars": 1832,
"preview": "# Next alt generator\n\nA Next.js 13 project for generating image alt tags automatically and in bulk.\n\n## Features\n\n- Radi"
},
{
"path": "next-env.d.ts",
"chars": 201,
"preview": "/// <reference types=\"next\" />\n/// <reference types=\"next/image-types/global\" />\n\n// NOTE: This file should not be edite"
},
{
"path": "next.config.mjs",
"chars": 356,
"preview": "/** @type {import('next').NextConfig} */\nconst nextConfig = {\n reactStrictMode: true,\n experimental: {\n appDir: tru"
},
{
"path": "package.json",
"chars": 2342,
"preview": "{\n \"name\": \"next-template\",\n \"version\": \"0.0.1\",\n \"private\": true,\n \"scripts\": {\n \"dev\": \"next dev\",\n \"build\":"
},
{
"path": "postcss.config.js",
"chars": 82,
"preview": "module.exports = {\n plugins: {\n tailwindcss: {},\n autoprefixer: {},\n },\n}\n"
},
{
"path": "prettier.config.js",
"chars": 740,
"preview": "/** @type {import('prettier').Config} */\nmodule.exports = {\n endOfLine: \"lf\",\n semi: false,\n singleQuote: true,\n tab"
},
{
"path": "src/app/head.tsx",
"chars": 143,
"preview": "import { FC } from 'react'\n\nconst head: FC = () => {\n return <title>ImageToAlt - Generate alt tags from images</title>\n"
},
{
"path": "src/app/layout.tsx",
"chars": 971,
"preview": "import { Inter as FontSans } from '@next/font/google'\n\nimport '@/styles/globals.css'\nimport { Toaster } from '@/ui/toast"
},
{
"path": "src/app/page.tsx",
"chars": 2156,
"preview": "'use client'\n\nimport { FC } from 'react'\nimport { Button, buttonVariants } from '@/ui/button'\nimport { FileInput } from "
},
{
"path": "src/app/privacy-policy/head.tsx",
"chars": 128,
"preview": "import { FC } from 'react'\n\nconst head: FC = () => {\n return <title>Privacy Policy - ImageToAlt</title>\n}\n\nexport defau"
},
{
"path": "src/app/privacy-policy/page.tsx",
"chars": 4190,
"preview": "import { FC } from 'react'\nimport Link from 'next/link'\n\nconst page: FC = () => {\n return (\n <div className=\"text-sl"
},
{
"path": "src/app/terms/head.tsx",
"chars": 134,
"preview": "import { FC } from 'react'\n\nconst head: FC = () => {\n return <title>Terms and Conditions - ImageToAlt</title>\n}\n\nexport"
},
{
"path": "src/app/terms/page.tsx",
"chars": 4692,
"preview": "import { FC } from 'react'\nimport Head from 'next/head'\n\nconst page: FC = () => {\n return (\n <div className=\"text-sl"
},
{
"path": "src/components/icons.tsx",
"chars": 6616,
"preview": "import {\n Laptop,\n LucideProps,\n Moon,\n SunMedium,\n type Icon as LucideIcon,\n} from 'lucide-react'\n\nexport type Ico"
},
{
"path": "src/components/main-nav.tsx",
"chars": 2505,
"preview": "import Link from 'next/link'\nimport { NavItem } from '@/src/types/nav'\n\nimport { siteConfig } from '@/config/site'\nimpor"
},
{
"path": "src/components/site-header.tsx",
"chars": 1741,
"preview": "import Link from 'next/link'\n\nimport { siteConfig } from '@/config/site'\nimport { Icons } from '@/components/icons'\nimpo"
},
{
"path": "src/components/ui/accordion.tsx",
"chars": 2060,
"preview": "'use client'\n\nimport * as React from 'react'\nimport * as AccordionPrimitive from '@radix-ui/react-accordion'\nimport { Ch"
},
{
"path": "src/components/ui/alert-dialog.tsx",
"chars": 5188,
"preview": "'use client'\n\nimport * as React from 'react'\nimport * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog'\n\nimpor"
},
{
"path": "src/components/ui/aspect-ratio.tsx",
"chars": 154,
"preview": "'use client'\n\nimport * as AspectRatioPrimitive from '@radix-ui/react-aspect-ratio'\n\nconst AspectRatio = AspectRatioPrimi"
},
{
"path": "src/components/ui/avatar.tsx",
"chars": 1441,
"preview": "'use client'\n\nimport * as React from 'react'\nimport * as AvatarPrimitive from '@radix-ui/react-avatar'\n\nimport { cn } fr"
},
{
"path": "src/components/ui/button.tsx",
"chars": 2418,
"preview": "import * as React from 'react'\nimport Link from 'next/link'\nimport { VariantProps, cva } from 'class-variance-authority'"
},
{
"path": "src/components/ui/checkbox.tsx",
"chars": 1032,
"preview": "'use client'\n\nimport * as React from 'react'\nimport * as CheckboxPrimitive from '@radix-ui/react-checkbox'\nimport { Chec"
},
{
"path": "src/components/ui/collapsible.tsx",
"chars": 329,
"preview": "'use client'\n\nimport * as CollapsiblePrimitive from '@radix-ui/react-collapsible'\n\nconst Collapsible = CollapsiblePrimit"
},
{
"path": "src/components/ui/context-menu.tsx",
"chars": 6786,
"preview": "'use client'\n\nimport * as React from 'react'\nimport * as ContextMenuPrimitive from '@radix-ui/react-context-menu'\nimport"
},
{
"path": "src/components/ui/dialog.tsx",
"chars": 3800,
"preview": "'use client'\n\nimport * as React from 'react'\nimport * as DialogPrimitive from '@radix-ui/react-dialog'\nimport { X } from"
},
{
"path": "src/components/ui/dropdown-menu.tsx",
"chars": 6992,
"preview": "'use client'\n\nimport * as React from 'react'\nimport * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'\nimpo"
},
{
"path": "src/components/ui/file-input.tsx",
"chars": 10497,
"preview": "'use client'\n\nimport {\n forwardRef,\n useReducer,\n useState,\n type ChangeEvent,\n type DragEvent,\n} from 'react'\nimpo"
},
{
"path": "src/components/ui/hover-card.tsx",
"chars": 914,
"preview": "'use client'\n\nimport * as React from 'react'\nimport * as HoverCardPrimitive from '@radix-ui/react-hover-card'\n\nimport { "
},
{
"path": "src/components/ui/image-upload.tsx",
"chars": 2554,
"preview": "'use client'\n\nimport { forwardRef } from 'react'\nimport Image from 'next/image'\nimport { useUploadFile } from '@/src/hoo"
},
{
"path": "src/components/ui/input.tsx",
"chars": 782,
"preview": "import * as React from 'react'\n\nimport { cn } from '@/lib/utils'\n\nexport interface InputProps\n extends React.InputHTMLA"
},
{
"path": "src/components/ui/label.tsx",
"chars": 587,
"preview": "'use client'\n\nimport * as React from 'react'\nimport * as LabelPrimitive from '@radix-ui/react-label'\n\nimport { cn } from"
},
{
"path": "src/components/ui/menubar.tsx",
"chars": 7658,
"preview": "'use client'\n\nimport * as React from 'react'\nimport * as MenubarPrimitive from '@radix-ui/react-menubar'\nimport { Check,"
},
{
"path": "src/components/ui/navigation-menu.tsx",
"chars": 5119,
"preview": "import * as React from 'react'\nimport * as NavigationMenuPrimitive from '@radix-ui/react-navigation-menu'\nimport { cva }"
},
{
"path": "src/components/ui/popover.tsx",
"chars": 1027,
"preview": "'use client'\n\nimport * as React from 'react'\nimport * as PopoverPrimitive from '@radix-ui/react-popover'\n\nimport { cn } "
},
{
"path": "src/components/ui/progress.tsx",
"chars": 1501,
"preview": "'use client'\n\nimport * as React from 'react'\nimport * as ProgressPrimitive from '@radix-ui/react-progress'\nimport { cva "
},
{
"path": "src/components/ui/radio-group.tsx",
"chars": 1616,
"preview": "'use client'\n\nimport * as React from 'react'\nimport * as RadioGroupPrimitive from '@radix-ui/react-radio-group'\nimport {"
},
{
"path": "src/components/ui/scroll-area.tsx",
"chars": 1668,
"preview": "'use client'\n\nimport * as React from 'react'\nimport * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area'\n\nimport "
},
{
"path": "src/components/ui/select.tsx",
"chars": 3813,
"preview": "'use client'\n\nimport * as React from 'react'\nimport * as SelectPrimitive from '@radix-ui/react-select'\nimport { Check, C"
},
{
"path": "src/components/ui/separator.tsx",
"chars": 782,
"preview": "'use client'\n\nimport * as React from 'react'\nimport * as SeparatorPrimitive from '@radix-ui/react-separator'\n\nimport { c"
},
{
"path": "src/components/ui/slider.tsx",
"chars": 1183,
"preview": "'use client'\n\nimport * as React from 'react'\nimport * as SliderPrimitive from '@radix-ui/react-slider'\n\nimport { cn } fr"
},
{
"path": "src/components/ui/spinner.tsx",
"chars": 392,
"preview": "import type { FC, HTMLAttributes } from 'react'\nimport { Loader2 } from 'lucide-react'\n\nimport { cn } from '@/lib/utils'"
},
{
"path": "src/components/ui/switch.tsx",
"chars": 1225,
"preview": "'use client'\n\nimport * as React from 'react'\nimport * as SwitchPrimitives from '@radix-ui/react-switch'\n\nimport { cn } f"
},
{
"path": "src/components/ui/tabs.tsx",
"chars": 1780,
"preview": "'use client'\n\nimport * as React from 'react'\nimport * as TabsPrimitive from '@radix-ui/react-tabs'\n\nimport { cn } from '"
},
{
"path": "src/components/ui/textarea.tsx",
"chars": 812,
"preview": "import * as React from 'react'\n\nimport { cn } from '@/lib/utils'\n\nexport interface TextareaProps\n extends React.Textare"
},
{
"path": "src/components/ui/toast.tsx",
"chars": 5154,
"preview": "import * as React from 'react'\nimport * as ToastPrimitives from '@radix-ui/react-toast'\nimport { VariantProps, cva } fro"
},
{
"path": "src/components/ui/toaster.tsx",
"chars": 775,
"preview": "'use client'\n\nimport { useToast } from '@/hooks/use-toast'\nimport {\n Toast,\n ToastClose,\n ToastDescription,\n ToastPr"
},
{
"path": "src/components/ui/tooltip.tsx",
"chars": 1212,
"preview": "'use client'\n\nimport * as React from 'react'\nimport * as TooltipPrimitive from '@radix-ui/react-tooltip'\n\nimport { cn } "
},
{
"path": "src/config/image.ts",
"chars": 44,
"preview": "export const MAX_FILE_SIZE = 5000000 // 5MB\n"
},
{
"path": "src/config/s3.ts",
"chars": 107,
"preview": "export const ALLOWED_FILE_TYPES = ['image/png', 'image/jpeg']\nexport const S3_BUCKET_NAME = 'image-to-alt'\n"
},
{
"path": "src/config/site.ts",
"chars": 517,
"preview": "import { NavItem } from '@/src/types/nav'\n\ninterface SiteConfig {\n name: string\n description: string\n mainNav: NavIte"
},
{
"path": "src/hooks/use-s3-upload.ts",
"chars": 2058,
"preview": "import { toast } from '@/hooks/use-toast'\n\nimport { MAX_FILE_SIZE } from '@/config/image'\nimport { FileTooLargeError } f"
},
{
"path": "src/hooks/use-toast.ts",
"chars": 3913,
"preview": "// Inspired by react-hot-toast library\nimport * as React from 'react'\nimport { ToastActionElement, type ToastProps } fro"
},
{
"path": "src/hooks/use-upload-file.ts",
"chars": 2682,
"preview": "import { useEffect, useReducer, useRef } from 'react'\nimport axios from 'axios'\n\nimport { useToast } from './use-toast'\n"
},
{
"path": "src/lib/api-middlewares/with-methods.ts",
"chars": 354,
"preview": "import type { NextApiHandler, NextApiRequest, NextApiResponse } from 'next'\n\nexport function withMethods(methods: string"
},
{
"path": "src/lib/exceptions.ts",
"chars": 217,
"preview": "import { MAX_FILE_SIZE } from '../config/image'\n\nexport class FileTooLargeError extends Error {\n constructor(\n messa"
},
{
"path": "src/lib/s3.ts",
"chars": 252,
"preview": "import S3 from 'aws-sdk/clients/s3'\n\nexport const s3 = new S3({\n apiVersion: '2006-03-01',\n accessKeyId: process.env.S"
},
{
"path": "src/lib/utils.ts",
"chars": 310,
"preview": "import { ClassValue, clsx } from 'clsx'\nimport { twMerge } from 'tailwind-merge'\n\nimport { ALLOWED_FILE_TYPES } from '.."
},
{
"path": "src/lib/validations/s3.ts",
"chars": 753,
"preview": "import { ALLOWED_FILE_TYPES } from '@/src/config/s3'\nimport { z } from 'zod'\n\nexport const fileTypeSchema = z.string().r"
},
{
"path": "src/pages/api/image/presign.ts",
"chars": 1900,
"preview": "import { NextApiRequest, NextApiResponse } from 'next'\nimport { MAX_FILE_SIZE } from '@/src/config/image'\nimport { S3_BU"
},
{
"path": "src/pages/api/image/process.ts",
"chars": 2711,
"preview": "import { NextApiRequest, NextApiResponse } from 'next'\nimport { ImageResponseData } from '@/src/types/api/image'\n\nimport"
},
{
"path": "src/styles/globals.css",
"chars": 59,
"preview": "@tailwind base;\n@tailwind components;\n@tailwind utilities;\n"
},
{
"path": "src/types/api/image.ts",
"chars": 291,
"preview": "import { s3ResponseSchema } from '@/src/lib/validations/s3'\nimport { ZodIssue, z } from 'zod'\n\nexport type ImageResponse"
},
{
"path": "src/types/nav.ts",
"chars": 103,
"preview": "export interface NavItem {\n title: string\n href?: string\n disabled?: boolean\n external?: boolean\n}\n"
},
{
"path": "tailwind.config.js",
"chars": 1076,
"preview": "const { fontFamily } = require(\"tailwindcss/defaultTheme\")\n\n/** @type {import('tailwindcss').Config} */\nmodule.exports ="
},
{
"path": "tsconfig.json",
"chars": 880,
"preview": "{\n \"compilerOptions\": {\n \"lib\": [\"dom\", \"dom.iterable\", \"esnext\"],\n \"allowJs\": true,\n \"skipLibCheck\": true,\n "
}
]
About this extraction
This page contains the full source code of the joschan21/image-alt-generator GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 71 files (126.6 KB), approximately 35.5k tokens, and a symbol index with 48 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.