From 4046de77ba84e127e165970eddd0af4f1aea8f56 Mon Sep 17 00:00:00 2001 From: Alexander Mangel Date: Fri, 28 Mar 2025 09:02:43 +0000 Subject: [PATCH 001/183] feat: UI overhaul + first pass component implementation + Work submission flow (#108) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: pnpm filter fix Signed-off-by: Alexander Mangel * chore: ENV bypass for desktop development Signed-off-by: Alexander Mangel * wip: convert to TW 4 CSS - exclode contracts/lib - remove postcss/autoprefixer - use tailwindcss/vite - convert existing tv defs -> tailwind utilities - convert existing typo -> typography.css - appbar style conform figma - remove customized tv export, remove customized cn export -> tailwind CSS classes now first class - remove padding from root + add to appView Signed-off-by: Alexander Mangel * wip: retooling base Signed-off-by: Alexander Mangel * feat(client): initial components, profile + garden screen Custom based off radix/shadcn, lowlevel components with tv variants: - accordion - avatar - badge - card - profile - tabs Signed-off-by: Alexander Mangel * style: profile, faq, cards, accordion, typography Signed-off-by: Alexander Mangel * wip: carousel, badges, actioncard, gardencard Signed-off-by: Alexander Mangel * wip: broke all the colors after receiving Marcus export Signed-off-by: Alexander Mangel * style: carousel, garden card, workcard, actioncard Signed-off-by: Alexander Mangel * fix: typography error Signed-off-by: Alexander Mangel * feat: garden (work) full flow Signed-off-by: Alexander Mangel * chore: remove debug flag Signed-off-by: Alexander Mangel * fix: strokee -> stroke Signed-off-by: Alexander Mangel * chore(media): put preview modal back in (but doesn’t work) Signed-off-by: Alexander Mangel * fix: ts errors Signed-off-by: Alexander Mangel * exclude test in build * updated vite config --------- Signed-off-by: Alexander Mangel Co-authored-by: Afo --- .vscode/settings.json | 17 +- package.json | 2 +- packages/client/dev-dist/sw.js | 2 +- packages/client/index.html | 7 +- packages/client/package.json | 12 +- packages/client/postcss.config.js | 6 - packages/client/src/App.tsx | 26 +- .../client/src/components/Button/Base.tsx | 127 -- .../client/src/components/Button/index.tsx | 41 - packages/client/src/components/Form/Card.tsx | 37 - .../client/src/components/Form/Progress.tsx | 55 - .../client/src/components/Garden/Card.tsx | 57 - .../src/components/Garden/Gardeners.tsx | 2 +- .../client/src/components/Garden/Work.tsx | 28 +- .../client/src/components/Layout/AppBar.tsx | 56 +- .../client/src/components/Layout/Header.tsx | 4 +- .../client/src/components/Layout/Hero.tsx | 12 +- .../client/src/components/Layout/Splash.tsx | 7 +- .../src/components/UI/Accordion/Accordion.tsx | 62 + .../src/components/UI/Accordion/Faq.tsx | 69 + .../src/components/UI/Avatar/Avatar.tsx | 81 + .../client/src/components/UI/Badge/Badge.tsx | 51 + .../client/src/components/UI/Button/Base.tsx | 322 ++++ .../client/src/components/UI/Button/index.tsx | 25 + .../src/components/UI/Card/ActionCard.tsx | 75 + .../client/src/components/UI/Card/Card.tsx | 135 ++ .../src/components/UI/Card/GardenCard.tsx | 130 ++ .../src/components/UI/Card/WorkCard.tsx | 74 + .../src/components/UI/Carousel/Carousel.tsx | 220 +++ .../client/src/components/UI/Form/Card.tsx | 32 + .../src/components/{ => UI}/Form/Date.tsx | 0 .../src/components/{ => UI}/Form/Info.tsx | 24 +- .../src/components/{ => UI}/Form/Input.tsx | 23 +- .../src/components/UI/Form/Progress.tsx | 69 + .../src/components/{ => UI}/Form/Select.tsx | 6 +- .../src/components/{ => UI}/Form/Text.tsx | 11 +- .../src/components/UI/Profile/Profile.tsx | 55 + .../client/src/components/UI/Tabs/Tabs.tsx | 72 + .../components/UI/UploadModal/UploadModal.tsx | 75 + packages/client/src/index.css | 108 +- packages/client/src/main.tsx | 1 + packages/client/src/modules/urql.ts | 2 +- packages/client/src/providers/work.tsx | 16 +- packages/client/src/styles/animation.css | 77 + packages/client/src/styles/colors.css | 730 ++++++++ packages/client/src/styles/typography.css | 319 ++++ packages/client/src/styles/utilities.css | 107 ++ packages/client/src/utils/cn.ts | 261 +-- .../client/src/utils/date-time-formater.ts | 46 +- packages/client/src/utils/tv.ts | 8 - .../client/src/views/Garden/Completed.tsx | 67 + .../src/views/{Work => Garden}/Details.tsx | 24 +- packages/client/src/views/Garden/Intro.tsx | 68 + .../src/views/{Work => Garden}/Media.tsx | 65 +- packages/client/src/views/Garden/Review.tsx | 86 + .../src/views/{Work => Garden}/index.tsx | 135 +- packages/client/src/views/Gardens/index.tsx | 64 - .../views/{Gardens => Home}/Assessment.tsx | 0 .../src/views/{Gardens => Home}/Garden.tsx | 87 +- .../views/{Gardens => Home}/Notifications.tsx | 2 +- .../views/{Gardens => Home}/WorkApproval.tsx | 4 +- packages/client/src/views/Home/index.tsx | 65 + packages/client/src/views/Landing/index.tsx | 4 +- packages/client/src/views/Profile/Account.tsx | 76 +- packages/client/src/views/Profile/Help.tsx | 77 +- packages/client/src/views/Profile/Setting.tsx | 10 +- packages/client/src/views/Profile/index.tsx | 112 +- packages/client/src/views/Work/Intro.tsx | 82 - packages/client/src/views/Work/Review.tsx | 97 - packages/client/src/views/index.tsx | 40 +- packages/client/tailwind.config.ts | 14 - packages/client/tsconfig.app.json | 3 +- packages/client/vite.config.ts | 9 + pnpm-lock.yaml | 1653 +++++++++++++---- 74 files changed, 4863 insertions(+), 1765 deletions(-) delete mode 100644 packages/client/postcss.config.js delete mode 100644 packages/client/src/components/Button/Base.tsx delete mode 100644 packages/client/src/components/Button/index.tsx delete mode 100644 packages/client/src/components/Form/Card.tsx delete mode 100644 packages/client/src/components/Form/Progress.tsx delete mode 100644 packages/client/src/components/Garden/Card.tsx create mode 100644 packages/client/src/components/UI/Accordion/Accordion.tsx create mode 100644 packages/client/src/components/UI/Accordion/Faq.tsx create mode 100644 packages/client/src/components/UI/Avatar/Avatar.tsx create mode 100644 packages/client/src/components/UI/Badge/Badge.tsx create mode 100644 packages/client/src/components/UI/Button/Base.tsx create mode 100644 packages/client/src/components/UI/Button/index.tsx create mode 100644 packages/client/src/components/UI/Card/ActionCard.tsx create mode 100644 packages/client/src/components/UI/Card/Card.tsx create mode 100644 packages/client/src/components/UI/Card/GardenCard.tsx create mode 100644 packages/client/src/components/UI/Card/WorkCard.tsx create mode 100644 packages/client/src/components/UI/Carousel/Carousel.tsx create mode 100644 packages/client/src/components/UI/Form/Card.tsx rename packages/client/src/components/{ => UI}/Form/Date.tsx (100%) rename packages/client/src/components/{ => UI}/Form/Info.tsx (50%) rename packages/client/src/components/{ => UI}/Form/Input.tsx (54%) create mode 100644 packages/client/src/components/UI/Form/Progress.tsx rename packages/client/src/components/{ => UI}/Form/Select.tsx (87%) rename packages/client/src/components/{ => UI}/Form/Text.tsx (70%) create mode 100644 packages/client/src/components/UI/Profile/Profile.tsx create mode 100644 packages/client/src/components/UI/Tabs/Tabs.tsx create mode 100644 packages/client/src/components/UI/UploadModal/UploadModal.tsx create mode 100644 packages/client/src/styles/animation.css create mode 100644 packages/client/src/styles/colors.css create mode 100644 packages/client/src/styles/typography.css create mode 100644 packages/client/src/styles/utilities.css delete mode 100644 packages/client/src/utils/tv.ts create mode 100644 packages/client/src/views/Garden/Completed.tsx rename packages/client/src/views/{Work => Garden}/Details.tsx (80%) create mode 100644 packages/client/src/views/Garden/Intro.tsx rename packages/client/src/views/{Work => Garden}/Media.tsx (70%) create mode 100644 packages/client/src/views/Garden/Review.tsx rename packages/client/src/views/{Work => Garden}/index.tsx (50%) delete mode 100644 packages/client/src/views/Gardens/index.tsx rename packages/client/src/views/{Gardens => Home}/Assessment.tsx (100%) rename packages/client/src/views/{Gardens => Home}/Garden.tsx (61%) rename packages/client/src/views/{Gardens => Home}/Notifications.tsx (97%) rename packages/client/src/views/{Gardens => Home}/WorkApproval.tsx (97%) create mode 100644 packages/client/src/views/Home/index.tsx delete mode 100644 packages/client/src/views/Work/Intro.tsx delete mode 100644 packages/client/src/views/Work/Review.tsx delete mode 100644 packages/client/tailwind.config.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index e0916c1b..46d2061c 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -5,7 +5,22 @@ "[toml]": { "editor.defaultFormatter": "tamasfe.even-better-toml" }, + "[css]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[typescriptreact]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[typescript]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "editor.formatOnSave": true, "solidity.formatter": "forge", "typescript.tsdk": "node_modules/typescript/lib", - "typescript.enablePromptUseWorkspaceTsdk": true + "typescript.enablePromptUseWorkspaceTsdk": true, + "search.exclude": { + "**/node_modules": true, + "**/contracts/lib": true, + "**/*.code-search": true + } } diff --git a/package.json b/package.json index 354e457b..7fbee768 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "lint": "pnpm recursive run lint", "test": "pnpm recursive run test", "dev": "concurrently -n \"APP,CONTRACTS\" -c \"bgMagenta.bold,bgCyan.bold\" \"pnpm run dev:app\" \"pnpm run dev:contracts\"", - "dev:app": "pnpm --filter 'app' run dev", + "dev:app": "pnpm --filter 'client' run dev", "dev:contracts": "pnpm --filter 'contracts' run dev" }, "dependencies": { diff --git a/packages/client/dev-dist/sw.js b/packages/client/dev-dist/sw.js index 170752b3..471eb2da 100644 --- a/packages/client/dev-dist/sw.js +++ b/packages/client/dev-dist/sw.js @@ -82,7 +82,7 @@ define(['./workbox-c982e567'], (function (workbox) { 'use strict'; "revision": "3ca0b8505b4bec776b69afdba2768812" }, { "url": "index.html", - "revision": "0.g55tkv2gkvg" + "revision": "0.kvr3kfbm5ng" }], {}); workbox.cleanupOutdatedCaches(); workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), { diff --git a/packages/client/index.html b/packages/client/index.html index 6bf4d0b0..a406d581 100644 --- a/packages/client/index.html +++ b/packages/client/index.html @@ -71,11 +71,8 @@ Green Goods - -
+ +
diff --git a/packages/client/package.json b/packages/client/package.json index 5851e1a0..7108b155 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -18,11 +18,18 @@ "@phosphor-icons/react": "2.1.7", "@privy-io/react-auth": "^2.4.4", "@privy-io/server-auth": "1.18.9", + "@radix-ui/react-accordion": "^1.2.3", + "@radix-ui/react-avatar": "^1.1.3", + "@radix-ui/react-select": "^2.1.6", + "@radix-ui/react-slot": "^1.1.2", + "@radix-ui/react-tabs": "^1.1.3", "@remixicon/react": "^4.6.0", + "@tailwindcss/vite": "^4.0.14", "@tanstack/react-query": "^5.52.2", "@urql/core": "^5.0.4", "@vercel/functions": "1.4.1", "clsx": "2.1.1", + "embla-carousel-react": "^8.5.2", "gql.tada": "^1.8.10", "pinata": "^0.4.0", "react": "^18.3.1", @@ -40,6 +47,7 @@ "devDependencies": { "@eslint/js": "^9.9.0", "@tailwindcss/forms": "^0.5.7", + "@tailwindcss/postcss": "^4.0.14", "@tailwindcss/typography": "0.5.15", "@tanstack/eslint-plugin-query": "5.52.0", "@testing-library/jest-dom": "6.6.3", @@ -49,7 +57,6 @@ "@types/react-dom": "^18.3.0", "@vercel/node": "3.2.10", "@vitejs/plugin-react": "^4.3.1", - "autoprefixer": "^10.4.20", "daisyui": "4.12.10", "dotenv-expand": "11.0.6", "eslint": "^9.9.0", @@ -57,8 +64,7 @@ "eslint-plugin-react-refresh": "^0.4.9", "globals": "^15.9.0", "jsdom": "25.0.1", - "postcss": "^8.4.41", - "tailwindcss": "^3.4.10", + "tailwindcss": "^4.0.14", "tailwindcss-animate": "^1.0.7", "typescript": "^5.7.2", "typescript-eslint": "^8.0.1", diff --git a/packages/client/postcss.config.js b/packages/client/postcss.config.js deleted file mode 100644 index 2e7af2b7..00000000 --- a/packages/client/postcss.config.js +++ /dev/null @@ -1,6 +0,0 @@ -export default { - plugins: { - tailwindcss: {}, - autoprefixer: {}, - }, -} diff --git a/packages/client/src/App.tsx b/packages/client/src/App.tsx index 5a65985c..71d742e3 100644 --- a/packages/client/src/App.tsx +++ b/packages/client/src/App.tsx @@ -38,31 +38,39 @@ function App() { - : !isAuthenticated ? + ) : !isAuthenticated ? ( - : - : + ) : ( + + ) + ) : ( + + ) } /> {/* Main: Show app or navigate to login, onboarding, or landing page based on conditions */} - : - : + ) : ( + + ) + ) : ( + + ) } /> {/* Catch-all: Redirect to the appropriate place */} diff --git a/packages/client/src/components/Button/Base.tsx b/packages/client/src/components/Button/Base.tsx deleted file mode 100644 index a18427af..00000000 --- a/packages/client/src/components/Button/Base.tsx +++ /dev/null @@ -1,127 +0,0 @@ -// button.base.tsx -import * as React from "react"; -import { tv, type VariantProps } from "../../utils/tv"; -import type { PolymorphicComponentProps } from "../../utils/polymorphic"; - -export const buttonVariants = tv({ - // A common base class if needed (here we ensure we always have a flex container) - base: "flex items-center text-center", - variants: { - variant: { - primary: "", - secondary: "", - danger: "", - }, - mode: { - filled: "", - outline: "", - inactive: "", - }, - size: { - large: "", - small: "", - }, - }, - compoundVariants: [ - { - // primaryFilledLargePill - variant: "primary", - mode: "filled", - size: "large", - class: - "bg-green-500 text-white rounded-full px-6 py-2 flex items-center justify-between", - }, - { - // primaryFilledSmallRounded - variant: "primary", - mode: "filled", - size: "small", - class: "bg-green-500 text-white rounded-md px-4 py-1", - }, - { - // secondaryOutlineLargePill - variant: "secondary", - mode: "outline", - size: "large", - class: - "border border-gray-300 text-gray-700 rounded-full px-6 py-2 flex items-center", - }, - { - // primaryInactiveLargePill - variant: "primary", - mode: "inactive", - size: "large", - class: - "bg-gray-200 text-gray-400 rounded-full px-6 py-2 flex items-center justify-between cursor-not-allowed", - }, - { - // secondaryOutlineSmallRounded - variant: "secondary", - mode: "outline", - size: "small", - class: "border border-gray-300 text-gray-700 rounded-md px-4 py-1", - }, - { - // dangerOutlineLargePill - variant: "danger", - mode: "outline", - size: "large", - class: - "px-6 py-3 border border-red-500 text-red-500 rounded-full hover:bg-red-50 active:translate-y-[1px]", - }, - { - // dangerFilledSmallRounded - variant: "danger", - mode: "filled", - size: "small", - class: "bg-red-500 text-white rounded-md px-4 py-1 flex items-center", - }, - { - // primaryFilledSmallRounded - variant: "primary", - mode: "filled", - size: "small", - class: "bg-green-500 text-white rounded px-3 py-1 flex items-center", - }, - ], - defaultVariants: { - variant: "primary", - mode: "filled", - size: "large", - }, -}); - -// Type for the props that our tv function understands -export type ButtonVariantProps = VariantProps; - -// The low-level Button root component. -export type ButtonRootProps = React.ButtonHTMLAttributes & - ButtonVariantProps & { - asChild?: boolean; - }; - -const ButtonRoot = React.forwardRef( - ({ children, asChild, className, variant, mode, size, ...rest }, ref) => { - // const Component = asChild ? "div" : "button"; - const classes = buttonVariants({ variant, mode, size, class: className }); - return ( - - ); - } -); -ButtonRoot.displayName = "ButtonRoot"; - -// Optional: a ButtonIcon component if you need icon styling. -function ButtonIcon({ - as, - className, - ...rest -}: PolymorphicComponentProps) { - const Component = as || "span"; - return ; -} -ButtonIcon.displayName = "ButtonIcon"; - -export { ButtonRoot as Root, ButtonIcon as Icon }; diff --git a/packages/client/src/components/Button/index.tsx b/packages/client/src/components/Button/index.tsx deleted file mode 100644 index 50d88e0d..00000000 --- a/packages/client/src/components/Button/index.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import * as React from "react"; -import type { ButtonRootProps } from "./Base"; -import { Root as ButtonRoot, Icon as ButtonIcon } from "./Base"; - -export interface ButtonProps extends ButtonRootProps { - /** - * Primary content of the button. - * If no children are provided, the label will be rendered. - */ - children?: React.ReactNode; - label?: React.ReactNode; - /** - * An optional icon displayed at the start. - */ - startIcon?: React.ReactElement; - /** - * An optional icon displayed at the end. - */ - endIcon?: React.ReactElement; -} - -/** - * A minimal Button component that leverages the base (Root and Icon) components. - * - * You can control appearance via the variant, mode, and size props (as defined in your base). - */ -export const Button = React.forwardRef( - ({ label, children, startIcon, endIcon, ...props }, ref) => { - // Use children if provided; otherwise use label. - const content = children || label; - - return ( - - {startIcon && {startIcon}} - {content} - {endIcon && {endIcon}} - - ); - } -); -Button.displayName = "Button"; diff --git a/packages/client/src/components/Form/Card.tsx b/packages/client/src/components/Form/Card.tsx deleted file mode 100644 index 0b957cec..00000000 --- a/packages/client/src/components/Form/Card.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { RemixiconComponentType } from "@remixicon/react"; - -interface FormCardProps { - label: string; - value: string; - variant?: "primary" | "secondary" | "tertiary"; - Icon?: RemixiconComponentType; -} - -const variants = { - primary: "bg-transparent border-slate-100 border-2 shadow-sm", - secondary: "bg-green-100 text-green-700", - tertiary: "bg-yellow-100 text-yellow-700", -}; - -export const FormCard = ({ - label, - value, - variant = "primary", - Icon, - ...props -}: FormCardProps) => { - const variantClasses = variants[variant]; - - return ( -
-
- {Icon && } -

{label}

-
-

{value}

-
- ); -}; diff --git a/packages/client/src/components/Form/Progress.tsx b/packages/client/src/components/Form/Progress.tsx deleted file mode 100644 index 5f493e09..00000000 --- a/packages/client/src/components/Form/Progress.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import { RiCheckFill } from "@remixicon/react"; - -interface FormProgressProps { - currentStep: number; - steps: string[]; -} - -export const FormProgress = ({ currentStep, steps }: FormProgressProps) => { - return ( -
- {steps.map((step, index) => ( -
-
- index + 1 ? "bg-teal-500" - : currentStep === index + 1 ? - "bg-teal-500 text-white before:absolute before:-inset-1 before:bg-teal-200 before:rounded-full before:w-13 before:h-13 before:z-[-1] before:m-auto" - : "bg-slate-200 text-black" - } - `} - > - {currentStep > index + 1 ? - - : index + 1} - -
index + 1 ? "bg-teal-400" : "bg-slate-400"} ms-2 w-full h-px flex-1 group-last:hidden`} - >
-
- {/*
- - {step} - -
*/} -
- ))} -
- ); -}; - -{ - /*
    -
  • Register
  • -
  • Choose plan
  • -
  • Purchase
  • -
  • Receive Product
  • -
*/ -} diff --git a/packages/client/src/components/Garden/Card.tsx b/packages/client/src/components/Garden/Card.tsx deleted file mode 100644 index bd010462..00000000 --- a/packages/client/src/components/Garden/Card.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import React from "react"; -import { - RiMapPin2Fill, - // RiCalendar2Fill, - // RiThumbUpFill, -} from "@remixicon/react"; -import { Button } from "@/components/Button"; - -// import { truncateDescription } from "../../utils/text"; -// import { Button } from "../Button"; - -export interface GardenCardProps extends Garden { - index: number; - onCardClick: () => void; -} - -export const GardenCard: React.FC = ({ - // id, - index, - name, - description, - location, - bannerImage, - // operators, - onCardClick, -}) => { - return ( -
  • -
    - Image Description -
    -
    -

    {name}

    -
    - - {location} -
    -

    {description}

    -
    -
    -
    -
  • - ); -}; diff --git a/packages/client/src/components/Garden/Gardeners.tsx b/packages/client/src/components/Garden/Gardeners.tsx index fcfa4007..ba8b45d3 100644 --- a/packages/client/src/components/Garden/Gardeners.tsx +++ b/packages/client/src/components/Garden/Gardeners.tsx @@ -13,7 +13,7 @@ export const GardenGardeners: React.FC = ({ gardeners.map((user) => (
  • = ({ case "pending": return ; case "success": - return works.length ? - works.map((work) => ( + return works.length ? ( + works.map((work) => ( + <> +
  • navigate(`/gardens/${work.gardenAddress}/work/${work.id}`) } @@ -39,13 +42,13 @@ export const GardenWork: React.FC = ({ {work.title}
    -
    +
    {actions.find((a) => a.id === work.actionUID)?.title}
    -
    +
    = ({
    -
    +
    {work.status}
    -
    +
    {0} @@ -92,10 +95,13 @@ export const GardenWork: React.FC = ({
  • - )) - :

    - No works done yet, get started by clicking an action above -

    ; + + )) + ) : ( +

    + No works done yet, get started by clicking an action above +

    + ); case "error": return (

    diff --git a/packages/client/src/components/Layout/AppBar.tsx b/packages/client/src/components/Layout/AppBar.tsx index ac6013b5..a4f061d1 100644 --- a/packages/client/src/components/Layout/AppBar.tsx +++ b/packages/client/src/components/Layout/AppBar.tsx @@ -7,8 +7,9 @@ import { RiHomeLine, RiPlantLine, RiUserLine, - RemixiconComponentType, + type RemixiconComponentType, } from "@remixicon/react"; +import { cn } from "@/utils/cn"; const tabs: { path: string; @@ -40,29 +41,38 @@ export const AppBar = () => { const { pathname } = useLocation(); return ( -

    ); }; diff --git a/packages/client/src/components/Layout/Header.tsx b/packages/client/src/components/Layout/Header.tsx index d1047a34..9c88d654 100644 --- a/packages/client/src/components/Layout/Header.tsx +++ b/packages/client/src/components/Layout/Header.tsx @@ -3,9 +3,9 @@ import { useApp } from "@/providers/app"; import { RiGithubLine, RiTwitterLine, - RemixiconComponentType, + type RemixiconComponentType, } from "@remixicon/react"; -import React from "react"; +import type React from "react"; interface HeaderProps {} diff --git a/packages/client/src/components/Layout/Hero.tsx b/packages/client/src/components/Layout/Hero.tsx index d702325b..cbde8300 100644 --- a/packages/client/src/components/Layout/Hero.tsx +++ b/packages/client/src/components/Layout/Hero.tsx @@ -8,7 +8,7 @@ interface HeroProps { handleSubscribe: (e: React.FormEvent) => void; } -export const Hero: React.FC = ({ handleSubscribe }) => { +export const Hero: React.FC = () => { const { isMobile, platform } = useApp(); return ( @@ -62,11 +62,11 @@ export const Hero: React.FC = ({ handleSubscribe }) => {

    Install Green Goods

    - {platform === "ios" ? - "Tap the share button and then 'Add to Home Screen'." - : platform === "android" ? - "Tap the menu button and then 'Add to Home Screen'." - : "Tap the menu button and then 'Add to Home Screen'."} + {platform === "ios" + ? "Tap the share button and then 'Add to Home Screen'." + : platform === "android" + ? "Tap the menu button and then 'Add to Home Screen'." + : "Tap the menu button and then 'Add to Home Screen'."}

    diff --git a/packages/client/src/components/Layout/Splash.tsx b/packages/client/src/components/Layout/Splash.tsx index 2a856e55..152c2a79 100644 --- a/packages/client/src/components/Layout/Splash.tsx +++ b/packages/client/src/components/Layout/Splash.tsx @@ -2,7 +2,7 @@ import React from "react"; import { APP_NAME } from "@/constants"; -import { Button } from "../Button"; +import { Button } from "../UI/Button"; interface SplashProps { login: () => void; @@ -23,11 +23,10 @@ export const Splash: React.FC = ({ onClick={login} disabled={isLoggingIn} className="w-full" + label={buttonLabel} // variant="secondary" // fullWidth - > - {buttonLabel} - + /> ); }; diff --git a/packages/client/src/components/UI/Accordion/Accordion.tsx b/packages/client/src/components/UI/Accordion/Accordion.tsx new file mode 100644 index 00000000..a211d638 --- /dev/null +++ b/packages/client/src/components/UI/Accordion/Accordion.tsx @@ -0,0 +1,62 @@ +"use client"; + +import * as React from "react"; +import * as AccordionPrimitive from "@radix-ui/react-accordion"; +import { RiAddLine, RiQuestionLine } from "@remixicon/react"; +import { cn } from "@/utils/cn"; +import { FlexCard } from "../Card/Card"; + +const Accordion = AccordionPrimitive.Root; + +const AccordionItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)); +AccordionItem.displayName = "AccordionItem"; + +const AccordionTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + svg:first-of-type]:text-primary [&[data-state=open]>svg:first-of-type]:scale-110 [&[data-state=open]>svg:first-of-type]:animate-spring-bump grow text-left items-start", + className + )} + {...props} + > + +
    {children}
    + +
    +)); +AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName; + +const AccordionContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + {children} + +)); + +AccordionContent.displayName = AccordionPrimitive.Content.displayName; + +export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }; diff --git a/packages/client/src/components/UI/Accordion/Faq.tsx b/packages/client/src/components/UI/Accordion/Faq.tsx new file mode 100644 index 00000000..e4a2c45e --- /dev/null +++ b/packages/client/src/components/UI/Accordion/Faq.tsx @@ -0,0 +1,69 @@ +"use client"; + +import * as React from "react"; +import * as AccordionPrimitive from "@radix-ui/react-accordion"; +import { RiAddLine, RiQuestionLine } from "@remixicon/react"; +import { cn } from "@/utils/cn"; +import { FlexCard } from "../Card/Card"; + +const Faq = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +Faq.displayName = "Faq"; + +const FaqItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)); +FaqItem.displayName = "FaqItem"; + +const FaqTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + svg:first-of-type]:text-primary [&[data-state=open]>svg:first-of-type]:scale-110 [&[data-state=open]>svg:first-of-type]:animate-spring-bump grow text-left items-start", + className + )} + {...props} + > + +
    {children}
    + +
    +)); +FaqTrigger.displayName = AccordionPrimitive.Trigger.displayName; + +const FaqContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + {children} + +)); + +FaqContent.displayName = AccordionPrimitive.Content.displayName; + +export { Faq, FaqItem, FaqTrigger, FaqContent }; \ No newline at end of file diff --git a/packages/client/src/components/UI/Avatar/Avatar.tsx b/packages/client/src/components/UI/Avatar/Avatar.tsx new file mode 100644 index 00000000..3168d9c2 --- /dev/null +++ b/packages/client/src/components/UI/Avatar/Avatar.tsx @@ -0,0 +1,81 @@ +"use client" + +import * as React from "react" +import * as AvatarPrimitive from "@radix-ui/react-avatar" + +import { cn } from "@/utils/cn" +import { tv, type VariantProps } from "tailwind-variants" + +export const avatarVariants = tv({ + base: "relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full", + variants: { + variant: { + primary: "", + }, + mode: { + "no-outline": "border-0 shadow-0", + outline: "border-slate-200 border", + }, + shadow: { + "no-shadow": "shadow-none", + shadow: "shadow-sm", + }, + }, + defaultVariants: { + variant: "primary", + mode: "outline", + shadow: "no-shadow" + }, +}) + +export type AvatarVariantProps = VariantProps; + +export type AvatarRootProps = React.HTMLAttributes & React.ComponentPropsWithoutRef & + AvatarVariantProps & { + asChild?: boolean; + }; + +const Avatar = React.forwardRef< + React.ElementRef, + AvatarRootProps +>(({ className, variant, mode, shadow, ...props }, ref) => { + const avatar = avatarVariants({ variant, mode, shadow, class: className }); + return ( + +)}) +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/packages/client/src/components/UI/Badge/Badge.tsx b/packages/client/src/components/UI/Badge/Badge.tsx new file mode 100644 index 00000000..10d99f62 --- /dev/null +++ b/packages/client/src/components/UI/Badge/Badge.tsx @@ -0,0 +1,51 @@ +import { cn } from "@/utils/cn"; +import type * as React from "react"; +import { tv, type VariantProps } from "tailwind-variants"; + +const badgeVariants = tv({ + base: "inline-flex items-center rounded-md border px-.5 py-.25 text-sm transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 text-nowrap whitespace-nowrap", + variants: { + variant: { + transparent: + "font-medium border-transparent bg-primary text-foreground hover:bg-primary/80", + pill: "border-0 border-transparent rounded-2xl text-sm p-0.5 px-2 font-medium", + outline: "text-foreground border-card p-.5 px-1 text-xs", + }, + tint: { + primary: "bg-primary text-primary-foreground", + secondary: "bg-secondary text-secondary-foreground", + tertiary: "bg-tertiary text-tertiary-foreground", + accent: "bg-accent text-accent-foreground", + destructive: "bg-destructive text-destructive-foreground", + black: "bg-black text-white", + muted: "bg-muted text-mute-foreground", + none: "bg-transparent", + }, + }, + compoundVariants: [ + { + variant: "transparent", + class: "bg-transparent", + tint: "none", + }, + ], + defaultVariants: { + variant: "transparent", + tint: "none", + }, +}); + +export interface BadgeProps + extends React.HTMLAttributes, + VariantProps {} + +function Badge({ className, variant, tint, ...props }: BadgeProps) { + return ( +
    + ); +} + +export { Badge, badgeVariants }; diff --git a/packages/client/src/components/UI/Button/Base.tsx b/packages/client/src/components/UI/Button/Base.tsx new file mode 100644 index 00000000..935ff899 --- /dev/null +++ b/packages/client/src/components/UI/Button/Base.tsx @@ -0,0 +1,322 @@ +// AlignUI Button v0.0.0 + +import * as React from "react"; + +import { Slot } from "@radix-ui/react-slot"; +import type { PolymorphicComponentProps } from "../../../utils/polymorphic"; +import { recursiveCloneChildren } from "../../../utils/recursive-clone-children"; +import { tv, type VariantProps } from "tailwind-variants"; + +const BUTTON_ROOT_NAME = "ButtonRoot"; +const BUTTON_ICON_NAME = "ButtonIcon"; + +export const buttonVariants = tv({ + slots: { + root: [ + // base + "group relative inline-flex items-center justify-center whitespace-nowrap outline-none", + "transition duration-200 ease-out", + // focus + "focus:outline-none", + // disabled + "disabled:pointer-events-none disabled:bg-weak-50 disabled:text-disabled-300 disabled:outline-transparent", + "active:scale-95", + "user-select-none", + ], + icon: [ + // base + "flex size-5 shrink-0 items-center justify-center", + ], + }, + variants: { + variant: { + primary: {}, + neutral: {}, + error: {}, + }, + mode: { + filled: { + root: "disabled:text-disabled-300 disabled:bg-bg-weak-50 disabled:text-text-disabled-300", + }, + stroke: { + root: [ + "outline outline-inset", // disabled + "disabled:pointer-events-none disabled:text-disabled-300 disabled:bg-bg-weak-50 disabled:text-text-disabled-300", + ], + }, + lighter: { + root: "outline outline-inset disabled:text-disabled-300 disabled:bg-bg-weak-50 disabled:text-text-disabled-300", + }, + ghost: { + root: "outline outline-inset disabled:text-disabled-300 disabled:bg-bg-weak-50 disabled:text-text-disabled-300", + }, + }, + shape: { + regular: { root: "rounded-lg" }, + pilled: { root: "rounded-full" }, + }, + size: { + medium: { + root: "h-10 gap-3 px-3.5 text-label-sm", + icon: "-mx-1", + }, + small: { + root: "h-9 gap-3 px-3 text-label-sm", + icon: "-mx-1", + }, + xsmall: { + root: "h-8 gap-2.5 px-2.5 text-label-xs", + icon: "-mx-1", + }, + xxsmall: { + root: "h-7 gap-2.5 px-2 text-label-xs", + icon: "-mx-1", + }, + }, + }, + compoundVariants: [ + //#region variant=primary + { + variant: "primary", + mode: "filled", + class: { + root: [ + // base + "bg-primary-base text-static-white", + // hover + "hover:bg-primary-darker", + // focus + "focus-visible:shadow-button-primary-focus", + ], + }, + }, + { + variant: "primary", + mode: "stroke", + class: { + root: [ + // base + "text-primary-base outline-primary-base bg-bg-white-0", + // hover + "hover:bg-primary-alpha-10 hover:outline-transparent", + // focus + "focus-visible:shadow-button-primary-focus", + ], + }, + }, + { + variant: "primary", + mode: "lighter", + class: { + root: [ + // base + "bg-primary-alpha-10 text-primary-base outline-transparent", + // hover + "hover:outline-primary-base hover:bg-bg-white-0", + // focus + "focus-visible:outline-primary-base focus-visible:bg-bg-white-0 focus-visible:shadow-button-primary-focus", + ], + }, + }, + { + variant: "primary", + mode: "ghost", + class: { + root: [ + // base + "text-primary-base bg-transparent outline-transparent", + // hover + "hover:bg-primary-alpha-10", + // focus + "focus-visible:outline-primary-base focus-visible:bg-bg-white-0 focus-visible:shadow-button-primary-focus", + ], + }, + }, + //#endregion + + //#region variant=neutral + { + variant: "neutral", + mode: "filled", + class: { + root: [ + // base + "bg-bg-strong-950 text-text-white-0", + // hover + "hover:bg-bg-surface-800", + // focus + "focus-visible:shadow-button-important-focus", + ], + }, + }, + { + variant: "neutral", + mode: "stroke", + class: { + root: [ + // base + "bg-bg-white-0 text-text-sub-600 shadow-regular-sm outline-stroke-soft-200", + // hover + "hover:bg-bg-weak-50 hover:text-text-strong-950 hover:shadow-none hover:outline-transparent", + // focus + "focus-visible:text-text-strong-950 focus-visible:shadow-button-important-focus focus-visible:outline-stroke-strong-950", + ], + }, + }, + { + variant: "neutral", + mode: "lighter", + class: { + root: [ + // base + "bg-bg-weak-50 text-text-sub-600 outline-transparent", + // hover + "hover:bg-bg-white-0 hover:text-text-strong-950 hover:shadow-regular-xs hover:outline-stroke-soft-200", + // focus + "focus-visible:bg-bg-white-0 focus-visible:text-text-strong-950 focus-visible:shadow-button-important-focus focus-visible:outline-stroke-strong-950", + ], + }, + }, + { + variant: "neutral", + mode: "ghost", + class: { + root: [ + // base + "bg-transparent text-text-sub-600 outline-transparent", + // hover + "hover:bg-bg-weak-50 hover:text-text-strong-950", + // focus + "focus-visible:bg-bg-white-0 focus-visible:text-text-strong-950 focus-visible:shadow-button-important-focus focus-visible:outline-stroke-strong-950", + ], + }, + }, + //#endregion + + //#region variant=error + { + variant: "error", + mode: "filled", + class: { + root: [ + // base + "bg-error-base text-static-white", + // hover + "hover:bg-red-700", + // focus + "focus-visible:shadow-button-error-focus", + ], + }, + }, + { + variant: "error", + mode: "stroke", + class: { + root: [ + // base + "text-error-base outline-error-base bg-bg-white-0", + // hover + "hover:bg-red-alpha-10 hover:outline-transparent", + // focus + "focus-visible:shadow-button-error-focus", + ], + }, + }, + { + variant: "error", + mode: "lighter", + class: { + root: [ + // base + "text-error-base bg-red-alpha-10 outline-transparent", + // hover + "hover:outline-error-base hover:bg-bg-white-0", + // focus + "focus-visible:outline-error-base focus-visible:bg-bg-white-0 focus-visible:shadow-button-error-focus", + ], + }, + }, + { + variant: "error", + mode: "ghost", + class: { + root: [ + // base + "text-error-base bg-transparent outline-transparent", + // hover + "hover:bg-red-alpha-10", + // focus + "focus-visible:outline-error-base focus-visible:bg-bg-white-0 focus-visible:shadow-button-error-focus", + ], + }, + }, + //#endregion + ], + defaultVariants: { + variant: "primary", + mode: "filled", + size: "medium", + shape: "regular", + }, +}); + +type ButtonSharedProps = VariantProps; + +export type ButtonRootProps = VariantProps & + React.ButtonHTMLAttributes & { + asChild?: boolean; + }; + +const ButtonRoot = React.forwardRef( + ( + { children, variant, mode, size, asChild, className, shape, ...rest }, + forwardedRef + ) => { + const uniqueId = React.useId(); + const Component = asChild ? Slot : "button"; + const { root } = buttonVariants({ variant, mode, size, shape }); + + const sharedProps: ButtonSharedProps = { + variant, + mode, + size, + shape, + }; + + const extendedChildren = recursiveCloneChildren( + children as React.ReactElement[], + sharedProps, + [BUTTON_ICON_NAME], + uniqueId, + asChild + ); + + return ( + + {extendedChildren} + + ); + } +); +ButtonRoot.displayName = BUTTON_ROOT_NAME; + +function ButtonIcon({ + variant, + mode, + size, + as, + className, + ...rest +}: PolymorphicComponentProps) { + const Component = as || "div"; + const { icon } = buttonVariants({ mode, variant, size }); + + return ; +} +ButtonIcon.displayName = BUTTON_ICON_NAME; + +export { ButtonRoot as Root, ButtonIcon as Icon }; diff --git a/packages/client/src/components/UI/Button/index.tsx b/packages/client/src/components/UI/Button/index.tsx new file mode 100644 index 00000000..de8c9769 --- /dev/null +++ b/packages/client/src/components/UI/Button/index.tsx @@ -0,0 +1,25 @@ +import type { SyntheticEvent } from "react"; +import { Root, type ButtonRootProps } from "./Base"; + +export type ButtonProps = { + label: string; + leadingIcon?: React.ReactNode; + trailingIcon?: React.ReactNode; + onClick?: (e: SyntheticEvent) => void; +} & ButtonRootProps; + +/** Primary UI component for user interaction */ +export const Button = ({ + label, + leadingIcon, + trailingIcon, + ...props +}: ButtonProps) => { + return ( + + {leadingIcon} + {label} + {trailingIcon} + + ); +}; diff --git a/packages/client/src/components/UI/Card/ActionCard.tsx b/packages/client/src/components/UI/Card/ActionCard.tsx new file mode 100644 index 00000000..61e07c6f --- /dev/null +++ b/packages/client/src/components/UI/Card/ActionCard.tsx @@ -0,0 +1,75 @@ +import { cn } from "@/utils/cn"; +import * as React from "react"; +import { tv, type VariantProps } from "tailwind-variants"; +import { Card, type CardRootProps } from "./Card"; +import { RiCamera3Line } from "@remixicon/react"; + +export const cardVariants = tv({ + base: "relative flex flex-col grow border-0 rounded-lg overflow-clip rounded-lg justify-between p-0 gap-0", + variants: { + media: { + large: "", + small: "", + }, + }, + defaultVariants: { + media: "large", + }, +}); + +export type ActionCardVariantProps = VariantProps; + +export type ActionCardRootProps = React.HTMLAttributes & + ActionCardVariantProps & + CardRootProps & { action: Action; selected: boolean }; + +const ActionCard = React.forwardRef( + ({ media, className, selected, action, ...props }, ref) => { + const classes = cardVariants({ media, class: className }); + return ( + + {action.description} +
    +
    +
    +
    + + {action.title} +
    +
    +
    + {action.mediaInfo.description} +
    +
    + + ); + } +); +ActionCard.displayName = "ActionCard"; + +export { ActionCard }; diff --git a/packages/client/src/components/UI/Card/Card.tsx b/packages/client/src/components/UI/Card/Card.tsx new file mode 100644 index 00000000..db14bcd2 --- /dev/null +++ b/packages/client/src/components/UI/Card/Card.tsx @@ -0,0 +1,135 @@ +import { cn } from "@/utils/cn"; +import * as React from "react"; +import { tv, type VariantProps } from "tailwind-variants"; + +export const cardVariants = tv({ + base: "rounded-2xl border flex gap-0.5 justify-between border-border p-1 px-4 bg-white", + variants: { + variant: { + primary: "", + avatar: "", + }, + mode: { + "no-outline": "border-0 shadow-0", + outline: "border-border border shadow-xs", + filled: "bg-card border-0" + }, + size: { + large: "min-h-18", + small: "h-auto py-4", + }, + shadow: { + "no-shadow": "shadow-none", + shadow: "shadow-md" + }, + animating: { + default: "active:brightness-102 active:bg-primary/1.5 transition-all duration-200 ease-in-out", + none: "" + } + }, + defaultVariants: { + variant: "primary", + mode: "outline", + size: "large", + animating: "default" + }, +}); + +export type CardVariantProps = VariantProps; + +export type CardRootProps = React.HTMLAttributes & + CardVariantProps & { + asChild?: boolean; + }; + +const Card = React.forwardRef( + ({ className, variant, mode, size, ...props }, ref) => { + const classes = cardVariants({ variant, mode, size, class: className }); + return ( +
    + ); + } +); +Card.displayName = "Card"; + +const CardHeader = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
    +)); +CardHeader.displayName = "CardHeader"; + +const CardTitle = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
    +)); +CardTitle.displayName = "CardTitle"; + +const CardDescription = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
    +)); +CardDescription.displayName = "CardDescription"; + +const CardContent = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
    +)); +CardContent.displayName = "CardContent"; + +const CardFooter = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
    +)); +CardFooter.displayName = "CardFooter"; + +const FlexCard = React.forwardRef( + ({ className, ...props }, ref) => ( + + ) +); + +export { + Card, + CardHeader, + CardFooter, + CardTitle, + CardDescription, + CardContent, + FlexCard +}; diff --git a/packages/client/src/components/UI/Card/GardenCard.tsx b/packages/client/src/components/UI/Card/GardenCard.tsx new file mode 100644 index 00000000..ab66e77a --- /dev/null +++ b/packages/client/src/components/UI/Card/GardenCard.tsx @@ -0,0 +1,130 @@ +import { cn } from "@/utils/cn"; +import * as React from "react"; +import { tv, type VariantProps } from "tailwind-variants"; +import { Card, type CardRootProps } from "./Card"; +import { + RiUser2Fill, + RiUserCommunityFill, + RiUserLocationFill, +} from "@remixicon/react"; +import { Badge } from "../Badge/Badge"; +import { formatAddress } from "@/utils/text"; + +export const cardVariants = tv({ + base: "relative flex flex-col grow border-0 rounded-lg overflow-clip rounded-lg justify-between p-0 gap-0", + variants: { + media: { + large: "", + small: "", + }, + }, + defaultVariants: { + media: "large", + }, +}); + +export type GardenCardVariantProps = VariantProps; + +export type GardenCardOptions = { + showOperators?: boolean; + showDescription?: boolean; + showBanner?: boolean; +}; + +export type GardenCardRootProps = React.HTMLAttributes & + GardenCardVariantProps & + CardRootProps & { garden: Garden; selected: boolean } & GardenCardOptions; + +const GardenCard = React.forwardRef( + ( + { + media, + className, + selected, + garden, + showOperators = false, + showDescription = true, + showBanner = true, + ...props + }, + ref + ) => { + const classes = cardVariants({ media, class: className }); + return ( + + {garden.description} +
    +
    +
    +
    + {garden.name} +
    +
    + + + {garden.operators.length} Gardeners + + + + {garden.location} + +
    + {showOperators && ( + <> +
    + Operators +
    +
    + <> + {garden.operators.slice(0, 2).map((operator) => ( + + + {formatAddress(operator)} + + ))} + {garden.operators.length > 2 && ( + + + and {garden.operators.length - 2} others + + )} + +
    + + )} +
    + {showDescription && ( +
    {garden.description}
    + )} +
    + + ); + } +); +GardenCard.displayName = "GardenCard"; + +export { GardenCard }; diff --git a/packages/client/src/components/UI/Card/WorkCard.tsx b/packages/client/src/components/UI/Card/WorkCard.tsx new file mode 100644 index 00000000..51209d97 --- /dev/null +++ b/packages/client/src/components/UI/Card/WorkCard.tsx @@ -0,0 +1,74 @@ +import { cn } from "@/utils/cn"; +import * as React from "react"; +import { tv, type VariantProps } from "tailwind-variants"; +import { Card, type CardRootProps } from "./Card"; +import { Button } from "../Button"; + +export const cardVariants = tv({ + base: "relative flex flex-col grow border-0 rounded-lg overflow-clip rounded-lg justify-between p-0 gap-0", + variants: { + media: { + large: "", + small: "", + }, + }, + defaultVariants: { + media: "large", + }, +}); + +export type ActionCardVariantProps = VariantProps; + +export type ActionCardRootProps = React.HTMLAttributes & + ActionCardVariantProps & + CardRootProps & { work: Work; selected: boolean }; + +const WorkCard = React.forwardRef( + ({ media, className, selected, work, ...props }, ref) => { + const classes = cardVariants({ media, class: className }); + return ( + + {work.feedback} +
    +
    +
    +
    + {work.title} +
    +
    +
    {work.feedback}
    +
    +
    +
    + Published on {new Date(work.createdAt).toLocaleString()} +
    +
    +
    + + ); + } +); +WorkCard.displayName = "WorkCard"; + +export { WorkCard }; diff --git a/packages/client/src/components/UI/Carousel/Carousel.tsx b/packages/client/src/components/UI/Carousel/Carousel.tsx new file mode 100644 index 00000000..c16607b8 --- /dev/null +++ b/packages/client/src/components/UI/Carousel/Carousel.tsx @@ -0,0 +1,220 @@ +"use client"; + +import * as React from "react"; +import useEmblaCarousel, { + type UseEmblaCarouselType, +} from "embla-carousel-react"; +import { cn } from "@/utils/cn"; + +type CarouselApi = UseEmblaCarouselType[1]; +type UseCarouselParameters = Parameters; +type CarouselOptions = UseCarouselParameters[0]; +type CarouselPlugin = UseCarouselParameters[1]; + +type CarouselProps = { + opts?: CarouselOptions; + plugins?: CarouselPlugin; + orientation?: "horizontal" | "vertical"; + setApi?: (api: CarouselApi) => void; +}; + +type CarouselContextProps = { + carouselRef: ReturnType[0]; + api: ReturnType[1]; + scrollPrev: () => void; + scrollNext: () => void; + canScrollPrev: boolean; + canScrollNext: boolean; +} & CarouselProps; + +const CarouselContext = React.createContext(null); + +function useCarousel() { + const context = React.useContext(CarouselContext); + + if (!context) { + throw new Error("useCarousel must be used within a "); + } + + return context; +} + +const Carousel = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes & CarouselProps +>( + ( + { + orientation = "horizontal", + opts, + setApi, + plugins, + className, + children, + ...props + }, + ref + ) => { + const [carouselRef, api] = useEmblaCarousel( + { + ...opts, + axis: orientation === "horizontal" ? "x" : "y", + }, + plugins + ); + const [canScrollPrev, setCanScrollPrev] = React.useState(false); + const [canScrollNext, setCanScrollNext] = React.useState(false); + + const onSelect = React.useCallback((api: CarouselApi) => { + if (!api) { + return; + } + + setCanScrollPrev(api.canScrollPrev()); + setCanScrollNext(api.canScrollNext()); + }, []); + + const scrollPrev = React.useCallback(() => { + api?.scrollPrev(); + }, [api]); + + const scrollNext = React.useCallback(() => { + api?.scrollNext(); + }, [api]); + + const handleKeyDown = React.useCallback( + (event: React.KeyboardEvent) => { + if (event.key === "ArrowLeft") { + event.preventDefault(); + scrollPrev(); + } else if (event.key === "ArrowRight") { + event.preventDefault(); + scrollNext(); + } + }, + [scrollPrev, scrollNext] + ); + + React.useEffect(() => { + if (!api || !setApi) { + return; + } + + setApi(api); + }, [api, setApi]); + + React.useEffect(() => { + if (!api) { + return; + } + + onSelect(api); + api.on("reInit", onSelect); + api.on("select", onSelect); + + return () => { + api?.off("select", onSelect); + }; + }, [api, onSelect]); + + return ( + +
    + {children} +
    +
    + ); + } +); +Carousel.displayName = "Carousel"; + +const CarouselContent = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => { + const { carouselRef, orientation } = useCarousel(); + + return ( +
    +
    +
    + ); +}); +CarouselContent.displayName = "CarouselContent"; + +const CarouselItem = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => { + const { orientation } = useCarousel(); + + return ( +
    + ); +}); +CarouselItem.displayName = "CarouselItem"; + +const GardenCarousel = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes & { garden: Garden } +>(({ className, children, garden, ...props }, ref) => { + return ( +
    + {garden.description} +
    +
    {garden.name}
    +
    + {children} +
    + ); +}); +GardenCarousel.displayName = "GardenCarousel"; + +export { + type CarouselApi, + Carousel, + CarouselContent, + CarouselItem, + GardenCarousel, +}; diff --git a/packages/client/src/components/UI/Form/Card.tsx b/packages/client/src/components/UI/Form/Card.tsx new file mode 100644 index 00000000..f55d8464 --- /dev/null +++ b/packages/client/src/components/UI/Form/Card.tsx @@ -0,0 +1,32 @@ +import type { RemixiconComponentType } from "@remixicon/react"; +import { Card, type CardRootProps } from "../Card/Card"; +import { cn } from "@/utils/cn"; + +interface FormCardProps { + label: string; + value: string; + Icon?: RemixiconComponentType; +} + +export const FormCard = ({ + label, + value, + variant = "primary", + Icon, + className, + ...props +}: FormCardProps & CardRootProps) => { + return ( + +
    +
    + {Icon && } +
    {label}
    +
    +
    + {value} +
    +
    +
    + ); +}; diff --git a/packages/client/src/components/Form/Date.tsx b/packages/client/src/components/UI/Form/Date.tsx similarity index 100% rename from packages/client/src/components/Form/Date.tsx rename to packages/client/src/components/UI/Form/Date.tsx diff --git a/packages/client/src/components/Form/Info.tsx b/packages/client/src/components/UI/Form/Info.tsx similarity index 50% rename from packages/client/src/components/Form/Info.tsx rename to packages/client/src/components/UI/Form/Info.tsx index acfc5fae..baa26002 100644 --- a/packages/client/src/components/Form/Info.tsx +++ b/packages/client/src/components/UI/Form/Info.tsx @@ -1,14 +1,17 @@ -import { RemixiconComponentType } from "@remixicon/react"; +import type { RemixiconComponentType } from "@remixicon/react"; +import { Card } from "../Card/Card"; +import { cn } from "@/utils/cn"; interface FormInfoProps { title: string; info: string; variant?: "primary" | "secondary" | "tertiary"; Icon?: RemixiconComponentType; + className?: string; } const variants = { - primary: "bg-slate-100 border-slate-200 border-1 shadow-md", + primary: "bg-slate-100", secondary: "bg-green-100 text-green-700", tertiary: "bg-yellow-100 text-yellow-700", }; @@ -18,24 +21,27 @@ export const FormInfo = ({ info, variant = "primary", Icon, + className = "", ...props }: FormInfoProps) => { const variantClasses = variants[variant]; return ( -
    {Icon && (
    - +
    )} -
    -

    {title}

    -

    {info}

    +
    +
    {title}
    +
    {info}
    -
    + ); }; diff --git a/packages/client/src/components/Form/Input.tsx b/packages/client/src/components/UI/Form/Input.tsx similarity index 54% rename from packages/client/src/components/Form/Input.tsx rename to packages/client/src/components/UI/Form/Input.tsx index 0cbbd3da..7c9d1787 100644 --- a/packages/client/src/components/Form/Input.tsx +++ b/packages/client/src/components/UI/Form/Input.tsx @@ -8,24 +8,29 @@ interface FormInputProps extends InputHTMLAttributes { export const FormInput = forwardRef( ({ label, helperText, error, className, ...props }, ref) => ( -
    -