├── 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 |
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 |
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 |
19 | );
20 | }
21 | );
22 | Textarea.displayName = 'Textarea';
23 |
24 | export { Textarea };
25 |
--------------------------------------------------------------------------------
/app/not-found.tsx:
--------------------------------------------------------------------------------
1 | import { Button } from '@/components/ui/button';
2 | import Link from 'next/link';
3 | import Image from 'next/image';
4 | import { paths } from '@/lib/routes/paths';
5 |
6 | export default function PageNotFound() {
7 | return (
8 |
9 |
10 |
11 |
12 | 404, Page Not Found
13 |
14 |
15 | The page you are looking for cannot be found.
16 |
17 |
18 |
21 |
22 |
23 | );
24 | }
25 |
--------------------------------------------------------------------------------
/components/ui/separator.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as SeparatorPrimitive from "@radix-ui/react-separator"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Separator = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(
12 | (
13 | { className, orientation = "horizontal", decorative = true, ...props },
14 | ref
15 | ) => (
16 |
27 | )
28 | )
29 | Separator.displayName = SeparatorPrimitive.Root.displayName
30 |
31 | export { Separator }
32 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/components/containers/profile-detail/components/profile-not-found.tsx:
--------------------------------------------------------------------------------
1 | import { Button } from '@/components/ui/button';
2 | import Link from 'next/link';
3 | import { paths } from '@/lib/routes/paths';
4 |
5 | export default function ProfileNotFound() {
6 | return (
7 |
8 |
9 |
10 |
11 | Oops... Profile not found
12 |
13 |
14 | The profile you are looking for cannot be found.
15 |
16 |
17 |
20 |
21 |
22 | );
23 | }
24 |
--------------------------------------------------------------------------------
/components/containers/profile-list/hooks/filters/filter-definitions/search.filter.ts:
--------------------------------------------------------------------------------
1 | import { useFilter } from '../../use-filter';
2 | import { useQueryState, parseAsString } from 'nuqs';
3 |
4 | const filterId = 'search';
5 |
6 | export const useSearchFilter = () => {
7 | const [value, setValue] = useQueryState(
8 | filterId,
9 | parseAsString.withDefault('')
10 | );
11 |
12 | return useFilter({
13 | id: filterId,
14 | type: 'search',
15 | initialValue: value,
16 | onChange: newValue => setValue(newValue),
17 | getQueryConditions: value => ({
18 | _and: [
19 | {
20 | _or: [
21 | { name: { _like: `%${value}%` } },
22 | {
23 | root: {
24 | products: {
25 | name: { _like: `%${value}%` }
26 | }
27 | }
28 | }
29 | ]
30 | }
31 | ]
32 | })
33 | });
34 | };
35 |
--------------------------------------------------------------------------------
/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 '@/hooks/use-toast';
12 |
13 | export function Toaster() {
14 | const { toasts } = useToast();
15 |
16 | return (
17 |
18 | {toasts.map(function ({ id, title, description, action, ...props }) {
19 | return (
20 |
21 |
22 | {title && {title}}
23 | {description && (
24 | {description}
25 | )}
26 |
27 | {action}
28 |
29 |
30 | );
31 | })}
32 |
33 |
34 | );
35 | }
36 |
--------------------------------------------------------------------------------
/lib/utils/media-utils.ts:
--------------------------------------------------------------------------------
1 | type MediaTypeObject = { mediaType?: { slug?: string | null } | null };
2 |
3 | // TODO: should extract this from TGS
4 | const MEDIA_SLUGS = {
5 | LogoLightBg: 'logo_light_bg',
6 | IconDarkBG: 'icon',
7 | IconLightBG: 'icon',
8 | LogoDarkBG: 'logo_dark_bg',
9 | ProfileHeader: 'profile_header'
10 | };
11 |
12 | function findMediaByType(type: keyof typeof MEDIA_SLUGS) {
13 | return (media: MediaTypeObject | null) =>
14 | !media ? undefined : media.mediaType?.slug === MEDIA_SLUGS[type];
15 | }
16 |
17 | export const findMedia = {
18 | /**
19 | * Return Logo Light BG from the media array
20 | * @example const validLogoUrl = profile.media?.find(findMedia.logo)?.url;
21 | * */
22 | logo: findMediaByType('LogoLightBg'),
23 | /**
24 | * Return Icon Light BG from the media array
25 | * @example const validIconUrl = profile.media?.find(findMedia.icon)?.url;
26 | * */
27 | icon: findMediaByType('IconLightBG')
28 | };
29 |
--------------------------------------------------------------------------------
/components/containers/profile-detail/components/powered-by.tsx:
--------------------------------------------------------------------------------
1 | import Link from 'next/link';
2 | import { paths } from '@/lib/routes/paths';
3 | import Image from 'next/image';
4 | import { useTheme } from 'next-themes';
5 |
6 | export const PoweredBy = () => {
7 | const { resolvedTheme } = useTheme();
8 |
9 | const darkLogoSrc = '/thegrid-logo-rounded-white.png';
10 | const lightLogoSrc = '/thegrid-logo-rounded.png';
11 |
12 | const logoSrc = resolvedTheme === 'dark' ? darkLogoSrc : lightLogoSrc;
13 |
14 | return (
15 |
16 | Powered by{' '}
17 |
23 | {' '}
24 |
25 | The Grid
26 |
27 |
28 | );
29 | };
30 |
--------------------------------------------------------------------------------
/components/ui/spinner.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { cva, type VariantProps } from 'class-variance-authority';
3 | import { cn } from '@/lib/utils';
4 | import { Loader } from 'lucide-react';
5 |
6 | const spinnerVariants = cva('inline-block animate-spin text-current', {
7 | variants: {
8 | size: {
9 | sm: 'w-4 h-4',
10 | md: 'w-6 h-6',
11 | lg: 'w-8 h-8'
12 | }
13 | },
14 | defaultVariants: {
15 | size: 'sm'
16 | }
17 | });
18 |
19 | export interface SpinnerProps
20 | extends React.SVGAttributes,
21 | VariantProps {}
22 |
23 | export const Spinner = React.forwardRef(
24 | ({ className, size, ...props }, ref) => {
25 | return (
26 |
31 | );
32 | }
33 | );
34 |
35 | Spinner.displayName = 'Spinner';
36 |
--------------------------------------------------------------------------------
/components/containers/profile-list/components/profile-list-search/profile-list-search.tsx:
--------------------------------------------------------------------------------
1 | import { Input } from '@/components/ui/input';
2 | import { Label } from '@/components/ui/label';
3 | import { useProfileFiltersContext } from '@/providers/filters-provider';
4 |
5 | export const ProfileListSearch = () => {
6 | const { filters } = useProfileFiltersContext();
7 |
8 | return (
9 |
10 |
17 | {
23 | filters.searchFilter.setValue(event.target.value);
24 | }}
25 | />
26 |
27 | );
28 | };
29 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "lib": [
4 | "dom",
5 | "dom.iterable",
6 | "esnext"
7 | ],
8 | "allowJs": true,
9 | "skipLibCheck": true,
10 | "strict": true,
11 | "noEmit": true,
12 | "esModuleInterop": true,
13 | "module": "esnext",
14 | "moduleResolution": "bundler",
15 | "resolveJsonModule": true,
16 | "isolatedModules": true,
17 | "jsx": "preserve",
18 | "incremental": true,
19 | "plugins": [
20 | {
21 | "name": "next"
22 | },
23 | {
24 | "name": "@0no-co/graphqlsp",
25 | "schema": "./lib/graphql/generated/schema.graphql",
26 | "trackFieldUsage": false
27 | }
28 | ],
29 | "paths": {
30 | "@/*": [
31 | "./*"
32 | ]
33 | },
34 | "target": "ES2017"
35 | },
36 | "include": [
37 | "next-env.d.ts",
38 | "**/*.ts",
39 | "**/*.tsx",
40 | ".next/types/**/*.ts"
41 | ],
42 | "exclude": [
43 | "node_modules"
44 | ]
45 | }
46 |
--------------------------------------------------------------------------------
/components/ui/icon-link.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Tooltip,
3 | TooltipContent,
4 | TooltipProvider,
5 | TooltipTrigger
6 | } from '@/components/ui/tooltip';
7 | import Link from 'next/link';
8 | import { PropsWithChildren } from 'react';
9 |
10 | export const IconLink = ({
11 | children,
12 | url,
13 | tooltipLabel,
14 | noTooltip = false
15 | }: PropsWithChildren<{
16 | url: string;
17 | tooltipLabel?: string;
18 | noTooltip?: boolean;
19 | }>) => {
20 | if (noTooltip) {
21 | return (
22 |
23 | {children}
24 |
25 | );
26 | }
27 |
28 | return (
29 |
30 |
31 |
32 |
33 | {children}
34 |
35 |
36 | {tooltipLabel}
37 |
38 |
39 | );
40 | };
41 |
--------------------------------------------------------------------------------
/lib/storage/blob.ts:
--------------------------------------------------------------------------------
1 | import { put } from '@vercel/blob';
2 |
3 | export const putFile = async (path: string, data: any) => {
4 | const blob = await put(path, data, {
5 | access: 'public',
6 | addRandomSuffix: false,
7 | cacheControlMaxAge: 0
8 | });
9 | return blob;
10 | };
11 |
12 | export const getFileContent = async (path: string) => {
13 | const url = getFileUrl(path);
14 | if (!url) return null;
15 | const response = await fetch(url);
16 | if (!response.ok) return null;
17 | return response.text();
18 | };
19 |
20 | export const getFileUrl = (path: string) => {
21 | const BLOB_READ_WRITE_TOKEN = process.env.BLOB_READ_WRITE_TOKEN;
22 | const STORE_ID = process.env.BLOB_READ_WRITE_TOKEN?.match(
23 | /^vercel_blob_rw_([a-z0-9]+)_[a-z0-9]+$/i
24 | )?.[1].toLowerCase();
25 | if (!BLOB_READ_WRITE_TOKEN || !STORE_ID) return null;
26 | const BLOB_BASE_URL = `https://${STORE_ID}.public.blob.vercel-storage.com`;
27 | if (!BLOB_BASE_URL) return null;
28 | return `${BLOB_BASE_URL}/${path}`;
29 | };
30 |
--------------------------------------------------------------------------------
/components/containers/header/logo.tsx:
--------------------------------------------------------------------------------
1 | // Start of Selection
2 | 'use client';
3 |
4 | import { useEffect, useState } from 'react';
5 | import { siteConfig } from '@/lib/site-config';
6 | import Image from 'next/image';
7 | import { useTheme } from 'next-themes';
8 | import { Skeleton } from '@/components/ui/skeleton';
9 |
10 | export const Logo = () => {
11 | const { resolvedTheme } = useTheme();
12 | const [mounted, setMounted] = useState(false);
13 |
14 | useEffect(() => {
15 | setMounted(true);
16 | }, []);
17 |
18 | const currentTheme = resolvedTheme || 'dark';
19 |
20 | if (!mounted) {
21 | // Prevents mismatch by not rendering until client-side
22 | return ;
23 | }
24 |
25 | return (
26 |
36 | );
37 | };
38 |
--------------------------------------------------------------------------------
/components/containers/profile-detail/components/inline-data-point.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode } from 'react';
2 | import { Separator } from '@/components/ui/separator';
3 | import { cn } from '@/lib/utils';
4 |
5 | type InlineDataPointProps = {
6 | label: string;
7 | value?: string | boolean | ReactNode;
8 | children?: ReactNode;
9 | fullWidth?: boolean;
10 | separator?: boolean;
11 | };
12 |
13 | export const InlineDataPoint = ({
14 | label,
15 | value,
16 | children,
17 | fullWidth,
18 | separator = true
19 | }: InlineDataPointProps) => {
20 | return (
21 |
22 |
28 |
{label}
29 | {value ?
{value}
: children}
30 |
31 | {separator &&
}
32 |
33 | );
34 | };
35 |
--------------------------------------------------------------------------------
/lib/graphql/execute.ts:
--------------------------------------------------------------------------------
1 | import type { TypedDocumentString } from './generated/graphql';
2 |
3 | type GraphQLResponse =
4 | | { data?: undefined; errors: { message: string }[] }
5 | | { data: T; errors?: undefined };
6 |
7 | export const execute = async (
8 | query: TypedDocumentString,
9 | ...[variables]: TVariables extends Record ? [] : [TVariables]
10 | ): Promise => {
11 | if (!process.env.NEXT_PUBLIC_GRAPHQL_ENDPOINT_URL) {
12 | throw TypeError('NEXT_PUBLIC_GRAPHQL_ENDPOINT_URL is not defined');
13 | }
14 | const res = await fetch(process.env.NEXT_PUBLIC_GRAPHQL_ENDPOINT_URL, {
15 | method: 'POST',
16 | body: JSON.stringify({
17 | query,
18 | variables
19 | }),
20 | headers: {
21 | 'Content-Type': 'application/json'
22 | }
23 | });
24 |
25 | const graphqlResponse = (await res.json()) as GraphQLResponse;
26 |
27 | if (graphqlResponse.errors) {
28 | throw TypeError(graphqlResponse.errors[0]?.message);
29 | }
30 |
31 | return graphqlResponse.data;
32 | };
33 |
--------------------------------------------------------------------------------
/components/containers/profile-list/components/profile-card/profile-card-feature.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from '@/lib/utils';
2 | import { isEmpty } from '@/lib/utils/is-empty';
3 |
4 | export type ProfileCardFeatureProps = {
5 | label: string;
6 | value?: string | false;
7 | active?: boolean;
8 | };
9 |
10 | export const ProfileCardFeature = ({
11 | label,
12 | value,
13 | active
14 | }: ProfileCardFeatureProps) => {
15 | return (
16 |
17 |
24 | {label}
25 |
26 |
27 | {isEmpty(value) ? '-' : value}
28 |
29 |
30 | );
31 | };
32 |
--------------------------------------------------------------------------------
/components/containers/hero/hero.tsx:
--------------------------------------------------------------------------------
1 | import { Badge } from '@/components/ui/badge';
2 | import { siteConfig } from '@/lib/site-config';
3 |
4 | export const Hero = () => {
5 | return (
6 |
7 | {siteConfig.pages.home.hero.badge && (
8 |
9 | {siteConfig.pages.home.hero.badge}
10 |
11 | )}
12 |
13 | {siteConfig.pages.home.hero.title && (
14 |
15 | {siteConfig.pages.home.hero.title}
16 |
17 | )}
18 |
19 | {siteConfig.pages.home.hero.description && (
20 |
26 | )}
27 |
28 | );
29 | };
30 |
--------------------------------------------------------------------------------
/components/ui/progress.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import * as React from 'react';
4 | import * as ProgressPrimitive from '@radix-ui/react-progress';
5 | import { cn } from '@/lib/utils';
6 |
7 | interface ProgressProps
8 | extends React.ComponentPropsWithoutRef {
9 | indeterminate?: boolean;
10 | }
11 |
12 | const Progress = React.forwardRef<
13 | React.ElementRef,
14 | ProgressProps
15 | >(({ className, value, indeterminate = false, ...props }, ref) => (
16 |
24 |
31 |
32 | ));
33 | Progress.displayName = ProgressPrimitive.Root.displayName;
34 |
35 | export { Progress };
36 |
--------------------------------------------------------------------------------
/app/admin/update-config.ts:
--------------------------------------------------------------------------------
1 | 'use server';
2 |
3 | import { put } from '@vercel/blob';
4 | import { revalidatePath } from 'next/cache';
5 |
6 | export async function updateConfig(data: any) {
7 | const blob = new Blob([JSON.stringify(data)], { type: 'application/json' });
8 | const filename = 'config.json';
9 | const { url } = await put(filename, blob, {
10 | access: 'public',
11 | addRandomSuffix: false,
12 | cacheControlMaxAge: 0
13 | });
14 |
15 | return { url };
16 | }
17 |
18 | export async function triggerRedeploy() {
19 | const triggerRedeployHookUrl = process.env.TRIGGER_REDEPLOY_HOOK_URL;
20 | if (triggerRedeployHookUrl) {
21 | try {
22 | const response = await fetch(triggerRedeployHookUrl, {
23 | method: 'GET',
24 | headers: {
25 | 'Content-Type': 'application/json'
26 | }
27 | });
28 | if (!response.ok) {
29 | throw new Error('Failed to trigger redeploy');
30 | }
31 | } catch (error) {
32 | throw new Error('Failed to trigger redeploy');
33 | }
34 | } else {
35 | throw new Error('TRIGGER_REDEPLOY_HOOK_URL is not set');
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/components/containers/profile-list/components/profile-card/profile-card-data-point.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from '@/lib/utils';
2 | import { ReactNode } from 'react';
3 |
4 | export type ProfileCardDataPointProps = {
5 | label: string;
6 | value?: string | false;
7 | active?: boolean;
8 | children?: ReactNode;
9 | className?: string;
10 | };
11 |
12 | export const ProfileCardDataPoint = ({
13 | label,
14 | value,
15 | active,
16 | children,
17 | className
18 | }: ProfileCardDataPointProps) => (
19 |
20 |
26 | {label}:
27 |
28 |
29 | {children || {value || '-'}}
30 |
31 |
32 | );
33 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright © 2024 to present Enter The Grid B.V.
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, 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,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/components/ui/checkbox.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
5 | import { CheckIcon } from "@radix-ui/react-icons"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const Checkbox = React.forwardRef<
10 | React.ElementRef,
11 | React.ComponentPropsWithoutRef
12 | >(({ className, ...props }, ref) => (
13 |
21 |
24 |
25 |
26 |
27 | ))
28 | Checkbox.displayName = CheckboxPrimitive.Root.displayName
29 |
30 | export { Checkbox }
31 |
--------------------------------------------------------------------------------
/components/containers/profile-list/hooks/filters/utils.ts:
--------------------------------------------------------------------------------
1 | import { z } from 'zod';
2 | import { createParser } from 'nuqs';
3 | import deepmerge from 'deepmerge';
4 |
5 | export const validateAndFormatOptions = (options: unknown) => {
6 | const optionsSchema = z.array(
7 | z.object({
8 | label: z.string(),
9 | value: z.string(),
10 | description: z.string().nullable().optional(),
11 | count: z.number().nullable().optional()
12 | })
13 | );
14 |
15 | const result = optionsSchema.safeParse(options);
16 | const validatedOptions = result.success ? result.data : [];
17 | return validatedOptions.filter(item => item.label?.trim());
18 | };
19 |
20 | export const parseAsId = createParser({
21 | parse(queryValue) {
22 | if (!queryValue) return null;
23 | try {
24 | const decoded = decodeURIComponent(queryValue);
25 | return decoded;
26 | } catch {
27 | return null;
28 | }
29 | },
30 | serialize(value) {
31 | if (!value) return '';
32 | return encodeURIComponent(value.toString());
33 | }
34 | });
35 |
36 | export const mergeConditions = (conditions: Array