diff --git a/.changeset/config.json b/.changeset/config.json index fd27c8334..08da093ef 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -12,8 +12,10 @@ "storybook.namekit.io", "examples.nameguard.io", "namehashlabs.org", - "nameguard-api", "@namehash/seo", - "@namehash/internal" + "@namehash/internal", + "namerank.io", + "namekit.io", + "namegraph.dev" ] } diff --git a/.changeset/fluffy-laws-grow.md b/.changeset/fluffy-laws-grow.md deleted file mode 100644 index e3822d4ed..000000000 --- a/.changeset/fluffy-laws-grow.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@namehash/nameguard": minor ---- - -- Rename `ImpersonationStatus` to `ImpersonationEstimate` to better manage expectations. -- Rename `endpoint` param to `nameguardEndpoint` when creating a NameGuard Client for more self-documenting code. -- Refined unit tests. diff --git a/.changeset/late-phones-rush.md b/.changeset/late-phones-rush.md deleted file mode 100644 index 9a2bd56ab..000000000 --- a/.changeset/late-phones-rush.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -"@namehash/nameguard-js": minor ---- - -- Optimize startup time by lazily initializing in-memory data models. -- Refine documentation. -- Refine unit tests. -- Warn about likely timeout errors if Etherum provider environment variable is not set when - running unit tests. -- Upgrade to the latest NameGuard SDK. diff --git a/.github/workflows/nameguard-api-lambda-deploy.yml b/.github/workflows/nameguard-api-lambda-deploy.yml index 12d97c753..61089e058 100644 --- a/.github/workflows/nameguard-api-lambda-deploy.yml +++ b/.github/workflows/nameguard-api-lambda-deploy.yml @@ -23,40 +23,33 @@ concurrency: cancel-in-progress: false jobs: - build-image-deploy-serverless: + build-image-deploy: name: Build and deploy NameGuard API Lambda runs-on: ubuntu-latest steps: - - name: Checkout NameKit repo + - name: Checkout this repo uses: actions/checkout@v4 - - name: Setup pnpm - uses: pnpm/action-setup@v4 - - - name: Install Node.js - uses: actions/setup-node@v4 + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v4 with: - node-version-file: .nvmrc - cache: "pnpm" - - - name: Install npm dependencies - # We're installing pnpm / node dependencies to make use - # of the Serverless framework when we build and deploy the lambda. - run: pnpm install --frozen-lockfile - + role-to-assume: ${{ secrets.AWS_ROLE}} + aws-region: ${{ secrets.AWS_REGION }} + - name: Set up QEMU - # This GitHub action runs on x86_64, but we want to build the lambda - # for arm64 for increased cost savings in AWS when we deploy it. uses: docker/setup-qemu-action@v3 with: platforms: arm64 - - - name: Assume AWS Role - # Uses GitHub OIDC provider to assume AWS role - uses: aws-actions/configure-aws-credentials@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Setup Terraform + uses: hashicorp/setup-terraform@v2 with: - role-to-assume: ${{ secrets.AWS_ROLE}} - aws-region: us-east-1 + terraform_version: "1.5.7" + terraform_wrapper: false + - name: Build and deploy lambda env: @@ -66,49 +59,47 @@ jobs: ALCHEMY_URI_SEPOLIA: ${{ secrets.ALCHEMY_URI_SEPOLIA }} ENS_SUBGRAPH_URL_MAINNET: ${{ secrets.ENS_SUBGRAPH_URL_MAINNET }} ENS_SUBGRAPH_URL_SEPOLIA: ${{ secrets.ENS_SUBGRAPH_URL_SEPOLIA }} + AWS_REGION: ${{ secrets.AWS_REGION }} + CERTIFICATE_NAME: ${{ secrets.CERTIFICATE_NAME }} + HOSTED_ZONE_NAME: ${{ secrets.HOSTED_ZONE_NAME }} run: | if [[ ${{ github.ref }} == 'refs/heads/main' ]]; then - pnpm run deploy:prod + STAGE="prod" + DOMAIN_NAME=${{ secrets.PROD_DOMAIN_NAME }} elif [[ ${{ github.ref }} == 'refs/heads/staging' ]]; then - pnpm run deploy:staging + STAGE="staging" + DOMAIN_NAME=${{ secrets.STAGING_DOMAIN_NAME }} else echo "Deployment is only supported for main and staging branches" exit 1 fi - working-directory: apps/api.nameguard.io - - - name: Delete old images from ECR - env: - ECR_REPO: serverless-oss-nameguard-prod - run: | - ALL_IMAGES_TO_DELETE=$(aws ecr describe-images --repository-name $ECR_REPO --query 'sort_by(imageDetails,& imagePushedAt)[*].imageDigest' --filter "tagStatus=UNTAGGED" --output json ) - len=`echo $ALL_IMAGES_TO_DELETE | jq length` - IMAGES_TO_DELETE=$(aws ecr describe-images --repository-name $ECR_REPO --query 'sort_by(imageDetails,& imagePushedAt)[*].imageDigest' --filter "tagStatus=UNTAGGED" --output json | jq '.[0]') - if [[ $len > 5 ]]; then aws ecr batch-delete-image --repository-name $ECR_REPO --image-ids imageDigest=$IMAGES_TO_DELETE; fi + cd terraform + chmod +x ./deploy_lambda.sh + ./deploy_lambda.sh $STAGE $AWS_REGION $DOMAIN_NAME $CERTIFICATE_NAME $HOSTED_ZONE_NAME working-directory: apps/api.nameguard.io notify: name: Send Slack deployment event notification - needs: [build-image-deploy-serverless] + needs: [build-image-deploy] runs-on: ubuntu-latest steps: - name: Output status on deployment success - if: ${{ needs.build-image-deploy-serverless.result == 'success'}} + if: ${{ needs.build-image-deploy.result == 'success'}} run: | echo "STATUS=Success :rocket:" >> $GITHUB_ENV echo "TEXT=Lambda NameGuard deployed successfully! :white_check_mark:" >> $GITHUB_ENV echo "COLOR=good" >> $GITHUB_ENV - name: Output status on deployment failed - if: ${{ needs.build-image-deploy-serverless.result == 'failure' }} + if: ${{ needs.build-image-deploy.result == 'failure' }} run: | echo "STATUS=Failure :x:" >> $GITHUB_ENV echo "TEXT=Lambda NameGuard deployment failed! :rotating_light:" >> $GITHUB_ENV echo "COLOR=danger" >> $GITHUB_ENV - name: Output status on deployment cancellation - if: ${{ needs.build-image-deploy-serverless.result == 'cancelled' }} + if: ${{ needs.build-image-deploy.result == 'cancelled' }} run: | echo "STATUS=Cancelled :no_entry_sign:" >> $GITHUB_ENV echo "TEXT=Lambda NameGuard deployment was cancelled. :warning:" >> $GITHUB_ENV diff --git a/.github/workflows/nameguard-python-unit-tests.yml b/.github/workflows/nameguard-python-unit-tests.yml index 5e7834652..0ef82144e 100644 --- a/.github/workflows/nameguard-python-unit-tests.yml +++ b/.github/workflows/nameguard-python-unit-tests.yml @@ -1,5 +1,7 @@ name: NameGuard API - Unit Tests on: + schedule: + - cron: '0 5 * * 0' push: branches: - main diff --git a/apps/api.nameguard.io/package.json b/apps/api.nameguard.io/package.json deleted file mode 100644 index e35b0ac9d..000000000 --- a/apps/api.nameguard.io/package.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "name": "nameguard-api", - "private": true, - "description": "AWS Lambda handler for NameGuard.", - "version": "1.0.0", - "license": "MIT", - "repository": { - "type": "git", - "url": "git+https://github.com/namehash/namekit.git", - "directory": "apps/api.nameguard.io" - }, - "homepage": "https://github.com/namehash/namekit/tree/main/apps/api.nameguard.io", - "keywords": [ - "ENS", - "NameKit", - "NameGuard", - "NameHash" - ], - "scripts": { - "deploy:prod": "DOCKER_CONFIG=.docker serverless deploy --stage prod", - "deploy:staging": "DOCKER_CONFIG=.docker serverless deploy --stage staging" - }, - "dependencies": { - "serverless": "3.39.0" - }, - "devDependencies": { - "serverless-prune-plugin": "2.0.2" - } -} \ No newline at end of file diff --git a/apps/api.nameguard.io/serverless.yml b/apps/api.nameguard.io/serverless.yml deleted file mode 100644 index 76108148d..000000000 --- a/apps/api.nameguard.io/serverless.yml +++ /dev/null @@ -1,181 +0,0 @@ -service: oss-nameguard - -stages: - staging: - alias: api-staging.nameguard.io - lambda-role: DefaultNameGuardRoleStaging - lambda-policy-name: NameGuardPolicyStaging - prod: - alias: api.nameguard.io - lambda-role: DefaultNameGuardRole - lambda-policy-name: NameGuardPolicy - -custom: - stage: ${opt:stage} - apiDomain: ${self:stages.${self:custom.stage}.alias} - hostedZoneName: nameguard.io. - hostedZoneId: Z00825691ZLCWE2VKJQW0 - prune: - automatic: true - number: 5 - region: us-east-1 - -provider: - name: aws - stage: ${self:custom.stage} - architecture: arm64 - ecr: - images: - oss-nameguard: - path: ../../packages/nameguard-python/ - file: ../../apps/api.nameguard.io/Dockerfile - platform: linux/arm64 - -plugins: - - serverless-prune-plugin - -functions: - oss-nameguard: - image: - name: oss-nameguard - name: oss-nameguard-${self:custom.stage} - memorySize: 1769 - timeout: 60 - url: true - role: DefaultNameGuardRole - provisionedConcurrency: 1 - tags: - Stage: ${self:custom.stage} - environment: - PROVIDER_URI_MAINNET: ${env:PROVIDER_URI_MAINNET} - PROVIDER_URI_SEPOLIA: ${env:PROVIDER_URI_SEPOLIA} - ALCHEMY_URI_MAINNET: ${env:ALCHEMY_URI_MAINNET} - ALCHEMY_URI_SEPOLIA: ${env:ALCHEMY_URI_SEPOLIA} - ENS_SUBGRAPH_URL_MAINNET: ${env:ENS_SUBGRAPH_URL_MAINNET} - ENS_SUBGRAPH_URL_SEPOLIA: ${env:ENS_SUBGRAPH_URL_SEPOLIA} - -resources: - Resources: - DefaultNameGuardRole: - Type: AWS::IAM::Role - Properties: - Path: /my/default/path/ - RoleName: ${self:stages.${self:custom.stage}.lambda-role} - AssumeRolePolicyDocument: - Version: "2012-10-17" - Statement: - - Effect: Allow - Principal: - Service: - - lambda.amazonaws.com - Action: sts:AssumeRole - Policies: - - PolicyName: ${self:stages.${self:custom.stage}.lambda-policy-name} - PolicyDocument: - Version: "2012-10-17" - Statement: - - Effect: Allow # note that these rights are given in the default policy and are required if you want logs out of your lambda(s) - Action: - - logs:CreateLogGroup - - logs:CreateLogStream - - logs:PutLogEvents - - logs:TagResource - Resource: - - "Fn::Join": - - ":" - - - "arn:aws:logs" - - Ref: "AWS::Region" - - Ref: "AWS::AccountId" - - "log-group:/aws/lambda/*:*:*" - - Effect: "Allow" - Action: - - "s3:PutObject" - Resource: - Fn::Join: - - "" - - - "arn:aws:s3:::" - - "Ref": "ServerlessDeploymentBucket" - - Effect: Allow - Action: - - ecr:BatchGetImage - - ecr:GetDownloadUrlForLayer - Resource: ["*"] - - ACMCertificate: - Type: "AWS::CertificateManager::Certificate" - Properties: - DomainName: ${self:custom.apiDomain} - DomainValidationOptions: - - DomainName: ${self:custom.apiDomain} - HostedZoneId: ${self:custom.hostedZoneId} - ValidationMethod: DNS - - ApiCloudFrontDistribution: - Type: AWS::CloudFront::Distribution - DeletionPolicy: Delete - Properties: - DistributionConfig: - Enabled: true - PriceClass: PriceClass_100 - HttpVersion: http2 - Comment: Api distribution for ${self:custom.apiDomain} - Origins: - - Id: ApiGateway - DomainName: - !Select [ - 2, - !Split [ - "/", - !GetAtt OssDashnameguardLambdaFunctionUrl.FunctionUrl, - ], - ] - OriginPath: "" - CustomOriginConfig: - HTTPPort: 80 - HTTPSPort: 443 - OriginProtocolPolicy: https-only - OriginSSLProtocols: [TLSv1, TLSv1.1, TLSv1.2] - DefaultCacheBehavior: - TargetOriginId: ApiGateway - ViewerProtocolPolicy: redirect-to-https - Compress: true - DefaultTTL: 0 - AllowedMethods: - - HEAD - - DELETE - - POST - - GET - - OPTIONS - - PUT - - PATCH - CachedMethods: - - HEAD - - OPTIONS - - GET - ForwardedValues: - QueryString: true - Headers: - - Accept - - x-api-key - - Authorization - Cookies: - Forward: none - Aliases: - - ${self:custom.apiDomain} - ViewerCertificate: - SslSupportMethod: sni-only - MinimumProtocolVersion: TLSv1.2_2019 - AcmCertificateArn: !Ref ACMCertificate - ApiRecordSetGroup: - Type: AWS::Route53::RecordSetGroup - DeletionPolicy: Delete - DependsOn: - - ApiCloudFrontDistribution - Properties: - HostedZoneName: ${self:custom.hostedZoneName} - RecordSets: - - Name: ${self:custom.apiDomain} - Type: A - AliasTarget: - HostedZoneId: Z2FDTNDATAQYW2 #default for cloudfront - DNSName: { "Fn::GetAtt": [ApiCloudFrontDistribution, DomainName] } # set the domain of your cloudfront distribution diff --git a/apps/api.nameguard.io/terraform/backend.tf b/apps/api.nameguard.io/terraform/backend.tf new file mode 100644 index 000000000..f329bff1b --- /dev/null +++ b/apps/api.nameguard.io/terraform/backend.tf @@ -0,0 +1,3 @@ +terraform { + backend "s3" {} # Will be configured via deploy script +} \ No newline at end of file diff --git a/apps/api.nameguard.io/terraform/deploy_lambda.sh b/apps/api.nameguard.io/terraform/deploy_lambda.sh new file mode 100644 index 000000000..8ebd61aae --- /dev/null +++ b/apps/api.nameguard.io/terraform/deploy_lambda.sh @@ -0,0 +1,248 @@ +#!/bin/bash + +# ------------------------------------------------------------------------------ +# deploy_lambda.sh +# +# This script automates the deployment of an AWS Lambda function using a Docker +# container image. It builds the Docker image, tags it, pushes it to Amazon ECR +# (Elastic Container Registry), and updates the specified Lambda function with +# the new image. +# +# Prerequisites: +# Before executing this script, ensure that the following prerequisites are met: +# +# 1. **AWS Certificate**: +# - You must have an AWS certificate created in AWS Certificate Manager (ACM) +# to use with your Lambda function. +# - **Guide to Create an AWS Certificate**: +# [Request a Public Certificate in AWS Certificate Manager](https://docs.aws.amazon.com/acm/latest/userguide/gs-acm-request-public.html). +# +# 2. **AWS Hosted Zone**: +# - You must have a hosted zone created in Amazon Route 53 to manage your +# domain's DNS settings. +# - **Guide to Create a Hosted Zone in Route 53**: +# [Creating a Hosted Zone in Route 53](https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/CreatingHostedZone.html). +# +# 3. **Relationship Between AWS Certificate and Hosted Zone**: +# - After creating the certificate and hosted zone, you need to validate the +# certificate and ensure that the domain name associated with the certificate +# is correctly configured in the hosted zone. +# - **Guide to Validate Your Certificate**: +# [Validating an ACM Certificate](https://docs.aws.amazon.com/acm/latest/userguide/gs-acm-validate.html). +# - **Guide to Update DNS Records**: +# [Using Route 53 to Route Traffic to Your Resources](https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/routing-to-resources.html). +# +# 4. **AWS IAM Permissions**: +# - Ensure that the AWS user or role executing the script has the necessary +# permissions to interact with AWS Lambda, ECR, and Route 53. +# - **Guide to IAM Policies**: +# [Creating IAM Policies](https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies_create.html). +# +# 5. **Docker and AWS CLI**: +# - Ensure that Docker and the AWS CLI are installed and configured on your +# machine. +# - **Guide to Install Docker**: +# [Get Docker](https://docs.docker.com/get-docker/). +# - **Guide to Install AWS CLI**: +# [Installing the AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html). +# +# Usage: +# ./deploy_lambda.sh +# +# Example: +# ./deploy_lambda.sh staging us-east-1 api.example.com "*.example.com" example.com. +# +# This script performs the following steps: +# 1. Validates input parameters. +# 2. Checks for the presence of required tools (Docker, AWS CLI). +# 3. Builds the Docker image from the specified context. +# 4. Tags the Docker image for ECR. +# 5. Pushes the Docker image to the specified ECR repository. +# 6. Retrieves the full image URI from ECR. +# 7. Updates the specified AWS Lambda function with the new image. + + +# Get input variables from command line arguments +if [ $# -ne 5 ]; then + echo "Usage: $0 " + echo "Example: $0 staging us-east-1 rank-stage.namekit.io \"*.namekit.io\" namekit.io." + exit 1 +fi + +STAGE="$1" +REGION="$2" +DOMAIN_NAME="$3" +CERTIFICATE_NAME="$4" +HOSTED_ZONE_NAME="$5" + +APPLICATION_NAME="nameguard-${STAGE}" +S3_BUCKET_NAME="${APPLICATION_NAME}-terraform" +ECR_NAME="${APPLICATION_NAME}-ecr" + +# Check if all variables are set +for var in STAGE APPLICATION_NAME REGION S3_BUCKET_NAME ECR_NAME DOMAIN_NAME CERTIFICATE_NAME HOSTED_ZONE_NAME; do + if [ -z "${!var}" ]; then + echo "Error: $var is not set" + exit 1 + fi +done + +# Validate Lambda environment variables +echo "Validating Lambda environment variables..." +LAMBDA_ENV_VARS=( + "PROVIDER_URI_MAINNET" + "PROVIDER_URI_SEPOLIA" + "ALCHEMY_URI_MAINNET" + "ALCHEMY_URI_SEPOLIA" + "ENS_SUBGRAPH_URL_MAINNET" + "ENS_SUBGRAPH_URL_SEPOLIA" +) + +# Check if Lambda environment variables are set +for var in "${LAMBDA_ENV_VARS[@]}"; do + if [ -z "${!var}" ]; then + echo "Error: Lambda environment variable $var is not set" + exit 1 + fi +done + +# Function to check if an S3 bucket exists +s3_bucket_exists() { + aws s3api head-bucket --bucket "$S3_BUCKET_NAME" --region "$REGION" 2>/dev/null + return $? +} + +# Function to check if ECR repository exists +ecr_repo_exists() { + aws ecr describe-repositories --repository-names "${ECR_NAME}" --region "$REGION" --query 'repositories[0].repositoryName' --output text 2>/dev/null + return $? +} + +# Get certificate ARN +CERTIFICATE_ARN=$(aws acm list-certificates --query "CertificateSummaryList[?DomainName=='${CERTIFICATE_NAME}'].CertificateArn" --output text) +if [ -z "$CERTIFICATE_ARN" ]; then + echo "Error: Certificate not found for domain ${CERTIFICATE_NAME}" + exit 1 +fi + +# Get zone ID +HOSTED_ZONE_ID=`aws route53 list-hosted-zones --query "HostedZones[?Name=='${HOSTED_ZONE_NAME}'].Id" --output text | cut -d'/' -f3` +if [ -z "$HOSTED_ZONE_ID" ]; then + echo "Error: Hosted zone not found for domain ${HOSTED_ZONE_NAME}" + exit 1 +fi + +# Create S3 bucket if it doesn't exist +if s3_bucket_exists; then + echo "Bucket $S3_BUCKET_NAME already exists." +else + echo "Creating S3 bucket $S3_BUCKET_NAME..." + if [ "$REGION" = "us-east-1" ]; then + # Special case for us-east-1: don't specify LocationConstraint + aws s3api create-bucket --bucket "$S3_BUCKET_NAME" + else + # All other regions need LocationConstraint + aws s3api create-bucket --bucket "$S3_BUCKET_NAME" \ + --region "$REGION" \ + --create-bucket-configuration LocationConstraint="$REGION" + fi + aws s3api put-bucket-versioning \ + --bucket "$S3_BUCKET_NAME" \ + --versioning-configuration Status=Enabled + echo "S3 bucket $S3_BUCKET_NAME created successfully." +fi + +# Create ECR repository if it doesn't exist +if ecr_repo_exists; then + echo "ECR repository ${ECR_NAME} already exists." +else + echo "Creating ECR repository ${ECR_NAME}..." + aws ecr create-repository --repository-name "${ECR_NAME}" --region "${REGION}" + echo "ECR repository ${ECR_NAME} created successfully." +fi + +# Get ECR repository URL +ECR_URL=$(aws ecr describe-repositories --repository-names "${ECR_NAME}" --query 'repositories[0].repositoryUri' --output text) +echo "ECR repository URL: ${ECR_URL}" + +# Check if Docker is installed +if ! command -v docker &> /dev/null; then + echo "Error: Docker is not installed" + exit 1 +fi + +# Check if Docker daemon is running +if ! docker info &> /dev/null; then + echo "Error: Docker daemon is not running" + exit 1 +fi + +# Check if user has permission to use Docker +if ! docker ps &> /dev/null; then + echo "Error: Current user doesn't have permission to use Docker" + echo "Try running 'sudo usermod -aG docker $USER' and then log out and back in" + exit 1 +fi + +# Verify we can pull from ECR (tests AWS credentials and connectivity) +if ! aws ecr get-login-password --region "${REGION}" 2>/dev/null | docker login --username AWS --password-stdin "${ECR_URL}" &> /dev/null; then + echo "Error: Failed to authenticate with ECR" + echo "Please check your AWS credentials and permissions" + exit 1 +fi + +# Add validation for Docker build context +if [ ! -f "../Dockerfile" ]; then + echo "Error: Dockerfile not found in parent directory" + exit 1 +fi + +echo "Building Docker image..." +cd ../../../packages/nameguard-python +cp ../../apps/api.nameguard.io/Dockerfile ./Dockerfile +if ! docker build . -t nameguard; then + echo "Error: Docker build failed" + exit 1 +fi +cd ../../apps/api.nameguard.io/terraform + +echo "Tagging Docker image..." +if ! docker tag nameguard:latest ${ECR_URL}:latest; then + echo "Error: Failed to tag Docker image" + exit 1 +fi + +echo "Pushing Docker image to ECR..." +if ! docker push ${ECR_URL}:latest; then + echo "Error: Failed to push Docker image to ECR" + exit 1 +fi + +IMAGE_URI="${ECR_URL}:latest" +echo "Using Image URI: ${IMAGE_URI}" + +# Export individual environment variables for Terraform +export TF_VAR_PROVIDER_URI_MAINNET="${PROVIDER_URI_MAINNET}" +export TF_VAR_PROVIDER_URI_SEPOLIA="${PROVIDER_URI_SEPOLIA}" +export TF_VAR_ALCHEMY_URI_MAINNET="${ALCHEMY_URI_MAINNET}" +export TF_VAR_ALCHEMY_URI_SEPOLIA="${ALCHEMY_URI_SEPOLIA}" +export TF_VAR_ENS_SUBGRAPH_URL_MAINNET="${ENS_SUBGRAPH_URL_MAINNET}" +export TF_VAR_ENS_SUBGRAPH_URL_SEPOLIA="${ENS_SUBGRAPH_URL_SEPOLIA}" + +# Initialize Terraform +echo "Initializing Terraform..." +terraform init \ + -backend=true \ + -backend-config="bucket=${S3_BUCKET_NAME}" \ + -backend-config="key=${STAGE}/terraform.tfstate" \ + -backend-config="region=${REGION}" \ + -backend-config="encrypt=true" \ + +# Terraform apply +terraform apply -auto-approve \ + -var="env=${STAGE}" \ + -var="image_uri=${IMAGE_URI}" \ + -var="domain_name=${DOMAIN_NAME}" \ + -var="certificate_arn=${CERTIFICATE_ARN}" \ + -var="hosted_zone_id=${HOSTED_ZONE_ID}" \ + -var="aws_region=${REGION}" \ \ No newline at end of file diff --git a/apps/api.nameguard.io/terraform/main.tf b/apps/api.nameguard.io/terraform/main.tf new file mode 100644 index 000000000..49286c4c7 --- /dev/null +++ b/apps/api.nameguard.io/terraform/main.tf @@ -0,0 +1,22 @@ +provider "aws" { + region = var.aws_region +} + +module "lambda_api" { + source = "./modules/lambda_api" + + env = var.env + image_uri = var.image_uri + domain_name = var.domain_name + certificate_arn = var.certificate_arn + hosted_zone_id = var.hosted_zone_id + aws_region = var.aws_region + + # Lambda environment variables + PROVIDER_URI_MAINNET = var.PROVIDER_URI_MAINNET + PROVIDER_URI_SEPOLIA = var.PROVIDER_URI_SEPOLIA + ALCHEMY_URI_MAINNET = var.ALCHEMY_URI_MAINNET + ALCHEMY_URI_SEPOLIA = var.ALCHEMY_URI_SEPOLIA + ENS_SUBGRAPH_URL_MAINNET = var.ENS_SUBGRAPH_URL_MAINNET + ENS_SUBGRAPH_URL_SEPOLIA = var.ENS_SUBGRAPH_URL_SEPOLIA +} \ No newline at end of file diff --git a/apps/api.nameguard.io/terraform/modules/lambda_api/main.tf b/apps/api.nameguard.io/terraform/modules/lambda_api/main.tf new file mode 100644 index 000000000..b8c04ec3a --- /dev/null +++ b/apps/api.nameguard.io/terraform/modules/lambda_api/main.tf @@ -0,0 +1,158 @@ +data "aws_iam_policy_document" "assume_role" { + statement { + effect = "Allow" + + principals { + type = "Service" + identifiers = ["lambda.amazonaws.com"] + } + + actions = ["sts:AssumeRole"] + } +} + +locals { + common_tags = { + Environment = var.env + Project = "nameguard" + ManagedBy = "terraform" + } +} + +resource "aws_iam_role" "iam_for_lambda" { + name = "nameguard-lambda-role-${var.env}" + assume_role_policy = data.aws_iam_policy_document.assume_role.json + tags = local.common_tags +} + +data "aws_iam_policy_document" "lambda_logging" { + statement { + effect = "Allow" + + actions = [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents", + ] + + resources = ["arn:aws:logs:*:*:*"] + } +} + +resource "aws_iam_policy" "lambda_logging" { + name = "nameguard_lambda_logging-${var.env}" + path = "/" + description = "IAM policy for logging from a lambda" + policy = data.aws_iam_policy_document.lambda_logging.json +} + +resource "aws_iam_role_policy_attachment" "lambda_logs" { + role = aws_iam_role.iam_for_lambda.name + policy_arn = aws_iam_policy.lambda_logging.arn +} + + +resource "aws_lambda_function" "nameguard_lambda" { + function_name = "nameguard-lambda-${var.env}" + role = aws_iam_role.iam_for_lambda.arn + memory_size = "1769" + timeout = 60 + package_type = "Image" + image_uri = var.image_uri + architectures = ["arm64"] + publish = true + + environment { + variables = { + PROVIDER_URI_MAINNET = var.PROVIDER_URI_MAINNET + PROVIDER_URI_SEPOLIA = var.PROVIDER_URI_SEPOLIA + ALCHEMY_URI_MAINNET = var.ALCHEMY_URI_MAINNET + ALCHEMY_URI_SEPOLIA = var.ALCHEMY_URI_SEPOLIA + ENS_SUBGRAPH_URL_MAINNET = var.ENS_SUBGRAPH_URL_MAINNET + ENS_SUBGRAPH_URL_SEPOLIA = var.ENS_SUBGRAPH_URL_SEPOLIA + } + } + + tags = merge(local.common_tags, { + Function = "nameguard-api" + }) +} + +resource "aws_lambda_provisioned_concurrency_config" "concurrency_config" { + function_name = aws_lambda_function.nameguard_lambda.function_name + provisioned_concurrent_executions = 1 + qualifier = aws_lambda_function.nameguard_lambda.version +} + +resource "aws_lambda_function_url" "lambda_url" { + function_name = aws_lambda_function.nameguard_lambda.function_name + authorization_type = "NONE" +} + +resource "aws_cloudfront_distribution" "api_distribution" { + enabled = true + comment = "Distribution for nameguard API ${var.env}" + price_class = "PriceClass_100" + + aliases = [var.domain_name] + + origin { + domain_name = split("/", aws_lambda_function_url.lambda_url.function_url)[2] + origin_id = "LambdaFunctionOrigin" + origin_path = "" + + custom_origin_config { + http_port = 80 + https_port = 443 + origin_protocol_policy = "https-only" + origin_ssl_protocols = ["TLSv1.2"] + origin_read_timeout = 60 + origin_keepalive_timeout = 60 + } + } + + default_cache_behavior { + allowed_methods = ["DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT"] + cached_methods = ["GET", "HEAD"] + target_origin_id = "LambdaFunctionOrigin" + viewer_protocol_policy = "redirect-to-https" + + forwarded_values { + query_string = true + headers = ["Origin", "Authorization", "x-api-key"] + + cookies { + forward = "none" + } + } + + default_ttl = 0 + compress = true + } + + restrictions { + geo_restriction { + restriction_type = "none" + } + } + + viewer_certificate { + acm_certificate_arn = var.certificate_arn + ssl_support_method = "sni-only" + minimum_protocol_version = "TLSv1.2_2021" + } + + tags = local.common_tags +} + +resource "aws_route53_record" "dns_record" { + name = var.domain_name + type = "A" + zone_id = var.hosted_zone_id + + alias { + name = aws_cloudfront_distribution.api_distribution.domain_name + zone_id = aws_cloudfront_distribution.api_distribution.hosted_zone_id + evaluate_target_health = false + } +} \ No newline at end of file diff --git a/apps/api.nameguard.io/terraform/modules/lambda_api/variables.tf b/apps/api.nameguard.io/terraform/modules/lambda_api/variables.tf new file mode 100644 index 000000000..7f21bf754 --- /dev/null +++ b/apps/api.nameguard.io/terraform/modules/lambda_api/variables.tf @@ -0,0 +1,66 @@ +variable "env" { + description = "Environment name" + type = string +} + +variable "image_uri" { + description = "URI of the Lambda container image" + type = string +} + +variable "domain_name" { + description = "Custom domain name for Cloudfront" + type = string +} + +variable "certificate_arn" { + description = "ARN of ACM certificate" + type = string +} + +variable "hosted_zone_id" { + description = "Route53 hosted zone ID" + type = string +} + +variable "aws_region" { + description = "AWS region" + type = string +} + +# Lambda environment variables +variable "PROVIDER_URI_MAINNET" { + description = "Provider URI for Mainnet" + type = string + sensitive = true +} + +variable "PROVIDER_URI_SEPOLIA" { + description = "Provider URI for Sepolia" + type = string + sensitive = true +} + +variable "ALCHEMY_URI_MAINNET" { + description = "Alchemy URI for Mainnet" + type = string + sensitive = true +} + +variable "ALCHEMY_URI_SEPOLIA" { + description = "Alchemy URI for Sepolia" + type = string + sensitive = true +} + +variable "ENS_SUBGRAPH_URL_MAINNET" { + description = "ENS Subgraph URL for Mainnet" + type = string + sensitive = true +} + +variable "ENS_SUBGRAPH_URL_SEPOLIA" { + description = "ENS Subgraph URL for Sepolia" + type = string + sensitive = true +} \ No newline at end of file diff --git a/apps/api.nameguard.io/terraform/modules/lambda_api/versions.tf b/apps/api.nameguard.io/terraform/modules/lambda_api/versions.tf new file mode 100644 index 000000000..43e5f649f --- /dev/null +++ b/apps/api.nameguard.io/terraform/modules/lambda_api/versions.tf @@ -0,0 +1,9 @@ +terraform { + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 5.0" + } + } + required_version = ">= 1.0" +} \ No newline at end of file diff --git a/apps/api.nameguard.io/terraform/variables.tf b/apps/api.nameguard.io/terraform/variables.tf new file mode 100644 index 000000000..680e0bf9e --- /dev/null +++ b/apps/api.nameguard.io/terraform/variables.tf @@ -0,0 +1,66 @@ +variable "env" { + description = "Environment name" + type = string +} + +variable "image_uri" { + description = "URI of the Lambda container image" + type = string +} + +variable "domain_name" { + description = "Custom domain name for API" + type = string +} + +variable "certificate_arn" { + description = "ARN of ACM certificate" + type = string +} + +variable "hosted_zone_id" { + description = "Route53 hosted zone ID" + type = string +} + +variable "aws_region" { + description = "AWS region" + type = string +} + +# Lambda environment variables +variable "PROVIDER_URI_MAINNET" { + description = "Provider URI for Mainnet" + type = string + sensitive = true +} + +variable "PROVIDER_URI_SEPOLIA" { + description = "Provider URI for Sepolia" + type = string + sensitive = true +} + +variable "ALCHEMY_URI_MAINNET" { + description = "Alchemy URI for Mainnet" + type = string + sensitive = true +} + +variable "ALCHEMY_URI_SEPOLIA" { + description = "Alchemy URI for Sepolia" + type = string + sensitive = true +} + +variable "ENS_SUBGRAPH_URL_MAINNET" { + description = "ENS Subgraph URL for Mainnet" + type = string + sensitive = true +} + +variable "ENS_SUBGRAPH_URL_SEPOLIA" { + description = "ENS Subgraph URL for Sepolia" + type = string + sensitive = true +} \ No newline at end of file diff --git a/apps/docs.nameguard.io/background.png b/apps/docs.nameguard.io/background.png deleted file mode 100644 index 6437076be..000000000 Binary files a/apps/docs.nameguard.io/background.png and /dev/null differ diff --git a/apps/docs.nameguard.io/mint.json b/apps/docs.nameguard.io/mint.json deleted file mode 100644 index c20d19359..000000000 --- a/apps/docs.nameguard.io/mint.json +++ /dev/null @@ -1,83 +0,0 @@ -{ - "$schema": "https://mintlify.com/schema.json", - "name": "NameGuard", - "logo": { - "dark": "/logo/dark.svg", - "light": "/logo/light.svg" - }, - "favicon": "/favicon.png", - "backgroundImage": "/background.png", - "colors": { - "primary": "#00AA67", - "light": "#00AA67", - "dark": "#00AA67", - "anchors": { - "from": "#FF7F57", - "to": "#9563FF" - } - }, - "topbarLinks": [ - { - "name": "Support", - "url": "mailto:hello@namehashlabs.org" - } - ], - "topbarCtaButton": { - "name": "Website", - "url": "https://nameguard.io" - }, - "openapi": "https://api.nameguard.io/openapi.json", - "navigation": [ - { - "group": "NameGuard", - "pages": [ - "introduction", - "quickstart", - "specification", - "chains", - "self-hosting" - ] - }, - { - "group": "SDK", - "pages": [ - "sdk/js/setup", - "sdk/js/typescript", - { - "group": "Checks", - "pages": [ - "sdk/js/inspect-name", - "sdk/js/bulk-inspect-names", - "sdk/js/inspect-namehash", - "sdk/js/inspect-labelhash", - "sdk/js/secure-primary-name", - "sdk/js/fake-eth-name-check", - "sdk/js/inspect-grapheme" - ] - } - ] - }, - { - "group": "API", - "pages": [ - "api-reference/introduction", - { - "group": "Endpoints", - "pages": [ - "api-reference/inspect-name", - "api-reference/bulk-inspect-names", - "api-reference/inspect-namehash", - "api-reference/inspect-labelhash", - "api-reference/secure-primary-name", - "api-reference/fake-eth-name-check", - "api-reference/inspect-grapheme" - ] - } - ] - } - ], - "footerSocials": { - "twitter": "https://x.com/NamehashLabs", - "github": "https://github.com/namehash" - } -} diff --git a/apps/docs.nameguard.io/LICENSE b/apps/docs.namekit.io/LICENSE similarity index 100% rename from apps/docs.nameguard.io/LICENSE rename to apps/docs.namekit.io/LICENSE diff --git a/apps/docs.nameguard.io/README.md b/apps/docs.namekit.io/README.md similarity index 100% rename from apps/docs.nameguard.io/README.md rename to apps/docs.namekit.io/README.md diff --git a/apps/docs.nameguard.io/favicon.png b/apps/docs.namekit.io/favicon.png similarity index 100% rename from apps/docs.nameguard.io/favicon.png rename to apps/docs.namekit.io/favicon.png diff --git a/apps/docs.namekit.io/introduction.mdx b/apps/docs.namekit.io/introduction.mdx new file mode 100644 index 000000000..9059ab0ea --- /dev/null +++ b/apps/docs.namekit.io/introduction.mdx @@ -0,0 +1,5 @@ +--- +title: Welcome to NameKit +--- + +Hello world diff --git a/apps/docs.nameguard.io/logo/dark.svg b/apps/docs.namekit.io/logo/dark.svg similarity index 100% rename from apps/docs.nameguard.io/logo/dark.svg rename to apps/docs.namekit.io/logo/dark.svg diff --git a/apps/docs.nameguard.io/logo/light.svg b/apps/docs.namekit.io/logo/light.svg similarity index 100% rename from apps/docs.nameguard.io/logo/light.svg rename to apps/docs.namekit.io/logo/light.svg diff --git a/apps/docs.namekit.io/mint.json b/apps/docs.namekit.io/mint.json new file mode 100644 index 000000000..52b52842c --- /dev/null +++ b/apps/docs.namekit.io/mint.json @@ -0,0 +1,112 @@ +{ + "$schema": "https://mintlify.com/schema.json", + "name": "NameKit", + "theme": "quill", + "logo": { + "dark": "/logo/dark.svg", + "light": "/logo/light.svg" + }, + "favicon": "/favicon.png", + "backgroundImage": "/background.png", + "colors": { + "primary": "#00AA67", + "light": "#00AA67", + "dark": "#00AA67", + "anchors": { + "from": "#FF7F57", + "to": "#9563FF" + } + }, + "topbarLinks": [ + { + "name": "Support", + "url": "mailto:hello@namehashlabs.org" + } + ], + "topbarCtaButton": { + "type": "github", + "url": "https://github.com/namehash/namekit" + }, + "tabs": [ + { + "name": "NameGuard", + "url": "nameguard" + }, + { + "name": "NameRank", + "url": "namerank" + }, + { + "name": "NameGenerator", + "url": "namegenerator" + } + ], + "openapi": "https://api.nameguard.io/openapi.json", + "navigation": [ + { + "group": "NameKit", + "pages": ["introduction"] + }, + { + "group": "NameGuard", + "pages": [ + "nameguard/introduction", + "nameguard/quickstart", + "nameguard/specification", + "nameguard/chains", + "nameguard/self-hosting" + ] + }, + { + "group": "SDK", + "pages": [ + "nameguard/sdk/js/setup", + "nameguard/sdk/js/typescript", + { + "group": "Checks", + "pages": [ + "nameguard/sdk/js/inspect-name", + "nameguard/sdk/js/bulk-inspect-names", + "nameguard/sdk/js/inspect-namehash", + "nameguard/sdk/js/inspect-labelhash", + "nameguard/sdk/js/secure-primary-name", + "nameguard/sdk/js/fake-eth-name-check", + "nameguard/sdk/js/inspect-grapheme" + ] + } + ] + }, + { + "group": "API", + "pages": [ + "nameguard/api-reference/introduction", + { + "group": "Endpoints", + "pages": [ + "nameguard/api-reference/inspect-name", + "nameguard/api-reference/bulk-inspect-names", + "nameguard/api-reference/inspect-namehash", + "nameguard/api-reference/inspect-labelhash", + "nameguard/api-reference/secure-primary-name", + "nameguard/api-reference/fake-eth-name-check", + "nameguard/api-reference/inspect-grapheme" + ] + } + ] + }, + + { + "group": "NameRank", + "pages": ["namerank/introduction"] + }, + + { + "group": "NameGenerator", + "pages": ["namegenerator/introduction"] + } + ], + "footerSocials": { + "twitter": "https://x.com/NamehashLabs", + "github": "https://github.com/namehash" + } +} diff --git a/apps/docs.namekit.io/namegenerator/introduction.mdx b/apps/docs.namekit.io/namegenerator/introduction.mdx new file mode 100644 index 000000000..2b1c0ddfc --- /dev/null +++ b/apps/docs.namekit.io/namegenerator/introduction.mdx @@ -0,0 +1,5 @@ +--- +title: Introduction +--- + +Hello world diff --git a/apps/docs.nameguard.io/api-reference/bulk-inspect-names.mdx b/apps/docs.namekit.io/nameguard/api-reference/bulk-inspect-names.mdx similarity index 100% rename from apps/docs.nameguard.io/api-reference/bulk-inspect-names.mdx rename to apps/docs.namekit.io/nameguard/api-reference/bulk-inspect-names.mdx diff --git a/apps/docs.nameguard.io/api-reference/fake-eth-name-check.mdx b/apps/docs.namekit.io/nameguard/api-reference/fake-eth-name-check.mdx similarity index 100% rename from apps/docs.nameguard.io/api-reference/fake-eth-name-check.mdx rename to apps/docs.namekit.io/nameguard/api-reference/fake-eth-name-check.mdx diff --git a/apps/docs.nameguard.io/api-reference/inspect-grapheme.mdx b/apps/docs.namekit.io/nameguard/api-reference/inspect-grapheme.mdx similarity index 100% rename from apps/docs.nameguard.io/api-reference/inspect-grapheme.mdx rename to apps/docs.namekit.io/nameguard/api-reference/inspect-grapheme.mdx diff --git a/apps/docs.nameguard.io/api-reference/inspect-labelhash.mdx b/apps/docs.namekit.io/nameguard/api-reference/inspect-labelhash.mdx similarity index 100% rename from apps/docs.nameguard.io/api-reference/inspect-labelhash.mdx rename to apps/docs.namekit.io/nameguard/api-reference/inspect-labelhash.mdx diff --git a/apps/docs.nameguard.io/api-reference/inspect-name.mdx b/apps/docs.namekit.io/nameguard/api-reference/inspect-name.mdx similarity index 100% rename from apps/docs.nameguard.io/api-reference/inspect-name.mdx rename to apps/docs.namekit.io/nameguard/api-reference/inspect-name.mdx diff --git a/apps/docs.nameguard.io/api-reference/inspect-namehash.mdx b/apps/docs.namekit.io/nameguard/api-reference/inspect-namehash.mdx similarity index 100% rename from apps/docs.nameguard.io/api-reference/inspect-namehash.mdx rename to apps/docs.namekit.io/nameguard/api-reference/inspect-namehash.mdx diff --git a/apps/docs.nameguard.io/api-reference/introduction.mdx b/apps/docs.namekit.io/nameguard/api-reference/introduction.mdx similarity index 100% rename from apps/docs.nameguard.io/api-reference/introduction.mdx rename to apps/docs.namekit.io/nameguard/api-reference/introduction.mdx diff --git a/apps/docs.nameguard.io/api-reference/secure-primary-name.mdx b/apps/docs.namekit.io/nameguard/api-reference/secure-primary-name.mdx similarity index 100% rename from apps/docs.nameguard.io/api-reference/secure-primary-name.mdx rename to apps/docs.namekit.io/nameguard/api-reference/secure-primary-name.mdx diff --git a/apps/docs.nameguard.io/chains.mdx b/apps/docs.namekit.io/nameguard/chains.mdx similarity index 100% rename from apps/docs.nameguard.io/chains.mdx rename to apps/docs.namekit.io/nameguard/chains.mdx diff --git a/apps/docs.nameguard.io/faqs.mdx b/apps/docs.namekit.io/nameguard/faqs.mdx similarity index 100% rename from apps/docs.nameguard.io/faqs.mdx rename to apps/docs.namekit.io/nameguard/faqs.mdx diff --git a/apps/docs.nameguard.io/introduction.mdx b/apps/docs.namekit.io/nameguard/introduction.mdx similarity index 100% rename from apps/docs.nameguard.io/introduction.mdx rename to apps/docs.namekit.io/nameguard/introduction.mdx diff --git a/apps/docs.nameguard.io/quickstart.mdx b/apps/docs.namekit.io/nameguard/quickstart.mdx similarity index 100% rename from apps/docs.nameguard.io/quickstart.mdx rename to apps/docs.namekit.io/nameguard/quickstart.mdx diff --git a/apps/docs.nameguard.io/sdk/js/bulk-inspect-names.mdx b/apps/docs.namekit.io/nameguard/sdk/js/bulk-inspect-names.mdx similarity index 100% rename from apps/docs.nameguard.io/sdk/js/bulk-inspect-names.mdx rename to apps/docs.namekit.io/nameguard/sdk/js/bulk-inspect-names.mdx diff --git a/apps/docs.nameguard.io/sdk/js/fake-eth-name-check.mdx b/apps/docs.namekit.io/nameguard/sdk/js/fake-eth-name-check.mdx similarity index 100% rename from apps/docs.nameguard.io/sdk/js/fake-eth-name-check.mdx rename to apps/docs.namekit.io/nameguard/sdk/js/fake-eth-name-check.mdx diff --git a/apps/docs.nameguard.io/sdk/js/inspect-grapheme.mdx b/apps/docs.namekit.io/nameguard/sdk/js/inspect-grapheme.mdx similarity index 100% rename from apps/docs.nameguard.io/sdk/js/inspect-grapheme.mdx rename to apps/docs.namekit.io/nameguard/sdk/js/inspect-grapheme.mdx diff --git a/apps/docs.nameguard.io/sdk/js/inspect-labelhash.mdx b/apps/docs.namekit.io/nameguard/sdk/js/inspect-labelhash.mdx similarity index 100% rename from apps/docs.nameguard.io/sdk/js/inspect-labelhash.mdx rename to apps/docs.namekit.io/nameguard/sdk/js/inspect-labelhash.mdx diff --git a/apps/docs.nameguard.io/sdk/js/inspect-name.mdx b/apps/docs.namekit.io/nameguard/sdk/js/inspect-name.mdx similarity index 100% rename from apps/docs.nameguard.io/sdk/js/inspect-name.mdx rename to apps/docs.namekit.io/nameguard/sdk/js/inspect-name.mdx diff --git a/apps/docs.nameguard.io/sdk/js/inspect-namehash.mdx b/apps/docs.namekit.io/nameguard/sdk/js/inspect-namehash.mdx similarity index 100% rename from apps/docs.nameguard.io/sdk/js/inspect-namehash.mdx rename to apps/docs.namekit.io/nameguard/sdk/js/inspect-namehash.mdx diff --git a/apps/docs.nameguard.io/sdk/js/secure-primary-name.mdx b/apps/docs.namekit.io/nameguard/sdk/js/secure-primary-name.mdx similarity index 100% rename from apps/docs.nameguard.io/sdk/js/secure-primary-name.mdx rename to apps/docs.namekit.io/nameguard/sdk/js/secure-primary-name.mdx diff --git a/apps/docs.nameguard.io/sdk/js/setup.mdx b/apps/docs.namekit.io/nameguard/sdk/js/setup.mdx similarity index 100% rename from apps/docs.nameguard.io/sdk/js/setup.mdx rename to apps/docs.namekit.io/nameguard/sdk/js/setup.mdx diff --git a/apps/docs.nameguard.io/sdk/js/typescript.mdx b/apps/docs.namekit.io/nameguard/sdk/js/typescript.mdx similarity index 100% rename from apps/docs.nameguard.io/sdk/js/typescript.mdx rename to apps/docs.namekit.io/nameguard/sdk/js/typescript.mdx diff --git a/apps/docs.nameguard.io/self-hosting.mdx b/apps/docs.namekit.io/nameguard/self-hosting.mdx similarity index 100% rename from apps/docs.nameguard.io/self-hosting.mdx rename to apps/docs.namekit.io/nameguard/self-hosting.mdx diff --git a/apps/docs.nameguard.io/specification.mdx b/apps/docs.namekit.io/nameguard/specification.mdx similarity index 100% rename from apps/docs.nameguard.io/specification.mdx rename to apps/docs.namekit.io/nameguard/specification.mdx diff --git a/apps/docs.namekit.io/namerank/introduction.mdx b/apps/docs.namekit.io/namerank/introduction.mdx new file mode 100644 index 000000000..2b1c0ddfc --- /dev/null +++ b/apps/docs.namekit.io/namerank/introduction.mdx @@ -0,0 +1,5 @@ +--- +title: Introduction +--- + +Hello world diff --git a/apps/examples.nameguard.io/package.json b/apps/examples.nameguard.io/package.json index 005ce894e..a26f9a8c3 100644 --- a/apps/examples.nameguard.io/package.json +++ b/apps/examples.nameguard.io/package.json @@ -13,7 +13,7 @@ "@namehash/nameguard-react": "workspace:*", "@namehash/namekit-react": "workspace:*", "classcat": "5.0.5", - "next": "14.2.13", + "next": "14.2.21", "react": "18.3.1", "react-dom": "18.3.1", "sharp": "0.33.5", @@ -21,11 +21,11 @@ }, "devDependencies": { "@types/node": "22.7.4", - "@types/react": "18.3.11", - "@types/react-dom": "18.3.0", + "@types/react": "18.3.1", + "@types/react-dom": "18.3.1", "autoprefixer": "10.4.20", "eslint": "8.57.1", - "eslint-config-next": "14.2.3", + "eslint-config-next": "14.2.14", "postcss": "8.4.47", "tailwindcss": "3.4.13", "typescript": "5.6.2" diff --git a/apps/namegraph.dev/.eslintrc.json b/apps/namegraph.dev/.eslintrc.json new file mode 100644 index 000000000..bffb357a7 --- /dev/null +++ b/apps/namegraph.dev/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "next/core-web-vitals" +} diff --git a/apps/namegraph.dev/.gitignore b/apps/namegraph.dev/.gitignore new file mode 100644 index 000000000..fd3dbb571 --- /dev/null +++ b/apps/namegraph.dev/.gitignore @@ -0,0 +1,36 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js +.yarn/install-state.gz + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/apps/namegraph.dev/CHANGELOG.md b/apps/namegraph.dev/CHANGELOG.md new file mode 100644 index 000000000..05f6377e7 --- /dev/null +++ b/apps/namegraph.dev/CHANGELOG.md @@ -0,0 +1,12 @@ +# namegraph.dev + +## 0.2.0 + +### Minor Changes + +- 4361a2b: Create MVP of namegraph.dev app + +### Patch Changes + +- Updated dependencies [d5183f1] + - @namehash/namekit-react@0.9.0 diff --git a/apps/namegraph.dev/LICENSE b/apps/namegraph.dev/LICENSE new file mode 100644 index 000000000..f6a94f8b1 --- /dev/null +++ b/apps/namegraph.dev/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 NameHash + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/apps/namegraph.dev/README.md b/apps/namegraph.dev/README.md new file mode 100644 index 000000000..8373e2c83 --- /dev/null +++ b/apps/namegraph.dev/README.md @@ -0,0 +1,3 @@ +# NameGraph SDK use cases + +This is a [Next.js](https://nextjs.org/) application that showcases different [`NameGraph SDK`](https://github.com/namehash/namekit/tree/main/packages/namegraph-sdk) use cases. diff --git a/apps/namegraph.dev/app/explore-collections/page.tsx b/apps/namegraph.dev/app/explore-collections/page.tsx new file mode 100644 index 000000000..c39306cc7 --- /dev/null +++ b/apps/namegraph.dev/app/explore-collections/page.tsx @@ -0,0 +1,349 @@ +"use client"; + +import { + getCategoryID, + QuickJumpsByCategory, +} from "@/components/mini-apps/explore-collections/quick-jumps-by-category"; +import { SuggestionCategory } from "@/components/mini-apps/explore-collections/suggestion-category"; +import { + NameGraphFetchTopCollectionMembersResponse, + NameGraphGroupedByCategoryResponse, +} from "@namehash/namegraph-sdk/utils"; +import { getCollectionsForQuery } from "@/lib/utils"; +import { DebounceInput } from "react-debounce-input"; +import { useEffect, useState } from "react"; +import lodash from "lodash"; +import { MagnifyingGlassCircleIcon } from "@heroicons/react/24/solid"; + +const SUGGESTION_CATEGORY_CLASSNAME = "suggestionCategory"; + +export default function ExploreCollectionsPage() { + /** + * nameIdeas state: + * + * undefined is set when component never tried querying name ideas + * null is set when component tried querying name ideas but failed + * NameGraphGroupedByCategoryResponse is set when name ideas were successfully queried + */ + const [nameIdeas, setNameIdeas] = useState< + undefined | null | NameGraphGroupedByCategoryResponse + >(undefined); + + const [nameIdeasLoading, setNameIdeasLoading] = useState(true); + + const [debouncedValue, setDebouncedValue] = useState(""); + + useEffect(() => { + if (debouncedValue) { + let query = debouncedValue; + if (debouncedValue.includes(".")) { + query = debouncedValue.split(".")[0]; + } + + setNameIdeas(undefined); + setNameIdeasLoading(true); + getCollectionsForQuery(query) + .then((res) => setNameIdeas(res)) + .catch(() => setNameIdeas(null)) + .finally(() => setNameIdeasLoading(false)); + } else { + setNameIdeasLoading(false); + } + }, [debouncedValue]); + + const [activeCategoryID, setActiveCategoryID] = useState(""); + + useEffect(() => { + if (nameIdeas?.categories.length && !activeCategoryID) { + setActiveCategoryID(getCategoryID(nameIdeas.categories[0])); + } + }, [nameIdeas?.categories]); + + const setFirstQuickJumpPillAsActive = () => { + const firstCollectionPill = document.querySelector(".collectionPill"); + const categoryID = firstCollectionPill?.getAttribute( + "data-navigation-item", + ); + + if (categoryID) { + setActiveCategoryID(categoryID); + } + }; + + const setActiveQuickJumpPill = () => { + const scrollableContainer = document.getElementById("scrollable-elm"); + + if (scrollableContainer) { + console.log(scrollableContainer.getBoundingClientRect()); + + // if the container was not yet scrolled + if ( + scrollableContainer.scrollTop < + scrollableContainer?.getBoundingClientRect().y || + !scrollableContainer.scrollTop + ) { + setFirstQuickJumpPillAsActive(); + return; + } + + const containerScrollTopPosition = + scrollableContainer?.getBoundingClientRect().top + + scrollableContainer?.scrollTop; + const containerScrollBottomPosition = + scrollableContainer?.getBoundingClientRect().bottom + + scrollableContainer?.scrollTop; + + const categories = document.querySelectorAll( + `.${SUGGESTION_CATEGORY_CLASSNAME}`, + ); + + categories.forEach((category, idx) => { + if (idx === 0) { + const firstCategoryBottom = category.getAttribute( + "data-category-bottom", + ); + const secondCategoryTop = + categories[idx + 1].getAttribute("data-category-top"); + + if ( + containerScrollTopPosition <= Number(firstCategoryBottom) || + containerScrollBottomPosition < Number(secondCategoryTop) + ) { + setActiveCategoryID(category.id); + } + } else { + const currentCategoryTop = category.getAttribute("data-category-top"); + const currentCategoryBottom = category.getAttribute( + "data-category-bottom", + ); + + if ( + containerScrollTopPosition <= Number(currentCategoryBottom) && + containerScrollTopPosition >= Number(currentCategoryTop) + ) { + setActiveCategoryID(category.id); + } + } + }); + + // if the container was scrolled to its bottom + if ( + scrollableContainer.scrollHeight && + scrollableContainer.scrollTop && + scrollableContainer.clientHeight + scrollableContainer.scrollTop >= + scrollableContainer.scrollHeight + ) { + const lastCategory = categories[categories.length - 1]; + + if (lastCategory) { + setActiveCategoryID(lastCategory.id); + } + } + } + }; + const ACTIVE_QUICK_JUMP_PILL_CLASSNAME = "activeQuickJumpPill"; + const clearActiveQuickJumpPills = () => { + const activeCollectionPills = document.querySelectorAll( + `.${ACTIVE_QUICK_JUMP_PILL_CLASSNAME}`, + ); + activeCollectionPills.forEach((pill) => { + pill.classList.remove(ACTIVE_QUICK_JUMP_PILL_CLASSNAME); + }); + }; + + useEffect(() => { + const wrapper = document.getElementById("scrollable-elm"); + + if (wrapper) { + wrapper.addEventListener( + "scroll", + lodash.debounce(setActiveQuickJumpPill, 100), + ); + wrapper.addEventListener( + "resize", + lodash.debounce(setActiveQuickJumpPill, 100), + ); + } + + return () => { + if (wrapper) { + wrapper.removeEventListener( + "scroll", + lodash.debounce(setActiveQuickJumpPill, 100), + ); + wrapper.removeEventListener( + "resize", + lodash.debounce(setActiveQuickJumpPill, 100), + ); + } + }; + }, []); + + useEffect(() => { + if (nameIdeas) { + setActiveQuickJumpPill(); + } + }, [nameIdeas]); + + useEffect(() => { + if (activeCategoryID) { + clearActiveQuickJumpPills(); + + const collectionPill = document.querySelector( + '[data-collection-pill="' + activeCategoryID + '"]', + ); + collectionPill?.classList.add(ACTIVE_QUICK_JUMP_PILL_CLASSNAME); + } + }, [activeCategoryID]); + + return ( +
+
+
+
+
+

+ 🔎 Search for a name and see name ideas ⬇️ +

+
+
+ setDebouncedValue(e.target.value)} + className="w-full bg-gray-100 border border-gray-300 rounded-md p-3 px-4" + /> + {nameIdeasLoading ? ( + <> + {/* Display a Loading icon if the dApp query is being typed by the visitor */} +
+ + Loading... +
+ + ) : debouncedValue ? ( + <> + {/* Display a Check (similar to ✅) if the dApp submitted the debouncedValue query to NameGraph SDK */} + + + ) : ( + <> + {/* Display a Magnifying glass icon if the dApp is awaiting for a query */} + + + )} +
+

+ Use NameGraph SDK to generate multiple name ideas suggestions for + a single search. This works just as typing something like{" "} + {"'Batman'"} and getting back multiple name suggestions for + different categories related to Batman just as{" "} + {"'Batman Creators'"}, {"'Animated Batman Films'"},{" "} + {"'Batman Supporting Characters'"} and much more! +

+
+
+
+
+ {/* + NameIdeas component uses two states for loading + state management: reqLoading and allSuggestionsLoading. + + reqLoading is used to manage the loading state of the + entire NameIdeas component, while allSuggestionsLoading + is used to manage the loading state of the QuickJumpsByCategory. + + QuickJumpsByCategory loading directly affects reqLoading state. + It needs to be displayed as soon as we have the categories + available, so we can calculate the need of navigation arrows + displaying. This is why we use allSuggestionsLoading state + to manage the loading state of QuickJumpsByCategory. + + Once QuickJumpsByCategory is ready (which means, once it + knows the suggestedCategories and it knows wether to + show navigation arrows or not) we update reqLoading, + syncing the loading state of the entire NameIdeas. + */} + {debouncedValue && (nameIdeasLoading || nameIdeas) ? ( + <> +
+
+ +
+
+ + ) : null} + {debouncedValue && ( +
+
+ {nameIdeas + ? nameIdeas.categories.map( + ( + category: NameGraphFetchTopCollectionMembersResponse, + idx: number, + ) => { + return ( +
+ +
+ ); + }, + ) + : null} +
+
+
+ )} +
+
+
+
+
+
+ ); +} diff --git a/apps/namegraph.dev/app/favicon.ico b/apps/namegraph.dev/app/favicon.ico new file mode 100644 index 000000000..e6d169e81 Binary files /dev/null and b/apps/namegraph.dev/app/favicon.ico differ diff --git a/apps/namegraph.dev/app/globals.css b/apps/namegraph.dev/app/globals.css new file mode 100644 index 000000000..7c284fc2c --- /dev/null +++ b/apps/namegraph.dev/app/globals.css @@ -0,0 +1,49 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +:root { + --foreground-rgb: 0, 0, 0; + --background-start-rgb: 214, 219, 220; + --background-end-rgb: 255, 255, 255; +} + +@media (prefers-color-scheme: dark) { + :root { + --foreground-rgb: 255, 255, 255; + --background-start-rgb: 0, 0, 0; + --background-end-rgb: 0, 0, 0; + } +} + +@layer utilities { + .text-balance { + text-wrap: balance; + } +} + +@layer base { + :root { + --radius: 0.5rem; + } +} + +.collectionPill.activeQuickJumpPill { + background: black; + color: white; +} + +.collectionPill.activeQuickJumpPill:hover { + background: #1f2937; +} + +.scrollbar-hide { + /* IE and Edge */ + -ms-overflow-style: none; + /* Firefox */ + scrollbar-width: none; +} +/* Safari and Chrome */ +.scrollbar-hide::-webkit-scrollbar { + display: none; +} diff --git a/apps/namegraph.dev/app/ideate/page.tsx b/apps/namegraph.dev/app/ideate/page.tsx new file mode 100644 index 000000000..c4c640ced --- /dev/null +++ b/apps/namegraph.dev/app/ideate/page.tsx @@ -0,0 +1,50 @@ +"use client"; + +import { useEffect } from "react"; +import { useState } from "react"; +import { writersBlockSuggestions } from "@/lib/writers-block-suggestions"; +import { + sampleWritersBlockSuggestions, + WritersBlockCollection, + WritersBlockSuggestion, +} from "@namehash/namegraph-sdk/utils"; +import { WritersBlockPills } from "@/components/mini-apps/ideate/writers-block-pills"; +import { Catalog } from "@/components/mini-apps/ideate/catalog"; + +export default function IdeatePage() { + const [suggestions, setSuggestions] = useState([]); + const [collectionsToConsider, setCollectionsToConsider] = useState< + WritersBlockCollection[] + >(writersBlockSuggestions); + + const ideate = (catalog: WritersBlockCollection[]) => { + const wbSuggestions = sampleWritersBlockSuggestions(5, catalog); + setSuggestions(wbSuggestions); + }; + + const onCatalogChange = (parsedJSON: any) => { + setCollectionsToConsider(parsedJSON); + }; + + useEffect(() => { + ideate(writersBlockSuggestions); + }, []); + + return ( +
+
+
+
+ ideate(collectionsToConsider)} + /> +
+
+
+ +
+
+
+ ); +} diff --git a/apps/namegraph.dev/app/layout.tsx b/apps/namegraph.dev/app/layout.tsx new file mode 100644 index 000000000..43c72abf1 --- /dev/null +++ b/apps/namegraph.dev/app/layout.tsx @@ -0,0 +1,238 @@ +import { Inter } from "next/font/google"; +import "./globals.css"; +import Link from "next/link"; +import { NameHashLabsLogo } from "@/components/footer/namehash-labs-logo"; +import { ServiceProviderBadge } from "@/components/footer/service-provider-badge"; +import { EmailIcon } from "../components/footer/email-icon"; +import { GithubIcon } from "@/components/footer/github-icon"; +import { TwitterIcon } from "@/components/footer/twitter-icon"; +import { FarcasterIcon } from "@/components/footer/farcaster-icon"; +import { TelegramIcon } from "@/components/footer/telegram-icon"; +import { Button } from "@/components/ui/button"; +import "@namehash/namekit-react/styles.css"; +import NextLink from "next/link"; + +const inter = Inter({ subsets: ["latin"] }); + +export const metadata = { + title: "NameGraph", + description: "Explore and discover names", +}; + +const footerProducts = [ + { + name: "NameKit", + href: "https://namekit.io", + }, + { + name: "NameGuard", + href: "https://nameguard.io", + }, + { + name: "ENS Referral Program", + href: "/ens-referral-program", + }, +]; + +const footerResources = [ + { + name: "Contact us", + href: "https://namehashlabs.org/contact", + }, + { + name: "Careers", + href: "https://namehashlabs.org/careers", + }, + { + name: "Partners", + href: "https://namehashlabs.org/partners", + }, + { + name: "Brand assets", + href: "https://namehashlabs.org/brand-assets", + }, +]; + +export default function RootLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( + + +
+
+
+
+ + +
+

+ beta +

+
+
+
+
+
+
+ + + +
+ + +
+
+
+
+
{children}
+
+
+
+
+ + +

+ Founded in 2022, Namehash Labs is a technology organization + dedicated to infrastructure-level solutions that helps the + Ethereum Name Service (ENS) Protocol grow. +

+ + +
+ +
+
+ Products + +
+
+ + Resources + + +
+
+
+ +
+

+ © NameHash Labs. All Rights Reserved +

+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ + Made with + {"❤️"} + by + + + NameHash Labs + +
+
+
+
+
+ + + ); +} diff --git a/apps/namegraph.dev/app/page.tsx b/apps/namegraph.dev/app/page.tsx new file mode 100644 index 000000000..8e09990a5 --- /dev/null +++ b/apps/namegraph.dev/app/page.tsx @@ -0,0 +1,34 @@ +"use client"; + +import Link from "next/link"; + +export default function HomePage() { + return ( +
+
+

+ Welcome to NameGraph mini-apps +

+

+ Here you will be able to access different examples of how you can make + usage of NameGraph SDK +

+ +
    +
  • +

    ➡️

    + + Ideate + +
  • +
  • +

    ➡️

    + + Explore collections + +
  • +
+
+
+ ); +} diff --git a/apps/namegraph.dev/components.json b/apps/namegraph.dev/components.json new file mode 100644 index 000000000..dbcd4305a --- /dev/null +++ b/apps/namegraph.dev/components.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "tailwind.config.ts", + "css": "app/globals.css", + "baseColor": "neutral", + "cssVariables": false, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "iconLibrary": "lucide" +} \ No newline at end of file diff --git a/apps/namegraph.dev/components/footer/email-icon.tsx b/apps/namegraph.dev/components/footer/email-icon.tsx new file mode 100644 index 000000000..24bb8d9d7 --- /dev/null +++ b/apps/namegraph.dev/components/footer/email-icon.tsx @@ -0,0 +1,13 @@ +export const EmailIcon = (props: React.SVGProps) => ( + + + + +); diff --git a/apps/namegraph.dev/components/footer/farcaster-icon.tsx b/apps/namegraph.dev/components/footer/farcaster-icon.tsx new file mode 100644 index 000000000..f734791aa --- /dev/null +++ b/apps/namegraph.dev/components/footer/farcaster-icon.tsx @@ -0,0 +1,25 @@ +export const FarcasterIcon = (props: React.SVGProps) => { + return ( + + + + + + ); +}; diff --git a/apps/namegraph.dev/components/footer/github-icon.tsx b/apps/namegraph.dev/components/footer/github-icon.tsx new file mode 100644 index 000000000..80fa44694 --- /dev/null +++ b/apps/namegraph.dev/components/footer/github-icon.tsx @@ -0,0 +1,13 @@ +export const GithubIcon = (props: React.SVGProps) => { + return ( + + + + ); +}; diff --git a/apps/namegraph.dev/components/footer/namehash-labs-logo.tsx b/apps/namegraph.dev/components/footer/namehash-labs-logo.tsx new file mode 100644 index 000000000..da0c9f788 --- /dev/null +++ b/apps/namegraph.dev/components/footer/namehash-labs-logo.tsx @@ -0,0 +1,67 @@ +import { SVGProps } from "react"; + +export const NameHashLabsLogo = (props: SVGProps) => { + return ( + + + + + + + + + + + + + + + + ); +}; diff --git a/apps/namegraph.dev/components/footer/service-provider-badge.tsx b/apps/namegraph.dev/components/footer/service-provider-badge.tsx new file mode 100644 index 000000000..073f6a2c8 --- /dev/null +++ b/apps/namegraph.dev/components/footer/service-provider-badge.tsx @@ -0,0 +1,114 @@ +export const ServiceProviderBadge = ( + props: React.HTMLAttributes +) => { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +}; diff --git a/apps/namegraph.dev/components/footer/telegram-icon.tsx b/apps/namegraph.dev/components/footer/telegram-icon.tsx new file mode 100644 index 000000000..0a3a3a34e --- /dev/null +++ b/apps/namegraph.dev/components/footer/telegram-icon.tsx @@ -0,0 +1,12 @@ +export const TelegramIcon = (props: React.SVGProps) => ( + + + +); diff --git a/apps/namegraph.dev/components/footer/twitter-icon.tsx b/apps/namegraph.dev/components/footer/twitter-icon.tsx new file mode 100644 index 000000000..bd13e7f70 --- /dev/null +++ b/apps/namegraph.dev/components/footer/twitter-icon.tsx @@ -0,0 +1,13 @@ +export const TwitterIcon = (props: React.SVGProps) => { + return ( + + + + ); +}; diff --git a/apps/namegraph.dev/components/mini-apps/explore-collections/arrow-navigation-bar.tsx b/apps/namegraph.dev/components/mini-apps/explore-collections/arrow-navigation-bar.tsx new file mode 100644 index 000000000..bd4282b56 --- /dev/null +++ b/apps/namegraph.dev/components/mini-apps/explore-collections/arrow-navigation-bar.tsx @@ -0,0 +1,229 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +import { ChevronLeftIcon, ChevronRightIcon } from "@heroicons/react/24/solid"; +import { useEffect, useRef, useState } from "react"; +import lodash from "lodash"; +import cc from "classcat"; + +interface ArrowNavigationBarProps { + centerID?: string; + skeletonMarkup: JSX.Element; + barContentMarkup: JSX.Element; +} + +enum ScrollDirection { + LEFT, + RIGHT, +} + +interface ShowNavButtons { + left: boolean; + right: boolean; +} + +/* + Below number represents X in the following logic: Once + user clicks some navigation arrow, we scroll the bar Y pixels + - X pixels to the right or left. We always scroll a bit less than + the full width of the scroller element. This results in users + seeing a part of the navigation bar that was already being + seen before. This is a good UX practice to avoid users + getting lost when seeing the new visible content 👨🏼‍💻 +*/ +const VISIBLE_SCROLLER_WIDTH_AFTER_NAVIGATOR_CLICK = 75; + +/* + After the user uses navigation buttons to scroll + the 'barContentMarkup' scroller, we need to wait a while + so the DOM updates its properties. Later, we calculate + if we need to display the navigation buttons or not. +*/ +const DELAY_FOR_DOM_PROPERTY_UPDATE = 2000; + +export const ArrowNavigationBar = ({ + centerID, + skeletonMarkup, + barContentMarkup, +}: ArrowNavigationBarProps) => { + const navigationBarWrapper = useRef(null); + + const [showNavButtons, setShowNavButtons] = useState< + ShowNavButtons | undefined + >(undefined); + + useEffect(() => { + setDisplayOfInfiniteShadowsAndNavigationButtons(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [navigationBarWrapper]); + + useEffect(() => { + if (window) { + window.addEventListener( + "resize", + lodash.debounce(setDisplayOfInfiniteShadowsAndNavigationButtons, 100), + ); + } + + const instantSearchWrapper = document.getElementById("scrollable-elm"); + + if (instantSearchWrapper) { + instantSearchWrapper.addEventListener( + "scroll", + lodash.debounce(setDisplayOfInfiniteShadowsAndNavigationButtons, 100), + ); + } + + if (navigationBarWrapper.current) { + navigationBarWrapper.current.addEventListener( + "scroll", + lodash.debounce(setDisplayOfInfiniteShadowsAndNavigationButtons, 100), + ); + } + + return () => { + window.removeEventListener( + "resize", + lodash.debounce(setDisplayOfInfiniteShadowsAndNavigationButtons, 100), + ); + + instantSearchWrapper?.removeEventListener( + "scroll", + lodash.debounce(setDisplayOfInfiniteShadowsAndNavigationButtons, 100), + ); + + navigationBarWrapper.current?.removeEventListener( + "scroll", + lodash.debounce(setDisplayOfInfiniteShadowsAndNavigationButtons, 100), + ); + }; + }, []); + + const scrollQuickJumpScrollerTo = (scrollDirection: ScrollDirection) => { + if (navigationBarWrapper.current) { + const amountToScroll = + navigationBarWrapper.current.clientWidth - + VISIBLE_SCROLLER_WIDTH_AFTER_NAVIGATOR_CLICK; + + navigationBarWrapper.current.scroll({ + behavior: "smooth", + left: + scrollDirection === ScrollDirection.LEFT + ? navigationBarWrapper.current.scrollLeft - amountToScroll + : navigationBarWrapper.current.scrollLeft + amountToScroll, + }); + + setTimeout( + setDisplayOfInfiniteShadowsAndNavigationButtons, + DELAY_FOR_DOM_PROPERTY_UPDATE, + ); + } + }; + + const setDisplayOfInfiniteShadowsAndNavigationButtons = () => { + let showLeft, showRight; + + if (navigationBarWrapper.current) { + const needsNavigationArrows = + navigationBarWrapper.current?.scrollWidth > + navigationBarWrapper.current?.clientWidth; + + const userScrolledToRight = + navigationBarWrapper.current?.scrollLeft > 0 && needsNavigationArrows; + showLeft = userScrolledToRight; + const scrolledToTheFartest = + Math.floor( + navigationBarWrapper.current?.scrollLeft + + navigationBarWrapper.current?.clientWidth, + ) === Math.floor(navigationBarWrapper.current?.scrollWidth); + + showRight = !scrolledToTheFartest && needsNavigationArrows; + + setShowNavButtons({ + left: showLeft, + right: showRight, + }); + } + }; + + useEffect(() => { + if (centerID) { + const centerElement = document.querySelector( + '[data-navigation-item="' + centerID + '"]', + ) as HTMLElement; + + if (centerElement && navigationBarWrapper.current) { + navigationBarWrapper.current.scrollTo({ + behavior: "smooth", + left: centerElement.offsetLeft - 80, + }); + + setTimeout( + setDisplayOfInfiniteShadowsAndNavigationButtons, + DELAY_FOR_DOM_PROPERTY_UPDATE, + ); + } + } + }, [centerID]); + + return ( +
+
+
+ +
+
+ {/* + 'showNavButtons' 'left' or 'right' properties are updated + based on 'navigationBarWrapper' HTML DOM element. Once quick jump + pills are loaded we put the 'barContentMarkup' invisibly into the + DOM in order to calculate the need of displaying navigation buttons + or not, based on the width the 'barContentMarkup' used. While we are + doings these calcs, 'skeletonMarkup' is visible in the Ui. Once we have + this state calculated, we display the scroller with out without nav + arrows. Later, only the action of scrolling, which is user + triggered, will make changes to 'showNavButtons'. + */} + {showNavButtons === undefined && skeletonMarkup} + { +
+ {barContentMarkup} +
+ } +
+
+
+ +
+
+ ); +}; diff --git a/apps/namegraph.dev/components/mini-apps/explore-collections/quick-jumps-by-category.tsx b/apps/namegraph.dev/components/mini-apps/explore-collections/quick-jumps-by-category.tsx new file mode 100644 index 000000000..a95d027a1 --- /dev/null +++ b/apps/namegraph.dev/components/mini-apps/explore-collections/quick-jumps-by-category.tsx @@ -0,0 +1,183 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +import { useEffect, useState } from "react"; +import Skeleton from "@/components/skeleton"; +import { + NameGraphFetchTopCollectionMembersResponse, + NameGraphGroupedByCategoryResponse, + NameGraphGroupingCategory, + NameGraphRelatedCollectionResponse, +} from "@namehash/namegraph-sdk/utils"; +import { ArrowNavigationBar } from "./arrow-navigation-bar"; +import { NameGraphSuggestionCategoryTypes } from "@/lib/utils"; + +interface QuickJumpsByCategoryProps { + search: string; + activeCategoryID: string; + /** + * undefined is set when component never tried querying name ideas + * null is set when component tried querying name ideas but failed + * NameGraphGroupedByCategoryResponse is set when name ideas were successfully queried + */ + nameIdeas: undefined | null | NameGraphGroupedByCategoryResponse; +} + +export const QuickJumpsByCategory = ({ + search, + nameIdeas, + activeCategoryID, +}: QuickJumpsByCategoryProps) => { + /** + * quickJumpCategories state: + * + * undefined is set when nameIdeas was not yet parsed + * null is set if nameIdeas was parsed but does not contain an array of categories + * NameGraphFetchTopCollectionMembersResponse[] is set if nameIdeas categories were parsed into quickJumpCategories + */ + const [quickJumpCategories, setQuickJumpCategories] = useState< + undefined | null | NameGraphFetchTopCollectionMembersResponse[] + >(undefined); + const [loadingQuickJumpPills, setLoadingQuickJumpPills] = + useState(true); + + // Start of navigation buttons logic + const buildQuickJumpPills = () => { + if (nameIdeas && Array.isArray(nameIdeas.categories)) { + const options: any[] = []; + + nameIdeas?.categories.forEach((category: any) => { + options.push(category); + }); + + setQuickJumpCategories(options); + } else { + setQuickJumpCategories(null); + } + }; + + const quickJumpTo = ( + category: NameGraphFetchTopCollectionMembersResponse, + ) => { + scrollToNameIdeasCategory(category); + }; + + useEffect(() => { + buildQuickJumpPills(); + }, [nameIdeas?.categories]); + + useEffect(() => { + if (quickJumpCategories) { + setTimeout(() => { + setLoadingQuickJumpPills(false); + }, 5000); + } + }, [quickJumpCategories]); + + useEffect(() => { + setLoadingQuickJumpPills(true); + }, [search]); + + if (nameIdeas?.categories === null) return null; + + return ( +
+

+ 📚 Collections and name ideas found for{" "} + {search.includes(".") ? search.split(".")[0] : search} ⬇️ +

+ {!quickJumpCategories || loadingQuickJumpPills ? ( +
+ +
+ ) : ( +
+ } + centerID={activeCategoryID} + barContentMarkup={ +
+ {/* Quick jump pills */} + {quickJumpCategories?.map((category) => { + return ( + + ); + })} +
+ } + /> +
+ )} +
+ ); +}; + +const QuickJumpPillsSkeleton = () => { + return ( +
+ {[...Array(NameGraphSuggestionCategoryTypes.length).fill(0)].map( + (idx) => ( + + ), + )} +
+ ); +}; + +export const getCategoryID = ( + category: NameGraphFetchTopCollectionMembersResponse, +) => { + const postfix = + category.type === NameGraphGroupingCategory.related + ? `${category.name}-${ + (category as NameGraphRelatedCollectionResponse).collection_id + }` + : `${category.name}`; + + return `${category.type}-category-${postfix}`; +}; + +export const scrollToNameIdeasCategory = ( + category: NameGraphFetchTopCollectionMembersResponse, +) => { + const categoryID = getCategoryID(category); + + if (categoryID) { + const scrollableContainer = document.getElementById("scrollable-elm"); + const categoryElm = document.getElementById(categoryID); + + const categoryElmTopPosition = Number( + categoryElm?.getAttribute("data-category-top"), + ); + console.log(categoryElmTopPosition, categoryElm, scrollableContainer); + if (scrollableContainer && categoryElm && categoryElmTopPosition) { + setTimeout(() => { + scrollableContainer.scrollTo({ + top: categoryElmTopPosition - 425, + behavior: "smooth", + }); + }, 100); + } + } +}; diff --git a/apps/namegraph.dev/components/mini-apps/explore-collections/recursive-related-collection-pills.tsx b/apps/namegraph.dev/components/mini-apps/explore-collections/recursive-related-collection-pills.tsx new file mode 100644 index 000000000..dcbbff75c --- /dev/null +++ b/apps/namegraph.dev/components/mini-apps/explore-collections/recursive-related-collection-pills.tsx @@ -0,0 +1,53 @@ +import Skeleton from "@/components/skeleton"; +import { ArrowNavigationBar } from "./arrow-navigation-bar"; +import { RelatedCollectionPill } from "./related-collection-pill"; +import { NameGraphRelatedCollectionResponse } from "@namehash/namegraph-sdk/utils"; +import { RELATED_COLLECTION_PILLS_RESULTS_NUMBER } from "@/lib/utils"; + +interface RecursiveRelatedCollectionPillsProps { + recursiveRelatedCollections: NameGraphRelatedCollectionResponse[]; +} + +export const RecursiveRelatedCollectionPills = ({ + recursiveRelatedCollections, +}: RecursiveRelatedCollectionPillsProps) => { + if (recursiveRelatedCollections.length === 0) { + return null; + } + + const RelatedCollectionPillsSkeleton = ( +
+

Check out related collections:

+ {Array(RELATED_COLLECTION_PILLS_RESULTS_NUMBER).map((idx) => ( + + ))} +
+ ); + + if ( + recursiveRelatedCollections === null || + recursiveRelatedCollections.length === 0 + ) + return RelatedCollectionPillsSkeleton; + + return ( + <> + +

+ Check out related collections: +

+ {recursiveRelatedCollections.map((collection) => ( + + ))} + + } + /> + + ); +}; diff --git a/apps/namegraph.dev/components/mini-apps/explore-collections/related-collection-pill.tsx b/apps/namegraph.dev/components/mini-apps/explore-collections/related-collection-pill.tsx new file mode 100644 index 000000000..d55a4e87e --- /dev/null +++ b/apps/namegraph.dev/components/mini-apps/explore-collections/related-collection-pill.tsx @@ -0,0 +1,20 @@ +import { NameGraphRelatedCollectionResponse } from "@namehash/namegraph-sdk/utils"; +import { TruncatedText } from "@namehash/namekit-react/client"; + +interface RelatedCollectionPillProps { + relatedCollection: NameGraphRelatedCollectionResponse; +} + +export const RelatedCollectionPill = ({ + relatedCollection, +}: RelatedCollectionPillProps) => { + return ( +
+ +
+ ); +}; diff --git a/apps/namegraph.dev/components/mini-apps/explore-collections/suggestion-category-header.tsx b/apps/namegraph.dev/components/mini-apps/explore-collections/suggestion-category-header.tsx new file mode 100644 index 000000000..10bf3e937 --- /dev/null +++ b/apps/namegraph.dev/components/mini-apps/explore-collections/suggestion-category-header.tsx @@ -0,0 +1,22 @@ +import Skeleton from "@/components/skeleton"; +import { NameGraphFetchTopCollectionMembersResponse } from "@namehash/namegraph-sdk/utils"; + +interface SuggestionCategoryHeaderProps { + category: NameGraphFetchTopCollectionMembersResponse; +} + +export const SuggestionCategoryHeader = ({ + category, +}: SuggestionCategoryHeaderProps) => { + return ( +
+ {category ? ( +

+ {category.name} +

+ ) : ( + + )} +
+ ); +}; diff --git a/apps/namegraph.dev/components/mini-apps/explore-collections/suggestion-category.tsx b/apps/namegraph.dev/components/mini-apps/explore-collections/suggestion-category.tsx new file mode 100644 index 000000000..edef796af --- /dev/null +++ b/apps/namegraph.dev/components/mini-apps/explore-collections/suggestion-category.tsx @@ -0,0 +1,203 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +import { useEffect, useState } from "react"; +import lodash from "lodash"; +import { getCategoryID } from "./quick-jumps-by-category"; +import { + NameGraphFetchTopCollectionMembersResponse, + NameGraphGroupingCategory, + NameGraphRelatedCollectionResponse, +} from "@namehash/namegraph-sdk/utils"; +import { RecursiveRelatedCollectionPills } from "./recursive-related-collection-pills"; +import { SuggestionCategoryHeader } from "./suggestion-category-header"; +import Skeleton from "@/components/skeleton"; + +interface SuggestionCategoryProps { + category: NameGraphFetchTopCollectionMembersResponse; +} + +/* + Whenever a user is scrolling through Name Ideas + categories, the active Quick Jump Pill is being updated + based on the currently visible category. We consider a + category as active if it is the first category that is + visible in the viewport. However, when category C is not yet + visible anymore but the viewport has not reached category C+1 + top position yet, since these are separated by a white spacing, + we want to consider category C+1 as active. This is what the below + constant is used for: it is the amount of pixels that we consider between + the top of a category and the top of NameIdeas container to consider it active. +*/ +const CATEGORY_TOP_OFFEST_TO_CONSIDER_IT_ACTIVE = 32; + +export const SuggestionCategory = ({ category }: SuggestionCategoryProps) => { + const [relatedCollectionsList, setRelatedCollectionsList] = useState< + undefined | NameGraphRelatedCollectionResponse[] + >(undefined); + + useEffect(() => { + if ( + category.type === NameGraphGroupingCategory.related && + category.related_collections + ) { + setRelatedCollectionsList(category.related_collections); + } + }, [category]); + + useEffect(() => { + setTimeout(() => { + setCategoryDimensions(); + }, 4000); + }, []); + + const setCategoryDimensions = () => { + const categoryElm = document.getElementById(getCategoryID(category)); + const categoryTop = categoryElm?.getBoundingClientRect().top; + const categoryBottom = categoryElm?.getBoundingClientRect().bottom; + + const wrapperElm = document.getElementById("scrollable-elm"); + const wrapperScrollTop = Number(wrapperElm?.scrollTop); + + let categoryTopRelativeToWrapper = + Number(categoryTop) - CATEGORY_TOP_OFFEST_TO_CONSIDER_IT_ACTIVE; + let categoryBottomRelativeToWrapper = Number(categoryBottom); + + if (wrapperScrollTop > 0) { + categoryTopRelativeToWrapper = + categoryTopRelativeToWrapper + Number(wrapperScrollTop); + categoryBottomRelativeToWrapper = + categoryBottomRelativeToWrapper + Number(wrapperScrollTop); + } + + if (categoryElm) { + categoryElm.setAttribute( + "data-category-top", + categoryTopRelativeToWrapper.toString(), + ); + categoryElm.setAttribute( + "data-category-bottom", + categoryBottomRelativeToWrapper.toString(), + ); + } + }; + + useEffect(() => { + window.addEventListener( + "scroll", + lodash.debounce(setCategoryDimensions, 100), + ); + + return () => { + window.removeEventListener( + "scroll", + lodash.debounce(setCategoryDimensions, 100), + ); + }; + }, []); + + const loadingSkeletonMarkup = ( +
+ <>{category.name} +
+ +
+
+ ); + + useEffect(() => { + window.addEventListener( + "resize", + lodash.debounce(setCategoryDimensions, 100), + ); + + return () => { + window.removeEventListener( + "resize", + lodash.debounce(setCategoryDimensions, 100), + ); + }; + }, []); + + const customizedPillsColors = [ + "#E7DBF7", + "#1FA3C7", + "#FE097C", + "#FFBE00", + "#DB3D58", + "#01C69A", + "#8464CA", + "#E84233", + "#F5851E", + "#CBECEC", + "#FDE2CB", + "#F0C3F3", + ]; + + const getRandomCustomizedPill = () => { + const defaultClasses = "rounded-xl px-2.5 py-1 bg-opacity-70"; + + const randomColor = + customizedPillsColors[ + Math.floor(Math.random() * customizedPillsColors.length) + ]; + + return `bg-[${randomColor}] ${defaultClasses}`; + }; + + const [categoryPillsColors, setCategoryPillsColors] = useState< + undefined | string[] + >(undefined); + + useEffect(() => { + if (category) { + const numberOfPills = category.suggestions.length; + + let pillsColors = []; + for (let i = 0; i < numberOfPills; i++) { + pillsColors.push(getRandomCustomizedPill()); + } + + setCategoryPillsColors(pillsColors); + } + }, [category]); + + return ( +
+ {category.suggestions.length === 0 ? ( + loadingSkeletonMarkup + ) : ( +
+
+ +
+ {category.suggestions + ? category.suggestions.map((suggestion, idx) => { + return ( +
+ {suggestion.name} +
+ ); + }) + : null} +
+
+
+ {category?.type === NameGraphGroupingCategory.related ? ( + <> + {relatedCollectionsList ? ( + + ) : ( + + )} + + ) : null} +
+
+ )} +
+ ); +}; diff --git a/apps/namegraph.dev/components/mini-apps/ideate/catalog.tsx b/apps/namegraph.dev/components/mini-apps/ideate/catalog.tsx new file mode 100644 index 000000000..52bbe938c --- /dev/null +++ b/apps/namegraph.dev/components/mini-apps/ideate/catalog.tsx @@ -0,0 +1,76 @@ +"use client"; + +import { useRef, useState } from "react"; +import { Textarea } from "@/components/ui/textarea"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { writersBlockSuggestions } from "@/lib/writers-block-suggestions"; +import { Tooltip } from "@namehash/namekit-react/client"; +import { InfoIcon } from "lucide-react"; +import { WritersBlockCollection } from "@namehash/namegraph-sdk/utils"; + +interface CatalogProps { + onJsonChange: (parsedJson: any) => void; +} + +export function Catalog({ onJsonChange }: CatalogProps) { + const catalogTextarea = useRef(null); + const [hasJSONFormatError, setHasJSONFormatError] = useState(false); + const [collection, setCollection] = useState( + writersBlockSuggestions, + ); + const [jsonText, setJsonText] = useState( + JSON.stringify(collection, null, 2), + ); + + const handleJsonChange = (event: React.ChangeEvent) => { + setJsonText(event.target.value); + try { + const parsed = JSON.parse(event.target.value); + setCollection(parsed); + onJsonChange(parsed); + setHasJSONFormatError(false); + } catch (error) { + setHasJSONFormatError(true); + } + }; + + return ( +
+
+

Writer's Block Catalog

+ { + catalogTextarea.current?.focus(); + }} + /> + } + > +
+ 📖 By modifying the below text you
+ can customize the catalog of name +
collections you want to ideate around 🌏 +
+
+
+ +
+