├── components ├── containers │ ├── hero │ │ ├── index.ts │ │ └── hero.tsx │ ├── banner │ │ ├── index.ts │ │ └── banner.tsx │ ├── header │ │ ├── index.ts │ │ ├── logo.tsx │ │ ├── toggle-theme-button.tsx │ │ └── header.tsx │ ├── url-type-icon │ │ ├── index.ts │ │ ├── url-type-icon.tsx │ │ └── url-type-icon-list.tsx │ ├── profile-detail │ │ ├── index.ts │ │ └── components │ │ │ ├── profile-loading.tsx │ │ │ ├── profile-data-section.tsx │ │ │ ├── profile-data-point.tsx │ │ │ ├── profile-not-found.tsx │ │ │ ├── powered-by.tsx │ │ │ ├── inline-data-point.tsx │ │ │ ├── profile-tags.tsx │ │ │ ├── profile-data-card.tsx │ │ │ ├── Item-with-sheet.tsx │ │ │ ├── contract-address-badge.tsx │ │ │ ├── entity-card.tsx │ │ │ ├── overview-section.tsx │ │ │ ├── media-dropdown.tsx │ │ │ └── profile-heading.tsx │ ├── profile-list │ │ ├── index.ts │ │ ├── hooks │ │ │ ├── filters │ │ │ │ ├── index.ts │ │ │ │ ├── filter-definitions │ │ │ │ │ ├── profile-founding-date.filter.ts │ │ │ │ │ ├── product-launch-date.filter.ts │ │ │ │ │ ├── search.filter.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── entity-name.filter.ts │ │ │ │ │ ├── profile-type.filter.ts │ │ │ │ │ ├── entity-country.filter.ts │ │ │ │ │ ├── profile-statuses.filter.ts │ │ │ │ │ ├── asset-type.filter.ts │ │ │ │ │ ├── entity-type.filter.ts │ │ │ │ │ ├── product-status.filter.ts │ │ │ │ │ ├── asset-ticker.filter.ts │ │ │ │ │ ├── asset-standard.filter.ts │ │ │ │ │ ├── product-asset-relationships.filter.ts │ │ │ │ │ ├── supports-products.filter.ts │ │ │ │ │ ├── asset-deployed-on.filter.ts │ │ │ │ │ └── product-deployed-on.filter.ts │ │ │ │ └── utils.ts │ │ │ ├── use-profile-sorting.ts │ │ │ └── use-profile-filters.ts │ │ ├── components │ │ │ ├── profile-list-cards │ │ │ │ └── index.ts │ │ │ ├── profile-list-search │ │ │ │ ├── index.ts │ │ │ │ └── profile-list-search.tsx │ │ │ ├── profile-list-sorting │ │ │ │ └── index.ts │ │ │ ├── profile-list-hero-filters │ │ │ │ └── index.ts │ │ │ ├── profile-card │ │ │ │ ├── index.ts │ │ │ │ ├── profile-card-feature.tsx │ │ │ │ ├── profile-card-data-point.tsx │ │ │ │ ├── profile-card-skeleton.tsx │ │ │ │ └── product-badge.tsx │ │ │ └── profile-list-filters │ │ │ │ ├── index.ts │ │ │ │ ├── components │ │ │ │ └── filter-group.tsx │ │ │ │ ├── profile-list-filters-label.tsx │ │ │ │ └── profile-list-filters.tsx │ │ └── profile-list.tsx │ └── query-dialog-button │ │ ├── index.ts │ │ └── query-dialog-button.tsx ├── ui │ ├── skeleton.tsx │ ├── deep-link.tsx │ ├── label.tsx │ ├── deep-link-badge.tsx │ ├── textarea.tsx │ ├── separator.tsx │ ├── input.tsx │ ├── toaster.tsx │ ├── spinner.tsx │ ├── icon-link.tsx │ ├── progress.tsx │ ├── checkbox.tsx │ ├── tooltip.tsx │ ├── badge.tsx │ ├── popover.tsx │ ├── avatar.tsx │ ├── collapsible-list.tsx │ ├── scroll-area.tsx │ ├── button.tsx │ ├── copy-button.tsx │ ├── card.tsx │ ├── date-picker.tsx │ ├── filter-container.tsx │ ├── drawer.tsx │ ├── dialog.tsx │ ├── checkbox-grid.tsx │ └── calendar.tsx └── claim-badge.tsx ├── lib ├── utils │ ├── index.ts │ ├── is-nil.ts │ ├── cn.ts │ ├── is-not-empty.ts │ ├── is-empty.ts │ ├── media-utils.ts │ └── default-where-filter.ts ├── graphql │ ├── generated │ │ ├── index.ts │ │ └── fragment-masking.ts │ ├── execute.ts │ └── codegen.ts ├── site-config.ts ├── auth │ └── auth.ts ├── routes │ └── paths.ts ├── storage │ └── blob.ts └── config │ ├── default-config.json │ ├── config.json │ └── config.schema.ts ├── .eslintrc.json ├── app ├── icon.png ├── apple-icon.png ├── api │ └── admin-auth │ │ └── route.ts ├── page.tsx ├── not-found.tsx ├── admin │ ├── update-config.ts │ ├── page.tsx │ ├── trigger-redeploy-button.tsx │ └── config-field.tsx ├── layout.tsx ├── profiles │ └── [slug] │ │ └── page.tsx └── globals.css ├── public ├── favicon.ico ├── thegrid-logo-rounded.png ├── thegrid-logo-rounded-white.png ├── thegrid-logo-white.svg └── thegrid-logo.svg ├── postcss.config.mjs ├── next.config.mjs ├── .replit ├── .env.default ├── .prettierrc ├── providers ├── theme-provider.tsx ├── react-query-provider.tsx ├── index.tsx ├── filters-provider.tsx ├── sorting-provider.tsx └── profiles-query-provider.tsx ├── components.json ├── hooks ├── use-media-query.ts └── use-event-listener.ts ├── .gitignore ├── middleware.ts ├── .github └── workflows │ └── ci.yml ├── tsconfig.json ├── LICENSE ├── CONTRIBUTING.md ├── scripts └── retrieve-config.ts ├── package.json └── tailwind.config.ts /components/containers/hero/index.ts: -------------------------------------------------------------------------------- 1 | export * from './hero'; 2 | -------------------------------------------------------------------------------- /components/containers/banner/index.ts: -------------------------------------------------------------------------------- 1 | export * from './banner'; 2 | -------------------------------------------------------------------------------- /components/containers/header/index.ts: -------------------------------------------------------------------------------- 1 | export * from './header'; 2 | -------------------------------------------------------------------------------- /components/containers/url-type-icon/index.ts: -------------------------------------------------------------------------------- 1 | export * from './url-type-icon'; -------------------------------------------------------------------------------- /lib/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './cn'; 2 | export * from './is-nil'; 3 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["next/core-web-vitals", "prettier"] 3 | } 4 | -------------------------------------------------------------------------------- /app/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/The-Grid-Data/Explorer/HEAD/app/icon.png -------------------------------------------------------------------------------- /components/containers/profile-detail/index.ts: -------------------------------------------------------------------------------- 1 | export * from './profile-detail'; 2 | -------------------------------------------------------------------------------- /components/containers/profile-list/index.ts: -------------------------------------------------------------------------------- 1 | export * from './profile-list'; 2 | -------------------------------------------------------------------------------- /components/containers/query-dialog-button/index.ts: -------------------------------------------------------------------------------- 1 | export * from './query-dialog-button'; 2 | -------------------------------------------------------------------------------- /lib/graphql/generated/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./fragment-masking"; 2 | export * from "./gql"; -------------------------------------------------------------------------------- /app/apple-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/The-Grid-Data/Explorer/HEAD/app/apple-icon.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/The-Grid-Data/Explorer/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /components/containers/profile-list/hooks/filters/index.ts: -------------------------------------------------------------------------------- 1 | export * from './filter-definitions'; 2 | -------------------------------------------------------------------------------- /components/containers/profile-list/components/profile-list-cards/index.ts: -------------------------------------------------------------------------------- 1 | export * from './profile-list-cards'; 2 | -------------------------------------------------------------------------------- /components/containers/profile-list/components/profile-list-search/index.ts: -------------------------------------------------------------------------------- 1 | export * from './profile-list-search'; 2 | -------------------------------------------------------------------------------- /components/containers/profile-list/components/profile-list-sorting/index.ts: -------------------------------------------------------------------------------- 1 | export * from './profile-list-sorting'; 2 | -------------------------------------------------------------------------------- /public/thegrid-logo-rounded.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/The-Grid-Data/Explorer/HEAD/public/thegrid-logo-rounded.png -------------------------------------------------------------------------------- /components/containers/profile-list/components/profile-list-hero-filters/index.ts: -------------------------------------------------------------------------------- 1 | export * from './profile-list-hero-filters'; 2 | -------------------------------------------------------------------------------- /public/thegrid-logo-rounded-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/The-Grid-Data/Explorer/HEAD/public/thegrid-logo-rounded-white.png -------------------------------------------------------------------------------- /components/containers/profile-list/components/profile-card/index.ts: -------------------------------------------------------------------------------- 1 | export * from './profile-card'; 2 | export * from './profile-card-skeleton'; 3 | -------------------------------------------------------------------------------- /lib/utils/is-nil.ts: -------------------------------------------------------------------------------- 1 | export const isNil = (value: unknown): value is null | undefined => { 2 | return value === null || value === undefined; 3 | }; 4 | -------------------------------------------------------------------------------- /components/containers/profile-list/components/profile-list-filters/index.ts: -------------------------------------------------------------------------------- 1 | export * from './profile-list-filters'; 2 | export * from './profile-list-filters-label'; 3 | -------------------------------------------------------------------------------- /lib/site-config.ts: -------------------------------------------------------------------------------- 1 | import config from '@/lib/config/config.json'; 2 | import { Config } from './config/config.schema'; 3 | 4 | export const siteConfig = config satisfies Config; 5 | -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | tailwindcss: {}, 5 | }, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | images: { 4 | domains: ['img.thegrid.id'], 5 | }, 6 | }; 7 | 8 | export default nextConfig; 9 | -------------------------------------------------------------------------------- /lib/utils/cn.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 | -------------------------------------------------------------------------------- /.replit: -------------------------------------------------------------------------------- 1 | modules = ["nodejs-20", "web"] 2 | run = "npm run dev" 3 | 4 | [nix] 5 | channel = "stable-24_05" 6 | 7 | [deployment] 8 | run = ["sh", "-c", "npm run dev"] 9 | 10 | [[ports]] 11 | localPort = 3000 12 | externalPort = 80 13 | -------------------------------------------------------------------------------- /app/api/admin-auth/route.ts: -------------------------------------------------------------------------------- 1 | export async function GET(request: Request) { 2 | return new Response('Authentication Required!', { 3 | status: 401, 4 | headers: { 5 | 'WWW-Authenticate': 'Basic realm="Admin Area"' 6 | } 7 | }); 8 | } 9 | -------------------------------------------------------------------------------- /components/containers/profile-detail/components/profile-loading.tsx: -------------------------------------------------------------------------------- 1 | export default function ProfileLoading() { 2 | return ( 3 |
4 |

Loading...

5 |
6 | ); 7 | } 8 | -------------------------------------------------------------------------------- /.env.default: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_GRAPHQL_ENDPOINT_URL=https://beta.node.thegrid.id/graphql 2 | ADMIN_USER=admin 3 | ADMIN_PASSWORD=password 4 | LOAD_CONFIG_FROM_VERCEL_STORAGE=true 5 | BLOB_READ_WRITE_TOKEN=ADD_YOUR_OWN 6 | TRIGGER_REDEPLOY_HOOK_URL=https://api.vercel.com/v1/ADD_YOUR_OWN 7 | -------------------------------------------------------------------------------- /lib/utils/is-not-empty.ts: -------------------------------------------------------------------------------- 1 | export const isNotEmpty = ( 2 | value: T | T[] | null | undefined 3 | ): value is NonNullable => { 4 | if (typeof value === 'string' || Array.isArray(value)) { 5 | return value.length > 0; 6 | } 7 | return value !== null && value !== undefined; 8 | }; 9 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "none", 4 | "arrowParens": "avoid", 5 | "proseWrap": "preserve", 6 | "quoteProps": "as-needed", 7 | "bracketSameLine": false, 8 | "bracketSpacing": true, 9 | "tabWidth": 2, 10 | "plugins": ["prettier-plugin-tailwindcss"] 11 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /providers/theme-provider.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as React from 'react'; 4 | import { ThemeProvider as NextThemesProvider } from 'next-themes'; 5 | 6 | export function ThemeProvider({ 7 | children, 8 | ...props 9 | }: React.ComponentProps) { 10 | return {children}; 11 | } 12 | -------------------------------------------------------------------------------- /lib/auth/auth.ts: -------------------------------------------------------------------------------- 1 | export function checkAuth(basicAuth: string): boolean { 2 | const authValue = basicAuth.split(' ')[1]; 3 | const [user, pwd] = atob(authValue).split(':'); 4 | 5 | const validUser = process.env.ADMIN_USER || 'admin'; 6 | const validPassword = process.env.ADMIN_PASSWORD || 'password'; 7 | 8 | return user === validUser && pwd === validPassword; 9 | } 10 | -------------------------------------------------------------------------------- /providers/react-query-provider.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; 4 | import { PropsWithChildren } from 'react'; 5 | 6 | const client = new QueryClient(); 7 | 8 | export const ReactQueryProvider = ({ children }: PropsWithChildren) => { 9 | return {children}; 10 | }; 11 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "app/globals.css", 9 | "baseColor": "gray", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/page.tsx: -------------------------------------------------------------------------------- 1 | import { Hero } from '@/components/containers/hero'; 2 | import { ProfileList } from '@/components/containers/profile-list'; 3 | import { Suspense } from 'react'; 4 | 5 | export const dynamic = 'force-dynamic'; 6 | 7 | export default function Page() { 8 | return ( 9 |
10 | 11 | 12 | 13 | 14 |
15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /components/containers/banner/banner.tsx: -------------------------------------------------------------------------------- 1 | import { siteConfig } from '@/lib/site-config'; 2 | 3 | export const Banner = () => { 4 | return ( 5 |
6 | 10 |
11 | ); 12 | }; 13 | -------------------------------------------------------------------------------- /lib/utils/is-empty.ts: -------------------------------------------------------------------------------- 1 | import { isNil } from './is-nil'; 2 | 3 | export const isEmpty = ( 4 | value: string | number | null | undefined | false 5 | ): value is null | undefined | '' => { 6 | if (isNil(value)) { 7 | return true; 8 | } 9 | 10 | if (typeof value === 'string') { 11 | return value.trim() === ''; 12 | } 13 | 14 | if (typeof value === 'number') { 15 | return false; 16 | } 17 | 18 | if (Array.isArray(value)) { 19 | return value.length === 0; 20 | } 21 | 22 | return value === false; 23 | }; 24 | -------------------------------------------------------------------------------- /hooks/use-media-query.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | export function useMediaQuery(query: string) { 4 | const [value, setValue] = React.useState(false); 5 | 6 | React.useEffect(() => { 7 | function onChange(event: MediaQueryListEvent) { 8 | setValue(event.matches); 9 | } 10 | 11 | const result = matchMedia(query); 12 | result.addEventListener('change', onChange); 13 | setValue(result.matches); 14 | 15 | return () => result.removeEventListener('change', onChange); 16 | }, [query]); 17 | 18 | return value; 19 | } 20 | -------------------------------------------------------------------------------- /components/containers/profile-list/components/profile-list-filters/components/filter-group.tsx: -------------------------------------------------------------------------------- 1 | import { PropsWithChildren } from 'react'; 2 | import { Separator } from '@/components/ui/separator'; 3 | 4 | export type FilterGroupProps = PropsWithChildren<{ 5 | title: string; 6 | }>; 7 | 8 | export const FilterGroup = ({ title, children }: FilterGroupProps) => { 9 | return ( 10 |
11 |
12 |

{title}

13 |
14 | {children} 15 |
16 | ); 17 | }; 18 | -------------------------------------------------------------------------------- /.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 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | 38 | .env 39 | .envrc 40 | -------------------------------------------------------------------------------- /middleware.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from 'next/server'; 2 | import { checkAuth } from './lib/auth/auth'; 3 | 4 | export const config = { 5 | matcher: ['/admin/:path*'] // Apply to all routes under /admin 6 | }; 7 | 8 | export function middleware(req: NextRequest) { 9 | const basicAuth = req.headers.get('authorization'); 10 | const url = req.nextUrl; 11 | 12 | if (basicAuth) { 13 | if (checkAuth(basicAuth)) { 14 | return NextResponse.next(); 15 | } 16 | } 17 | 18 | url.pathname = '/api/admin-auth'; 19 | return NextResponse.rewrite(url); 20 | } 21 | -------------------------------------------------------------------------------- /components/ui/deep-link.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | 3 | export type DeepLinkProps = { 4 | value?: string | false; 5 | href?: string; 6 | }; 7 | 8 | export const DeepLink = ({ value, href }: DeepLinkProps) => { 9 | return ( 10 |
11 | {href && value ? ( 12 | 16 | {value} 17 | 18 | ) : ( 19 | {value || '-'} 20 | )} 21 |
22 | ); 23 | }; 24 | -------------------------------------------------------------------------------- /hooks/use-event-listener.ts: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useEffect } from 'react'; 4 | 5 | export function useEventListener(eventName: string, handler: EventListener) { 6 | useEffect(() => { 7 | // Add event listener 8 | window?.addEventListener(eventName, handler); 9 | 10 | // Remove event listener on cleanup 11 | return () => { 12 | window?.removeEventListener(eventName, handler); 13 | }; 14 | }, [eventName, handler]); 15 | } 16 | 17 | export function dispatchCustomEvent(eventName: string, detail: any = null) { 18 | const event = new CustomEvent(eventName, { detail }); 19 | window?.dispatchEvent(event); 20 | } 21 | -------------------------------------------------------------------------------- /components/containers/profile-detail/components/profile-data-section.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { PropsWithChildren, ReactNode } from 'react'; 4 | 5 | export type ProfileDataSectionProps = PropsWithChildren<{ 6 | id?: string; 7 | title: string; 8 | icon: ReactNode; 9 | }>; 10 | 11 | export const ProfileDataSection = ({ 12 | icon, 13 | title, 14 | children, 15 | id 16 | }: ProfileDataSectionProps) => { 17 | return ( 18 |
19 |

20 | {icon} 21 | {title} 22 |

23 | {children} 24 |
25 | ); 26 | }; 27 | -------------------------------------------------------------------------------- /lib/routes/paths.ts: -------------------------------------------------------------------------------- 1 | export const paths = { 2 | thegrid: { 3 | base: 'https://thegrid.id', 4 | terms: 'https://thegrid.id/legal/web-services-terms' 5 | }, 6 | base: '/', 7 | profile: { 8 | base: '/profiles', 9 | detail: (slug: string, opts?: { section?: string }) => { 10 | const section = opts?.section; 11 | return `/profiles/${slug}${section ? `#${section}` : ''}`; 12 | } 13 | } 14 | } as const; 15 | 16 | export const getCanonicalProfileUrl = (slug: string) => { 17 | return `${paths.thegrid.base}/profiles/${slug}`; 18 | }; 19 | 20 | export const getProfileUrl = (slug: string, preferCanonical = true) => { 21 | if (preferCanonical) { 22 | return getCanonicalProfileUrl(slug); 23 | } 24 | return paths.profile.detail(slug); 25 | }; 26 | -------------------------------------------------------------------------------- /providers/index.tsx: -------------------------------------------------------------------------------- 1 | import { PropsWithChildren } from 'react'; 2 | import { ReactQueryProvider } from './react-query-provider'; 3 | import { TooltipProvider } from '@/components/ui/tooltip'; 4 | import { Toaster } from '@/components/ui/toaster'; 5 | import { ThemeProvider } from './theme-provider'; 6 | import { NuqsAdapter } from 'nuqs/adapters/next/app'; 7 | 8 | export const Providers = ({ children }: PropsWithChildren) => ( 9 | 15 | 16 | 17 | 18 | {children} 19 | 20 | 21 | 22 | ); 23 | -------------------------------------------------------------------------------- /components/containers/profile-list/hooks/filters/filter-definitions/profile-founding-date.filter.ts: -------------------------------------------------------------------------------- 1 | import { useFilter } from '../../use-filter'; 2 | import { useQueryState, parseAsArrayOf, parseAsString } from 'nuqs'; 3 | 4 | const filterId = 'profileFoundingDate'; 5 | 6 | export const useProfileFoundingDateFilter = () => { 7 | const [value, setValue] = useQueryState( 8 | filterId, 9 | parseAsArrayOf(parseAsString) 10 | ); 11 | 12 | return useFilter({ 13 | id: filterId, 14 | type: 'range', 15 | initialValue: value as [string, string] | null, 16 | onChange: newValue => setValue(newValue), 17 | getQueryConditions: value => ({ 18 | foundingDate: { 19 | _gte: value[0], 20 | _lte: value[1] 21 | } 22 | }) 23 | }); 24 | }; 25 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | branches: 8 | - main 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout repository 15 | uses: actions/checkout@v4 16 | 17 | - uses: pnpm/action-setup@v4 18 | name: Install pnpm 19 | with: 20 | run_install: false 21 | 22 | - name: Setup Node.js 23 | uses: actions/setup-node@v4 24 | with: 25 | node-version: '20.x' 26 | cache: 'pnpm' 27 | 28 | - name: Install pnpm 29 | run: npm install -g pnpm@latest 30 | 31 | - name: Install dependencies 32 | run: pnpm install 33 | 34 | - name: Build Application 35 | run: pnpm build 36 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /components/ui/deep-link-badge.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | import { ReactNode } from 'react'; 3 | import { Badge } from './badge'; 4 | 5 | export type DeepLinkProps = { 6 | value?: string | false; 7 | href?: string; 8 | icon?: ReactNode; 9 | }; 10 | 11 | export const DeepLinkBadge = ({ value, href, icon }: DeepLinkProps) => { 12 | return ( 13 |
14 | {href && value ? ( 15 | 20 | 21 | {icon} {value} 22 | 23 | 24 | ) : ( 25 | '-' 26 | )} 27 |
28 | ); 29 | }; 30 | -------------------------------------------------------------------------------- /components/containers/profile-list/hooks/filters/filter-definitions/product-launch-date.filter.ts: -------------------------------------------------------------------------------- 1 | import { useFilter } from '../../use-filter'; 2 | import { useQueryState, parseAsArrayOf, parseAsString } from 'nuqs'; 3 | 4 | const filterId = 'productLaunchDate'; 5 | 6 | export const useProductLaunchDateFilter = () => { 7 | const [value, setValue] = useQueryState( 8 | filterId, 9 | parseAsArrayOf(parseAsString) 10 | ); 11 | 12 | return useFilter({ 13 | id: filterId, 14 | type: 'range', 15 | initialValue: value as [string, string] | null, 16 | onChange: newValue => setValue(newValue), 17 | getQueryConditions: value => ({ 18 | root: { 19 | products: { 20 | launchDate: { 21 | _gte: value[0], 22 | _lte: value[1] 23 | } 24 | } 25 | } 26 | }) 27 | }); 28 | }; 29 | -------------------------------------------------------------------------------- /providers/filters-provider.tsx: -------------------------------------------------------------------------------- 1 | import { createContext, useContext } from 'react'; 2 | import { 3 | Filters, 4 | useProfileFilters 5 | } from '@/components/containers/profile-list/hooks/use-profile-filters'; 6 | 7 | const ProfileFiltersContext = createContext(null); 8 | 9 | export const ProfileFiltersProvider = ({ 10 | children 11 | }: React.PropsWithChildren) => { 12 | const filters = useProfileFilters(); 13 | return ( 14 | 15 | {children} 16 | 17 | ); 18 | }; 19 | 20 | export const useProfileFiltersContext = () => { 21 | const context = useContext(ProfileFiltersContext); 22 | if (!context) { 23 | throw new Error( 24 | 'useProfileFiltersContext must be used within a ProfileFiltersProvider' 25 | ); 26 | } 27 | return context; 28 | }; 29 | -------------------------------------------------------------------------------- /providers/sorting-provider.tsx: -------------------------------------------------------------------------------- 1 | import { createContext, useContext } from 'react'; 2 | import { 3 | Sorting, 4 | useProfileSorting 5 | } from '@/components/containers/profile-list/hooks/use-profile-sorting'; 6 | 7 | const ProfileSortingContext = createContext(null); 8 | 9 | export const ProfileSortingProvider = ({ 10 | children 11 | }: React.PropsWithChildren) => { 12 | const sorting = useProfileSorting(); 13 | return ( 14 | 15 | {children} 16 | 17 | ); 18 | }; 19 | 20 | export const useProfileSortingContext = () => { 21 | const context = useContext(ProfileSortingContext); 22 | if (!context) { 23 | throw new Error( 24 | 'useProfileSortingContext must be used within a ProfileSortingProvider' 25 | ); 26 | } 27 | return context; 28 | }; 29 | -------------------------------------------------------------------------------- /components/containers/profile-detail/components/profile-data-point.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from '@/lib/utils'; 2 | import { PropsWithChildren } from 'react'; 3 | 4 | export type ProfileDataPointProps = { 5 | label: string; 6 | value?: string | false; 7 | opts?: { 8 | breakAll?: boolean; 9 | }; 10 | }; 11 | 12 | export const ProfileDataPoint = ({ 13 | label, 14 | value, 15 | opts = { breakAll: false } 16 | }: ProfileDataPointProps) => ( 17 |
18 |

{label}:

19 |

20 | {value || '-'} 21 |

22 |
23 | ); 24 | 25 | export const ProfileDataPointContainer = ({ children }: PropsWithChildren) => { 26 | return
{children}
; 27 | }; 28 | -------------------------------------------------------------------------------- /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 |