├── .prettierignore ├── app ├── favicon.ico ├── sitemap.ts ├── robots.ts ├── globals.css ├── layout.tsx └── (home) │ ├── sections │ ├── hero.tsx │ ├── variants.tsx │ └── setup.tsx │ └── page.tsx ├── postcss.config.js ├── prettier.config.js ├── lib ├── utils.ts └── helpers.ts ├── types └── index.d.ts ├── next.config.js ├── components ├── theme-provider.tsx ├── snippet.tsx ├── ui │ ├── label.tsx │ ├── toaster.tsx │ ├── input.tsx │ ├── popover.tsx │ ├── country-select.tsx │ ├── scroll-area.tsx │ ├── region-select.tsx │ ├── button.tsx │ ├── tabs.tsx │ ├── accordion.tsx │ ├── table.tsx │ ├── dialog.tsx │ ├── use-toast.ts │ ├── form.tsx │ ├── toast.tsx │ ├── command.tsx │ ├── phone-input.tsx │ ├── select.tsx │ └── dropdown-menu.tsx ├── code-block.tsx ├── mode-toggle.tsx ├── pre.tsx └── copy-button.tsx ├── components.json ├── config └── site.ts ├── .gitignore ├── public ├── vercel.svg └── next.svg ├── .eslintrc.json ├── contentlayer.config.ts ├── tsconfig.json ├── LICENSE.md ├── hooks └── use-copy.tsx ├── content └── snippets │ ├── country-select.mdx │ ├── region-select.mdx │ └── helpers.mdx ├── package.json ├── tailwind.config.ts └── README.md /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .next 3 | .contentlayer -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inextdeve/shadcn-country-region-select/HEAD/app/favicon.ico -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('prettier').Config} */ 2 | module.exports = { 3 | plugins: ["prettier-plugin-tailwindcss"], 4 | } 5 | -------------------------------------------------------------------------------- /lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from "clsx"; 2 | import { twMerge } from "tailwind-merge"; 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)); 6 | } 7 | -------------------------------------------------------------------------------- /types/index.d.ts: -------------------------------------------------------------------------------- 1 | export type SiteConfig = { 2 | name: string; 3 | description: string; 4 | url: string; 5 | links: { 6 | twitter: string; 7 | github: string; 8 | website: string; 9 | }; 10 | }; 11 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | const { withContentlayer } = require("next-contentlayer"); 2 | 3 | /** @type {import('next').NextConfig} */ 4 | const nextConfig = { reactStrictMode: true, swcMinify: true }; 5 | 6 | module.exports = withContentlayer(nextConfig); 7 | -------------------------------------------------------------------------------- /app/sitemap.ts: -------------------------------------------------------------------------------- 1 | import { type MetadataRoute } from "next"; 2 | import { siteConfig } from "../config/site"; 3 | 4 | export default function sitemap(): MetadataRoute.Sitemap { 5 | return [ 6 | { 7 | url: `${siteConfig.url}/`, 8 | lastModified: new Date(), 9 | }, 10 | ]; 11 | } 12 | -------------------------------------------------------------------------------- /app/robots.ts: -------------------------------------------------------------------------------- 1 | import { MetadataRoute } from "next"; 2 | import { siteConfig } from "../config/site"; 3 | 4 | export default function robots(): MetadataRoute.Robots { 5 | return { 6 | rules: { 7 | userAgent: "*", 8 | allow: "/", 9 | disallow: "/private/", 10 | }, 11 | sitemap: `${siteConfig.url}/sitemap.xml`, 12 | }; 13 | } 14 | -------------------------------------------------------------------------------- /components/theme-provider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { ThemeProvider as NextThemesProvider } from "next-themes"; 4 | import { type ThemeProviderProps } from "next-themes/dist/types"; 5 | import * as React from "react"; 6 | 7 | export function ThemeProvider({ children, ...props }: ThemeProviderProps) { 8 | return {children}; 9 | } 10 | -------------------------------------------------------------------------------- /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": "app/globals.css", 9 | "baseColor": "zinc", 10 | "cssVariables": true 11 | }, 12 | "aliases": { 13 | "components": "@/components", 14 | "utils": "@/lib/utils" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /components/snippet.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useMDXComponent } from "next-contentlayer/hooks"; 4 | import React from "react"; 5 | import Pre from "./pre"; 6 | import type { Snippet as SnippetType } from ".contentlayer/generated"; 7 | 8 | const components = { 9 | pre: Pre, 10 | }; 11 | 12 | export function Snippet({ snippet }: { snippet: SnippetType }) { 13 | const MDXContent = useMDXComponent(snippet.body.code); 14 | return ; 15 | } 16 | -------------------------------------------------------------------------------- /config/site.ts: -------------------------------------------------------------------------------- 1 | import { SiteConfig } from "@/types"; 2 | 3 | export const siteConfig: SiteConfig = { 4 | name: "Shadcn Country / Region Select", 5 | description: 6 | "A country / region select component implementation of Shadcn's select component", 7 | url: "https://shadcn-country-region.inext.dev", 8 | links: { 9 | twitter: "https://twitter.com/inext_devv", 10 | github: "https://github.com/inextdeve/shadcn-country-region-select", 11 | website: "https://inext.dev", 12 | }, 13 | }; 14 | -------------------------------------------------------------------------------- /.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 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env*.local 29 | 30 | # vercel 31 | .vercel 32 | 33 | # typescript 34 | *.tsbuildinfo 35 | next-env.d.ts 36 | 37 | # contentlayer 38 | .contentlayer -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/eslintrc", 3 | "plugins": ["@typescript-eslint", "tailwindcss"], 4 | "extends": [ 5 | "next/core-web-vitals", 6 | "prettier", 7 | "plugin:tailwindcss/recommended" 8 | ], 9 | "rules": { 10 | "@next/next/no-html-link-for-pages": "off", 11 | "react/jsx-key": "off", 12 | "tailwindcss/no-custom-classname": "off", 13 | "tailwindcss/classnames-order": "off", 14 | "tailwindcss/enforces-shorthand": "off", 15 | "tailwindcss/no-unnecessary-arbitrary-value": "off" 16 | }, 17 | "settings": { 18 | "tailwindcss": { 19 | "callees": ["cn", "cva"], 20 | "config": "./tailwind.config.ts", 21 | "classRegex": "^(class(Name)?|tw)$" 22 | }, 23 | "next": { 24 | "rootDir": "./" 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as LabelPrimitive from "@radix-ui/react-label"; 4 | import { type VariantProps, cva } from "class-variance-authority"; 5 | import * as React from "react"; 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 | -------------------------------------------------------------------------------- /components/ui/toaster.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { 4 | Toast, 5 | ToastClose, 6 | ToastDescription, 7 | ToastProvider, 8 | ToastTitle, 9 | ToastViewport, 10 | } from "@/components/ui/toast"; 11 | import { useToast } from "@/components/ui/use-toast"; 12 | 13 | export function Toaster() { 14 | const { toasts } = useToast(); 15 | 16 | return ( 17 | 18 | {toasts.map(({ id, title, description, action, ...props }) => ( 19 | 20 |
21 | {title && {title}} 22 | {description && {description}} 23 |
24 | {action} 25 | 26 |
27 | ))} 28 | 29 |
30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /components/code-block.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils"; 2 | import CopyButton from "./copy-button"; 3 | 4 | function CodeBlock({ 5 | value, 6 | className, 7 | copyable = true, 8 | }: { 9 | value: string; 10 | className?: string; 11 | codeClass?: string; 12 | copyable?: boolean; 13 | codeWrap?: boolean; 14 | noCodeFont?: boolean; 15 | noMask?: boolean; 16 | }) { 17 | value = value || ""; 18 | 19 | return ( 20 |
28 |       
29 |       {value}
30 |     
31 | ); 32 | } 33 | 34 | export default CodeBlock; 35 | -------------------------------------------------------------------------------- /contentlayer.config.ts: -------------------------------------------------------------------------------- 1 | import { makeSource } from "contentlayer/source-files"; 2 | 3 | import { defineDocumentType } from "contentlayer/source-files"; 4 | 5 | export const Snippet = defineDocumentType(() => ({ 6 | name: "Snippet", 7 | filePathPattern: `snippets/**/*.mdx`, 8 | contentType: "mdx", 9 | fields: { 10 | file: { 11 | type: "string", 12 | description: "The name of the snippet", 13 | required: true, 14 | }, 15 | order: { 16 | type: "number", 17 | description: "The order of the snippet", 18 | required: true, 19 | }, 20 | }, 21 | computedFields: { 22 | slug: { 23 | type: "string", 24 | resolve: (_) => _._raw.sourceFileName.replace(/\.[^.$]+$/, ""), 25 | }, 26 | }, 27 | })); 28 | 29 | export default makeSource({ 30 | contentDirPath: "content", 31 | documentTypes: [Snippet], 32 | }); 33 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "baseUrl": ".", 5 | "lib": ["dom", "dom.iterable", "esnext"], 6 | "allowJs": true, 7 | "skipLibCheck": true, 8 | "strict": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "bundler", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true, 17 | "plugins": [ 18 | { 19 | "name": "next" 20 | } 21 | ], 22 | "paths": { 23 | "@/*": ["./*"], 24 | "contentlayer/generated": ["./.contentlayer/generated"] 25 | } 26 | }, 27 | "include": [ 28 | "next-env.d.ts", 29 | "**/*.ts", 30 | "**/*.tsx", 31 | ".next/types/**/*.ts", 32 | ".contentlayer/generated" 33 | ], 34 | "exclude": ["node_modules"] 35 | } 36 | -------------------------------------------------------------------------------- /components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import { cn } from "@/lib/utils"; 4 | 5 | export interface InputProps 6 | extends React.InputHTMLAttributes {} 7 | 8 | const Input = React.forwardRef( 9 | ({ className, type, ...props }, ref) => { 10 | return ( 11 | 20 | ); 21 | }, 22 | ); 23 | Input.displayName = "Input"; 24 | 25 | export { Input }; 26 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | Copyright (c) 2024 Yassine Farroud 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /hooks/use-copy.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback, useState } from "react"; 2 | 3 | function useClipboard() { 4 | const [isCopied, setIsCopied] = useState(false); 5 | 6 | const copyToClipboard = useCallback(async (text: string) => { 7 | if (navigator.clipboard && window.isSecureContext) { 8 | // Navigator Clipboard API method' 9 | try { 10 | await navigator.clipboard.writeText(text); 11 | setIsCopied(true); 12 | } catch (err) { 13 | console.error(err); 14 | setIsCopied(false); 15 | } 16 | } else { 17 | // Clipboard API not available, use fallback 18 | const textArea = document.createElement("textarea"); 19 | textArea.value = text; 20 | document.body.appendChild(textArea); 21 | textArea.focus(); 22 | textArea.select(); 23 | try { 24 | const successful: boolean = document.execCommand("copy"); 25 | setIsCopied(successful); 26 | } catch (err) { 27 | console.error(err); 28 | setIsCopied(false); 29 | } 30 | document.body.removeChild(textArea); 31 | } 32 | }, []); 33 | 34 | return { isCopied, copyToClipboard }; 35 | } 36 | 37 | export default useClipboard; 38 | -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /components/ui/popover.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as PopoverPrimitive from "@radix-ui/react-popover"; 4 | import * as React from "react"; 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 | -------------------------------------------------------------------------------- /components/mode-toggle.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { Button } from "@/components/ui/button"; 3 | import { 4 | DropdownMenu, 5 | DropdownMenuContent, 6 | DropdownMenuItem, 7 | DropdownMenuTrigger, 8 | } from "@/components/ui/dropdown-menu"; 9 | import { MoonIcon, SunIcon } from "@radix-ui/react-icons"; 10 | import { useTheme } from "next-themes"; 11 | import * as React from "react"; 12 | 13 | export function ModeToggle() { 14 | const { setTheme } = useTheme(); 15 | 16 | return ( 17 | 18 | 19 | 28 | 29 | 30 | setTheme("light")}> 31 | Light 32 | 33 | setTheme("dark")}> 34 | Dark 35 | 36 | setTheme("system")}> 37 | System 38 | 39 | 40 | 41 | ); 42 | } 43 | -------------------------------------------------------------------------------- /components/pre.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils"; 2 | import { Check, Copy } from "lucide-react"; 3 | import React from "react"; 4 | import { Button } from "./ui/button"; 5 | 6 | export default function Pre({ 7 | children, 8 | className, 9 | ...props 10 | }: React.HTMLAttributes) { 11 | const [copied, setCopied] = React.useState(false); 12 | const ref = React.useRef(null); 13 | 14 | React.useEffect(() => { 15 | let timer: ReturnType; 16 | if (copied) { 17 | timer = setTimeout(() => { 18 | setCopied(false); 19 | }, 2000); 20 | } 21 | return () => { 22 | clearTimeout(timer); 23 | }; 24 | }, [copied]); 25 | 26 | const onClick = () => { 27 | setCopied(true); 28 | const content = ref.current?.textContent; 29 | if (content) { 30 | navigator.clipboard.writeText(content); 31 | } 32 | }; 33 | 34 | return ( 35 |
36 | 46 |
54 |         {children}
55 |       
56 |
57 | ); 58 | } 59 | -------------------------------------------------------------------------------- /app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | :root { 7 | --background: 0 0% 100%; 8 | --foreground: 240 10% 3.9%; 9 | 10 | --card: 0 0% 100%; 11 | --card-foreground: 240 10% 3.9%; 12 | 13 | --popover: 0 0% 100%; 14 | --popover-foreground: 240 10% 3.9%; 15 | 16 | --primary: 240 5.9% 10%; 17 | --primary-foreground: 0 0% 98%; 18 | 19 | --secondary: 240 4.8% 95.9%; 20 | --secondary-foreground: 240 5.9% 10%; 21 | 22 | --muted: 240 4.8% 95.9%; 23 | --muted-foreground: 240 3.8% 46.1%; 24 | 25 | --accent: 240 4.8% 95.9%; 26 | --accent-foreground: 240 5.9% 10%; 27 | 28 | --destructive: 0 84.2% 60.2%; 29 | --destructive-foreground: 0 0% 98%; 30 | 31 | --border: 240 5.9% 90%; 32 | --input: 240 5.9% 90%; 33 | --ring: 240 10% 3.9%; 34 | 35 | --radius: 0.5rem; 36 | } 37 | 38 | .dark { 39 | --background: 240 10% 3.9%; 40 | --foreground: 0 0% 98%; 41 | 42 | --card: 240 10% 3.9%; 43 | --card-foreground: 0 0% 98%; 44 | 45 | --popover: 240 10% 3.9%; 46 | --popover-foreground: 0 0% 98%; 47 | 48 | --primary: 0 0% 98%; 49 | --primary-foreground: 240 5.9% 10%; 50 | 51 | --secondary: 240 3.7% 15.9%; 52 | --secondary-foreground: 0 0% 98%; 53 | 54 | --muted: 240 3.7% 15.9%; 55 | --muted-foreground: 240 5% 64.9%; 56 | 57 | --accent: 240 3.7% 15.9%; 58 | --accent-foreground: 0 0% 98%; 59 | 60 | --destructive: 0 62.8% 30.6%; 61 | --destructive-foreground: 0 0% 98%; 62 | 63 | --border: 240 3.7% 15.9%; 64 | --input: 240 3.7% 15.9%; 65 | --ring: 240 4.9% 83.9%; 66 | } 67 | } 68 | 69 | @layer base { 70 | * { 71 | @apply border-border; 72 | } 73 | body { 74 | @apply bg-background text-foreground; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /components/ui/country-select.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Select, 3 | SelectContent, 4 | SelectItem, 5 | SelectTrigger, 6 | SelectValue, 7 | } from "@/components/ui/select"; 8 | import { filterCountries } from "@/lib/helpers"; 9 | //@ts-ignore 10 | import countryRegionData from "country-region-data/dist/data-umd"; 11 | import { useEffect, useState } from "react"; 12 | 13 | export interface Region { 14 | name: string; 15 | shortCode: string; 16 | } 17 | 18 | export interface CountryRegion { 19 | countryName: string; 20 | countryShortCode: string; 21 | regions: Region[]; 22 | } 23 | 24 | interface CountrySelectProps { 25 | priorityOptions?: string[]; 26 | whitelist?: string[]; 27 | blacklist?: string[]; 28 | onChange?: (value: string) => void; 29 | className?: string; 30 | placeholder?: string; 31 | } 32 | 33 | function CountrySelect({ 34 | priorityOptions = [], 35 | whitelist = [], 36 | blacklist = [], 37 | onChange = () => {}, 38 | className, 39 | placeholder = "Country", 40 | }: CountrySelectProps) { 41 | const [countries, setCountries] = useState([]); 42 | 43 | useEffect(() => { 44 | setCountries( 45 | filterCountries(countryRegionData, priorityOptions, whitelist, blacklist), 46 | ); 47 | }, []); 48 | 49 | return ( 50 | 66 | ); 67 | } 68 | 69 | export default CountrySelect; 70 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /content/snippets/country-select.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | file: country-select.tsx 3 | order: 4 4 | --- 5 | 6 | ```tsx 7 | import { 8 | Select, 9 | SelectContent, 10 | SelectItem, 11 | SelectTrigger, 12 | SelectValue, 13 | } from "@/components/ui/select"; 14 | import { filterCountries } from "@/lib/helpers"; 15 | //@ts-ignore 16 | import countryRegionData from "country-region-data/dist/data-umd"; 17 | import { useEffect, useState } from "react"; 18 | 19 | export interface Region { 20 | name: string; 21 | shortCode: string; 22 | } 23 | 24 | export interface CountryRegion { 25 | countryName: string; 26 | countryShortCode: string; 27 | regions: Region[]; 28 | } 29 | 30 | interface CountrySelectProps { 31 | priorityOptions?: string[]; 32 | whitelist?: string[]; 33 | blacklist?: string[]; 34 | onChange?: (value: string) => void; 35 | className?: string; 36 | placeholder?: string; 37 | } 38 | 39 | function CountrySelect({ 40 | priorityOptions = [], 41 | whitelist = [], 42 | blacklist = [], 43 | onChange = () => {}, 44 | className, 45 | placeholder = "Country", 46 | }: CountrySelectProps) { 47 | const [countries, setCountries] = useState([]); 48 | 49 | useEffect(() => { 50 | setCountries( 51 | filterCountries(countryRegionData, priorityOptions, whitelist, blacklist), 52 | ); 53 | }, []); 54 | 55 | return ( 56 | 72 | ); 73 | } 74 | 75 | export default CountrySelect; 76 | ``` 77 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import { ThemeProvider } from "@/components/theme-provider"; 2 | import { Toaster } from "@/components/ui/toaster"; 3 | import { Analytics } from "@vercel/analytics/react"; 4 | import type { Metadata } from "next"; 5 | import { Inter } from "next/font/google"; 6 | import { siteConfig } from "../config/site"; 7 | import "./globals.css"; 8 | 9 | const inter = Inter({ subsets: ["latin"] }); 10 | 11 | export const metadata: Metadata = { 12 | title: "Shadcn Country / Region Select", 13 | description: `A country / region select component implementation of Shadcn's select component`, 14 | keywords: [ 15 | "shadcn", 16 | "country select", 17 | "region select", 18 | "country region select", 19 | "shadcn/ui", 20 | "shadcn country select", 21 | "country select component", 22 | "shadcn country select component", 23 | "select", 24 | "radix ui", 25 | "react country select", 26 | ], 27 | authors: [ 28 | { 29 | name: "Yassine Farroud", 30 | url: "https://resume.inext.dev", 31 | }, 32 | ], 33 | creator: "Yassine Farroud", 34 | openGraph: { 35 | type: "website", 36 | locale: "en_US", 37 | url: siteConfig.url, 38 | title: siteConfig.name, 39 | description: siteConfig.description, 40 | siteName: siteConfig.name, 41 | }, 42 | twitter: { 43 | card: "summary_large_image", 44 | title: siteConfig.name, 45 | description: siteConfig.description, 46 | creator: "@inext_devv", 47 | }, 48 | }; 49 | 50 | export default function RootLayout({ 51 | children, 52 | }: { 53 | children: React.ReactNode; 54 | }) { 55 | return ( 56 | 57 | 58 | 64 | {children} 65 | 66 | 67 | 68 | 69 | 70 | ); 71 | } 72 | -------------------------------------------------------------------------------- /components/ui/region-select.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Select, 3 | SelectContent, 4 | SelectItem, 5 | SelectTrigger, 6 | SelectValue, 7 | } from "@/components/ui/select"; 8 | import { filterRegions } from "@/lib/helpers"; 9 | 10 | //@ts-ignore 11 | import countryRegionData from "country-region-data/dist/data-umd"; 12 | import { useEffect, useState } from "react"; 13 | 14 | export interface Region { 15 | name: string; 16 | shortCode: string; 17 | } 18 | 19 | export interface CountryRegion { 20 | countryName: string; 21 | countryShortCode: string; 22 | regions: Region[]; 23 | } 24 | 25 | interface RegionSelectProps { 26 | countryCode: string; 27 | priorityOptions?: string[]; 28 | whitelist?: string[]; 29 | blacklist?: string[]; 30 | onChange?: (value: string) => void; 31 | className?: string; 32 | placeholder?: string; 33 | } 34 | 35 | function RegionSelect({ 36 | countryCode, 37 | priorityOptions = [], 38 | whitelist = [], 39 | blacklist = [], 40 | onChange = () => {}, 41 | className, 42 | placeholder = "Region", 43 | }: RegionSelectProps) { 44 | const [regions, setRegions] = useState([]); 45 | 46 | useEffect(() => { 47 | const regions = countryRegionData.find( 48 | (country: CountryRegion) => country.countryShortCode === countryCode, 49 | ); 50 | 51 | if (regions) { 52 | setRegions( 53 | filterRegions(regions.regions, priorityOptions, whitelist, blacklist), 54 | ); 55 | } 56 | }, [countryCode]); 57 | 58 | return ( 59 | 75 | ); 76 | } 77 | 78 | export default RegionSelect; 79 | -------------------------------------------------------------------------------- /components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import { Slot } from "@radix-ui/react-slot"; 2 | import { type VariantProps, cva } from "class-variance-authority"; 3 | import * as React from "react"; 4 | 5 | import { cn } from "@/lib/utils"; 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", 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-background hover:bg-accent 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-4 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 | -------------------------------------------------------------------------------- /components/copy-button.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils"; 2 | import { AnimatePresence, MotionConfig, motion } from "framer-motion"; 3 | import { Check, Copy } from "lucide-react"; 4 | import { useCallback, useState } from "react"; 5 | import { Button } from "./ui/button"; 6 | 7 | export default function CopyButton({ 8 | value, 9 | }: { 10 | value: string; 11 | copyable?: boolean; 12 | }) { 13 | const [copying, setCopying] = useState(0); 14 | 15 | const onCopy = useCallback(async () => { 16 | try { 17 | await navigator.clipboard.writeText(value); 18 | setCopying((c) => c + 1); 19 | setTimeout(() => { 20 | setCopying((c) => c - 1); 21 | }, 2000); 22 | } catch (err) { 23 | console.error("Failed to copy text: ", err); 24 | } 25 | }, [value]); 26 | 27 | const variants = { 28 | visible: { opacity: 1, scale: 1 }, 29 | hidden: { opacity: 0, scale: 0.5 }, 30 | }; 31 | 32 | return ( 33 | 67 | ); 68 | } 69 | -------------------------------------------------------------------------------- /content/snippets/region-select.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | file: region-select.tsx 3 | order: 4 4 | --- 5 | 6 | ```tsx 7 | import { 8 | Select, 9 | SelectContent, 10 | SelectItem, 11 | SelectTrigger, 12 | SelectValue, 13 | } from "@/components/ui/select"; 14 | import { filterRegions } from "@/lib/helpers"; 15 | 16 | //@ts-ignore 17 | import countryRegionData from "country-region-data/dist/data-umd"; 18 | import { useEffect, useState } from "react"; 19 | 20 | export interface Region { 21 | name: string; 22 | shortCode: string; 23 | } 24 | 25 | export interface CountryRegion { 26 | countryName: string; 27 | countryShortCode: string; 28 | regions: Region[]; 29 | } 30 | 31 | interface RegionSelectProps { 32 | countryCode: string; 33 | priorityOptions?: string[]; 34 | whitelist?: string[]; 35 | blacklist?: string[]; 36 | onChange?: (value: string) => void; 37 | className?: string; 38 | placeholder?: string; 39 | } 40 | 41 | function RegionSelect({ 42 | countryCode, 43 | priorityOptions = [], 44 | whitelist = [], 45 | blacklist = [], 46 | onChange = () => {}, 47 | className, 48 | placeholder = "Region", 49 | }: RegionSelectProps) { 50 | const [regions, setRegions] = useState([]); 51 | 52 | useEffect(() => { 53 | const regions = countryRegionData.find( 54 | (country: CountryRegion) => country.countryShortCode === countryCode, 55 | ); 56 | 57 | if (regions) { 58 | setRegions( 59 | filterRegions(regions.regions, priorityOptions, whitelist, blacklist), 60 | ); 61 | } 62 | }, [countryCode]); 63 | 64 | return ( 65 | 81 | ); 82 | } 83 | 84 | export default RegionSelect; 85 | ``` 86 | -------------------------------------------------------------------------------- /components/ui/tabs.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as TabsPrimitive from "@radix-ui/react-tabs"; 4 | import * as React from "react"; 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, TabsContent, TabsList, TabsTrigger }; 56 | -------------------------------------------------------------------------------- /components/ui/accordion.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as AccordionPrimitive from "@radix-ui/react-accordion"; 4 | import { ChevronDown } from "lucide-react"; 5 | import * as React from "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, AccordionContent, AccordionItem, AccordionTrigger }; 59 | -------------------------------------------------------------------------------- /app/(home)/sections/hero.tsx: -------------------------------------------------------------------------------- 1 | import { buttonVariants } from "@/components/ui/button"; 2 | import { siteConfig } from "@/config/site"; 3 | import Link from "next/link"; 4 | import CountrySelect from "@/components/ui/country-select"; 5 | import RegionSelect from "@/components/ui/region-select"; 6 | import { useState } from "react"; 7 | 8 | export default function Hero() { 9 | const [countryCode, setCountryCode] = useState(""); 10 | 11 | return ( 12 | <> 13 |
14 |
15 |

16 | Shadcn Country / Region Select 17 |

18 |

19 | An implementation of a Country / Region Select component built on 20 | top of Shadcn UI's input component. 21 |

22 |
23 | 30 | Try it out 31 | 32 | 39 | Github 40 | 41 |
42 |
43 | 44 |
45 |
46 |
47 | setCountryCode(value)} 50 | /> 51 | 55 |
56 |
57 |
58 |
59 | 60 | ); 61 | } 62 | -------------------------------------------------------------------------------- /app/(home)/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import Hero from "@/app/(home)/sections/hero"; 3 | import Setup from "@/app/(home)/sections/setup"; 4 | import { ModeToggle } from "@/components/mode-toggle"; 5 | import { siteConfig } from "@/config/site"; 6 | import Variants from "./sections/variants"; 7 | 8 | export default function Home() { 9 | return ( 10 | <> 11 |
12 | 13 |
14 | 15 | 16 | 17 |
18 | 38 |
39 | 67 | 68 | ); 69 | } 70 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "shadcn-phone-input", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "contentlayer build && next build", 8 | "start": "next start", 9 | "lint": "next lint", 10 | "typecheck": "tsc --noEmit", 11 | "lint:fix": "next lint --fix", 12 | "format:write": "prettier --write \"**/*.{ts,tsx,mdx}\" --cache", 13 | "format:check": "prettier --check \"**/*.{ts,tsx,mdx}\" --cache" 14 | }, 15 | "dependencies": { 16 | "@hookform/resolvers": "^3.3.4", 17 | "@radix-ui/react-accordion": "^1.1.2", 18 | "@radix-ui/react-dialog": "1.0.5", 19 | "@radix-ui/react-dropdown-menu": "^2.0.6", 20 | "@radix-ui/react-icons": "^1.3.0", 21 | "@radix-ui/react-label": "^2.0.2", 22 | "@radix-ui/react-popover": "^1.0.7", 23 | "@radix-ui/react-scroll-area": "^1.0.5", 24 | "@radix-ui/react-select": "^2.0.0", 25 | "@radix-ui/react-slot": "^1.0.2", 26 | "@radix-ui/react-tabs": "^1.0.4", 27 | "@radix-ui/react-toast": "^1.1.5", 28 | "@vercel/analytics": "^1.2.2", 29 | "class-variance-authority": "^0.7.0", 30 | "classnames": "^2.5.1", 31 | "clsx": "^2.1.0", 32 | "cmdk": "^0.2.1", 33 | "contentlayer": "^0.3.4", 34 | "country-region-data": "^3.0.0", 35 | "framer-motion": "^10.16.16", 36 | "lucide-react": "^0.368.0", 37 | "next": "14.2.1", 38 | "next-contentlayer": "^0.3.4", 39 | "next-themes": "^0.3.0", 40 | "prop-types": "^15.8.1", 41 | "react": "18.2.0", 42 | "react-dom": "18.2.0", 43 | "react-hook-form": "^7.51.3", 44 | "react-phone-number-input": "^3.3.12", 45 | "tailwind-merge": "^2.2.2", 46 | "zod": "^3.22.4" 47 | }, 48 | "devDependencies": { 49 | "@types/eslint": "^8.56.7", 50 | "@types/node": "20.12.7", 51 | "@types/react": "18.2.78", 52 | "@types/react-dom": "18.2.25", 53 | "@typescript-eslint/eslint-plugin": "^7.6.0", 54 | "@typescript-eslint/parser": "^7.6.0", 55 | "autoprefixer": "10.4.19", 56 | "eslint": "^8.41.0", 57 | "eslint-config-next": "^14.1.4", 58 | "eslint-config-prettier": "^9.1.0", 59 | "eslint-plugin-tailwindcss": "^3.15.1", 60 | "postcss": "8.4.38", 61 | "prettier": "^3.2.5", 62 | "prettier-plugin-tailwindcss": "^0.5.13", 63 | "rehype": "^13.0.1", 64 | "rehype-pretty-code": "^0.13.1", 65 | "shiki": "^1.3.0", 66 | "tailwindcss": "3.4.3", 67 | "tailwindcss-animate": "^1.0.7", 68 | "typescript": "^5.2.0", 69 | "unist-builder": "4.0.0", 70 | "unist-util-visit": "^5.0.0" 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /content/snippets/helpers.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | file: helpers.tsx 3 | order: 5 4 | --- 5 | 6 | ```tsx 7 | export interface Region { 8 | name: string; 9 | shortCode: string; 10 | } 11 | 12 | export interface CountryRegion { 13 | countryName: string; 14 | countryShortCode: string; 15 | regions: Region[]; 16 | } 17 | 18 | export const filterCountries = ( 19 | countries: CountryRegion[], 20 | priorityCountries: string[], 21 | whitelist: string[], 22 | blacklist: string[], 23 | ): CountryRegion[] => { 24 | let countriesListedFirst: any[] = []; 25 | let filteredCountries = countries; 26 | 27 | if (whitelist.length > 0) { 28 | filteredCountries = countries.filter( 29 | ({ countryShortCode }) => whitelist.indexOf(countryShortCode) > -1, 30 | ); 31 | } else if (blacklist.length > 0) { 32 | filteredCountries = countries.filter( 33 | ({ countryShortCode }) => blacklist.indexOf(countryShortCode) === -1, 34 | ); 35 | } 36 | 37 | if (priorityCountries.length > 0) { 38 | // ensure the countries are added in the order in which they are specified by the user 39 | priorityCountries.forEach((slug) => { 40 | const result = filteredCountries.find( 41 | ({ countryShortCode }) => countryShortCode === slug, 42 | ); 43 | if (result) { 44 | countriesListedFirst.push(result); 45 | } 46 | }); 47 | 48 | filteredCountries = filteredCountries.filter( 49 | ({ countryShortCode }) => 50 | priorityCountries.indexOf(countryShortCode) === -1, 51 | ); 52 | } 53 | 54 | return countriesListedFirst.length 55 | ? [...countriesListedFirst, ...filteredCountries] 56 | : filteredCountries; 57 | }; 58 | 59 | export const filterRegions = ( 60 | regions: Region[], 61 | priorityRegions: string[], 62 | whitelist: string[], 63 | blacklist: string[], 64 | ) => { 65 | let regionsListedFirst: any[] = []; 66 | let filteredRegions = regions; 67 | 68 | if (whitelist.length > 0) { 69 | filteredRegions = regions.filter( 70 | ({ shortCode }) => whitelist.indexOf(shortCode) > -1, 71 | ); 72 | } else if (blacklist.length > 0) { 73 | filteredRegions = regions.filter( 74 | ({ shortCode }) => blacklist.indexOf(shortCode) === -1, 75 | ); 76 | } 77 | 78 | if (priorityRegions.length > 0) { 79 | // ensure the Regions are added in the order in which they are specified by the user 80 | priorityRegions.forEach((slug) => { 81 | const result = filteredRegions.find( 82 | ({ shortCode }) => shortCode === slug, 83 | ); 84 | if (result) { 85 | regionsListedFirst.push(result); 86 | } 87 | }); 88 | 89 | filteredRegions = filteredRegions.filter( 90 | ({ shortCode }) => priorityRegions.indexOf(shortCode) === -1, 91 | ); 92 | } 93 | 94 | return regionsListedFirst.length 95 | ? [...regionsListedFirst, ...filteredRegions] 96 | : filteredRegions; 97 | }; 98 | ``` -------------------------------------------------------------------------------- /lib/helpers.ts: -------------------------------------------------------------------------------- 1 | // reduces the subset of countries depending on whether the user specified a white/blacklist, and lists priority 2 | // countries first 3 | 4 | export interface Region { 5 | name: string; 6 | shortCode: string; 7 | } 8 | 9 | export interface CountryRegion { 10 | countryName: string; 11 | countryShortCode: string; 12 | regions: Region[]; 13 | } 14 | 15 | export const filterCountries = ( 16 | countries: CountryRegion[], 17 | priorityCountries: string[], 18 | whitelist: string[], 19 | blacklist: string[], 20 | ): CountryRegion[] => { 21 | let countriesListedFirst: any[] = []; 22 | let filteredCountries = countries; 23 | 24 | if (whitelist.length > 0) { 25 | filteredCountries = countries.filter( 26 | ({ countryShortCode }) => whitelist.indexOf(countryShortCode) > -1, 27 | ); 28 | } else if (blacklist.length > 0) { 29 | filteredCountries = countries.filter( 30 | ({ countryShortCode }) => blacklist.indexOf(countryShortCode) === -1, 31 | ); 32 | } 33 | 34 | if (priorityCountries.length > 0) { 35 | // ensure the countries are added in the order in which they are specified by the user 36 | priorityCountries.forEach((slug) => { 37 | const result = filteredCountries.find( 38 | ({ countryShortCode }) => countryShortCode === slug, 39 | ); 40 | if (result) { 41 | countriesListedFirst.push(result); 42 | } 43 | }); 44 | 45 | filteredCountries = filteredCountries.filter( 46 | ({ countryShortCode }) => 47 | priorityCountries.indexOf(countryShortCode) === -1, 48 | ); 49 | } 50 | 51 | return countriesListedFirst.length 52 | ? [...countriesListedFirst, ...filteredCountries] 53 | : filteredCountries; 54 | }; 55 | 56 | export const filterRegions = ( 57 | regions: Region[], 58 | priorityRegions: string[], 59 | whitelist: string[], 60 | blacklist: string[], 61 | ) => { 62 | let regionsListedFirst: any[] = []; 63 | let filteredRegions = regions; 64 | 65 | if (whitelist.length > 0) { 66 | filteredRegions = regions.filter( 67 | ({ shortCode }) => whitelist.indexOf(shortCode) > -1, 68 | ); 69 | } else if (blacklist.length > 0) { 70 | filteredRegions = regions.filter( 71 | ({ shortCode }) => blacklist.indexOf(shortCode) === -1, 72 | ); 73 | } 74 | 75 | if (priorityRegions.length > 0) { 76 | // ensure the Regions are added in the order in which they are specified by the user 77 | priorityRegions.forEach((slug) => { 78 | const result = filteredRegions.find( 79 | ({ shortCode }) => shortCode === slug, 80 | ); 81 | if (result) { 82 | regionsListedFirst.push(result); 83 | } 84 | }); 85 | 86 | filteredRegions = filteredRegions.filter( 87 | ({ shortCode }) => priorityRegions.indexOf(shortCode) === -1, 88 | ); 89 | } 90 | 91 | return regionsListedFirst.length 92 | ? [...regionsListedFirst, ...filteredRegions] 93 | : filteredRegions; 94 | }; 95 | -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | darkMode: ["class"], 4 | content: [ 5 | "./pages/**/*.{ts,tsx}", 6 | "./components/**/*.{ts,tsx}", 7 | "./app/**/*.{ts,tsx}", 8 | "./src/**/*.{ts,tsx}", 9 | ], 10 | theme: { 11 | container: { 12 | center: true, 13 | padding: "2rem", 14 | screens: { 15 | "2xl": "1400px", 16 | }, 17 | }, 18 | extend: { 19 | colors: { 20 | border: "hsl(var(--border))", 21 | input: "hsl(var(--input))", 22 | ring: "hsl(var(--ring))", 23 | background: "hsl(var(--background))", 24 | foreground: "hsl(var(--foreground))", 25 | primary: { 26 | DEFAULT: "hsl(var(--primary))", 27 | foreground: "hsl(var(--primary-foreground))", 28 | }, 29 | secondary: { 30 | DEFAULT: "hsl(var(--secondary))", 31 | foreground: "hsl(var(--secondary-foreground))", 32 | }, 33 | destructive: { 34 | DEFAULT: "hsl(var(--destructive))", 35 | foreground: "hsl(var(--destructive-foreground))", 36 | }, 37 | muted: { 38 | DEFAULT: "hsl(var(--muted))", 39 | foreground: "hsl(var(--muted-foreground))", 40 | }, 41 | accent: { 42 | DEFAULT: "hsl(var(--accent))", 43 | foreground: "hsl(var(--accent-foreground))", 44 | }, 45 | popover: { 46 | DEFAULT: "hsl(var(--popover))", 47 | foreground: "hsl(var(--popover-foreground))", 48 | }, 49 | card: { 50 | DEFAULT: "hsl(var(--card))", 51 | foreground: "hsl(var(--card-foreground))", 52 | }, 53 | }, 54 | borderRadius: { 55 | lg: "var(--radius)", 56 | md: "calc(var(--radius) - 2px)", 57 | sm: "calc(var(--radius) - 4px)", 58 | }, 59 | keyframes: { 60 | "accordion-down": { 61 | from: { height: 0 }, 62 | to: { height: "var(--radix-accordion-content-height)" }, 63 | }, 64 | "accordion-up": { 65 | from: { height: "var(--radix-accordion-content-height)" }, 66 | to: { height: 0 }, 67 | }, 68 | fadeIn: { 69 | "0%": { opacity: 0 }, 70 | "100%": { opacity: 1 }, 71 | }, 72 | slideIn: { 73 | "0%": { transform: "translateX(-100%)" }, 74 | "100%": { transform: "translateX(0)" }, 75 | }, 76 | bounce: { 77 | "0%, 100%": { transform: "translateY(0)" }, 78 | "50%": { transform: "translateY(-5px)" }, 79 | }, 80 | }, 81 | animation: { 82 | "accordion-down": "accordion-down 0.2s ease-out", 83 | "accordion-up": "accordion-up 0.2s ease-out", 84 | fadeIn: "fadeIn 0.3s ease-in-out", 85 | slideIn: "slideIn 0.3s ease-in-out", 86 | bounce: "bounce 0.6s ease-in-out", 87 | }, 88 | }, 89 | }, 90 | plugins: [require("tailwindcss-animate")], 91 | }; 92 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Shadcn country region select 2 | 3 | Shadcn Country & Region Select is a select input component built as part of the Shadcn design system. It offers a blend of customization and out-of-the-box styling, adhering to Shadcn's sleek and modern design principles. 4 | 5 | ## Why 6 | 7 | I needed a country - region select component for a project. I looked around for any country - region select components that used Shadcn's design system, but I couldn't find any. So, I decided to make one myself. I hope you find it useful! 8 | 9 | > [!WARNING] 10 | > Before you dive in, just double-check that you're using installing the shadcn correctly! 11 | 12 | ## Usage 13 | 14 | ```tsx 15 | import { useState } from "react" 16 | import CountrySelect from "@/components/ui/country-select"; 17 | import RegionSelect from "@/components/ui/region-select"; 18 | 19 | function CountryRegion() { 20 | const [countryCode, setCountryCode] = useState(""); 21 | 22 | return ( 23 |
24 | setCountryCode(value)} 27 | priorityOptions={["US"]} 28 | placeholder="Country" 29 | > 30 | console.log(value)} 32 | className="w-[180px] mt-2" 33 | countryCode={countryCode} 34 | > 35 |
36 | ``` 37 | 38 | #### CountrySelect properties 39 | 40 | | Prop | Type | Description | 41 | | --------------- | --------------------- | ----------------------------------------------------- | 42 | | className | string | accept a class string | 43 | | onChange | func | callback function fired when the select value changed | 44 | | placeholder | string | placeholder value, default "Country" | 45 | | priorityOptions | string[] | Array of countries prioritized in the select list | 46 | | whiteList | string[] | Array of allowed countries | 47 | | blackList | string[] | Array of banned countries | 48 | 49 | #### RegionSelect properties 50 | 51 | | Prop | Type | Description | 52 | | --------------- | --------------------- | ----------------------------------------------------- | 53 | | className | string | accept a class string | 54 | | onChange | func | callback function fired when the select value changed | 55 | | placeholder | string | placeholder value, default "Region" | 56 | | priorityOptions | string[] | Array of regions prioritized in the select list | 57 | | whiteList | string[] | Array of allowed regions | 58 | | blackList | string[] | Array of banned regions | 59 | -------------------------------------------------------------------------------- /components/ui/table.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import { cn } from "@/lib/utils"; 4 | 5 | const Table = React.forwardRef< 6 | HTMLTableElement, 7 | React.HTMLAttributes 8 | >(({ className, ...props }, ref) => ( 9 |
10 | 15 | 16 | )); 17 | Table.displayName = "Table"; 18 | 19 | const TableHeader = React.forwardRef< 20 | HTMLTableSectionElement, 21 | React.HTMLAttributes 22 | >(({ className, ...props }, ref) => ( 23 | 24 | )); 25 | TableHeader.displayName = "TableHeader"; 26 | 27 | const TableBody = React.forwardRef< 28 | HTMLTableSectionElement, 29 | React.HTMLAttributes 30 | >(({ className, ...props }, ref) => ( 31 | 36 | )); 37 | TableBody.displayName = "TableBody"; 38 | 39 | const TableFooter = React.forwardRef< 40 | HTMLTableSectionElement, 41 | React.HTMLAttributes 42 | >(({ className, ...props }, ref) => ( 43 | 48 | )); 49 | TableFooter.displayName = "TableFooter"; 50 | 51 | const TableRow = React.forwardRef< 52 | HTMLTableRowElement, 53 | React.HTMLAttributes 54 | >(({ className, ...props }, ref) => ( 55 | 63 | )); 64 | TableRow.displayName = "TableRow"; 65 | 66 | const TableHead = React.forwardRef< 67 | HTMLTableCellElement, 68 | React.ThHTMLAttributes 69 | >(({ className, ...props }, ref) => ( 70 |
78 | )); 79 | TableHead.displayName = "TableHead"; 80 | 81 | const TableCell = React.forwardRef< 82 | HTMLTableCellElement, 83 | React.TdHTMLAttributes 84 | >(({ className, ...props }, ref) => ( 85 | 90 | )); 91 | TableCell.displayName = "TableCell"; 92 | 93 | const TableCaption = React.forwardRef< 94 | HTMLTableCaptionElement, 95 | React.HTMLAttributes 96 | >(({ className, ...props }, ref) => ( 97 |
102 | )); 103 | TableCaption.displayName = "TableCaption"; 104 | 105 | export { 106 | Table, 107 | TableHeader, 108 | TableBody, 109 | TableFooter, 110 | TableHead, 111 | TableRow, 112 | TableCell, 113 | TableCaption, 114 | }; 115 | -------------------------------------------------------------------------------- /app/(home)/sections/variants.tsx: -------------------------------------------------------------------------------- 1 | import CountrySelect from "@/components/ui/country-select"; 2 | import { PhoneInput } from "@/components/ui/phone-input"; 3 | import { useState } from "react"; 4 | import { 5 | Country, 6 | formatPhoneNumber, 7 | formatPhoneNumberIntl, 8 | getCountryCallingCode, 9 | } from "react-phone-number-input"; 10 | import tr from "react-phone-number-input/locale/tr"; 11 | 12 | export default function Variants() { 13 | const [country, setCountry] = useState(); 14 | const [phoneNumber, setPhoneNumber] = useState(""); 15 | 16 | return ( 17 |
18 |

19 | Properties 20 |

21 |
22 |

23 | With country / region 24 |

25 |

26 | This properties are valid with the two component country and region. 27 |

28 |
29 |

30 | whiteList 31 |

32 |

33 | Specify which countries should appear. This just shows the United 34 | States and China. 35 |

36 |
37 | 38 |
39 |
40 |
41 |

42 | blackList 43 |

44 |

45 | Specify which countries should NOT appear. This will omit 46 | Afhganistan, Aland Islands and Albania. 47 |

48 |
49 | 53 |
54 |
55 |
56 |

57 | priorityOptions 58 |

59 |

60 | Make Canada, United States and the Brazil appear first in the 61 | dropdown list. 62 |

63 |
64 | 68 |
69 |
70 |
71 |
72 | ); 73 | } 74 | -------------------------------------------------------------------------------- /app/(home)/sections/setup.tsx: -------------------------------------------------------------------------------- 1 | import CodeBlock from "@/components/code-block"; 2 | import { Snippet } from "@/components/snippet"; 3 | import { 4 | Accordion, 5 | AccordionContent, 6 | AccordionItem, 7 | AccordionTrigger, 8 | } from "@/components/ui/accordion"; 9 | import { Snippet as SnippetType, allSnippets } from "contentlayer/generated"; 10 | 11 | const snippets: SnippetType[] = allSnippets.sort((a, b) => a.order - b.order); 12 | 13 | export default function Setup() { 14 | return ( 15 |
16 |

17 | Setup 18 |

19 |
20 |

21 | Install Shadcn via CLI 22 |

23 |

24 | Run the{" "} 25 | 26 | shadcn-ui 27 | {" "} 28 | init command to setup your project: 29 |

30 | 31 |
32 |
33 |

34 | Install necessary Shadcn components: 35 |

36 |

37 | Run the{" "} 38 | 39 | shadcn-ui 40 | {" "} 41 | add command to add the necessary shadcn components to your project: 42 |

43 |
44 | 48 |
49 |
50 |
51 |

52 | Install necessary Country Region Data package: 53 |

54 | 55 |
56 |
57 |

58 | To use the country/region select component: 59 |

60 | {/* */} 70 |
71 |

72 | Snippets 73 |

74 | 75 | {snippets.map((snippet) => ( 76 | 77 | 78 | {snippet.file} 79 | 80 | 81 | 82 | 83 | 84 | ))} 85 | 86 |
87 |
88 |
89 | ); 90 | } 91 | -------------------------------------------------------------------------------- /components/ui/dialog.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as DialogPrimitive from "@radix-ui/react-dialog"; 4 | import { X } from "lucide-react"; 5 | import * as React from "react"; 6 | 7 | import { cn } from "@/lib/utils"; 8 | 9 | const Dialog = DialogPrimitive.Root; 10 | 11 | const DialogTrigger = DialogPrimitive.Trigger; 12 | 13 | const DialogPortal = ({ ...props }: DialogPrimitive.DialogPortalProps) => ( 14 | 15 | ); 16 | DialogPortal.displayName = DialogPrimitive.Portal.displayName; 17 | 18 | const DialogOverlay = React.forwardRef< 19 | React.ElementRef, 20 | React.ComponentPropsWithoutRef 21 | >(({ className, ...props }, ref) => ( 22 | 30 | )); 31 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName; 32 | 33 | const DialogContent = React.forwardRef< 34 | React.ElementRef, 35 | React.ComponentPropsWithoutRef 36 | >(({ className, children, ...props }, ref) => ( 37 | 38 | 39 | 47 | {children} 48 | 49 | 50 | Close 51 | 52 | 53 | 54 | )); 55 | DialogContent.displayName = DialogPrimitive.Content.displayName; 56 | 57 | const DialogHeader = ({ 58 | className, 59 | ...props 60 | }: React.HTMLAttributes) => ( 61 |
68 | ); 69 | DialogHeader.displayName = "DialogHeader"; 70 | 71 | const DialogFooter = ({ 72 | className, 73 | ...props 74 | }: React.HTMLAttributes) => ( 75 |
82 | ); 83 | DialogFooter.displayName = "DialogFooter"; 84 | 85 | const DialogTitle = React.forwardRef< 86 | React.ElementRef, 87 | React.ComponentPropsWithoutRef 88 | >(({ className, ...props }, ref) => ( 89 | 97 | )); 98 | DialogTitle.displayName = DialogPrimitive.Title.displayName; 99 | 100 | const DialogDescription = React.forwardRef< 101 | React.ElementRef, 102 | React.ComponentPropsWithoutRef 103 | >(({ className, ...props }, ref) => ( 104 | 109 | )); 110 | DialogDescription.displayName = DialogPrimitive.Description.displayName; 111 | 112 | export { 113 | Dialog, 114 | DialogContent, 115 | DialogDescription, 116 | DialogFooter, 117 | DialogHeader, 118 | DialogTitle, 119 | DialogTrigger, 120 | }; 121 | -------------------------------------------------------------------------------- /components/ui/use-toast.ts: -------------------------------------------------------------------------------- 1 | // Inspired by react-hot-toast library 2 | import * as React from "react"; 3 | 4 | import type { ToastActionElement, ToastProps } from "@/components/ui/toast"; 5 | 6 | const TOAST_LIMIT = 1; 7 | const TOAST_REMOVE_DELAY = 1000000; 8 | 9 | type ToasterToast = ToastProps & { 10 | id: string; 11 | title?: React.ReactNode; 12 | description?: React.ReactNode; 13 | action?: ToastActionElement; 14 | }; 15 | 16 | const actionTypes = { 17 | ADD_TOAST: "ADD_TOAST", 18 | UPDATE_TOAST: "UPDATE_TOAST", 19 | DISMISS_TOAST: "DISMISS_TOAST", 20 | REMOVE_TOAST: "REMOVE_TOAST", 21 | } as const; 22 | 23 | let count = 0; 24 | 25 | function genId() { 26 | count = (count + 1) % Number.MAX_VALUE; 27 | return count.toString(); 28 | } 29 | 30 | type ActionType = typeof actionTypes; 31 | 32 | type Action = 33 | | { 34 | type: ActionType["ADD_TOAST"]; 35 | toast: ToasterToast; 36 | } 37 | | { 38 | type: ActionType["UPDATE_TOAST"]; 39 | toast: Partial; 40 | } 41 | | { 42 | type: ActionType["DISMISS_TOAST"]; 43 | toastId?: ToasterToast["id"]; 44 | } 45 | | { 46 | type: ActionType["REMOVE_TOAST"]; 47 | toastId?: ToasterToast["id"]; 48 | }; 49 | 50 | interface State { 51 | toasts: ToasterToast[]; 52 | } 53 | 54 | const toastTimeouts = new Map>(); 55 | 56 | const addToRemoveQueue = (toastId: string) => { 57 | if (toastTimeouts.has(toastId)) { 58 | return; 59 | } 60 | 61 | const timeout = setTimeout(() => { 62 | toastTimeouts.delete(toastId); 63 | dispatch({ 64 | type: "REMOVE_TOAST", 65 | toastId: toastId, 66 | }); 67 | }, TOAST_REMOVE_DELAY); 68 | 69 | toastTimeouts.set(toastId, timeout); 70 | }; 71 | 72 | export const reducer = (state: State, action: Action): State => { 73 | switch (action.type) { 74 | case "ADD_TOAST": 75 | return { 76 | ...state, 77 | toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT), 78 | }; 79 | 80 | case "UPDATE_TOAST": 81 | return { 82 | ...state, 83 | toasts: state.toasts.map((t) => 84 | t.id === action.toast.id ? { ...t, ...action.toast } : t, 85 | ), 86 | }; 87 | 88 | case "DISMISS_TOAST": { 89 | const { toastId } = action; 90 | 91 | // ! Side effects ! - This could be extracted into a dismissToast() action, 92 | // but I'll keep it here for simplicity 93 | if (toastId) { 94 | addToRemoveQueue(toastId); 95 | } else { 96 | state.toasts.forEach((toast) => { 97 | addToRemoveQueue(toast.id); 98 | }); 99 | } 100 | 101 | return { 102 | ...state, 103 | toasts: state.toasts.map((t) => 104 | t.id === toastId || toastId === undefined 105 | ? { 106 | ...t, 107 | open: false, 108 | } 109 | : t, 110 | ), 111 | }; 112 | } 113 | case "REMOVE_TOAST": 114 | if (action.toastId === undefined) { 115 | return { 116 | ...state, 117 | toasts: [], 118 | }; 119 | } 120 | return { 121 | ...state, 122 | toasts: state.toasts.filter((t) => t.id !== action.toastId), 123 | }; 124 | } 125 | }; 126 | 127 | const listeners: Array<(state: State) => void> = []; 128 | 129 | let memoryState: State = { toasts: [] }; 130 | 131 | function dispatch(action: Action) { 132 | memoryState = reducer(memoryState, action); 133 | listeners.forEach((listener) => { 134 | listener(memoryState); 135 | }); 136 | } 137 | 138 | type Toast = Omit; 139 | 140 | function toast({ ...props }: Toast) { 141 | const id = genId(); 142 | 143 | const update = (props: ToasterToast) => 144 | dispatch({ 145 | type: "UPDATE_TOAST", 146 | toast: { ...props, id }, 147 | }); 148 | const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id }); 149 | 150 | dispatch({ 151 | type: "ADD_TOAST", 152 | toast: { 153 | ...props, 154 | id, 155 | open: true, 156 | onOpenChange: (open) => { 157 | if (!open) dismiss(); 158 | }, 159 | }, 160 | }); 161 | 162 | return { 163 | id: id, 164 | dismiss, 165 | update, 166 | }; 167 | } 168 | 169 | function useToast() { 170 | const [state, setState] = React.useState(memoryState); 171 | 172 | React.useEffect(() => { 173 | listeners.push(setState); 174 | return () => { 175 | const index = listeners.indexOf(setState); 176 | if (index > -1) { 177 | listeners.splice(index, 1); 178 | } 179 | }; 180 | }, [state]); 181 | 182 | return { 183 | ...state, 184 | toast, 185 | dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }), 186 | }; 187 | } 188 | 189 | export { useToast, toast }; 190 | -------------------------------------------------------------------------------- /components/ui/form.tsx: -------------------------------------------------------------------------------- 1 | import * as LabelPrimitive from "@radix-ui/react-label"; 2 | import { Slot } from "@radix-ui/react-slot"; 3 | import * as React from "react"; 4 | import { 5 | Controller, 6 | ControllerProps, 7 | FieldPath, 8 | FieldValues, 9 | FormProvider, 10 | useFormContext, 11 | } from "react-hook-form"; 12 | 13 | import { Label } from "@/components/ui/label"; 14 | import { cn } from "@/lib/utils"; 15 | 16 | const Form = FormProvider; 17 | 18 | type FormFieldContextValue< 19 | TFieldValues extends FieldValues = FieldValues, 20 | TName extends FieldPath = FieldPath, 21 | > = { 22 | name: TName; 23 | }; 24 | 25 | const FormFieldContext = React.createContext( 26 | {} as FormFieldContextValue, 27 | ); 28 | 29 | const FormField = < 30 | TFieldValues extends FieldValues = FieldValues, 31 | TName extends FieldPath = FieldPath, 32 | >({ 33 | ...props 34 | }: ControllerProps) => { 35 | return ( 36 | 37 | 38 | 39 | ); 40 | }; 41 | 42 | const useFormField = () => { 43 | const fieldContext = React.useContext(FormFieldContext); 44 | const itemContext = React.useContext(FormItemContext); 45 | const { getFieldState, formState } = useFormContext(); 46 | 47 | const fieldState = getFieldState(fieldContext.name, formState); 48 | 49 | if (!fieldContext) { 50 | throw new Error("useFormField should be used within "); 51 | } 52 | 53 | const { id } = itemContext; 54 | 55 | return { 56 | id, 57 | name: fieldContext.name, 58 | formItemId: `${id}-form-item`, 59 | formDescriptionId: `${id}-form-item-description`, 60 | formMessageId: `${id}-form-item-message`, 61 | ...fieldState, 62 | }; 63 | }; 64 | 65 | type FormItemContextValue = { 66 | id: string; 67 | }; 68 | 69 | const FormItemContext = React.createContext( 70 | {} as FormItemContextValue, 71 | ); 72 | 73 | const FormItem = React.forwardRef< 74 | HTMLDivElement, 75 | React.HTMLAttributes 76 | >(({ className, ...props }, ref) => { 77 | const id = React.useId(); 78 | 79 | return ( 80 | 81 |
82 | 83 | ); 84 | }); 85 | FormItem.displayName = "FormItem"; 86 | 87 | const FormLabel = React.forwardRef< 88 | React.ElementRef, 89 | React.ComponentPropsWithoutRef 90 | >(({ className, ...props }, ref) => { 91 | const { error, formItemId } = useFormField(); 92 | 93 | return ( 94 |