├── .env.example ├── .eslintrc.json ├── .gitignore ├── .graphqlrc.yml ├── .husky └── pre-commit ├── .prettierignore ├── .vscode └── settings.json ├── .watchmanconfig ├── README.md ├── components.json ├── docs └── project-structure.md ├── drizzle.config.ts ├── graphql └── schema │ └── schema.graphql ├── jest.config.js ├── next-env.d.ts ├── next.config.mjs ├── package-lock.json ├── package.json ├── postcss.config.js ├── public ├── assets │ ├── bathroom-planning.jpg │ └── cutingcardImage.jpg ├── github-mark.svg ├── next.svg └── vercel.svg ├── scripts └── fetchGraphQLSchema.js ├── src ├── _actions │ ├── medias.ts │ └── products.ts ├── app │ ├── (admin) │ │ ├── admin │ │ │ ├── collections │ │ │ │ ├── [collectionId] │ │ │ │ │ └── page.tsx │ │ │ │ ├── new │ │ │ │ │ └── page.tsx │ │ │ │ └── page.tsx │ │ │ ├── dashboard │ │ │ │ └── page.tsx │ │ │ ├── layout.tsx │ │ │ ├── medias │ │ │ │ ├── @mediaModal │ │ │ │ │ ├── [mediaId] │ │ │ │ │ │ └── page.tsx │ │ │ │ │ └── default.tsx │ │ │ │ ├── [mediaId] │ │ │ │ │ └── page.tsx │ │ │ │ ├── layout.tsx │ │ │ │ ├── new │ │ │ │ │ └── page.tsx │ │ │ │ └── page.tsx │ │ │ ├── orders │ │ │ │ └── page.tsx │ │ │ ├── page.tsx │ │ │ ├── products │ │ │ │ ├── [productId] │ │ │ │ │ └── page.tsx │ │ │ │ ├── new │ │ │ │ │ └── page.tsx │ │ │ │ └── page.tsx │ │ │ └── users │ │ │ │ ├── [userId] │ │ │ │ └── page.tsx │ │ │ │ ├── new │ │ │ │ └── page.tsx │ │ │ │ ├── page.tsx │ │ │ │ └── profiles │ │ │ │ └── page.tsx │ │ └── layout.tsx │ ├── (auth) │ │ ├── auth │ │ │ └── callback │ │ │ │ └── route.ts │ │ ├── error │ │ │ └── page.tsx │ │ ├── layout.tsx │ │ ├── sign-in │ │ │ └── page.tsx │ │ └── sign-up │ │ │ └── page.tsx │ ├── (store) │ │ ├── cart │ │ │ └── page.tsx │ │ ├── collections │ │ │ └── [collectionSlug] │ │ │ │ └── page.tsx │ │ ├── layout.tsx │ │ ├── og-image.jpg │ │ ├── orders │ │ │ ├── [orderId] │ │ │ │ └── page.tsx │ │ │ └── page.tsx │ │ ├── page.tsx │ │ ├── setting │ │ │ ├── account │ │ │ │ └── page.tsx │ │ │ ├── address │ │ │ │ └── page.tsx │ │ │ ├── layout.tsx │ │ │ ├── newsletter │ │ │ │ └── page.tsx │ │ │ └── page.tsx │ │ ├── shop │ │ │ ├── [slug] │ │ │ │ └── page.tsx │ │ │ └── page.tsx │ │ └── wish-list │ │ │ └── page.tsx │ ├── api │ │ ├── create-checkout-session │ │ │ └── route.ts │ │ ├── medias │ │ │ ├── [id] │ │ │ │ └── route.ts │ │ │ └── route.ts │ │ ├── users │ │ │ └── promote-user │ │ │ │ └── route.ts │ │ └── webhook │ │ │ └── route.ts │ ├── favicon.ico │ ├── globals.css │ ├── layout.tsx │ └── middleware.ts ├── components │ ├── __test__ │ │ ├── Header.test.tsx │ │ └── QuantityInput.test.tsx │ ├── admin │ │ ├── AdminShell.tsx │ │ ├── PaginationTable.tsx │ │ └── SidebarNav.tsx │ ├── layouts │ │ ├── BackButton.tsx │ │ ├── Branding.tsx │ │ ├── ErrorToaster.tsx │ │ ├── Header.tsx │ │ ├── MainFooter.tsx │ │ ├── MainNavbar.tsx │ │ ├── MobileNavbar.tsx │ │ ├── MobileSearchInput.tsx │ │ ├── NewsletterForm.tsx │ │ ├── QuantityInput.tsx │ │ ├── SearchInput.tsx │ │ ├── SectionHeading.tsx │ │ ├── SettingSidebar.tsx │ │ ├── Shell.tsx │ │ ├── SideMenu.tsx │ │ ├── SocialMedias.tsx │ │ └── icons.tsx │ └── ui │ │ ├── CloseButton.tsx │ │ ├── DisabledFormData.tsx │ │ ├── Modal.tsx │ │ ├── PriceRange.tsx │ │ ├── accordion.tsx │ │ ├── alert-dialog.tsx │ │ ├── alert.tsx │ │ ├── avatar.tsx │ │ ├── badge.tsx │ │ ├── button.tsx │ │ ├── calendar.tsx │ │ ├── card.tsx │ │ ├── carousel.tsx │ │ ├── checkbox.tsx │ │ ├── command.tsx │ │ ├── data-table-pagination.tsx │ │ ├── data-table-toolbar.tsx │ │ ├── deleteDialog.tsx │ │ ├── dialog.tsx │ │ ├── drawer.tsx │ │ ├── dropdown-menu.tsx │ │ ├── form.tsx │ │ ├── input.tsx │ │ ├── label.tsx │ │ ├── multi-select.tsx │ │ ├── popover.tsx │ │ ├── progress.tsx │ │ ├── rating.tsx │ │ ├── scroll-area.tsx │ │ ├── scrollArea.tsx │ │ ├── select.tsx │ │ ├── separator.tsx │ │ ├── sheet.tsx │ │ ├── skeleton.tsx │ │ ├── slider.tsx │ │ ├── sonner.tsx │ │ ├── spinner.tsx │ │ ├── table.tsx │ │ ├── tabs.tsx │ │ ├── tagsField.tsx │ │ ├── tagsInput.tsx │ │ ├── textarea.tsx │ │ ├── toast.tsx │ │ ├── toaster.tsx │ │ └── use-toast.ts ├── config │ ├── collections.ts │ ├── dashboard.ts │ └── site.ts ├── env.mjs ├── features │ ├── auth │ │ ├── components │ │ │ ├── OAuthLoginButtons.tsx │ │ │ ├── PasswordInput.tsx │ │ │ ├── SigninForm.tsx │ │ │ ├── SignupForm.tsx │ │ │ ├── UserNav.tsx │ │ │ └── index.ts │ │ ├── index.ts │ │ └── validations │ │ │ └── index.ts │ ├── carts │ │ ├── components │ │ │ ├── AddProductToCartForm.tsx │ │ │ ├── AddToCartButton.tsx │ │ │ ├── CartItemCard.tsx │ │ │ ├── CartLink.tsx │ │ │ ├── CartNav.tsx │ │ │ ├── CartSection.tsx │ │ │ ├── CartSectionSkeleton.tsx │ │ │ ├── CartSheet.tsx │ │ │ ├── CheckoutButton.tsx │ │ │ ├── EmptyCart.tsx │ │ │ ├── GuestCartSection.tsx │ │ │ ├── UserCartSection.tsx │ │ │ └── index.ts │ │ ├── hooks │ │ │ ├── useCartActions.tsx │ │ │ └── useCartStore.ts │ │ ├── index.ts │ │ ├── query.ts │ │ ├── useCartStore.ts │ │ └── validations │ │ │ └── index.ts │ ├── cms │ │ ├── components │ │ │ ├── BadgeSelectField.tsx │ │ │ ├── CalendarDateRangePicker.tsx │ │ │ ├── DataTable.tsx │ │ │ ├── DataTableSkeleton.tsx │ │ │ ├── Overview.tsx │ │ │ ├── PaginationTable.tsx │ │ │ ├── RecentSales.tsx │ │ │ └── index.ts │ │ ├── hooks │ │ │ └── use-debounce.ts │ │ └── index.ts │ ├── collections │ │ ├── actions.ts │ │ ├── components │ │ │ ├── CollectionBanner.tsx │ │ │ ├── CollectionsCard.tsx │ │ │ ├── CollectionsColumns.tsx │ │ │ ├── admin │ │ │ │ └── CollectionForm.tsx │ │ │ └── index.ts │ │ ├── index.ts │ │ └── query.ts │ ├── comments │ │ ├── components │ │ │ ├── ProductComments.tsx │ │ │ └── index.ts │ │ └── index.ts │ ├── medias │ │ ├── components │ │ │ ├── ImageDialog.tsx │ │ │ ├── ImageGrid.tsx │ │ │ ├── ImageGridSkeleton.tsx │ │ │ ├── ImagePreviewCard.tsx │ │ │ ├── MediasPageContent.tsx │ │ │ ├── MultiImagesField.tsx │ │ │ ├── UpdateMediaForm.tsx │ │ │ ├── UploadMediaContainer.tsx │ │ │ └── index.ts │ │ └── index.ts │ ├── orders │ │ ├── components │ │ │ ├── BuyAgainCard.tsx │ │ │ ├── OrderProgress.tsx │ │ │ ├── OrdersList.tsx │ │ │ ├── admin │ │ │ │ └── OrdersColumns.tsx │ │ │ └── index.ts │ │ └── index.ts │ ├── products │ │ ├── components │ │ │ ├── BuyNowButton.tsx │ │ │ ├── ProductCard.tsx │ │ │ ├── ProductCardSkeleton.tsx │ │ │ ├── ProductImageShowcase.tsx │ │ │ ├── ProductImagesCarousel.tsx │ │ │ ├── RecommendationProducts.tsx │ │ │ ├── RecommendationProductsSkeleton.tsx │ │ │ ├── ReviewCard.tsx │ │ │ ├── ShipReturns.tsx │ │ │ ├── admin │ │ │ │ ├── ProductForm.tsx │ │ │ │ ├── ProductsColumns.tsx │ │ │ │ └── ProductsDataTable.tsx │ │ │ └── index.ts │ │ ├── index.ts │ │ ├── query.ts │ │ └── types │ │ │ └── index.ts │ ├── search │ │ ├── components │ │ │ ├── CollectionsSelection.tsx │ │ │ ├── FilterBadges.tsx │ │ │ ├── FilterSelections.tsx │ │ │ ├── FilterSheet.tsx │ │ │ ├── SearchProductsGridSkeleton.tsx │ │ │ ├── SearchProductsInifiteScroll.tsx │ │ │ ├── SearchResultPage.tsx │ │ │ ├── SortSelection.tsx │ │ │ └── index.ts │ │ ├── hooks │ │ │ └── useSearchStore.ts │ │ └── index.ts │ ├── users │ │ ├── actions.ts │ │ ├── components │ │ │ ├── AdminUserForm.tsx │ │ │ ├── AdminUserNav.tsx │ │ │ ├── UpdateUserForm.tsx │ │ │ ├── UsersColumns.tsx │ │ │ └── index.ts │ │ ├── index.ts │ │ └── validations │ │ │ └── index.ts │ └── wishlists │ │ ├── components │ │ ├── AddToWishListButton.tsx │ │ └── index.ts │ │ ├── index.ts │ │ └── useWishlistStore.ts ├── gql │ ├── .gitignore │ ├── gql.ts │ ├── graphql.ts │ └── index.ts ├── lib │ ├── s3.ts │ ├── stripe │ │ ├── index.ts │ │ └── stripeClient.ts │ ├── supabase │ │ ├── client.ts │ │ ├── db.ts │ │ ├── schema.ts │ │ ├── seed.ts │ │ ├── seedData │ │ │ ├── address.ts │ │ │ ├── collections.ts │ │ │ ├── index.ts │ │ │ ├── medias.ts │ │ │ ├── orderLines.ts │ │ │ ├── productVariants.ts │ │ │ ├── products.ts │ │ │ └── shopOrders.ts │ │ └── server.ts │ ├── urql.ts │ ├── use-paginated-query.ts │ └── utils.ts ├── providers │ ├── AuthProvider.tsx │ ├── CustomProvider.tsx │ └── UrqlProvider.tsx ├── test │ ├── fileMock.js │ ├── jest.setup.ts │ ├── server.js │ ├── setEnvVars.js │ └── styleMock.js ├── types │ ├── index.ts │ └── types.d.ts └── validations │ ├── medias.ts │ └── products.ts ├── tailwind.config.ts └── tsconfig.json /.env.example: -------------------------------------------------------------------------------- 1 | # Supabase 2 | NEXT_PUBLIC_SUPABASE_PROJECT_REF= 3 | NEXT_PUBLIC_SUPABASE_URL= 4 | NEXT_PUBLIC_SUPABASE_ANON_KEY= 5 | DATABASE_SERVICE_ROLE= 6 | 7 | # Database 8 | DATABASE_URL= 9 | 10 | # AWS S3 11 | NEXT_PUBLIC_S3_BUCKET= 12 | NEXT_PUBLIC_S3_REGION= 13 | S3_ACCESS_KEY_ID= 14 | S3_SECRET_ACCESS_KEY= 15 | 16 | # STRIPE 17 | NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY= 18 | STRIPE_SECRET_KEY= 19 | STRIPE_WEBHOOK_SECERT_KEY= 20 | 21 | # Auth (GITHUB) 22 | NEXT_PUBLIC_REACT_APP_GITHUB_AUTH_TOKEN= 23 | 24 | # Deployment 25 | NEXT_PUBLIC_SITE_URL= 26 | 27 | 28 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "eslint:recommended", 4 | "plugin:@typescript-eslint/eslint-recommended", 5 | "plugin:@typescript-eslint/recommended", 6 | "next/core-web-vitals" 7 | ], 8 | "rules": { 9 | "@typescript-eslint/no-unused-vars": "off", 10 | "@typescript-eslint/ban-types": "off", 11 | "@typescript-eslint/no-explicit-any": "off", 12 | "react-hooks/exhaustive-deps": "off", 13 | "@typescript-eslint/ban-ts-comment": "off", 14 | "no-empty-pattern": "off", 15 | "no-case-declarations": "off", 16 | "no-constant-condition": "off", 17 | "react-hooks/rules-of-hooks": "off", 18 | "no-undef": "off", 19 | "@typescript-eslint/no-var-requires": "off" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | .env 31 | 32 | # vercel 33 | .vercel 34 | 35 | # typescript 36 | *.tsbuildinfo 37 | next-env.d.ts 38 | -------------------------------------------------------------------------------- /.graphqlrc.yml: -------------------------------------------------------------------------------- 1 | schema: ./graphql/schema/schema.graphql 2 | documents: ./src/**/*.{graphql,js,ts,jsx,tsx} 3 | extensions: 4 | codegen: 5 | generates: 6 | ./src/gql: 7 | preset: gql-tag-operations-preset 8 | hooks: 9 | afterOneFileWrite: 10 | - prettier --write 11 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/user/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | # npm test 5 | npm run lint && npm run format -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | .next 4 | build 5 | drizzle -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib", 3 | "typescript.enablePromptUseWorkspaceTsdk": true 4 | } -------------------------------------------------------------------------------- /.watchmanconfig: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "src/app/globals.css", 9 | "baseColor": "slate", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils" 16 | } 17 | } -------------------------------------------------------------------------------- /drizzle.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "drizzle-kit"; 2 | import * as dotenv from "dotenv"; 3 | dotenv.config(); 4 | 5 | if (!process.env.DATABASE_URL) { 6 | throw new Error("DATABASE_URL is missing"); 7 | } 8 | 9 | export default { 10 | schema: "./src/lib/supabase/schema.ts", 11 | out: "./drizzle", 12 | driver: "pg", 13 | dbCredentials: { 14 | connectionString: process.env.DATABASE_URL, 15 | }, 16 | } satisfies Config; 17 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | // jest.config.js 2 | const nextJest = require("next/jest") 3 | 4 | // Providing the path to your Next.js app which will enable loading next.config.js and .env files 5 | const createJestConfig = nextJest({ 6 | dir: "./", 7 | }) 8 | 9 | // Any custom config you want to pass to Jest 10 | const customJestConfig = { 11 | moduleDirectories: ["node_modules", __dirname], 12 | setupFiles: ["/src/test/setEnvVars.js"], 13 | setupFilesAfterEnv: ["/src/test/jest.setup.ts"], 14 | testEnvironment: "jest-environment-jsdom", 15 | modulePathIgnorePatterns: ["/src/components/ui/"], 16 | } 17 | 18 | // createJestConfig is exported in this way to ensure that next/jest can load the Next.js configuration, which is async 19 | module.exports = createJestConfig(customJestConfig) 20 | -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | 3 | const nextConfig = { 4 | reactStrictMode: true, 5 | swcMinify: true, 6 | images: { 7 | remotePatterns: [ 8 | { 9 | protocol: "https", 10 | hostname: `${process.env.NEXT_PUBLIC_S3_BUCKET}.s3.${process.env.NEXT_PUBLIC_S3_REGION}.amazonaws.com`, 11 | }, 12 | { 13 | protocol: "https", 14 | hostname: "source.unsplash.com", 15 | }, 16 | ], 17 | }, 18 | experimental: { 19 | serverComponentsExternalPackages: ["@aws-sdk/client-s3", "sharp"], 20 | }, 21 | } 22 | 23 | export default nextConfig 24 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /public/assets/bathroom-planning.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clonglam/HiyoRi-Ecommerce-Nextjs-Supabase/98b6f29e223567e25bdf25742d07e107f50a6955/public/assets/bathroom-planning.jpg -------------------------------------------------------------------------------- /public/assets/cutingcardImage.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clonglam/HiyoRi-Ecommerce-Nextjs-Supabase/98b6f29e223567e25bdf25742d07e107f50a6955/public/assets/cutingcardImage.jpg -------------------------------------------------------------------------------- /public/github-mark.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /scripts/fetchGraphQLSchema.js: -------------------------------------------------------------------------------- 1 | require("dotenv").config() 2 | 3 | const fs = require("fs") 4 | const gradient = require("gradient-string") 5 | const path = require("path") 6 | const ProgressBar = require("progress") 7 | const { fetch } = require("cross-undici-fetch") 8 | 9 | const { 10 | buildClientSchema, 11 | getIntrospectionQuery, 12 | printSchema, 13 | } = require("graphql") 14 | 15 | const supagradient = gradient(["#00CB8A", "#78E0B8"]) 16 | 17 | function fetchGraphQLSchema(url, options) { 18 | options = options || {} // eslint-disable-line no-param-reassign 19 | 20 | const bar = new ProgressBar("🔦 Introspecting schema [:bar]", 24) 21 | 22 | const id = setInterval(function () { 23 | bar.tick() 24 | if (bar.complete) { 25 | clearInterval(id) 26 | } 27 | }, 250) 28 | 29 | return fetch(url, { 30 | method: "POST", 31 | headers: { 32 | Accept: "application/json", 33 | "Content-Type": "application/json", 34 | apiKey: process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY, 35 | }, 36 | body: JSON.stringify({ 37 | query: getIntrospectionQuery(), 38 | }), 39 | }) 40 | .then((res) => res.json()) 41 | .then((schemaJSON) => { 42 | if (options.readable) { 43 | return printSchema(buildClientSchema(schemaJSON.data)) 44 | } 45 | 46 | bar.complete() 47 | return JSON.stringify(schemaJSON, null, 2) 48 | }) 49 | } 50 | 51 | const filePath = path.join(__dirname, "../graphql/schema/", "schema.graphql") 52 | 53 | console.log( 54 | supagradient( 55 | `🗞 Fetching GraphQL Schema from ${process.env.SUPABASE_PROJECT_REF} ...` 56 | ) 57 | ) 58 | 59 | fetchGraphQLSchema( 60 | `https://${process.env.NEXT_PUBLIC_SUPABASE_PROJECT_REF}.supabase.co/graphql/v1`, 61 | { 62 | readable: true, 63 | } 64 | ).then((schema) => { 65 | fs.writeFileSync(filePath, schema, "utf-8") 66 | console.log(supagradient(`✨ Saved to ${filePath}`)) 67 | console.log('💡 Be sure to run "yarn run codegen" to generate latest types.') 68 | }) 69 | -------------------------------------------------------------------------------- /src/_actions/medias.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import db from "@/lib/supabase/db"; 4 | import { productMedias } from "@/lib/supabase/schema"; 5 | import { eq } from "drizzle-orm"; 6 | 7 | export async function getMedia(id: string) { 8 | return await db.query.medias.findFirst({ where: eq(productMedias.id, id) }); 9 | } 10 | -------------------------------------------------------------------------------- /src/_actions/products.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import db from "@/lib/supabase/db"; 4 | import { InsertProducts, products } from "@/lib/supabase/schema"; 5 | import { eq, inArray } from "drizzle-orm"; 6 | import { createInsertSchema } from "drizzle-zod"; 7 | 8 | type SearchProductsActionProps = { 9 | query: string; 10 | limit?: number; 11 | collections?: string; 12 | sort?: string; 13 | }; 14 | 15 | export const createProductAction = async (product: InsertProducts) => { 16 | createInsertSchema(products).parse(product); 17 | const data = await db.insert(products).values(product).returning(); 18 | return data; 19 | }; 20 | 21 | export const updateProductAction = async ( 22 | productId: string, 23 | product: InsertProducts, 24 | ) => { 25 | createInsertSchema(products).parse(product); 26 | const insertedProduct = await db 27 | .update(products) 28 | .set(product) 29 | .where(eq(products.id, productId)) 30 | .returning(); 31 | 32 | return insertedProduct; 33 | }; 34 | 35 | export const getProductsByIds = async (productIds: string[]) => { 36 | return await db 37 | .select() 38 | .from(products) 39 | .where(inArray(products.id, productIds)); 40 | }; 41 | -------------------------------------------------------------------------------- /src/app/(admin)/admin/collections/[collectionId]/page.tsx: -------------------------------------------------------------------------------- 1 | import AdminShell from "@/components/admin/AdminShell"; 2 | import { gql } from "@/gql"; 3 | import { getClient } from "@/lib/urql"; 4 | 5 | import { notFound } from "next/navigation"; 6 | import { CollectionForm } from "@/features/collections"; 7 | 8 | type EditCollectionPageProps = { 9 | params: { 10 | collectionId: string; 11 | }; 12 | }; 13 | 14 | const updateCollectionPageQuery = gql(/* GraphQL */ ` 15 | query UPDATE_COLLECTION_PAGE_QUERY($collectionId: String) { 16 | collectionsCollection(filter: { id: { eq: $collectionId } }, first: 1) { 17 | edges { 18 | node { 19 | __typename 20 | id 21 | ...CollectionFromFragment 22 | } 23 | } 24 | } 25 | } 26 | `); 27 | 28 | async function EditCollectionPage({ 29 | params: { collectionId }, 30 | }: EditCollectionPageProps) { 31 | const { data } = await getClient().query(updateCollectionPageQuery, { 32 | collectionId, 33 | }); 34 | if (!data || !data?.collectionsCollection?.edges[0]) return notFound(); 35 | 36 | return ( 37 | 41 |
42 | 43 |
44 |
45 | ); 46 | } 47 | 48 | export default EditCollectionPage; 49 | -------------------------------------------------------------------------------- /src/app/(admin)/admin/collections/new/page.tsx: -------------------------------------------------------------------------------- 1 | import AdminShell from "@/components/admin/AdminShell"; 2 | import { CollectionForm } from "@/features/collections"; 3 | 4 | type Props = {}; 5 | 6 | async function NewProjectPage({}: Props) { 7 | return ( 8 | 12 | 13 | 14 | ); 15 | } 16 | 17 | export default NewProjectPage; 18 | -------------------------------------------------------------------------------- /src/app/(admin)/admin/collections/page.tsx: -------------------------------------------------------------------------------- 1 | import AdminShell from "@/components/admin/AdminShell"; 2 | import { buttonVariants } from "@/components/ui/button"; 3 | import { gql } from "@/gql"; 4 | import { getClient } from "@/lib/urql"; 5 | import { cn } from "@/lib/utils"; 6 | import Link from "next/link"; 7 | import { notFound } from "next/navigation"; 8 | import { CollectionsColumns } from "@/features/collections"; 9 | import { DataTable } from "@/features/cms"; 10 | 11 | type AdminCollectionsPageProps = { 12 | searchParams: { 13 | [key: string]: string | string[] | undefined; 14 | }; 15 | }; 16 | 17 | const AdminCollectionsPageQuery = gql(/* GraphQL */ ` 18 | query AdminCollectionsPageQuery { 19 | collectionsCollection(orderBy: [{ title: AscNullsLast }]) { 20 | edges { 21 | node { 22 | __typename 23 | id 24 | ...CollectionColumnsFragment 25 | } 26 | } 27 | } 28 | } 29 | `); 30 | 31 | async function collectionsPage({ searchParams }: AdminCollectionsPageProps) { 32 | const { data } = await getClient().query(AdminCollectionsPageQuery, {}); 33 | 34 | if (!data) return notFound(); 35 | 36 | return ( 37 | 41 |
42 | 43 | New Collection 44 | 45 |
46 | 47 | 51 |
52 | ); 53 | } 54 | 55 | export default collectionsPage; 56 | -------------------------------------------------------------------------------- /src/app/(admin)/admin/layout.tsx: -------------------------------------------------------------------------------- 1 | import { SidebarNav } from "@/components/admin/SidebarNav"; 2 | import { ScrollArea } from "@/components/ui/scrollArea"; 3 | import { dashboardConfig } from "@/config/dashboard"; 4 | import createServerClient from "@/lib/supabase/server"; 5 | import { cookies } from "next/headers"; 6 | import { redirect } from "next/navigation"; 7 | 8 | interface DashboardLayoutProps { 9 | children: React.ReactNode; 10 | } 11 | 12 | export default async function DashboardLayout({ 13 | children, 14 | }: DashboardLayoutProps) { 15 | const cookieStore = cookies(); 16 | const supabase = createServerClient({ cookieStore }); 17 | 18 | const { 19 | data: { user }, 20 | error: authError, 21 | } = await supabase.auth.getUser(); 22 | if (authError || !user) { 23 | redirect("/sign-in"); 24 | } 25 | 26 | return ( 27 |
28 | 33 |
34 | {children} 35 |
36 |
37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /src/app/(admin)/admin/medias/@mediaModal/[mediaId]/page.tsx: -------------------------------------------------------------------------------- 1 | import { getMedia } from "@/_actions/medias"; 2 | import { UpdateMediaForm } from "@/features/medias"; 3 | import Modal from "@/components/ui/Modal"; 4 | import { keytoUrl } from "@/lib/utils"; 5 | import Image from "next/image"; 6 | import { notFound } from "next/navigation"; 7 | 8 | type Props = { params: { mediaId: string } }; 9 | 10 | async function EditMediaModals({ params: { mediaId } }: Props) { 11 | // TODO: Change from server Action to GrahpQL 12 | const media = await getMedia(mediaId); 13 | if (!media) return notFound(); 14 | 15 | return ( 16 | 17 |
18 |
19 | {media.alt} 26 |
27 |
28 | 29 |
30 |
31 |
32 | ); 33 | } 34 | 35 | export default EditMediaModals; 36 | -------------------------------------------------------------------------------- /src/app/(admin)/admin/medias/@mediaModal/default.tsx: -------------------------------------------------------------------------------- 1 | export default function Default() { 2 | return null; 3 | } 4 | -------------------------------------------------------------------------------- /src/app/(admin)/admin/medias/[mediaId]/page.tsx: -------------------------------------------------------------------------------- 1 | import { redirect } from "next/navigation"; 2 | import React from "react"; 3 | 4 | type Props = {}; 5 | 6 | function MediaPage({}: Props) { 7 | redirect("/admin/medias"); 8 | return
MediaPage
; 9 | } 10 | 11 | export default MediaPage; 12 | -------------------------------------------------------------------------------- /src/app/(admin)/admin/medias/layout.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | type Props = { 4 | children: React.ReactNode; 5 | mediaModal: React.ReactNode; 6 | }; 7 | 8 | function layout({ mediaModal, children }: Props) { 9 | return ( 10 | <> 11 | {children} 12 | {mediaModal} 13 | 14 | ); 15 | } 16 | 17 | export default layout; 18 | -------------------------------------------------------------------------------- /src/app/(admin)/admin/medias/new/page.tsx: -------------------------------------------------------------------------------- 1 | import { redirect } from "next/navigation"; 2 | import React from "react"; 3 | 4 | type Props = {}; 5 | 6 | function AddMediaPage({}: Props) { 7 | return
AddMediaPage
; 8 | } 9 | 10 | export default AddMediaPage; 11 | -------------------------------------------------------------------------------- /src/app/(admin)/admin/medias/page.tsx: -------------------------------------------------------------------------------- 1 | import AdminShell from "@/components/admin/AdminShell"; 2 | import { ImageGridSkeleton, MediasPageContent } from "@/features/medias"; 3 | import { Suspense } from "react"; 4 | 5 | type Props = {}; 6 | 7 | async function MediasPage({}: Props) { 8 | return ( 9 | 13 | }> 14 | 15 | 16 | 17 | ); 18 | } 19 | 20 | export default MediasPage; 21 | -------------------------------------------------------------------------------- /src/app/(admin)/admin/orders/page.tsx: -------------------------------------------------------------------------------- 1 | import AdminShell from "@/components/admin/AdminShell"; 2 | import { buttonVariants } from "@/components/ui/button"; 3 | import { DataTable } from "@/features/cms"; 4 | import { OrdersColumns } from "@/features/orders"; 5 | import { gql } from "@/gql"; 6 | import { getClient } from "@/lib/urql"; 7 | import { cn } from "@/lib/utils"; 8 | import Link from "next/link"; 9 | import { notFound } from "next/navigation"; 10 | 11 | type AdminOrdersPageProps = { 12 | searchParams: { 13 | [key: string]: string | string[] | undefined; 14 | }; 15 | }; 16 | 17 | const AdminOrdersPageQuery = gql(/* GraphQL */ ` 18 | query AdminOrdersPageQuery { 19 | ordersCollection(orderBy: [{ created_at: DescNullsLast }]) { 20 | edges { 21 | node { 22 | __typename 23 | id 24 | ...OrderColumnsFragment 25 | } 26 | } 27 | } 28 | } 29 | `); 30 | 31 | async function OrdersPage({ searchParams }: AdminOrdersPageProps) { 32 | const { data } = await getClient().query(AdminOrdersPageQuery, {}); 33 | 34 | if (!data) return notFound(); 35 | 36 | return ( 37 | 41 |
42 | 43 | New Order 44 | 45 |
46 | 47 | 51 |
52 | ); 53 | } 54 | 55 | export default OrdersPage; 56 | -------------------------------------------------------------------------------- /src/app/(admin)/admin/page.tsx: -------------------------------------------------------------------------------- 1 | import { redirect } from "next/navigation"; 2 | 3 | async function AdminDashboard() { 4 | redirect("/admin/dashboard"); 5 | } 6 | 7 | export default AdminDashboard; 8 | -------------------------------------------------------------------------------- /src/app/(admin)/admin/products/[productId]/page.tsx: -------------------------------------------------------------------------------- 1 | import AdminShell from "@/components/admin/AdminShell"; 2 | import { ProductForm } from "@/features/products"; 3 | import db from "@/lib/supabase/db"; 4 | import { products } from "@/lib/supabase/schema"; 5 | import { eq } from "drizzle-orm"; 6 | import { notFound } from "next/navigation"; 7 | import { Suspense } from "react"; 8 | 9 | type EditProjectPageProps = { 10 | params: { 11 | productId: string; 12 | }; 13 | }; 14 | 15 | async function EditProjectPage({ 16 | params: { productId }, 17 | }: EditProjectPageProps) { 18 | const product = await db.query.products.findFirst({ 19 | where: eq(products.id, productId), 20 | }); 21 | if (!product) return notFound(); 22 | 23 | return ( 24 | 28 | 29 | 30 | 31 | 32 | ); 33 | } 34 | 35 | export default EditProjectPage; 36 | -------------------------------------------------------------------------------- /src/app/(admin)/admin/products/new/page.tsx: -------------------------------------------------------------------------------- 1 | import React, { Suspense } from "react"; 2 | import { notFound } from "next/navigation"; 3 | import AdminShell from "@/components/admin/AdminShell"; 4 | import { ProductForm } from "@/features/products"; 5 | import db from "@/lib/supabase/db"; 6 | 7 | async function NewProjectPage() { 8 | const products = await db.query.products.findMany(); 9 | if (!products) return notFound(); 10 | 11 | return ( 12 | 16 | 17 | 18 | 19 | 20 | ); 21 | } 22 | 23 | export default NewProjectPage; 24 | -------------------------------------------------------------------------------- /src/app/(admin)/admin/products/page.tsx: -------------------------------------------------------------------------------- 1 | import AdminShell from "@/components/admin/AdminShell"; 2 | import { buttonVariants } from "@/components/ui/button"; 3 | import { DataTableSkeleton } from "@/features/cms"; 4 | import { ProductsColumns, ProductsDataTable } from "@/features/products"; 5 | import { gql } from "@/gql"; 6 | import { getClient } from "@/lib/urql"; 7 | import { cn } from "@/lib/utils"; 8 | import Link from "next/link"; 9 | import { notFound } from "next/navigation"; 10 | import { Suspense } from "react"; 11 | 12 | type AdminProjectsPageProps = { 13 | searchParams: { 14 | [key: string]: string | string[] | undefined; 15 | }; 16 | }; 17 | 18 | async function ProductsPage({ searchParams }: AdminProjectsPageProps) { 19 | const AdminProductsPageQuery = gql(/* GraphQL */ ` 20 | query AdminProductsPageQuery { 21 | productsCollection(orderBy: [{ created_at: DescNullsLast }]) { 22 | edges { 23 | node { 24 | id 25 | ...ProductColumnFragment 26 | } 27 | } 28 | } 29 | } 30 | `); 31 | 32 | const { data } = await getClient().query(AdminProductsPageQuery, {}); 33 | 34 | if (!data) return notFound(); 35 | 36 | return ( 37 | 41 |
42 | 43 | New Product 44 | 45 |
46 | 47 | }> 48 | 52 | 53 |
54 | ); 55 | } 56 | 57 | export default ProductsPage; 58 | -------------------------------------------------------------------------------- /src/app/(admin)/admin/users/[userId]/page.tsx: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import AdminShell from "@/components/admin/AdminShell"; 4 | import { getUser, UpdateUserForm } from "@/features/users"; 5 | import { notFound } from "next/navigation"; 6 | 7 | type UpdateUserPageProps = { params: { userId: string } }; 8 | 9 | async function UpdateUserPage({ params: { userId } }: UpdateUserPageProps) { 10 | const { user } = await getUser({ userId }); 11 | if (!user) return notFound(); 12 | 13 | return ( 14 | 19 | 20 | 21 | ); 22 | } 23 | 24 | export default UpdateUserPage; 25 | -------------------------------------------------------------------------------- /src/app/(admin)/admin/users/new/page.tsx: -------------------------------------------------------------------------------- 1 | import AdminShell from "@/components/admin/AdminShell"; 2 | import { AdminUserForm } from "@/features/users"; 3 | import React from "react"; 4 | 5 | type Props = {}; 6 | 7 | function NewUserPage({}: Props) { 8 | return ( 9 | 14 | 15 | 16 | ); 17 | } 18 | 19 | export default NewUserPage; 20 | -------------------------------------------------------------------------------- /src/app/(admin)/admin/users/page.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | getCurrentUser, 3 | listUsers, 4 | UsersColumns, 5 | AdminUserNav, 6 | } from "@/features/users"; 7 | import AdminShell from "@/components/admin/AdminShell"; 8 | import { ProductsDataTable } from "@/features/products"; 9 | import ErrorToaster from "@/components/layouts/ErrorToaster"; 10 | // TODO: CREATE New Data Table for golbaluse 11 | 12 | type AdminUsersPageProps = { 13 | searchParams: { 14 | [key: string]: string | string[] | undefined; 15 | }; 16 | }; 17 | 18 | async function UsersPage({ searchParams }: AdminUsersPageProps) { 19 | const currentUser = await getCurrentUser(); 20 | 21 | const users = await listUsers({}); 22 | 23 | return ( 24 | 25 | 26 | 27 | 28 | 29 | ); 30 | } 31 | 32 | export default UsersPage; 33 | -------------------------------------------------------------------------------- /src/app/(admin)/admin/users/profiles/page.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | type Props = {}; 4 | 5 | function page({}: Props) { 6 | return
page
; 7 | } 8 | 9 | export default page; 10 | -------------------------------------------------------------------------------- /src/app/(admin)/layout.tsx: -------------------------------------------------------------------------------- 1 | import { getCurrentUser, isAdmin } from "@/features/users/actions"; 2 | import MainFooter from "@/components/layouts/MainFooter"; 3 | import Navbar from "@/components/layouts/MainNavbar"; 4 | import { redirect } from "next/navigation"; 5 | import { ReactNode } from "react"; 6 | 7 | type Props = { children: ReactNode }; 8 | 9 | async function AdminLayout({ children }: Props) { 10 | const currentUser = await getCurrentUser(); 11 | 12 | if (!isAdmin(currentUser)) 13 | redirect(`/sign-in?error=Only authenticated users can access`); 14 | 15 | return ( 16 |
17 | 18 | {children} 19 | 20 |
21 | ); 22 | } 23 | 24 | export default AdminLayout; 25 | -------------------------------------------------------------------------------- /src/app/(auth)/auth/callback/route.ts: -------------------------------------------------------------------------------- 1 | import { createClient } from "@/lib/supabase/server"; 2 | import { type EmailOtpType } from "@supabase/supabase-js"; 3 | import { cookies } from "next/headers"; 4 | import { type NextRequest, NextResponse } from "next/server"; 5 | 6 | export async function GET(request: NextRequest) { 7 | const cookieStore = cookies(); 8 | 9 | const { searchParams } = new URL(request.url); 10 | const token_hash = searchParams.get("token_hash"); 11 | const type = searchParams.get("type") as EmailOtpType | null; 12 | const next = searchParams.get("next") ?? "/"; 13 | 14 | const redirectTo = request.nextUrl.clone(); 15 | redirectTo.pathname = next; 16 | redirectTo.searchParams.delete("token_hash"); 17 | redirectTo.searchParams.delete("type"); 18 | 19 | if (token_hash && type) { 20 | const supabase = createClient({ cookieStore }); 21 | 22 | const { error } = await supabase.auth.verifyOtp({ 23 | type, 24 | token_hash, 25 | }); 26 | if (!error) { 27 | redirectTo.searchParams.delete("next"); 28 | return NextResponse.redirect(redirectTo); 29 | } 30 | } 31 | 32 | // return the user to an error page with some instructions 33 | redirectTo.pathname = "/error"; 34 | return NextResponse.redirect(redirectTo); 35 | } 36 | -------------------------------------------------------------------------------- /src/app/(auth)/error/page.tsx: -------------------------------------------------------------------------------- 1 | export default function ErrorPage() { 2 | return

Sorry, something went wrong

; 3 | } 4 | -------------------------------------------------------------------------------- /src/app/(auth)/layout.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import Image from "next/image"; 3 | import Branding from "@/components/layouts/Branding"; 4 | 5 | interface AuthLayoutProps { 6 | children: React.ReactNode; 7 | } 8 | 9 | export default function AuthLayout({ children }: AuthLayoutProps) { 10 | return ( 11 |
12 |
13 |
14 | Living Room Design with a Sofa 21 |
22 | 23 | 24 |
25 |
26 | 27 |
28 | {children} 29 |
30 |
31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /src/app/(auth)/sign-up/page.tsx: -------------------------------------------------------------------------------- 1 | import { type Metadata } from "next"; 2 | import { Suspense } from "react"; 3 | import Link from "next/link"; 4 | 5 | import OAuthLoginButtons from "@/features/auth/components/OAuthLoginButtons"; 6 | import { SignupForm } from "@/features/auth"; 7 | import { 8 | Card, 9 | CardContent, 10 | CardDescription, 11 | CardFooter, 12 | CardHeader, 13 | CardTitle, 14 | } from "@/components/ui/card"; 15 | 16 | export const metadata: Metadata = { 17 | title: "Sign Up", 18 | description: "Sign up for an account", 19 | }; 20 | 21 | export default function SignUpPage() { 22 | return ( 23 |
24 | 25 | 26 | Sign up 27 | 28 | Choose your preferred sign up method 29 | 30 | 31 | 32 | 35 | } 36 | > 37 | 38 | 39 | 40 |
41 |
42 |
43 | 44 |
45 | 46 | Or continue with 47 | 48 |
49 | 50 |
51 | 52 |
53 |
54 |
55 | 56 |
57 | Already have an account?{" "} 58 | 63 | Sign in 64 | 65 |
66 |
67 |
68 |
69 | ); 70 | } 71 | -------------------------------------------------------------------------------- /src/app/(store)/cart/page.tsx: -------------------------------------------------------------------------------- 1 | import CartSection from "@/features/carts/components/CartSection"; 2 | import CartSectionSkeleton from "@/features/carts/components/CartSectionSkeleton"; 3 | import { Shell } from "@/components/layouts/Shell"; 4 | import { 5 | RecommendationProducts, 6 | RecommendationProductsSkeleton, 7 | } from "@/features/products"; 8 | 9 | import Link from "next/link"; 10 | import { Suspense } from "react"; 11 | 12 | async function CartPage() { 13 | return ( 14 | 15 |
16 |

Your Cart

17 | Continue shopping 18 |
19 | 20 | }> 21 | 22 | 23 | 24 | }> 25 | 26 | 27 |
28 | ); 29 | } 30 | 31 | export default CartPage; 32 | -------------------------------------------------------------------------------- /src/app/(store)/layout.tsx: -------------------------------------------------------------------------------- 1 | import { CartSheet } from "@/features/carts"; 2 | import MainFooter from "@/components/layouts/MainFooter"; 3 | import Navbar from "@/components/layouts/MainNavbar"; 4 | import { ReactNode } from "react"; 5 | 6 | type Props = { children: ReactNode }; 7 | 8 | async function StoreLayout({ children }: Props) { 9 | return ( 10 | <> 11 | 12 |
{children}
13 | 14 | 15 | 16 | ); 17 | } 18 | 19 | export default StoreLayout; 20 | -------------------------------------------------------------------------------- /src/app/(store)/og-image.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clonglam/HiyoRi-Ecommerce-Nextjs-Supabase/98b6f29e223567e25bdf25742d07e107f50a6955/src/app/(store)/og-image.jpg -------------------------------------------------------------------------------- /src/app/(store)/orders/[orderId]/page.tsx: -------------------------------------------------------------------------------- 1 | import { Shell } from "@/components/layouts/Shell"; 2 | import { Card, CardContent, CardHeader } from "@/components/ui/card"; 3 | import { OrderProgress } from "@/features/orders"; 4 | import Link from "next/link"; 5 | 6 | type TrackOrderProps = { 7 | params: { orderId: string }; 8 | }; 9 | 10 | function TrackOrderPage({ params: { orderId } }: TrackOrderProps) { 11 | return ( 12 | 13 |

Arrive at Tomorrow 22:00

14 |
15 |

16 | Order Status: 17 | Ordered 18 |

19 | 20 |

21 | {`Order Id: `} 22 | {`#${orderId}`} 23 |

24 | 25 |
26 | 27 |
28 | 29 | Shipping Address 30 | 31 |

Hugo Lam

32 |

4242 ORrder 122

33 |

Vancourver 332 212

34 |
35 |
36 | 37 | 38 | Track your Order 39 | 40 | 41 | #{orderId} 42 | 43 | 44 | 45 | 46 | 47 | Track your Order 48 | 49 | 50 | #{orderId} 51 | 52 | 53 | 54 |
55 |
56 | ); 57 | } 58 | 59 | export default TrackOrderPage; 60 | -------------------------------------------------------------------------------- /src/app/(store)/orders/page.tsx: -------------------------------------------------------------------------------- 1 | import { Shell } from "@/components/layouts/Shell"; 2 | import { BuyAgainCard, OrdersList } from "@/features/orders/components"; 3 | import { gql } from "@/gql"; 4 | import { createClient } from "@/lib/supabase/server"; 5 | import { getClient } from "@/lib/urql"; 6 | import { cookies } from "next/headers"; 7 | import { notFound, redirect } from "next/navigation"; 8 | import React from "react"; 9 | 10 | const OrderPageQuery = gql(/* GraphQL */ ` 11 | query OrderPageQuery($first: Int!, $userId: UUID) { 12 | ordersCollection( 13 | first: $first 14 | orderBy: [{ created_at: DescNullsLast }] 15 | filter: { user_id: { eq: $userId } } 16 | ) { 17 | __typename 18 | edges { 19 | ...OrdersListFragment 20 | } 21 | } 22 | 23 | productsCollection(first: 8) { 24 | edges { 25 | ...BuyAgainCardFragment 26 | } 27 | } 28 | } 29 | `); 30 | 31 | async function OrderPage() { 32 | const cookieStore = cookies(); 33 | const supabase = createClient({ cookieStore }); 34 | 35 | const { 36 | data: { user }, 37 | error: authError, 38 | } = await supabase.auth.getUser(); 39 | if (authError || !user) { 40 | redirect("/sign-in"); 41 | } 42 | 43 | const { data, error } = await getClient().query(OrderPageQuery, { 44 | first: 4, 45 | userId: user.id, 46 | }); 47 | 48 | if (!data) return notFound(); 49 | 50 | return ( 51 | 52 |

Orders

53 | 54 |
55 |
56 | 57 |
58 | 59 |
60 | 61 |
62 |
63 |
64 | ); 65 | } 66 | 67 | export default OrderPage; 68 | -------------------------------------------------------------------------------- /src/app/(store)/setting/account/page.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | type Props = {}; 4 | 5 | function AccountPage({}: Props) { 6 | return
Account Page
; 7 | } 8 | 9 | export default AccountPage; 10 | -------------------------------------------------------------------------------- /src/app/(store)/setting/address/page.tsx: -------------------------------------------------------------------------------- 1 | import CartSection from "@/features/carts/components/CartSection"; 2 | import CartSectionSkeleton from "@/features/carts/components/CartSectionSkeleton"; 3 | import { createClient } from "@/lib/supabase/server"; 4 | import { cookies } from "next/headers"; 5 | import Link from "next/link"; 6 | import { redirect } from "next/navigation"; 7 | import { Suspense } from "react"; 8 | 9 | async function CartPage() { 10 | const cookieStore = cookies(); 11 | const supabase = createClient({ cookieStore }); 12 | 13 | const { 14 | data: { user }, 15 | error: authError, 16 | } = await supabase.auth.getUser(); 17 | if (authError || !user) { 18 | redirect("/sign-in"); 19 | } 20 | 21 | return ( 22 |
23 |
24 |

Your Cart

25 | Continue shopping 26 |
27 | 28 | }> 29 | 30 | 31 |
32 | ); 33 | } 34 | 35 | export default CartPage; 36 | -------------------------------------------------------------------------------- /src/app/(store)/setting/layout.tsx: -------------------------------------------------------------------------------- 1 | import { SettingSidebarNav } from "@/components/layouts/SettingSidebar"; 2 | import { Separator } from "@/components/ui/separator"; 3 | import { Metadata } from "next"; 4 | import Image from "next/image"; 5 | 6 | export const metadata: Metadata = { 7 | title: "Forms", 8 | description: "Advanced form example using react-hook-form and Zod.", 9 | }; 10 | 11 | const sidebarNavItems = [ 12 | { 13 | title: "Profile", 14 | href: "/setting", 15 | }, 16 | { 17 | title: "Account", 18 | href: "/setting/account", 19 | }, 20 | { 21 | title: "Address", 22 | href: "/setting/address", 23 | }, 24 | { 25 | title: "News Letter", 26 | href: "/setting/newsletter", 27 | }, 28 | ]; 29 | 30 | interface SettingsLayoutProps { 31 | children: React.ReactNode; 32 | } 33 | 34 | export default function SettingsLayout({ children }: SettingsLayoutProps) { 35 | return ( 36 | <> 37 |
38 |
39 |

Settings

40 |

41 | Manage your account settings and set e-mail preferences. 42 |

43 |
44 | 45 |
46 | 49 |
{children}
50 |
51 |
52 | 53 | ); 54 | } 55 | -------------------------------------------------------------------------------- /src/app/(store)/setting/newsletter/page.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | type Props = {}; 4 | 5 | function NewsletterPage({}: Props) { 6 | return
News Letter
; 7 | } 8 | 9 | export default NewsletterPage; 10 | -------------------------------------------------------------------------------- /src/app/(store)/setting/page.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | type Props = {}; 4 | 5 | function ProfilePage({}: Props) { 6 | return
ProfilePage
; 7 | } 8 | 9 | export default ProfilePage; 10 | -------------------------------------------------------------------------------- /src/app/(store)/shop/page.tsx: -------------------------------------------------------------------------------- 1 | import Header from "@/components/layouts/Header"; 2 | import { Shell } from "@/components/layouts/Shell"; 3 | import { Skeleton } from "@/components/ui/skeleton"; 4 | import { listCollectionsAction } from "@/features/collections"; 5 | import { SearchProductsGridSkeleton } from "@/features/products"; 6 | import { 7 | FilterSelections, 8 | SearchProductsInifiteScroll, 9 | } from "@/features/search"; 10 | import { Suspense } from "react"; 11 | 12 | interface ProductsPageProps { 13 | searchParams: { 14 | [key: string]: string | string[] | undefined; 15 | }; 16 | } 17 | 18 | async function ProductsPage({}: ProductsPageProps) { 19 | // TODO: PROBLEM in server actrion 20 | // const collectionsData = await listCollectionsAction(); 21 | 22 | return ( 23 | 24 |
25 | 26 | {/* 29 | 30 | 31 |
32 | } 33 | > 34 | 35 | 36 | */} 37 | 38 | }> 39 | 40 | 41 | 42 | ); 43 | } 44 | 45 | export default ProductsPage; 46 | -------------------------------------------------------------------------------- /src/app/(store)/wish-list/page.tsx: -------------------------------------------------------------------------------- 1 | import { Shell } from "@/components/layouts/Shell"; 2 | import { 3 | RecommendationProducts, 4 | RecommendationProductsSkeleton, 5 | } from "@/features/products"; 6 | import Link from "next/link"; 7 | import React, { Suspense } from "react"; 8 | 9 | type Props = {}; 10 | 11 | function WishListPage({}: Props) { 12 | return ( 13 | 14 |
15 |

Your Wishlist

16 | Continue shopping 17 |
18 | {/* 19 | }> 20 | 21 | */} 22 | 23 | }> 24 | 25 | 26 |
27 | ); 28 | } 29 | 30 | export default WishListPage; 31 | -------------------------------------------------------------------------------- /src/app/api/medias/[id]/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from "next/server"; 2 | import { eq } from "drizzle-orm"; 3 | import db from "@/lib/supabase/db"; 4 | import { medias } from "@/lib/supabase/schema"; 5 | 6 | export async function GET( 7 | request: NextRequest, 8 | { params }: { params: { id: string } }, 9 | ) { 10 | const media = await db.query.medias.findFirst({ 11 | where: eq(medias.id, params.id), 12 | }); 13 | 14 | if (!media) 15 | return NextResponse.json( 16 | { 17 | message: "Media not found.", 18 | }, 19 | { status: 404 }, 20 | ); 21 | 22 | return NextResponse.json( 23 | { 24 | data: media, 25 | preview: "https://hugo-coding.s3.us-west-1.amazonaws.com/" + media.key, 26 | }, 27 | { status: 201 }, 28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /src/app/api/users/promote-user/route.ts: -------------------------------------------------------------------------------- 1 | import { PromoteAdminSchema, promoteAdminSchema } from "@/features/users"; 2 | import createClient from "@/lib/supabase/server"; 3 | import { cookies } from "next/headers"; 4 | import { NextRequest, NextResponse } from "next/server"; 5 | 6 | export async function POST(request: NextRequest) { 7 | const cookieStore = cookies(); 8 | const client = createClient({ cookieStore, isAdmin: true }); 9 | const session = await client.auth.getSession(); 10 | 11 | if (!session.data.session.user.app_metadata.isAdmin) 12 | return NextResponse.json( 13 | { message: `Only Admin allowed to do this action.` }, 14 | { status: 500 }, 15 | ); 16 | 17 | const data: PromoteAdminSchema = await request.json(); 18 | const validate = promoteAdminSchema.safeParse(data); 19 | 20 | if (!validate) 21 | return NextResponse.json( 22 | { message: "Error, Data validation failed." }, 23 | { status: 500 }, 24 | ); 25 | 26 | const { data: userResponse } = await client.auth.admin.getUserById( 27 | data.userId, 28 | ); 29 | 30 | if (!userResponse.user) 31 | return NextResponse.json( 32 | { message: `Error, UserId: ${data.userId} not found.` }, 33 | { status: 500 }, 34 | ); 35 | 36 | console.log("userResponse", userResponse.user); 37 | 38 | const { data: updatedUser, error } = await client.auth.admin.updateUserById( 39 | data.userId, 40 | { app_metadata: { isAdmin: true } }, 41 | ); 42 | 43 | if (error) 44 | return NextResponse.json({ message: error.message }, { status: 500 }); 45 | 46 | return NextResponse.json( 47 | { 48 | message: `User:${updatedUser.user.user_metadata.name} is promoted to Admin.`, 49 | }, 50 | { status: 201 }, 51 | ); 52 | } 53 | -------------------------------------------------------------------------------- /src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clonglam/HiyoRi-Ecommerce-Nextjs-Supabase/98b6f29e223567e25bdf25742d07e107f50a6955/src/app/favicon.ico -------------------------------------------------------------------------------- /src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import { Inter } from "next/font/google"; 3 | import "./globals.css"; 4 | import { Toaster } from "@/components/ui/toaster"; 5 | import CustomProvider from "../providers/CustomProvider"; 6 | 7 | const inter = Inter({ subsets: ["latin"] }); 8 | 9 | export const metadata: Metadata = { 10 | title: "HIYORI | Ecommerce Platforum Built with Nextjs 14.", 11 | description: "Generated by create next app", 12 | }; 13 | 14 | export default function RootLayout({ 15 | children, 16 | }: Readonly<{ 17 | children: React.ReactNode; 18 | }>) { 19 | return ( 20 | 21 | 22 | 23 | {children} 24 | 25 | 26 | 27 | 28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /src/app/middleware.ts: -------------------------------------------------------------------------------- 1 | import { createServerClient, type CookieOptions } from "@supabase/ssr"; 2 | import { NextResponse, type NextRequest } from "next/server"; 3 | 4 | export async function middleware(request: NextRequest) { 5 | let response = NextResponse.next({ 6 | request: { 7 | headers: request.headers, 8 | }, 9 | }); 10 | 11 | const supabase = createServerClient( 12 | process.env.NEXT_PUBLIC_SUPABASE_URL!, 13 | process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, 14 | { 15 | cookies: { 16 | get(name: string) { 17 | return request.cookies.get(name)?.value; 18 | }, 19 | set(name: string, value: string, options: CookieOptions) { 20 | request.cookies.set({ 21 | name, 22 | value, 23 | ...options, 24 | }); 25 | response = NextResponse.next({ 26 | request: { 27 | headers: request.headers, 28 | }, 29 | }); 30 | response.cookies.set({ 31 | name, 32 | value, 33 | ...options, 34 | }); 35 | }, 36 | remove(name: string, options: CookieOptions) { 37 | request.cookies.set({ 38 | name, 39 | value: "", 40 | ...options, 41 | }); 42 | response = NextResponse.next({ 43 | request: { 44 | headers: request.headers, 45 | }, 46 | }); 47 | response.cookies.set({ 48 | name, 49 | value: "", 50 | ...options, 51 | }); 52 | }, 53 | }, 54 | }, 55 | ); 56 | 57 | await supabase.auth.getUser(); 58 | 59 | return response; 60 | } 61 | 62 | export const config = { 63 | matcher: [ 64 | /* 65 | * Match all request paths except for the ones starting with: 66 | * - _next/static (static files) 67 | * - _next/image (image optimization files) 68 | * - favicon.ico (favicon file) 69 | * Feel free to modify this pattern to include more paths. 70 | */ 71 | "/((?!_next/static|_next/image|favicon.ico).*)", 72 | "/cart/:path*", 73 | ], 74 | }; 75 | -------------------------------------------------------------------------------- /src/components/__test__/Header.test.tsx: -------------------------------------------------------------------------------- 1 | import { render, screen } from "@testing-library/react"; 2 | import Header from "../layouts/Header"; 3 | 4 | it("should render Component Section Header", () => { 5 | render( 6 |
, 7 | ); 8 | expect(screen.getByText("Section Header")).toBeInTheDocument(); 9 | expect(screen.getByText("section description.")).toBeInTheDocument(); 10 | }); 11 | -------------------------------------------------------------------------------- /src/components/__test__/QuantityInput.test.tsx: -------------------------------------------------------------------------------- 1 | import { render, screen } from "@testing-library/react"; 2 | import QuantityInput from "../layouts/QuantityInput"; 3 | 4 | it("should render Component Section Header", () => { 5 | render( 6 | , 15 | ); 16 | 17 | expect(screen.getByLabelText("add")).toBeInTheDocument(); 18 | expect(screen.getByLabelText("minus")).toBeInTheDocument(); 19 | 20 | // expect(screen.getByText("section description.")).toBeInTheDocument() 21 | }); 22 | -------------------------------------------------------------------------------- /src/components/admin/AdminShell.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode } from "react"; 2 | import { Icons } from "../layouts/icons"; 3 | import Link from "next/link"; 4 | import { Button } from "../ui/button"; 5 | import BackButton from "../layouts/BackButton"; 6 | 7 | type AdminShellProps = { 8 | heading: string; 9 | description: string; 10 | showBackButton?: boolean; 11 | children: ReactNode; 12 | }; 13 | 14 | function AdminShell({ 15 | heading, 16 | description, 17 | showBackButton, 18 | children, 19 | }: AdminShellProps) { 20 | return ( 21 |
22 |
23 | {showBackButton && } 24 |
25 |
26 |

27 | {heading} 28 |

29 |

30 | {description} 31 |

32 |
33 |
34 |
35 | 36 | {children} 37 |
38 | ); 39 | } 40 | 41 | export default AdminShell; 42 | -------------------------------------------------------------------------------- /src/components/admin/SidebarNav.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import Link from "next/link"; 4 | import { usePathname } from "next/navigation"; 5 | import type { SidebarNavItem } from "@/types"; 6 | 7 | import { cn } from "@/lib/utils"; 8 | import { Icons } from "../layouts/icons"; 9 | 10 | export interface SidebarNavProps { 11 | items: SidebarNavItem[]; 12 | } 13 | 14 | export function SidebarNav({ items }: SidebarNavProps) { 15 | const pathname = usePathname(); 16 | 17 | if (!items?.length) return null; 18 | 19 | return ( 20 |
21 | {items.map((item, index) => { 22 | const Icon = Icons[item.icon ?? "chevronLeft"]; 23 | 24 | return item.href ? ( 25 | 31 | 40 | 43 | 44 | ) : ( 45 | 49 | {item.title} 50 | 51 | ); 52 | })} 53 |
54 | ); 55 | } 56 | -------------------------------------------------------------------------------- /src/components/layouts/BackButton.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import React from "react"; 3 | import { Icons } from "./icons"; 4 | import { Button } from "../ui/button"; 5 | import { useRouter } from "next/navigation"; 6 | 7 | function BackButton() { 8 | const router = useRouter(); 9 | return ( 10 | 13 | ); 14 | } 15 | 16 | export default BackButton; 17 | -------------------------------------------------------------------------------- /src/components/layouts/Branding.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils"; 2 | import Link from "next/link"; 3 | import React from "react"; 4 | 5 | type Props = { className?: string }; 6 | 7 | function Branding({ className }: Props) { 8 | return ( 9 | 13 | HIYORI 14 | 15 | ); 16 | } 17 | 18 | export default Branding; 19 | -------------------------------------------------------------------------------- /src/components/layouts/ErrorToaster.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { useSearchParams } from "next/navigation"; 3 | import { useEffect } from "react"; 4 | import { useToast } from "../ui/use-toast"; 5 | 6 | type Props = {}; 7 | 8 | function ErrorToaster({}: Props) { 9 | const { toast } = useToast(); 10 | const searchParams = useSearchParams(); 11 | 12 | useEffect(() => { 13 | const message = searchParams.get("message"); 14 | 15 | if (message) toast({ title: "Error", description: message }); 16 | }, [searchParams]); 17 | 18 | return <>; 19 | } 20 | 21 | export default ErrorToaster; 22 | -------------------------------------------------------------------------------- /src/components/layouts/Header.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode } from "react"; 2 | 3 | interface HeaderProps 4 | extends React.DetailedHTMLProps< 5 | React.HTMLAttributes, 6 | HTMLDivElement 7 | > { 8 | heading: string; 9 | description?: string; 10 | children?: ReactNode; 11 | } 12 | 13 | function Header({ heading, description, children, ...props }: HeaderProps) { 14 | return ( 15 |
16 |

{heading}

17 |

18 | {description} 19 |

20 | {children} 21 |
22 | ); 23 | } 24 | 25 | export default Header; 26 | -------------------------------------------------------------------------------- /src/components/layouts/MainNavbar.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils"; 2 | import Link from "next/link"; 3 | import { Suspense } from "react"; 4 | import { CartLink, CartNav } from "../../features/carts"; 5 | import { UserNav } from "@/features/auth"; 6 | import { Icons } from "./icons"; 7 | import Branding from "./Branding"; 8 | import MobileNavbar from "./MobileNavbar"; 9 | import SearchInput from "./SearchInput"; 10 | import { SideMenu } from "./SideMenu"; 11 | 12 | interface MainNavbarProps { 13 | adminLayout?: boolean; 14 | } 15 | 16 | async function MainNavbar({ adminLayout = false }: MainNavbarProps) { 17 | return ( 18 | 58 | ); 59 | } 60 | 61 | export default MainNavbar; 62 | -------------------------------------------------------------------------------- /src/components/layouts/MobileNavbar.tsx: -------------------------------------------------------------------------------- 1 | import { Suspense } from "react"; 2 | import CartNav from "../../features/carts/components/CartNav"; 3 | import Branding from "./Branding"; 4 | import MobileSearchInput from "./MobileSearchInput"; 5 | import { SideMenu } from "./SideMenu"; 6 | import CartLink from "../../features/carts/components/CartLink"; 7 | 8 | type Props = { adminLayout: boolean }; 9 | 10 | function MobileNavbar({ adminLayout }: Props) { 11 | return ( 12 |
13 |
14 | 15 | 16 |
17 | 18 | 19 | }> 20 | {!adminLayout && } 21 | 22 |
23 | ); 24 | } 25 | 26 | export default MobileNavbar; 27 | -------------------------------------------------------------------------------- /src/components/layouts/MobileSearchInput.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import React, { useState } from "react"; 3 | import { Icons } from "./icons"; 4 | import { Button } from "../ui/button"; 5 | import SearchInput from "./SearchInput"; 6 | 7 | type Props = {}; 8 | 9 | function MobileSearchInput({}: Props) { 10 | const [openSearchBar, setOpenSearchBar] = useState(false); 11 | return ( 12 | <> 13 | {openSearchBar ? ( 14 |
15 |
16 | 17 | 18 |
19 |
20 | ) : ( 21 | 24 | )} 25 | 26 | ); 27 | } 28 | 29 | export default MobileSearchInput; 30 | -------------------------------------------------------------------------------- /src/components/layouts/NewsletterForm.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import React from "react"; 3 | import { Input } from "../ui/input"; 4 | import { Label } from "../ui/label"; 5 | import { Button } from "../ui/button"; 6 | 7 | function NewsletterForm() { 8 | return ( 9 |
10 |

Sign up to Our Newsletter

11 |
12 | 13 | 14 |
15 | 18 |
19 | ); 20 | } 21 | 22 | export default NewsletterForm; 23 | -------------------------------------------------------------------------------- /src/components/layouts/QuantityInput.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import React from "react"; 3 | import { Icons } from "./icons"; 4 | import { cn } from "@/lib/utils"; 5 | 6 | export interface QuantitiyInputProps 7 | extends React.InputHTMLAttributes { 8 | onChange?: (...event: any[]) => void; 9 | value: number; 10 | addOneHandler: () => void; 11 | minusOneHandler: () => void; 12 | } 13 | 14 | const QuantityInput = React.forwardRef( 15 | ( 16 | { onChange, addOneHandler, minusOneHandler, value, className, ...props }, 17 | ref, 18 | ) => { 19 | return ( 20 |
26 | onChange(event.target.valueAsNumber)} 33 | className="w-6 flex-1 text-center shadow-none focus:ring-transparent focus:ring-0 active:ring-0 focus:border-none focus:ring-offset-0 max-w-6 order-2 h-8" 34 | /> 35 | 42 | 49 |
50 | ); 51 | }, 52 | ); 53 | 54 | QuantityInput.displayName = "QuantityInput"; 55 | 56 | export { QuantityInput }; 57 | export default QuantityInput; 58 | -------------------------------------------------------------------------------- /src/components/layouts/SectionHeading.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode } from "react"; 2 | 3 | interface HeaderProps 4 | extends React.DetailedHTMLProps< 5 | React.HTMLAttributes, 6 | HTMLDivElement 7 | > { 8 | heading: string; 9 | description?: string; 10 | children?: ReactNode; 11 | } 12 | 13 | function SectionHeading({ 14 | heading, 15 | description, 16 | children, 17 | ...props 18 | }: HeaderProps) { 19 | return ( 20 |
21 |

{heading}

22 |

23 | {description} 24 |

25 | {/* {children} */} 26 |
27 | ); 28 | } 29 | 30 | export default SectionHeading; 31 | -------------------------------------------------------------------------------- /src/components/layouts/SettingSidebar.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import Link from "next/link"; 4 | import { usePathname } from "next/navigation"; 5 | 6 | import { cn } from "@/lib/utils"; 7 | import { buttonVariants } from "../ui/button"; 8 | 9 | interface SettingSidebarNavProps extends React.HTMLAttributes { 10 | items: { 11 | href: string; 12 | title: string; 13 | }[]; 14 | } 15 | 16 | export function SettingSidebarNav({ 17 | className, 18 | items, 19 | ...props 20 | }: SettingSidebarNavProps) { 21 | const pathname = usePathname(); 22 | 23 | return ( 24 | 47 | ); 48 | } 49 | -------------------------------------------------------------------------------- /src/components/layouts/Shell.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import { cn } from "@/lib/utils"; 4 | 5 | interface ShellProps 6 | extends React.DetailedHTMLProps< 7 | React.HTMLAttributes, 8 | HTMLDivElement 9 | > { 10 | children: React.ReactNode; 11 | layout?: "default" | "dashboard" | "narrow"; 12 | } 13 | 14 | export function Shell({ 15 | children, 16 | layout = "default", 17 | className, 18 | ...props 19 | }: ShellProps) { 20 | return ( 21 |
31 | {children} 32 |
33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /src/components/layouts/SideMenu.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { Button } from "@/components/ui/button" 4 | import { 5 | Sheet, 6 | SheetContent, 7 | SheetFooter, 8 | SheetTrigger, 9 | } from "@/components/ui/sheet" 10 | import { siteConfig } from "@/config/site" 11 | import Link from "next/link" 12 | import { Icons } from "./icons" 13 | import Branding from "./Branding" 14 | import SocialMedias from "./SocialMedias" 15 | 16 | export function SideMenu() { 17 | return ( 18 | 19 | 20 | 23 | 24 | 25 | 30 |
31 | {siteConfig.mainNav.map(({ title, href }, index) => ( 32 | 33 | {title} 34 | 35 | ))} 36 |
37 | 38 | 39 | 40 | 41 |
42 |

{siteConfig.address}

43 |

44 | {siteConfig.phone} {` / `} 45 | 49 | {siteConfig.email} 50 | 51 |

52 |
53 | 54 | 55 |
56 |
57 |
58 | ) 59 | } 60 | -------------------------------------------------------------------------------- /src/components/layouts/SocialMedias.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Icons } from "./icons"; 3 | import { cn } from "@/lib/utils"; 4 | import Link from "next/link"; 5 | 6 | type Props = { 7 | containerClassName?: string; 8 | itemsClassName?: string; 9 | }; 10 | 11 | function SocialMedias({ containerClassName, itemsClassName }: Props) { 12 | return ( 13 |
14 | 15 | 21 | 22 | 23 | 24 | 30 | 31 | 32 | 33 | 39 | 40 |
41 | ); 42 | } 43 | 44 | export default SocialMedias; 45 | -------------------------------------------------------------------------------- /src/components/ui/CloseButton.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { useRouter } from "next/navigation"; 3 | import React from "react"; 4 | import { Icons } from "../layouts/icons"; 5 | 6 | type Props = {}; 7 | 8 | function CloseButton({}: Props) { 9 | const router = useRouter(); 10 | return ( 11 | 14 | ); 15 | } 16 | 17 | export default CloseButton; 18 | -------------------------------------------------------------------------------- /src/components/ui/DisabledFormData.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode } from "react"; 2 | 3 | type DisabledFormDataProps = { 4 | data: ReactNode; 5 | label: string; 6 | htmlFor?: string; 7 | }; 8 | 9 | function DisabledFormData({ data, label, htmlFor }: DisabledFormDataProps) { 10 | return ( 11 |
12 | 18 |

{data}

19 |
20 | ); 21 | } 22 | 23 | export default DisabledFormData; 24 | -------------------------------------------------------------------------------- /src/components/ui/Modal.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode } from "react"; 2 | import { cn } from "@/lib/utils"; 3 | import CloseButton from "./CloseButton"; 4 | import { 5 | Card, 6 | CardContent, 7 | CardDescription, 8 | CardFooter, 9 | CardHeader, 10 | CardTitle, 11 | } from "@/components/ui/card"; 12 | 13 | type Props = { 14 | header: string; 15 | children: ReactNode; 16 | containerClassName?: string; 17 | }; 18 | 19 | function Modal({ header, containerClassName, children }: Props) { 20 | return ( 21 |
26 | 32 | 33 |

34 | {header} 35 |

36 | 37 |
38 | 39 | 40 | {children} 41 | 42 |
43 |
44 | ); 45 | } 46 | 47 | export default Modal; 48 | -------------------------------------------------------------------------------- /src/components/ui/accordion.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as AccordionPrimitive from "@radix-ui/react-accordion"; 5 | import { ChevronDown } from "lucide-react"; 6 | 7 | import { cn } from "@/lib/utils"; 8 | 9 | const Accordion = AccordionPrimitive.Root; 10 | 11 | const AccordionItem = React.forwardRef< 12 | React.ElementRef, 13 | React.ComponentPropsWithoutRef 14 | >(({ className, ...props }, ref) => ( 15 | 20 | )); 21 | AccordionItem.displayName = "AccordionItem"; 22 | 23 | const AccordionTrigger = React.forwardRef< 24 | React.ElementRef, 25 | React.ComponentPropsWithoutRef 26 | >(({ className, children, ...props }, ref) => ( 27 | 28 | svg]:rotate-180", 32 | className, 33 | )} 34 | {...props} 35 | > 36 | {children} 37 | 38 | 39 | 40 | )); 41 | AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName; 42 | 43 | const AccordionContent = React.forwardRef< 44 | React.ElementRef, 45 | React.ComponentPropsWithoutRef 46 | >(({ className, children, ...props }, ref) => ( 47 | 52 |
{children}
53 |
54 | )); 55 | 56 | AccordionContent.displayName = AccordionPrimitive.Content.displayName; 57 | 58 | export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }; 59 | -------------------------------------------------------------------------------- /src/components/ui/alert.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { cva, type VariantProps } from "class-variance-authority"; 3 | 4 | import { cn } from "@/lib/utils"; 5 | 6 | const alertVariants = cva( 7 | "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", 8 | { 9 | variants: { 10 | variant: { 11 | default: "bg-background text-foreground", 12 | destructive: 13 | "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive", 14 | }, 15 | }, 16 | defaultVariants: { 17 | variant: "default", 18 | }, 19 | }, 20 | ); 21 | 22 | const Alert = React.forwardRef< 23 | HTMLDivElement, 24 | React.HTMLAttributes & VariantProps 25 | >(({ className, variant, ...props }, ref) => ( 26 |
32 | )); 33 | Alert.displayName = "Alert"; 34 | 35 | const AlertTitle = React.forwardRef< 36 | HTMLParagraphElement, 37 | React.HTMLAttributes 38 | >(({ className, ...props }, ref) => ( 39 |
44 | )); 45 | AlertTitle.displayName = "AlertTitle"; 46 | 47 | const AlertDescription = React.forwardRef< 48 | HTMLParagraphElement, 49 | React.HTMLAttributes 50 | >(({ className, ...props }, ref) => ( 51 |
56 | )); 57 | AlertDescription.displayName = "AlertDescription"; 58 | 59 | export { Alert, AlertTitle, AlertDescription }; 60 | -------------------------------------------------------------------------------- /src/components/ui/avatar.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as AvatarPrimitive from "@radix-ui/react-avatar"; 5 | 6 | import { cn } from "@/lib/utils"; 7 | 8 | const Avatar = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >(({ className, ...props }, ref) => ( 12 | 20 | )); 21 | Avatar.displayName = AvatarPrimitive.Root.displayName; 22 | 23 | const AvatarImage = React.forwardRef< 24 | React.ElementRef, 25 | React.ComponentPropsWithoutRef 26 | >(({ className, ...props }, ref) => ( 27 | 32 | )); 33 | AvatarImage.displayName = AvatarPrimitive.Image.displayName; 34 | 35 | const AvatarFallback = React.forwardRef< 36 | React.ElementRef, 37 | React.ComponentPropsWithoutRef 38 | >(({ className, ...props }, ref) => ( 39 | 47 | )); 48 | AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName; 49 | 50 | export { Avatar, AvatarImage, AvatarFallback }; 51 | -------------------------------------------------------------------------------- /src/components/ui/badge.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { cva, type VariantProps } from "class-variance-authority"; 3 | 4 | import { cn } from "@/lib/utils"; 5 | 6 | const badgeVariants = cva( 7 | "inline-flex items-center border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", 8 | { 9 | variants: { 10 | variant: { 11 | default: 12 | "border-transparent bg-primary text-primary-foreground hover:bg-primary/80", 13 | secondary: 14 | "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", 15 | destructive: 16 | "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80", 17 | new_product: 18 | "border-transparent bg-[#ff0000] text-destructive-foreground hover:bg-destructive/80", 19 | featured: 20 | "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80", 21 | best_sale: 22 | "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80", 23 | outline: "text-foreground", 24 | }, 25 | }, 26 | defaultVariants: { 27 | variant: "default", 28 | }, 29 | }, 30 | ); 31 | 32 | export interface BadgeProps 33 | extends React.HTMLAttributes, 34 | VariantProps {} 35 | 36 | function Badge({ className, variant, ...props }: BadgeProps) { 37 | return ( 38 |
39 | ); 40 | } 41 | 42 | export { Badge, badgeVariants }; 43 | -------------------------------------------------------------------------------- /src/components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Slot } from "@radix-ui/react-slot"; 3 | import { cva, type VariantProps } from "class-variance-authority"; 4 | 5 | import { cn } from "@/lib/utils"; 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center whitespace-nowrap 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", 9 | { 10 | variants: { 11 | variant: { 12 | default: "bg-primary text-primary-foreground hover:bg-primary/90", 13 | destructive: 14 | "bg-destructive text-destructive-foreground hover:bg-destructive/90", 15 | outline: 16 | "border border-input bg-transparent hover:bg-accent/20 hover:text-accent-foreground", 17 | secondary: 18 | "bg-secondary text-secondary-foreground hover:bg-secondary/80", 19 | ghost: "hover:bg-accent hover:text-accent-foreground", 20 | link: "text-primary underline-offset-4 hover:underline", 21 | }, 22 | size: { 23 | default: "h-10 px-5 py-2", 24 | sm: "h-9 rounded-md px-3", 25 | lg: "h-11 rounded-md px-8", 26 | icon: "h-10 w-10", 27 | }, 28 | }, 29 | defaultVariants: { 30 | variant: "default", 31 | size: "default", 32 | }, 33 | }, 34 | ); 35 | 36 | export interface ButtonProps 37 | extends React.ButtonHTMLAttributes, 38 | VariantProps { 39 | asChild?: boolean; 40 | } 41 | 42 | const Button = React.forwardRef( 43 | ({ className, variant, size, asChild = false, ...props }, ref) => { 44 | const Comp = asChild ? Slot : "button"; 45 | return ( 46 | 51 | ); 52 | }, 53 | ); 54 | Button.displayName = "Button"; 55 | 56 | export { Button, buttonVariants }; 57 | -------------------------------------------------------------------------------- /src/components/ui/card.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import { cn } from "@/lib/utils"; 4 | 5 | const Card = React.forwardRef< 6 | HTMLDivElement, 7 | React.HTMLAttributes 8 | >(({ className, ...props }, ref) => ( 9 |
17 | )); 18 | Card.displayName = "Card"; 19 | 20 | const CardHeader = React.forwardRef< 21 | HTMLDivElement, 22 | React.HTMLAttributes 23 | >(({ className, ...props }, ref) => ( 24 |
29 | )); 30 | CardHeader.displayName = "CardHeader"; 31 | 32 | const CardTitle = React.forwardRef< 33 | HTMLParagraphElement, 34 | React.HTMLAttributes 35 | >(({ className, ...props }, ref) => ( 36 |

44 | )); 45 | CardTitle.displayName = "CardTitle"; 46 | 47 | const CardDescription = React.forwardRef< 48 | HTMLParagraphElement, 49 | React.HTMLAttributes 50 | >(({ className, ...props }, ref) => ( 51 |

56 | )); 57 | CardDescription.displayName = "CardDescription"; 58 | 59 | const CardContent = React.forwardRef< 60 | HTMLDivElement, 61 | React.HTMLAttributes 62 | >(({ className, ...props }, ref) => ( 63 |

64 | )); 65 | CardContent.displayName = "CardContent"; 66 | 67 | const CardFooter = React.forwardRef< 68 | HTMLDivElement, 69 | React.HTMLAttributes 70 | >(({ className, ...props }, ref) => ( 71 |
76 | )); 77 | CardFooter.displayName = "CardFooter"; 78 | 79 | export { 80 | Card, 81 | CardHeader, 82 | CardFooter, 83 | CardTitle, 84 | CardDescription, 85 | CardContent, 86 | }; 87 | -------------------------------------------------------------------------------- /src/components/ui/checkbox.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as CheckboxPrimitive from "@radix-ui/react-checkbox"; 5 | import { Check } from "lucide-react"; 6 | 7 | import { cn } from "@/lib/utils"; 8 | 9 | const Checkbox = React.forwardRef< 10 | React.ElementRef, 11 | React.ComponentPropsWithoutRef 12 | >(({ className, ...props }, ref) => ( 13 | 21 | 24 | 25 | 26 | 27 | )); 28 | Checkbox.displayName = CheckboxPrimitive.Root.displayName; 29 | 30 | export { Checkbox }; 31 | -------------------------------------------------------------------------------- /src/components/ui/data-table-toolbar.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Table } from "@tanstack/react-table"; 4 | import { Input } from "./input"; 5 | import { Button } from "./button"; 6 | import { Icons } from "../layouts/icons"; 7 | 8 | // import { DataTableViewOptions } from "@/app/examples/tasks/components/data-table-view-options" 9 | 10 | // import { priorities, statuses } from "../data/data" 11 | // import { DataTableFacetedFilter } from "./data-table-faceted-filter" 12 | 13 | interface DataTableToolbarProps { 14 | table: Table; 15 | } 16 | 17 | export function DataTableToolbar({ 18 | table, 19 | }: DataTableToolbarProps) { 20 | const isFiltered = table.getState().columnFilters.length > 0; 21 | 22 | return ( 23 |
24 |
25 | 29 | table.getColumn("title")?.setFilterValue(event.target.value) 30 | } 31 | className="h-8 w-[150px] lg:w-[250px]" 32 | /> 33 | {/* {table.getColumn("status") && ( 34 | 39 | )} 40 | {table.getColumn("priority") && ( 41 | 46 | )} */} 47 | {isFiltered && ( 48 | 56 | )} 57 |
58 | {/* */} 59 |
60 | ); 61 | } 62 | -------------------------------------------------------------------------------- /src/components/ui/deleteDialog.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import React from "react"; 3 | 4 | import { 5 | AlertDialog, 6 | AlertDialogAction, 7 | AlertDialogCancel, 8 | AlertDialogContent, 9 | AlertDialogDescription, 10 | AlertDialogFooter, 11 | AlertDialogHeader, 12 | AlertDialogTitle, 13 | AlertDialogTrigger, 14 | } from "@/components/ui/alert-dialog"; 15 | import { Button } from "./button"; 16 | 17 | type DeleteDialogProps = { 18 | onClickHandler: (e: React.MouseEvent) => void; 19 | triggerLabel?: string; 20 | title?: string; 21 | description?: string; 22 | cancelLabel?: string; 23 | actionLabel?: string; 24 | }; 25 | 26 | function DeleteDialog({ 27 | onClickHandler, 28 | title, 29 | description, 30 | triggerLabel, 31 | actionLabel, 32 | cancelLabel, 33 | }: DeleteDialogProps) { 34 | return ( 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | {title || "Are you absolutely sure?"} 43 | 44 | 45 | {description || 46 | "This action cannot be undone. This will permanently delete your account and remove your data from our servers."} 47 | 48 | 49 | 50 | {cancelLabel || "Cancel"} 51 | 52 | {actionLabel || "Delete"} 53 | 54 | 55 | 56 | 57 | ); 58 | } 59 | 60 | export default DeleteDialog; 61 | -------------------------------------------------------------------------------- /src/components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as LabelPrimitive from "@radix-ui/react-label"; 5 | import { cva, type VariantProps } from "class-variance-authority"; 6 | 7 | import { cn } from "@/lib/utils"; 8 | 9 | const labelVariants = cva( 10 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70", 11 | ); 12 | 13 | const Label = React.forwardRef< 14 | React.ElementRef, 15 | React.ComponentPropsWithoutRef & 16 | VariantProps 17 | >(({ className, ...props }, ref) => ( 18 | 23 | )); 24 | Label.displayName = LabelPrimitive.Root.displayName; 25 | 26 | export { Label }; 27 | -------------------------------------------------------------------------------- /src/components/ui/popover.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as PopoverPrimitive from "@radix-ui/react-popover"; 5 | 6 | import { cn } from "@/lib/utils"; 7 | 8 | const Popover = PopoverPrimitive.Root; 9 | 10 | const PopoverTrigger = PopoverPrimitive.Trigger; 11 | 12 | const PopoverContent = React.forwardRef< 13 | React.ElementRef, 14 | React.ComponentPropsWithoutRef 15 | >(({ className, align = "center", sideOffset = 4, ...props }, ref) => ( 16 | 17 | 27 | 28 | )); 29 | PopoverContent.displayName = PopoverPrimitive.Content.displayName; 30 | 31 | export { Popover, PopoverTrigger, PopoverContent }; 32 | -------------------------------------------------------------------------------- /src/components/ui/progress.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as ProgressPrimitive from "@radix-ui/react-progress"; 5 | 6 | import { cn } from "@/lib/utils"; 7 | 8 | const Progress = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >(({ className, value, ...props }, ref) => ( 12 | 20 | 24 | 25 | )); 26 | Progress.displayName = ProgressPrimitive.Root.displayName; 27 | 28 | export { Progress }; 29 | -------------------------------------------------------------------------------- /src/components/ui/rating.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Icons } from "../layouts/icons"; 3 | import { cn } from "@/lib/utils"; 4 | 5 | type Props = { 6 | readOnly?: boolean; 7 | value: number; 8 | precision: number; 9 | }; 10 | 11 | export interface RatingProps 12 | extends React.InputHTMLAttributes { 13 | readOnly?: boolean; 14 | value: number; 15 | precision: number; 16 | max?: number; 17 | } 18 | 19 | const Rating = React.forwardRef( 20 | ({ className, value, max = 5, ...props }, ref) => { 21 | return ( 22 |
23 | {[...Array(max)].map((_, index) => ( 24 | 29 | ))} 30 |
31 | ); 32 | }, 33 | ); 34 | 35 | Rating.displayName = "Rating"; 36 | 37 | export { Rating }; 38 | -------------------------------------------------------------------------------- /src/components/ui/scroll-area.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"; 5 | 6 | import { cn } from "@/lib/utils"; 7 | 8 | const ScrollArea = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >(({ className, children, ...props }, ref) => ( 12 | 17 | 18 | {children} 19 | 20 | 21 | 22 | 23 | )); 24 | ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName; 25 | 26 | const ScrollBar = React.forwardRef< 27 | React.ElementRef, 28 | React.ComponentPropsWithoutRef 29 | >(({ className, orientation = "vertical", ...props }, ref) => ( 30 | 43 | 44 | 45 | )); 46 | ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName; 47 | 48 | export { ScrollArea, ScrollBar }; 49 | -------------------------------------------------------------------------------- /src/components/ui/scrollArea.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"; 5 | 6 | import { cn } from "@/lib/utils"; 7 | 8 | const ScrollArea = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >(({ className, children, ...props }, ref) => ( 12 | 17 | 18 | {children} 19 | 20 | 21 | 22 | 23 | )); 24 | ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName; 25 | 26 | const ScrollBar = React.forwardRef< 27 | React.ElementRef, 28 | React.ComponentPropsWithoutRef 29 | >(({ className, orientation = "vertical", ...props }, ref) => ( 30 | 43 | 44 | 45 | )); 46 | ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName; 47 | 48 | export { ScrollArea, ScrollBar }; 49 | -------------------------------------------------------------------------------- /src/components/ui/separator.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as SeparatorPrimitive from "@radix-ui/react-separator"; 5 | 6 | import { cn } from "@/lib/utils"; 7 | 8 | const Separator = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >( 12 | ( 13 | { className, orientation = "horizontal", decorative = true, ...props }, 14 | ref, 15 | ) => ( 16 | 27 | ), 28 | ); 29 | Separator.displayName = SeparatorPrimitive.Root.displayName; 30 | 31 | export { Separator }; 32 | -------------------------------------------------------------------------------- /src/components/ui/skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils"; 2 | 3 | function Skeleton({ 4 | className, 5 | ...props 6 | }: React.HTMLAttributes) { 7 | return ( 8 |
12 | ); 13 | } 14 | 15 | export { Skeleton }; 16 | -------------------------------------------------------------------------------- /src/components/ui/slider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as SliderPrimitive from "@radix-ui/react-slider"; 5 | 6 | import { cn } from "@/lib/utils"; 7 | 8 | const Slider = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef & { 11 | variant?: "default" | "range"; 12 | thickness?: "default" | "thin"; 13 | } 14 | >( 15 | ( 16 | { className, variant = "default", thickness = "default", ...props }, 17 | ref, 18 | ) => ( 19 | 27 | 33 | 34 | 35 | 41 | {variant === "range" && ( 42 | 48 | )} 49 | 50 | ), 51 | ); 52 | Slider.displayName = SliderPrimitive.Root.displayName; 53 | 54 | export { Slider }; 55 | -------------------------------------------------------------------------------- /src/components/ui/sonner.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useTheme } from "next-themes"; 4 | import { Toaster as Sonner } from "sonner"; 5 | 6 | type ToasterProps = React.ComponentProps; 7 | 8 | const Toaster = ({ ...props }: ToasterProps) => { 9 | const { theme = "system" } = useTheme(); 10 | 11 | return ( 12 | 28 | ); 29 | }; 30 | 31 | export { Toaster }; 32 | -------------------------------------------------------------------------------- /src/components/ui/spinner.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { cn } from "@/lib/utils"; 3 | 4 | interface SpinnerProps 5 | extends React.DetailedHTMLProps< 6 | React.HTMLAttributes, 7 | HTMLDivElement 8 | > {} 9 | 10 | function Spinner({ className, ...props }: SpinnerProps) { 11 | return ( 12 |
13 | 32 | Loading... 33 |
34 | ); 35 | } 36 | 37 | Spinner.displayName = "Spinner"; 38 | 39 | export { Spinner }; 40 | -------------------------------------------------------------------------------- /src/components/ui/tabs.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as TabsPrimitive from "@radix-ui/react-tabs"; 5 | 6 | import { cn } from "@/lib/utils"; 7 | 8 | const Tabs = TabsPrimitive.Root; 9 | 10 | const TabsList = React.forwardRef< 11 | React.ElementRef, 12 | React.ComponentPropsWithoutRef 13 | >(({ className, ...props }, ref) => ( 14 | 22 | )); 23 | TabsList.displayName = TabsPrimitive.List.displayName; 24 | 25 | const TabsTrigger = React.forwardRef< 26 | React.ElementRef, 27 | React.ComponentPropsWithoutRef 28 | >(({ className, ...props }, ref) => ( 29 | 37 | )); 38 | TabsTrigger.displayName = TabsPrimitive.Trigger.displayName; 39 | 40 | const TabsContent = React.forwardRef< 41 | React.ElementRef, 42 | React.ComponentPropsWithoutRef 43 | >(({ className, ...props }, ref) => ( 44 | 52 | )); 53 | TabsContent.displayName = TabsPrimitive.Content.displayName; 54 | 55 | export { Tabs, TabsList, TabsTrigger, TabsContent }; 56 | -------------------------------------------------------------------------------- /src/components/ui/tagsField.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { FC } from "react"; 3 | import { useFormContext, Controller } from "react-hook-form"; 4 | import TagsInput from "./tagsInput"; 5 | 6 | interface TagsFieldProps { 7 | name: string; 8 | defaultValue?: string[]; 9 | } 10 | 11 | export const TagsField: FC = ({ name, defaultValue }) => { 12 | const { control, getValues, setValue } = useFormContext(); // Use the form context 13 | 14 | return ( 15 | ( 20 | 25 | )} 26 | /> 27 | ); 28 | }; 29 | 30 | export default TagsField; 31 | -------------------------------------------------------------------------------- /src/components/ui/tagsInput.tsx: -------------------------------------------------------------------------------- 1 | // TagsInput.tsx 2 | import React, { ChangeEvent, KeyboardEvent, useState, FC } from "react"; 3 | import { Input } from "./input"; 4 | import { Badge } from "./badge"; 5 | import { Icons } from "../layouts/icons"; 6 | 7 | interface TagsInputProps { 8 | tags: string[]; 9 | setTags: (newTags: string[]) => void; 10 | onBlur: () => void; 11 | placeholder?: string; 12 | } 13 | 14 | const TagsInput: FC = ({ 15 | tags, 16 | setTags, 17 | onBlur, 18 | placeholder, 19 | }) => { 20 | const [input, setInput] = useState(""); 21 | 22 | const handleInputChange = (e: ChangeEvent) => { 23 | setInput(e.target.value); 24 | }; 25 | 26 | const addTag = () => { 27 | if (input && !tags.includes(input)) { 28 | // Prevent adding duplicates and empty tags 29 | setTags([...tags, input]); 30 | setInput(""); // Clear input field after adding 31 | } 32 | }; 33 | 34 | const removeTag = (indexToRemove: number) => { 35 | setTags(tags.filter((_, index) => index !== indexToRemove)); 36 | }; 37 | 38 | const handleKeyDown = (e: KeyboardEvent) => { 39 | if (e.key === "Enter") { 40 | e.preventDefault(); // Prevent form submission 41 | addTag(); 42 | } 43 | }; 44 | 45 | // Call onBlur when the input loses focus 46 | const handleBlur = () => { 47 | onBlur(); 48 | }; 49 | 50 | return ( 51 |
52 | {tags.map((tag, index) => ( 53 | 54 | {tag} 55 | 62 | 63 | ))} 64 | 65 | 75 | 78 |
79 | ); 80 | }; 81 | 82 | export default TagsInput; 83 | -------------------------------------------------------------------------------- /src/components/ui/textarea.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import { cn } from "@/lib/utils"; 4 | 5 | export interface TextareaProps 6 | extends React.TextareaHTMLAttributes {} 7 | 8 | const Textarea = React.forwardRef( 9 | ({ className, ...props }, ref) => { 10 | return ( 11 |