From 549c3d7b00762a3dd0614cfabb0d60faf1539504 Mon Sep 17 00:00:00 2001 From: Boostvolt <51777660+boostvolt@users.noreply.github.com> Date: Sun, 6 Aug 2023 16:49:46 +0200 Subject: [PATCH] initial commit --- .eslintignore | 2 + .eslintrc.cjs | 76 + .gitignore | 35 + .npmrc | 1 + .nvmrc | 1 + .prettierignore | 2 + CODE_OF_CONDUCT.md | 119 + LICENSE.md | 1 + README.md | 5 + SECURITY.md | 6 + app/favicon.ico | Bin 0 -> 109028 bytes app/layout.tsx | 40 + app/page.tsx | 300 ++ components.json | 16 + components/icons/apple-music.tsx | 34 + components/theme-provider.tsx | 9 + components/theme-toggle.tsx | 19 + components/ui/alert.tsx | 43 + components/ui/avatar.tsx | 40 + components/ui/button.tsx | 47 + components/ui/card.tsx | 41 + components/ui/dropdown-menu.tsx | 181 + components/ui/form.tsx | 129 + components/ui/input.tsx | 22 + components/ui/label.tsx | 19 + components/ui/radio-group.tsx | 38 + components/ui/separator.tsx | 22 + components/ui/switch.tsx | 29 + components/ui/tabs.tsx | 55 + components/ui/tooltip.tsx | 30 + lib/fonts/sf-pro-display.ts | 96 + lib/utils.ts | 6 + next.config.mjs | 5 + package.json | 65 + pnpm-lock.yaml | 4021 +++++++++++++++++ postcss.config.js | 6 + prettier.config.js | 7 + public/assets/brand/banner.png | Bin 0 -> 423007 bytes public/assets/brand/logo.png | Bin 0 -> 64803 bytes public/assets/fonts/SFProDisplay-Black.woff2 | Bin 0 -> 92456 bytes .../fonts/SFProDisplay-BlackItalic.woff2 | Bin 0 -> 56228 bytes public/assets/fonts/SFProDisplay-Bold.woff2 | Bin 0 -> 98712 bytes .../fonts/SFProDisplay-BoldItalic.woff2 | Bin 0 -> 61316 bytes public/assets/fonts/SFProDisplay-Heavy.woff2 | Bin 0 -> 100352 bytes .../fonts/SFProDisplay-HeavyItalic.woff2 | Bin 0 -> 62200 bytes public/assets/fonts/SFProDisplay-Light.woff2 | Bin 0 -> 99176 bytes .../fonts/SFProDisplay-LightItalic.woff2 | Bin 0 -> 62612 bytes public/assets/fonts/SFProDisplay-Medium.woff2 | Bin 0 -> 99572 bytes .../fonts/SFProDisplay-MediumItalic.woff2 | Bin 0 -> 62144 bytes .../assets/fonts/SFProDisplay-Regular.woff2 | Bin 0 -> 89452 bytes .../fonts/SFProDisplay-RegularItalic.woff2 | Bin 0 -> 56304 bytes .../assets/fonts/SFProDisplay-Semibold.woff2 | Bin 0 -> 100384 bytes .../fonts/SFProDisplay-SemiboldItalic.woff2 | Bin 0 -> 62464 bytes public/assets/fonts/SFProDisplay-Thin.woff2 | Bin 0 -> 99404 bytes .../fonts/SFProDisplay-ThinItalic.woff2 | Bin 0 -> 62832 bytes .../fonts/SFProDisplay-Ultralight.woff2 | Bin 0 -> 98500 bytes .../fonts/SFProDisplay-UltralightItalic.woff2 | Bin 0 -> 61864 bytes public/assets/gradients/0.png | Bin 0 -> 179541 bytes public/assets/gradients/1.png | Bin 0 -> 161177 bytes public/assets/gradients/10.png | Bin 0 -> 328909 bytes public/assets/gradients/11.png | Bin 0 -> 168513 bytes public/assets/gradients/12.png | Bin 0 -> 152255 bytes public/assets/gradients/13.png | Bin 0 -> 250864 bytes public/assets/gradients/14.png | Bin 0 -> 177482 bytes public/assets/gradients/15.png | Bin 0 -> 124425 bytes public/assets/gradients/16.png | Bin 0 -> 218290 bytes public/assets/gradients/17.png | Bin 0 -> 206615 bytes public/assets/gradients/18.png | Bin 0 -> 213583 bytes public/assets/gradients/19.png | Bin 0 -> 160265 bytes public/assets/gradients/2.png | Bin 0 -> 226769 bytes public/assets/gradients/20.png | Bin 0 -> 230571 bytes public/assets/gradients/21.png | Bin 0 -> 90539 bytes public/assets/gradients/22.png | Bin 0 -> 228202 bytes public/assets/gradients/23.png | Bin 0 -> 147567 bytes public/assets/gradients/24.png | Bin 0 -> 184441 bytes public/assets/gradients/25.png | Bin 0 -> 171399 bytes public/assets/gradients/26.png | Bin 0 -> 146282 bytes public/assets/gradients/27.png | Bin 0 -> 182398 bytes public/assets/gradients/28.png | Bin 0 -> 258312 bytes public/assets/gradients/29.png | Bin 0 -> 175763 bytes public/assets/gradients/3.png | Bin 0 -> 95479 bytes public/assets/gradients/30.png | Bin 0 -> 134959 bytes public/assets/gradients/31.png | Bin 0 -> 143174 bytes public/assets/gradients/4.png | Bin 0 -> 113136 bytes public/assets/gradients/5.png | Bin 0 -> 155708 bytes public/assets/gradients/6.png | Bin 0 -> 353825 bytes public/assets/gradients/7.png | Bin 0 -> 182075 bytes public/assets/gradients/8.png | Bin 0 -> 216671 bytes public/assets/gradients/9.png | Bin 0 -> 275794 bytes styles/globals.css | 78 + tailwind.config.js | 74 + tsconfig.json | 28 + 92 files changed, 5748 insertions(+) create mode 100644 .eslintignore create mode 100644 .eslintrc.cjs create mode 100644 .gitignore create mode 100644 .npmrc create mode 100644 .nvmrc create mode 100644 .prettierignore create mode 100644 CODE_OF_CONDUCT.md create mode 100644 LICENSE.md create mode 100644 README.md create mode 100644 SECURITY.md create mode 100644 app/favicon.ico create mode 100644 app/layout.tsx create mode 100644 app/page.tsx create mode 100644 components.json create mode 100644 components/icons/apple-music.tsx create mode 100644 components/theme-provider.tsx create mode 100644 components/theme-toggle.tsx create mode 100644 components/ui/alert.tsx create mode 100644 components/ui/avatar.tsx create mode 100644 components/ui/button.tsx create mode 100644 components/ui/card.tsx create mode 100644 components/ui/dropdown-menu.tsx create mode 100644 components/ui/form.tsx create mode 100644 components/ui/input.tsx create mode 100644 components/ui/label.tsx create mode 100644 components/ui/radio-group.tsx create mode 100644 components/ui/separator.tsx create mode 100644 components/ui/switch.tsx create mode 100644 components/ui/tabs.tsx create mode 100644 components/ui/tooltip.tsx create mode 100644 lib/fonts/sf-pro-display.ts create mode 100644 lib/utils.ts create mode 100644 next.config.mjs create mode 100644 package.json create mode 100644 pnpm-lock.yaml create mode 100644 postcss.config.js create mode 100644 prettier.config.js create mode 100644 public/assets/brand/banner.png create mode 100644 public/assets/brand/logo.png create mode 100644 public/assets/fonts/SFProDisplay-Black.woff2 create mode 100644 public/assets/fonts/SFProDisplay-BlackItalic.woff2 create mode 100644 public/assets/fonts/SFProDisplay-Bold.woff2 create mode 100644 public/assets/fonts/SFProDisplay-BoldItalic.woff2 create mode 100644 public/assets/fonts/SFProDisplay-Heavy.woff2 create mode 100644 public/assets/fonts/SFProDisplay-HeavyItalic.woff2 create mode 100644 public/assets/fonts/SFProDisplay-Light.woff2 create mode 100644 public/assets/fonts/SFProDisplay-LightItalic.woff2 create mode 100644 public/assets/fonts/SFProDisplay-Medium.woff2 create mode 100644 public/assets/fonts/SFProDisplay-MediumItalic.woff2 create mode 100644 public/assets/fonts/SFProDisplay-Regular.woff2 create mode 100644 public/assets/fonts/SFProDisplay-RegularItalic.woff2 create mode 100644 public/assets/fonts/SFProDisplay-Semibold.woff2 create mode 100644 public/assets/fonts/SFProDisplay-SemiboldItalic.woff2 create mode 100644 public/assets/fonts/SFProDisplay-Thin.woff2 create mode 100644 public/assets/fonts/SFProDisplay-ThinItalic.woff2 create mode 100644 public/assets/fonts/SFProDisplay-Ultralight.woff2 create mode 100644 public/assets/fonts/SFProDisplay-UltralightItalic.woff2 create mode 100644 public/assets/gradients/0.png create mode 100644 public/assets/gradients/1.png create mode 100644 public/assets/gradients/10.png create mode 100644 public/assets/gradients/11.png create mode 100644 public/assets/gradients/12.png create mode 100644 public/assets/gradients/13.png create mode 100644 public/assets/gradients/14.png create mode 100644 public/assets/gradients/15.png create mode 100644 public/assets/gradients/16.png create mode 100644 public/assets/gradients/17.png create mode 100644 public/assets/gradients/18.png create mode 100644 public/assets/gradients/19.png create mode 100644 public/assets/gradients/2.png create mode 100644 public/assets/gradients/20.png create mode 100644 public/assets/gradients/21.png create mode 100644 public/assets/gradients/22.png create mode 100644 public/assets/gradients/23.png create mode 100644 public/assets/gradients/24.png create mode 100644 public/assets/gradients/25.png create mode 100644 public/assets/gradients/26.png create mode 100644 public/assets/gradients/27.png create mode 100644 public/assets/gradients/28.png create mode 100644 public/assets/gradients/29.png create mode 100644 public/assets/gradients/3.png create mode 100644 public/assets/gradients/30.png create mode 100644 public/assets/gradients/31.png create mode 100644 public/assets/gradients/4.png create mode 100644 public/assets/gradients/5.png create mode 100644 public/assets/gradients/6.png create mode 100644 public/assets/gradients/7.png create mode 100644 public/assets/gradients/8.png create mode 100644 public/assets/gradients/9.png create mode 100644 styles/globals.css create mode 100644 tailwind.config.js create mode 100644 tsconfig.json diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..db5ddae --- /dev/null +++ b/.eslintignore @@ -0,0 +1,2 @@ +.next +node_modules \ No newline at end of file diff --git a/.eslintrc.cjs b/.eslintrc.cjs new file mode 100644 index 0000000..8bcd21c --- /dev/null +++ b/.eslintrc.cjs @@ -0,0 +1,76 @@ +// eslint-disable-next-line @typescript-eslint/no-var-requires +const fs = require("fs") + +module.exports = { + extends: [ + "next", + "prettier", + "plugin:@typescript-eslint/recommended", + "plugin:tailwindcss/recommended", + ], + parserOptions: { + babelOptions: { + presets: [require.resolve("next/babel")], + }, + }, + rules: { + "testing-library/prefer-screen-queries": "off", + "@next/next/no-html-link-for-pages": "off", + "@typescript-eslint/no-unused-vars": [ + "warn", + { + argsIgnorePattern: "^_", + varsIgnorePattern: "^_", + }, + ], + "sort-imports": [ + "error", + { + ignoreCase: true, + ignoreDeclarationSort: true, + }, + ], + "tailwindcss/classnames-order": "off", + "import/order": [ + 1, + { + groups: ["external", "builtin", "internal", "sibling", "parent", "index"], + pathGroups: [ + ...getDirectoriesToSort().map((singleDir) => ({ + pattern: `${singleDir}/**`, + group: "internal", + })), + { + pattern: "env", + group: "internal", + }, + { + pattern: "theme", + group: "internal", + }, + { + pattern: "public/**", + group: "internal", + position: "after", + }, + ], + pathGroupsExcludedImportTypes: ["internal"], + alphabetize: { + order: "asc", + caseInsensitive: true, + }, + }, + ], + }, +} + +function getDirectoriesToSort() { + const ignoredSortingDirectories = [".git", ".next", ".vscode", "node_modules"] + return getDirectories(process.cwd()).filter((f) => !ignoredSortingDirectories.includes(f)) +} + +function getDirectories(path) { + return fs.readdirSync(path).filter(function (file) { + return fs.statSync(path + "/" + file).isDirectory() + }) +} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8f322f0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,35 @@ +# 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/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..b7425b9 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +enable-pre-post-scripts=true \ No newline at end of file diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..25bf17f --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +18 \ No newline at end of file diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..db5ddae --- /dev/null +++ b/.prettierignore @@ -0,0 +1,2 @@ +.next +node_modules \ No newline at end of file diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..8905a54 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,119 @@ +# Code of Conduct + +## Conventional Commits + +Developers should use the Conventional Commits standard when committing changes to the codebase. + +| Type | Description | +| -------- | --------------------------------------------------------------------- | +| feat | Declares a new feature has been added | +| fix | Declares a bug have been fixed | +| chore | Declares changes which don’t modify source or test files (eg. assets) | +| ci | Declares a change on the CI or CD process | +| build | Declares changes on the build setup | +| docs | Declares changes on documentation | +| style | Declares changes on code style | +| refactor | Declares a change of code without an effective change on the program | +| perf | Declares a change on performance | +| revert | Declares that a previous commit has been reverted | +| test | Declares changes on tests | + +### Examples + +#### Commit Message + +``` +refactor: adjust vehicle texture size [#ISSUENUMBER] +refactor: adjust vehicle texture size [NOISSUE] +``` + +#### Branch Name + +``` +refactor/#ISSUENUMBER_adjust-vehicle-texture-size +refactor/NOISSUE_adjust-vehicle-texture-size +``` + +## Contributing + +Developers should follow the following guidelines when contributing to the project: + +### 1. Create a new branch + +When starting work on a new feature or bug fix, create a new branch from the `main` branch. The name +of the branch should be descriptive and should include the issue number and a short description of +the feature or bug fix. For example, if you are working on issue #123, the branch name should +be `feat/#123_add-new-feature`. + +### 2. Commit changes + +When committing changes to the codebase, developers should follow +the [Conventional Commits](#conventional-commits) standard. This will ensure that the commit +messages are consistent and descriptive, and will allow the commit history to be automatically +parsed to generate release notes. + +### 3. Create a draft pull request + +After committing changes to the codebase, create a draft pull request to inform other developers +that you are working on a new feature or bug fix. The pull request should be kept in draft mode +until the feature or bug fix is complete. + +### 4. Create a pull request + +When the feature or bug fix is complete, mark the pull request as ready to review to merge the +changes into the `main` branch. The pull request should be reviewed by at least one other developer +before it can be merged. + +### 5. Review pull request + +When a pull request is marked as ready for review, it should be reviewed by at least one other +developer. The reviewer should verify that the code meets +the [Definition of Done](#definition-of-done). + +### 6. Merge pull request + +Once the pull request has been reviewed and approved, it can be merged into the `main` branch. The +pull request should be merged using the "Rebase and merge" option to ensure that the commit history +remains clean and concise. + +## Definition of Done + +### 1. Code meets coding standards + +All code must adhere to the rules defined in the Clean Code handbook for at least level L1. Level L2 +rules should also be taken into consideration. Specifically, emphasis should be placed on: + +1. Correct abstraction level: The code should have a clear and appropriate level of abstraction, + with well-defined interfaces and separation of concerns. +2. Class diagram: The class diagram should be clear and well-organized, with high cohesion and low + coupling between classes. +3. Correct error handling: The code should handle errors correctly, including validating arguments + and handling exceptions in a consistent and appropriate manner. + +### 2. Unit tests pass + +All code changes must be accompanied by unit tests that verify the expected behavior of the code. +These tests must pass without any errors or failures before the code can be considered complete. + +### 3. Code is reviewed + +All code must be reviewed by at least one other developer to ensure quality and compliance with +coding standards. The code review should focus on identifying any bugs, security vulnerabilities, or +design flaws that could impact the quality or maintainability of the code. + +### 4. Documentation is complete + +All code must be fully documented, including comments within the code and external documentation +such as user manuals. The documentation should be comprehensive and accurate, and should provide +enough detail for other developers and stakeholders to understand the code. + +### 5. Acceptance criteria are met + +The code must meet all of the acceptance criteria as defined by the stakeholders. These acceptance +criteria are used as a basis for verifying that the code meets the intended requirements + +### 6. Security is considered + +The code must be reviewed for security vulnerabilities and any identified issues must be addressed. +The code should be designed with security in mind, and should be subject to regular security testing +to identify any new vulnerabilities. \ No newline at end of file diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..6aab65b --- /dev/null +++ b/LICENSE.md @@ -0,0 +1 @@ +Copyright © 2023 Boostvolt (Jan). All rights reserved. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..ec6370e --- /dev/null +++ b/README.md @@ -0,0 +1,5 @@ +# iCover + +[![iCover Banner](public/assets/brand/banner.png)](https://icover.vercel.app/) + +Design a captivating and visually appealing cover artwork for your Apple Music playlist to engage listeners and convey the theme or mood of the curated songs. [icover.vercel.app](https://icover.vercel.app) \ No newline at end of file diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..d7a4a65 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,6 @@ +# Reporting Security Issues + +If you believe you have found a security vulnerability in the codebase, we encourage you to let us +know right away. + +We will investigate all legitimate reports and do our best to quickly fix the problem. \ No newline at end of file diff --git a/app/favicon.ico b/app/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..18d5e6882c787e150a6d00ecc05f0fa073a48323 GIT binary patch literal 109028 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 jIYQ+H9TB5z=-xtL^lYCHdSN&oN)j1j> + + + + {children} + + + + ) +} diff --git a/app/page.tsx b/app/page.tsx new file mode 100644 index 0000000..003b40e --- /dev/null +++ b/app/page.tsx @@ -0,0 +1,300 @@ +"use client" + +import { zodResolver } from "@hookform/resolvers/zod" +import html2canvas from "html2canvas" +import { Cog, Info } from "lucide-react" +import Image from "next/image" +import { useForm } from "react-hook-form" +import * as z from "zod" +import AppleMusic from "@/components/icons/apple-music" +import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert" +import { Avatar, AvatarImage } from "@/components/ui/avatar" +import { Button } from "@/components/ui/button" +import { Card, CardContent, CardFooter, CardHeader, CardTitle } from "@/components/ui/card" +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuLabel, + DropdownMenuRadioGroup, + DropdownMenuRadioItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" +import { Form, FormControl, FormField, FormItem } from "@/components/ui/form" +import { Input } from "@/components/ui/input" +import { Label } from "@/components/ui/label" +import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group" +import { Separator } from "@/components/ui/separator" +import { Switch } from "@/components/ui/switch" +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip" +import { SfProDisplay } from "@/lib/fonts/sf-pro-display" + +const formSchema = z.object({ + imageFormat: z.enum(["png", "jpeg"]), + appleMusicLogo: z.boolean(), + bigTitle: z.string(), + subTitle: z.string(), + footer: z.string(), + gradient: z.string(), +}) + +export default function Home() { + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: formSchema.parse({ + imageFormat: "png", + appleMusicLogo: true, + bigTitle: "", + subTitle: "", + footer: "", + gradient: "0", + }), + }) + + const handleDownload = () => { + const element = document.getElementById("coverElement") + if (element) { + html2canvas(element).then((canvas) => { + const link = document.createElement("a") + link.download = `playlist-cover.${form.watch("imageFormat")}` + link.href = canvas.toDataURL() + link.click() + }) + } + } + + return ( +
+ + +
+ +
+
+ iCover +
+ +
+ + + + + + + Image Format + ( + + + + PNG + JPEG + + + + )} + /> + + +
+
+
+ +
+
+ {/* Background Image */} + 0 + {/* Apple Logo */} + {form.watch("appleMusicLogo") && ( +
+ +
+ )} + {/* Big Title */} +
+

{form.watch("bigTitle")}

+
+ {/* Sub Title */} +
+

{form.watch("subTitle")}

+
+ {/* Footer */} +
+

{form.watch("footer")}

+
+
+
+ + + +
+
+
+ + + + + + + +

Could cause your cover to be removed.

+
+
+
+
+ ( + + + + + + )} + /> +
+ + ( + + + + + + )} + /> + ( + + + + + + )} + /> + ( + + + + + + )} + /> +
+ + + + + + + Gradient + + + Color + + + + Image + + + + ( + + + + {[...Array(30)].map((_, index) => ( + + ))} + + + + )} + /> + + + + + Coming soon + Color selection will be available soon. + + + + + + Coming soon + Image upload will be available soon. + + {/* */} + + +
+ + + + +

+ Built by{" "} + + Boostvolt + + . The source code is available on{" "} + + GitHub + + . +

+ +
+ +
+ ) +} diff --git a/components.json b/components.json new file mode 100644 index 0000000..aeafa41 --- /dev/null +++ b/components.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "default", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "tailwind.config.js", + "css": "@/styles/globals.css", + "baseColor": "slate", + "cssVariables": true + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils" + } +} diff --git a/components/icons/apple-music.tsx b/components/icons/apple-music.tsx new file mode 100644 index 0000000..f4d4265 --- /dev/null +++ b/components/icons/apple-music.tsx @@ -0,0 +1,34 @@ +import * as React from "react" +import { forwardRef, memo, Ref, SVGProps } from "react" + +const AppleMusic = (props: SVGProps, ref: Ref) => ( + + + +) + +const ForwardRef = forwardRef(AppleMusic) +const Memo = memo(ForwardRef) + +export default Memo diff --git a/components/theme-provider.tsx b/components/theme-provider.tsx new file mode 100644 index 0000000..bafb34b --- /dev/null +++ b/components/theme-provider.tsx @@ -0,0 +1,9 @@ +"use client" + +import { ThemeProvider as NextThemesProvider } from "next-themes" +import { ThemeProviderProps } from "next-themes/dist/types" +import * as React from "react" + +export function ThemeProvider({ children, ...props }: ThemeProviderProps) { + return {children} +} diff --git a/components/theme-toggle.tsx b/components/theme-toggle.tsx new file mode 100644 index 0000000..e868ff1 --- /dev/null +++ b/components/theme-toggle.tsx @@ -0,0 +1,19 @@ +"use client" + +import { Moon, Sun } from "lucide-react" +import { useTheme } from "next-themes" +import * as React from "react" + +import { Button } from "@/components/ui/button" + +export function ThemeToggle() { + const { setTheme, theme } = useTheme() + + return ( + + ) +} diff --git a/components/ui/alert.tsx b/components/ui/alert.tsx new file mode 100644 index 0000000..dbfbf5c --- /dev/null +++ b/components/ui/alert.tsx @@ -0,0 +1,43 @@ +import { cva, type VariantProps } from "class-variance-authority" +import * as React from "react" + +import { cn } from "@/lib/utils" + +const alertVariants = cva( + "relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground", + { + variants: { + variant: { + default: "bg-background text-foreground", + destructive: "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +const Alert = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes & VariantProps +>(({ className, variant, ...props }, ref) => ( +
+)) +Alert.displayName = "Alert" + +const AlertTitle = React.forwardRef>( + ({ className, ...props }, ref) => ( +
+ ) +) +AlertTitle.displayName = "AlertTitle" + +const AlertDescription = React.forwardRef>( + ({ className, ...props }, ref) => ( +
+ ) +) +AlertDescription.displayName = "AlertDescription" + +export { Alert, AlertTitle, AlertDescription } diff --git a/components/ui/avatar.tsx b/components/ui/avatar.tsx new file mode 100644 index 0000000..c1b9f80 --- /dev/null +++ b/components/ui/avatar.tsx @@ -0,0 +1,40 @@ +"use client" + +import * as AvatarPrimitive from "@radix-ui/react-avatar" +import * as React from "react" + +import { cn } from "@/lib/utils" + +const Avatar = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +Avatar.displayName = AvatarPrimitive.Root.displayName + +const AvatarImage = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AvatarImage.displayName = AvatarPrimitive.Image.displayName + +const AvatarFallback = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName + +export { Avatar, AvatarImage, AvatarFallback } diff --git a/components/ui/button.tsx b/components/ui/button.tsx new file mode 100644 index 0000000..5242fe2 --- /dev/null +++ b/components/ui/button.tsx @@ -0,0 +1,47 @@ +import { Slot } from "@radix-ui/react-slot" +import { cva, type VariantProps } from "class-variance-authority" +import * as React from "react" + +import { cn } from "@/lib/utils" + +const buttonVariants = cva( + "inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", + { + variants: { + variant: { + default: "bg-primary text-primary-foreground hover:bg-primary/90", + destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90", + outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground", + secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80", + ghost: "hover:bg-accent hover:text-accent-foreground", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: "h-10 px-4 py-2", + sm: "h-9 rounded-md px-3", + lg: "h-11 rounded-md px-8", + icon: "h-10 w-10", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +) + +export interface ButtonProps + extends React.ButtonHTMLAttributes, + VariantProps { + asChild?: boolean +} + +const Button = React.forwardRef( + ({ className, variant, size, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : "button" + return + } +) +Button.displayName = "Button" + +export { Button, buttonVariants } diff --git a/components/ui/card.tsx b/components/ui/card.tsx new file mode 100644 index 0000000..4b08e31 --- /dev/null +++ b/components/ui/card.tsx @@ -0,0 +1,41 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +const Card = React.forwardRef>(({ className, ...props }, ref) => ( +
+)) +Card.displayName = "Card" + +const CardHeader = React.forwardRef>( + ({ className, ...props }, ref) => ( +
+ ) +) +CardHeader.displayName = "CardHeader" + +const CardTitle = React.forwardRef>( + ({ className, ...props }, ref) => ( +

+ ) +) +CardTitle.displayName = "CardTitle" + +const CardDescription = React.forwardRef>( + ({ className, ...props }, ref) => ( +

+ ) +) +CardDescription.displayName = "CardDescription" + +const CardContent = React.forwardRef>( + ({ className, ...props }, ref) =>

+) +CardContent.displayName = "CardContent" + +const CardFooter = React.forwardRef>( + ({ className, ...props }, ref) =>
+) +CardFooter.displayName = "CardFooter" + +export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } diff --git a/components/ui/dropdown-menu.tsx b/components/ui/dropdown-menu.tsx new file mode 100644 index 0000000..deafd08 --- /dev/null +++ b/components/ui/dropdown-menu.tsx @@ -0,0 +1,181 @@ +"use client" + +import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu" +import { Check, ChevronRight, Circle } from "lucide-react" +import * as React from "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, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, children, ...props }, ref) => ( + + {children} + + +)) +DropdownMenuSubTrigger.displayName = DropdownMenuPrimitive.SubTrigger.displayName + +const DropdownMenuSubContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayName + +const DropdownMenuContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, sideOffset = 4, ...props }, ref) => ( + + + +)) +DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName + +const DropdownMenuItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, ...props }, ref) => ( + +)) +DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName + +const DropdownMenuCheckboxItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, checked, ...props }, ref) => ( + + + + + + + {children} + +)) +DropdownMenuCheckboxItem.displayName = DropdownMenuPrimitive.CheckboxItem.displayName + +const DropdownMenuRadioItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + + + + {children} + +)) +DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName + +const DropdownMenuLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, ...props }, ref) => ( + +)) +DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName + +const DropdownMenuSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName + +const DropdownMenuShortcut = ({ className, ...props }: React.HTMLAttributes) => { + return +} +DropdownMenuShortcut.displayName = "DropdownMenuShortcut" + +export { + DropdownMenu, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuCheckboxItem, + DropdownMenuRadioItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuGroup, + DropdownMenuPortal, + DropdownMenuSub, + DropdownMenuSubContent, + DropdownMenuSubTrigger, + DropdownMenuRadioGroup, +} diff --git a/components/ui/form.tsx b/components/ui/form.tsx new file mode 100644 index 0000000..bd7d2d1 --- /dev/null +++ b/components/ui/form.tsx @@ -0,0 +1,129 @@ +import * as LabelPrimitive from "@radix-ui/react-label" +import { Slot } from "@radix-ui/react-slot" +import * as React from "react" +import { Controller, ControllerProps, FieldPath, FieldValues, FormProvider, useFormContext } from "react-hook-form" + +import { Label } from "@/components/ui/label" +import { cn } from "@/lib/utils" + +const Form = FormProvider + +type FormFieldContextValue< + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath, +> = { + name: TName +} + +const FormFieldContext = React.createContext({} as FormFieldContextValue) + +const FormField = < + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath, +>({ + ...props +}: ControllerProps) => { + return ( + + + + ) +} + +const useFormField = () => { + const fieldContext = React.useContext(FormFieldContext) + const itemContext = React.useContext(FormItemContext) + const { getFieldState, formState } = useFormContext() + + const fieldState = getFieldState(fieldContext.name, formState) + + if (!fieldContext) { + throw new Error("useFormField should be used within ") + } + + const { id } = itemContext + + return { + id, + name: fieldContext.name, + formItemId: `${id}-form-item`, + formDescriptionId: `${id}-form-item-description`, + formMessageId: `${id}-form-item-message`, + ...fieldState, + } +} + +type FormItemContextValue = { + id: string +} + +const FormItemContext = React.createContext({} as FormItemContextValue) + +const FormItem = React.forwardRef>( + ({ className, ...props }, ref) => { + const id = React.useId() + + return ( + +
+ + ) + } +) +FormItem.displayName = "FormItem" + +const FormLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => { + const { error, formItemId } = useFormField() + + return