mirror of
https://github.com/vercel/commerce.git
synced 2025-07-25 11:11:24 +00:00
add order details page
This commit is contained in:
@@ -1,15 +1,12 @@
|
||||
'use server';
|
||||
|
||||
import { TAGS } from 'lib/shopify/customer/constants';
|
||||
import { removeAllCookiesServerAction } from 'lib/shopify/customer/auth-helpers';
|
||||
import { CUSTOMER_API_URL, ORIGIN_URL, removeAllCookiesServerAction } from 'lib/shopify/auth';
|
||||
import { redirect } from 'next/navigation';
|
||||
import { revalidateTag } from 'next/cache';
|
||||
import { cookies } from 'next/headers';
|
||||
import { SHOPIFY_ORIGIN, SHOPIFY_CUSTOMER_ACCOUNT_API_URL } from 'lib/shopify/customer/constants';
|
||||
|
||||
export async function doLogout() {
|
||||
const origin = SHOPIFY_ORIGIN;
|
||||
const customerAccountApiUrl = SHOPIFY_CUSTOMER_ACCOUNT_API_URL;
|
||||
const origin = ORIGIN_URL;
|
||||
const customerAccountApiUrl = CUSTOMER_API_URL;
|
||||
let logoutUrl;
|
||||
try {
|
||||
const idToken = cookies().get('shop_id_token');
|
||||
@@ -26,7 +23,6 @@ export async function doLogout() {
|
||||
);
|
||||
}
|
||||
await removeAllCookiesServerAction();
|
||||
revalidateTag(TAGS.customer);
|
||||
} catch (e) {
|
||||
console.log('Error', e);
|
||||
//you can throw error here or return - return goes back to form b/c of state, throw will throw the error boundary
|
||||
|
@@ -2,26 +2,21 @@
|
||||
//https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations#forms
|
||||
'use server';
|
||||
|
||||
import { TAGS } from 'lib/shopify/customer/constants';
|
||||
import { redirect } from 'next/navigation';
|
||||
import { revalidateTag } from 'next/cache';
|
||||
import { cookies } from 'next/headers';
|
||||
//import { getOrigin } from 'lib/shopify/customer'
|
||||
import {
|
||||
generateCodeVerifier,
|
||||
generateCodeChallenge,
|
||||
generateRandomString
|
||||
} from 'lib/shopify/customer/auth-utils';
|
||||
import {
|
||||
SHOPIFY_CUSTOMER_ACCOUNT_API_URL,
|
||||
SHOPIFY_CLIENT_ID,
|
||||
SHOPIFY_ORIGIN
|
||||
} from 'lib/shopify/customer/constants';
|
||||
generateRandomString,
|
||||
CUSTOMER_API_CLIENT_ID,
|
||||
ORIGIN_URL,
|
||||
CUSTOMER_API_URL
|
||||
} from 'lib/shopify/auth';
|
||||
|
||||
export async function doLogin(prevState: any) {
|
||||
const customerAccountApiUrl = SHOPIFY_CUSTOMER_ACCOUNT_API_URL;
|
||||
const clientId = SHOPIFY_CLIENT_ID;
|
||||
const origin = SHOPIFY_ORIGIN;
|
||||
export async function doLogin(_: any) {
|
||||
const customerAccountApiUrl = CUSTOMER_API_URL;
|
||||
const clientId = CUSTOMER_API_CLIENT_ID;
|
||||
const origin = ORIGIN_URL;
|
||||
const loginUrl = new URL(`${customerAccountApiUrl}/auth/oauth/authorize`);
|
||||
//console.log ("previous", prevState)
|
||||
|
||||
@@ -64,6 +59,5 @@ export async function doLogin(prevState: any) {
|
||||
return 'Error logging in. Please try again';
|
||||
}
|
||||
|
||||
revalidateTag(TAGS.customer);
|
||||
redirect(`${loginUrl}`); // Navigate to the new post page
|
||||
}
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { ChevronRightIcon, EllipsisHorizontalIcon } from '@heroicons/react/16/solid';
|
||||
import { cn } from 'lib/utils';
|
||||
import { cn } from 'lib/shopify/utils';
|
||||
import Link, { LinkProps } from 'next/link';
|
||||
import { ComponentPropsWithoutRef, ReactNode, forwardRef } from 'react';
|
||||
|
||||
|
@@ -1,5 +1,4 @@
|
||||
import { getCollection, getMenu, getProduct } from 'lib/shopify';
|
||||
import { findParentCollection } from 'lib/utils';
|
||||
import { Fragment } from 'react';
|
||||
import {
|
||||
Breadcrumb,
|
||||
@@ -9,6 +8,7 @@ import {
|
||||
BreadcrumbPage,
|
||||
BreadcrumbSeparator
|
||||
} from './breadcrumb-list';
|
||||
import { findParentCollection } from 'lib/shopify/utils';
|
||||
|
||||
type BreadcrumbProps = {
|
||||
type: 'product' | 'collection';
|
||||
|
@@ -1,69 +1,77 @@
|
||||
'use client';
|
||||
import React from 'react';
|
||||
import { Slot } from '@radix-ui/react-slot';
|
||||
import { Button as ButtonBase, ButtonProps as ButtonBaseProps } from '@headlessui/react';
|
||||
import { tv, type VariantProps } from 'tailwind-variants';
|
||||
import clsx from 'clsx';
|
||||
import Spinner from './spinner';
|
||||
|
||||
const buttonVariants = tv({
|
||||
base: [
|
||||
// base
|
||||
'relative inline-flex items-center justify-center rounded-md border px-3 py-1.5 text-center text-sm font-medium transition-all duration-100 ease-in-out',
|
||||
// disabled
|
||||
'disabled:pointer-events-none disabled:shadow-none'
|
||||
],
|
||||
slots: {
|
||||
root: [
|
||||
// base
|
||||
'relative inline-flex items-center justify-center rounded-md',
|
||||
// text
|
||||
'text-center font-medium',
|
||||
// transition
|
||||
'transition-all duration-100 ease-in-out',
|
||||
// disabled
|
||||
'disabled:pointer-events-none disabled:shadow-none'
|
||||
],
|
||||
loading: 'pointer-events-none flex shrink-0 items-center justify-center gap-1.5'
|
||||
},
|
||||
variants: {
|
||||
size: {
|
||||
sm: 'text-xs px-2.5 py-1.5',
|
||||
md: 'text-sm px-3 py-2',
|
||||
lg: 'text-base px-4 py-2.5'
|
||||
sm: {
|
||||
root: 'text-xs px-2.5 py-1.5'
|
||||
},
|
||||
md: {
|
||||
root: 'text-sm px-3 py-2'
|
||||
},
|
||||
lg: {
|
||||
root: 'text-base px-4 py-2.5'
|
||||
}
|
||||
},
|
||||
variant: {
|
||||
primary: [
|
||||
// border
|
||||
'border-transparent',
|
||||
// text color
|
||||
'text-white',
|
||||
// background color
|
||||
'bg-tremor-brand',
|
||||
// hover color
|
||||
'hover:bg-tremor-brand-emphasis',
|
||||
// disabled
|
||||
'disabled:bg-gray-100',
|
||||
'disabled:bg-tremor-brand-muted'
|
||||
],
|
||||
secondary: [
|
||||
// border
|
||||
'border-gray-300',
|
||||
// text color
|
||||
'text-gray-900',
|
||||
// background color
|
||||
' bg-white',
|
||||
//hover color
|
||||
'hover:bg-gray-50',
|
||||
// disabled
|
||||
'disabled:text-gray-400'
|
||||
],
|
||||
text: [
|
||||
// border
|
||||
'border-transparent',
|
||||
// text color
|
||||
'text-tremor-brand',
|
||||
// background color
|
||||
'bg-transparent',
|
||||
// hover color
|
||||
'disabled:text-gray-400'
|
||||
],
|
||||
destructive: [
|
||||
// text color
|
||||
'text-white',
|
||||
// border
|
||||
'border-transparent',
|
||||
// background color
|
||||
'bg-red-600',
|
||||
// hover color
|
||||
'hover:bg-red-700',
|
||||
// disabled
|
||||
'disabled:bg-red-300 disabled:text-white'
|
||||
]
|
||||
primary: {
|
||||
root: [
|
||||
// border
|
||||
'border-transparent',
|
||||
// text color
|
||||
'text-white',
|
||||
// background color
|
||||
'bg-primary',
|
||||
// hover color
|
||||
'hover:bg-primary-empahsis',
|
||||
// disabled
|
||||
'disabled:bg-primary-muted'
|
||||
]
|
||||
},
|
||||
secondary: {
|
||||
root: [
|
||||
// border
|
||||
'border-gray-300',
|
||||
// text color
|
||||
'text-gray-900',
|
||||
// background color
|
||||
' bg-white',
|
||||
//hover color
|
||||
'hover:bg-gray-50',
|
||||
// disabled
|
||||
'disabled:text-gray-400'
|
||||
]
|
||||
},
|
||||
text: {
|
||||
root: [
|
||||
// border
|
||||
'border-transparent',
|
||||
// text color
|
||||
'text-tremor-brand',
|
||||
// background color
|
||||
'bg-transparent',
|
||||
// hover color
|
||||
'disabled:text-gray-400'
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
defaultVariants: {
|
||||
@@ -72,45 +80,44 @@ const buttonVariants = tv({
|
||||
}
|
||||
});
|
||||
|
||||
interface ButtonProps
|
||||
extends React.ComponentPropsWithoutRef<'button'>,
|
||||
VariantProps<typeof buttonVariants> {
|
||||
asChild?: boolean;
|
||||
interface ButtonProps extends ButtonBaseProps, VariantProps<typeof buttonVariants> {
|
||||
isLoading?: boolean;
|
||||
loadingText?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
(
|
||||
{
|
||||
asChild,
|
||||
isLoading = false,
|
||||
loadingText,
|
||||
children,
|
||||
className,
|
||||
disabled,
|
||||
isLoading,
|
||||
loadingText = 'Loading',
|
||||
size,
|
||||
variant,
|
||||
children,
|
||||
...props
|
||||
}: ButtonProps,
|
||||
forwardedRef
|
||||
) => {
|
||||
const Component = asChild ? Slot : 'button';
|
||||
const { loading, root } = buttonVariants({ variant, size });
|
||||
return (
|
||||
<Component
|
||||
<ButtonBase
|
||||
ref={forwardedRef}
|
||||
className={clsx(buttonVariants({ variant }), className)}
|
||||
className={clsx(root(), className)}
|
||||
disabled={disabled || isLoading}
|
||||
{...props}
|
||||
>
|
||||
{isLoading ? (
|
||||
<span className="pointer-events-none flex shrink-0 items-center justify-center gap-1.5">
|
||||
<span className="sr-only">{loadingText ? loadingText : 'Loading'}</span>
|
||||
{loadingText ? loadingText : children}
|
||||
<span className={loading()}>
|
||||
<Spinner />
|
||||
<span className="sr-only">{loadingText}</span>
|
||||
<span>{loadingText}</span>
|
||||
</span>
|
||||
) : (
|
||||
children
|
||||
)}
|
||||
</Component>
|
||||
</ButtonBase>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
@@ -2,7 +2,7 @@ import { PlusIcon } from '@heroicons/react/16/solid';
|
||||
import Price from 'components/price';
|
||||
import { DEFAULT_OPTION } from 'lib/constants';
|
||||
import { CartItem } from 'lib/shopify/types';
|
||||
import { createUrl } from 'lib/utils';
|
||||
import { createUrl } from 'lib/shopify/utils';
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
import { DeleteItemButton } from './delete-item-button';
|
||||
@@ -65,8 +65,10 @@ const LineItem = ({ item, closeCart }: LineItemProps) => {
|
||||
className="h-full w-full object-cover"
|
||||
width={64}
|
||||
height={64}
|
||||
alt={item.merchandise.product.featuredImage.altText || item.merchandise.product.title}
|
||||
src={item.merchandise.product.featuredImage.url}
|
||||
alt={
|
||||
item.merchandise.product?.featuredImage?.altText || item.merchandise.product.title
|
||||
}
|
||||
src={item.merchandise.product?.featuredImage?.url}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
@@ -2,7 +2,7 @@
|
||||
|
||||
import { CheckIcon } from '@heroicons/react/24/outline';
|
||||
import * as CheckboxPrimitive from '@radix-ui/react-checkbox';
|
||||
import { cn } from 'lib/utils';
|
||||
import { cn } from 'lib/shopify/utils';
|
||||
import { forwardRef } from 'react';
|
||||
|
||||
const Checkbox = forwardRef<
|
||||
|
54
components/divider.tsx
Normal file
54
components/divider.tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
import { tv } from 'tailwind-variants';
|
||||
|
||||
const divider = tv({
|
||||
slots: {
|
||||
root: '',
|
||||
element: 'bg-gray-200'
|
||||
},
|
||||
variants: {
|
||||
orientation: {
|
||||
horizontal: {
|
||||
root: 'w-full mx-auto flex justify-between items-center text-tremor-default text-tremor-content',
|
||||
element: 'w-full h-[1px] '
|
||||
},
|
||||
vertical: {
|
||||
root: 'flex justify-between items-stretch text-tremor-default text-tremor-content',
|
||||
element: 'h-full w-[1px]'
|
||||
}
|
||||
},
|
||||
hasSpacing: {
|
||||
true: {},
|
||||
false: {}
|
||||
}
|
||||
},
|
||||
compoundVariants: [
|
||||
{
|
||||
orientation: 'horizontal',
|
||||
hasSpacing: true,
|
||||
class: {
|
||||
root: 'my-6'
|
||||
}
|
||||
},
|
||||
{
|
||||
orientation: 'vertical',
|
||||
hasSpacing: true,
|
||||
class: {
|
||||
root: 'mx-6'
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
type DividerProps = {
|
||||
orientation?: 'horizontal' | 'vertical';
|
||||
hasSpacing?: boolean;
|
||||
};
|
||||
export default function Divider({ orientation = 'horizontal', hasSpacing = true }: DividerProps) {
|
||||
const { root, element } = divider({ orientation, hasSpacing });
|
||||
|
||||
return (
|
||||
<div className={root()}>
|
||||
<span className={element()} />
|
||||
</div>
|
||||
);
|
||||
}
|
@@ -3,7 +3,7 @@
|
||||
import { Button } from '@headlessui/react';
|
||||
import { MAKE_FILTER_ID, MODEL_FILTER_ID, PART_TYPES, YEAR_FILTER_ID } from 'lib/constants';
|
||||
import { Menu, Metaobject } from 'lib/shopify/types';
|
||||
import { createUrl, findParentCollection } from 'lib/utils';
|
||||
import { createUrl, findParentCollection } from 'lib/shopify/utils';
|
||||
import get from 'lodash.get';
|
||||
import { useParams, useRouter, useSearchParams } from 'next/navigation';
|
||||
import { useState } from 'react';
|
||||
|
@@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { MagnifyingGlassIcon } from '@heroicons/react/24/outline';
|
||||
import { createUrl } from 'lib/utils';
|
||||
import { createUrl } from 'lib/shopify/utils';
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
|
||||
export default function Search() {
|
||||
|
@@ -3,7 +3,7 @@ import { Disclosure, DisclosureButton, DisclosurePanel } from '@headlessui/react
|
||||
import { ChevronDownIcon } from '@heroicons/react/24/outline';
|
||||
import clsx from 'clsx';
|
||||
import { Filter, FilterType } from 'lib/shopify/types';
|
||||
import { createUrl } from 'lib/utils';
|
||||
import { createUrl } from 'lib/shopify/utils';
|
||||
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
|
||||
import PriceRange from './price-range';
|
||||
import SelectedList from './selected-list';
|
||||
|
@@ -3,7 +3,7 @@
|
||||
import Price from 'components/price';
|
||||
import { useDebounce } from 'hooks';
|
||||
import { Filter } from 'lib/shopify/types';
|
||||
import { createUrl } from 'lib/utils';
|
||||
import { createUrl } from 'lib/shopify/utils';
|
||||
import get from 'lodash.get';
|
||||
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
@@ -2,7 +2,7 @@
|
||||
|
||||
import { XMarkIcon } from '@heroicons/react/16/solid';
|
||||
import { Filter } from 'lib/shopify/types';
|
||||
import { createUrl } from 'lib/utils';
|
||||
import { createUrl } from 'lib/shopify/utils';
|
||||
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
|
||||
|
||||
const SelectedList = ({ filters }: { filters: Filter[] }) => {
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import clsx from 'clsx';
|
||||
import { SortFilterItem } from 'lib/constants';
|
||||
import { createUrl } from 'lib/utils';
|
||||
import { createUrl } from 'lib/shopify/utils';
|
||||
import Link from 'next/link';
|
||||
import { usePathname, useSearchParams } from 'next/navigation';
|
||||
|
||||
|
@@ -3,7 +3,7 @@
|
||||
import { ArrowRightIcon } from '@heroicons/react/16/solid';
|
||||
import { MAKE_FILTER_ID } from 'lib/constants';
|
||||
import { Metaobject } from 'lib/shopify/types';
|
||||
import { createUrl } from 'lib/utils';
|
||||
import { createUrl } from 'lib/shopify/utils';
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
|
||||
const ButtonGroup = ({ manufacturer }: { manufacturer: Metaobject }) => {
|
||||
|
@@ -9,7 +9,6 @@ type ManufacturersGridProps = {
|
||||
};
|
||||
|
||||
const ManufacturersGrid = ({ manufacturers, variant = 'home' }: ManufacturersGridProps) => {
|
||||
console.log('manufacturers', manufacturers);
|
||||
const popularManufacturers = manufacturers.filter(
|
||||
(manufacturer) => manufacturer.is_popular === 'true'
|
||||
);
|
||||
|
@@ -3,11 +3,13 @@ import clsx from 'clsx';
|
||||
const Price = ({
|
||||
amount,
|
||||
className,
|
||||
as,
|
||||
currencyCode = 'USD',
|
||||
currencyCodeClassName,
|
||||
showCurrency = false
|
||||
}: {
|
||||
amount: string;
|
||||
as?: 'p' | 'span';
|
||||
className?: string;
|
||||
currencyCode: string;
|
||||
currencyCodeClassName?: string;
|
||||
@@ -21,9 +23,10 @@ const Price = ({
|
||||
return <p className={className}>Included</p>;
|
||||
}
|
||||
|
||||
const Component = as || 'p';
|
||||
// Otherwise, format and display the price
|
||||
return (
|
||||
<p suppressHydrationWarning={true} className={className}>
|
||||
<Component suppressHydrationWarning={true} className={className}>
|
||||
{new Intl.NumberFormat(undefined, {
|
||||
style: 'currency',
|
||||
currency: currencyCode,
|
||||
@@ -32,7 +35,7 @@ const Price = ({
|
||||
{showCurrency && (
|
||||
<span className={clsx('ml-1 inline', currencyCodeClassName)}>{currencyCode}</span>
|
||||
)}
|
||||
</p>
|
||||
</Component>
|
||||
);
|
||||
};
|
||||
|
||||
|
@@ -6,7 +6,7 @@ import Price from 'components/price';
|
||||
import SideDialog from 'components/side-dialog';
|
||||
import { CORE_VARIANT_ID_KEY, CORE_WAIVER } from 'lib/constants';
|
||||
import { CoreChargeOption, ProductVariant } from 'lib/shopify/types';
|
||||
import { cn, createUrl } from 'lib/utils';
|
||||
import { cn, createUrl } from 'lib/shopify/utils';
|
||||
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
|
||||
import { useState } from 'react';
|
||||
|
||||
|
@@ -2,7 +2,7 @@
|
||||
|
||||
import { ArrowLeftIcon, ArrowRightIcon } from '@heroicons/react/24/outline';
|
||||
import { TileImage } from 'components/grid/tile';
|
||||
import { createUrl } from 'lib/utils';
|
||||
import { createUrl } from 'lib/shopify/utils';
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
import { usePathname, useSearchParams } from 'next/navigation';
|
||||
|
@@ -6,7 +6,7 @@ import clsx from 'clsx';
|
||||
import Price from 'components/price';
|
||||
import { CORE_VARIANT_ID_KEY, CORE_WAIVER } from 'lib/constants';
|
||||
import { CoreChargeOption, Money, ProductOption, ProductVariant } from 'lib/shopify/types';
|
||||
import { createUrl } from 'lib/utils';
|
||||
import { createUrl } from 'lib/shopify/utils';
|
||||
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
|
||||
import { Fragment, useEffect, useState } from 'react';
|
||||
|
||||
|
@@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import Price from 'components/price';
|
||||
import { cn } from 'lib/utils';
|
||||
import { cn } from 'lib/shopify/utils';
|
||||
import { ReactNode, useState } from 'react';
|
||||
|
||||
const options = ['Included', 'Premium Labor', '+1 Year'] as const;
|
||||
|
@@ -20,21 +20,15 @@ function SubmitButton(props: any) {
|
||||
<>
|
||||
{props?.message && <div className="my-5">{props?.message}</div>}
|
||||
<Button
|
||||
onClick={(e: React.FormEvent<HTMLButtonElement>) => {
|
||||
if (pending) e.preventDefault();
|
||||
}}
|
||||
type="submit"
|
||||
aria-label="Log in"
|
||||
aria-disabled={pending}
|
||||
disabled={pending}
|
||||
isLoading={pending}
|
||||
loadingText="Signing In..."
|
||||
className="w-full"
|
||||
>
|
||||
{pending ? (
|
||||
<>
|
||||
<span>Logging In...</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span>Log-In</span>
|
||||
</>
|
||||
)}
|
||||
Sign In
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
|
19
components/spinner.tsx
Normal file
19
components/spinner.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import clsx from 'clsx';
|
||||
import React from 'react';
|
||||
|
||||
export default function Spinner({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={clsx('flex-1 animate-spin stroke-current stroke-[3]', className)}
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22Z"
|
||||
className="stroke-current opacity-25"
|
||||
/>
|
||||
<path d="M12 2C6.47715 2 2 6.47715 2 12C2 14.7255 3.09032 17.1962 4.85857 19" />
|
||||
</svg>
|
||||
);
|
||||
}
|
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { cn } from 'lib/utils';
|
||||
import { cn } from 'lib/shopify/utils';
|
||||
import { ITooltip, Tooltip as ReactTooltip } from 'react-tooltip';
|
||||
|
||||
const Tooltip = ({ id, children, className }: ITooltip) => {
|
||||
|
35
components/ui/badge.tsx
Normal file
35
components/ui/badge.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import React from 'react';
|
||||
|
||||
import { VariantProps, tv } from 'tailwind-variants';
|
||||
|
||||
const badgeStyles = tv({
|
||||
base: [
|
||||
'absolute -right-2 -top-2 h-5 w-5',
|
||||
'flex items-center justify-center rounded-full text-xs font-semibold'
|
||||
],
|
||||
variants: {
|
||||
color: {
|
||||
primary: 'bg-primary text-white',
|
||||
secondary: 'bg-secondary text-white',
|
||||
content: 'bg-content text-white'
|
||||
}
|
||||
},
|
||||
defaultVariants: {
|
||||
color: 'content'
|
||||
}
|
||||
});
|
||||
|
||||
interface BadgeProps extends VariantProps<typeof badgeStyles> {
|
||||
content: string | number;
|
||||
className?: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export default function Badge({ className, color, children, content }: BadgeProps) {
|
||||
return (
|
||||
<span className="relative flex-none">
|
||||
{children}
|
||||
<span className={badgeStyles({ color, className })}>{content}</span>
|
||||
</span>
|
||||
);
|
||||
}
|
38
components/ui/card.tsx
Normal file
38
components/ui/card.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import React from 'react';
|
||||
import { Slot } from '@radix-ui/react-slot';
|
||||
|
||||
import { VariantProps, tv } from 'tailwind-variants';
|
||||
|
||||
const cardStyles = tv({
|
||||
base: 'rounded p-6 text-left w-full',
|
||||
variants: {
|
||||
outlined: {
|
||||
true: 'border bg-white',
|
||||
false: {}
|
||||
},
|
||||
elevated: {
|
||||
true: 'shadow-lg shadow-content/10 bg-white',
|
||||
false: {}
|
||||
}
|
||||
},
|
||||
defaultVariants: {
|
||||
outlined: true
|
||||
}
|
||||
});
|
||||
|
||||
interface CardProps extends React.ComponentPropsWithoutRef<'div'>, VariantProps<typeof cardStyles> {
|
||||
asChild?: boolean;
|
||||
}
|
||||
|
||||
const Card = React.forwardRef<HTMLDivElement, CardProps>(
|
||||
({ className, asChild, outlined, elevated, ...props }, forwardedRef) => {
|
||||
const Component = asChild ? Slot : 'div';
|
||||
return (
|
||||
<Component ref={forwardedRef} className={cardStyles({ outlined, className })} {...props} />
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
Card.displayName = 'Card';
|
||||
|
||||
export { Card, type CardProps };
|
26
components/ui/heading.tsx
Normal file
26
components/ui/heading.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import { VariantProps, tv } from 'tailwind-variants';
|
||||
|
||||
const heading = tv({
|
||||
base: [''],
|
||||
variants: {
|
||||
size: {
|
||||
sm: 'text-heading-sm',
|
||||
md: 'text-heading-md',
|
||||
lg: 'text-heading-lg'
|
||||
}
|
||||
},
|
||||
defaultVariants: {
|
||||
size: 'md'
|
||||
}
|
||||
});
|
||||
|
||||
interface HeadingProps extends VariantProps<typeof heading> {
|
||||
className?: string;
|
||||
children: React.ReactNode;
|
||||
as?: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'span' | 'p';
|
||||
}
|
||||
|
||||
export default function Heading({ children, className, size, as }: HeadingProps) {
|
||||
const Component = as || 'h2';
|
||||
return <Component className={heading({ size, className })}>{children}</Component>;
|
||||
}
|
32
components/ui/label.tsx
Normal file
32
components/ui/label.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import { VariantProps, tv } from 'tailwind-variants';
|
||||
|
||||
const label = tv(
|
||||
{
|
||||
base: 'text-content',
|
||||
variants: {
|
||||
size: {
|
||||
sm: 'text-label-sm',
|
||||
md: 'text-label-md',
|
||||
lg: 'text-label-lg'
|
||||
}
|
||||
},
|
||||
defaultVariants: {
|
||||
size: 'md'
|
||||
}
|
||||
},
|
||||
{
|
||||
twMerge: false
|
||||
}
|
||||
);
|
||||
|
||||
interface LabelProps extends VariantProps<typeof label> {
|
||||
className?: string;
|
||||
children: React.ReactNode;
|
||||
as?: 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'span' | 'p';
|
||||
}
|
||||
|
||||
export default function Label({ children, className, size, as }: LabelProps) {
|
||||
const Component = as || 'span';
|
||||
|
||||
return <Component className={label({ size, className })}>{children}</Component>;
|
||||
}
|
32
components/ui/text.tsx
Normal file
32
components/ui/text.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import { VariantProps, tv } from 'tailwind-variants';
|
||||
|
||||
const text = tv(
|
||||
{
|
||||
base: '',
|
||||
variants: {
|
||||
size: {
|
||||
sm: 'text-xs',
|
||||
md: 'text-sm',
|
||||
lg: 'text-md'
|
||||
}
|
||||
},
|
||||
defaultVariants: {
|
||||
size: 'md'
|
||||
}
|
||||
},
|
||||
{
|
||||
twMerge: false
|
||||
}
|
||||
);
|
||||
|
||||
interface TextProps extends VariantProps<typeof text> {
|
||||
className?: string;
|
||||
children: React.ReactNode;
|
||||
as?: 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'span' | 'p';
|
||||
}
|
||||
|
||||
export default function Text({ children, className, size, as }: TextProps) {
|
||||
const Component = as || 'p';
|
||||
|
||||
return <Component className={text({ size, className })}>{children}</Component>;
|
||||
}
|
Reference in New Issue
Block a user