mirror of
https://github.com/block/bitcoin-treasury.git
synced 2025-04-19 06:31:19 +00:00
initial commit
Co-authored-by: Nahiyan Khan <nahiyan.khan@gmail.com>
This commit is contained in:
parent
2d3aa652d1
commit
e6b3d1248f
94
.github/workflows/nextjs.yml
vendored
Normal file
94
.github/workflows/nextjs.yml
vendored
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
# Sample workflow for building and deploying a Next.js site to GitHub Pages
|
||||||
|
#
|
||||||
|
# To get started with Next.js see: https://nextjs.org/docs/getting-started
|
||||||
|
#
|
||||||
|
name: Deploy Next.js site to Pages
|
||||||
|
|
||||||
|
on:
|
||||||
|
# Runs on pushes targeting the default branch
|
||||||
|
push:
|
||||||
|
branches: ["main"]
|
||||||
|
|
||||||
|
# Allows you to run this workflow manually from the Actions tab
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
pages: write
|
||||||
|
id-token: write
|
||||||
|
|
||||||
|
# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
|
||||||
|
# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
|
||||||
|
concurrency:
|
||||||
|
group: "pages"
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
# Build job
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
- name: Detect package manager
|
||||||
|
id: detect-package-manager
|
||||||
|
run: |
|
||||||
|
if [ -f "${{ github.workspace }}/yarn.lock" ]; then
|
||||||
|
echo "manager=yarn" >> $GITHUB_OUTPUT
|
||||||
|
echo "command=install" >> $GITHUB_OUTPUT
|
||||||
|
echo "runner=yarn" >> $GITHUB_OUTPUT
|
||||||
|
exit 0
|
||||||
|
elif [ -f "${{ github.workspace }}/package.json" ]; then
|
||||||
|
echo "manager=npm" >> $GITHUB_OUTPUT
|
||||||
|
echo "command=ci" >> $GITHUB_OUTPUT
|
||||||
|
echo "runner=npx --no-install" >> $GITHUB_OUTPUT
|
||||||
|
exit 0
|
||||||
|
else
|
||||||
|
echo "Unable to determine package manager"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
- name: Setup Node
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: "20"
|
||||||
|
cache: ${{ steps.detect-package-manager.outputs.manager }}
|
||||||
|
- name: Setup Pages
|
||||||
|
uses: actions/configure-pages@v5
|
||||||
|
with:
|
||||||
|
# Automatically inject basePath in your Next.js configuration file and disable
|
||||||
|
# server side image optimization (https://nextjs.org/docs/api-reference/next/image#unoptimized).
|
||||||
|
#
|
||||||
|
# You may remove this line if you want to manage the configuration yourself.
|
||||||
|
static_site_generator: next
|
||||||
|
- name: Restore cache
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
.next/cache
|
||||||
|
# Generate a new cache whenever packages or source files change.
|
||||||
|
key: ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json', '**/yarn.lock') }}-${{ hashFiles('**.[jt]s', '**.[jt]sx') }}
|
||||||
|
# If source files changed but packages didn't, rebuild from a prior cache.
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json', '**/yarn.lock') }}-
|
||||||
|
- name: Install dependencies
|
||||||
|
run: ${{ steps.detect-package-manager.outputs.manager }} ${{ steps.detect-package-manager.outputs.command }}
|
||||||
|
- name: Build with Next.js
|
||||||
|
run: ${{ steps.detect-package-manager.outputs.runner }} next build
|
||||||
|
- name: Upload artifact
|
||||||
|
uses: actions/upload-pages-artifact@v3
|
||||||
|
with:
|
||||||
|
path: ./out
|
||||||
|
|
||||||
|
# Deployment job
|
||||||
|
deploy:
|
||||||
|
environment:
|
||||||
|
name: github-pages
|
||||||
|
url: ${{ steps.deployment.outputs.page_url }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: build
|
||||||
|
steps:
|
||||||
|
- name: Deploy to GitHub Pages
|
||||||
|
id: deployment
|
||||||
|
uses: actions/deploy-pages@v4
|
||||||
|
|
42
.gitignore
vendored
Normal file
42
.gitignore
vendored
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
/node_modules
|
||||||
|
/.pnp
|
||||||
|
.pnp.*
|
||||||
|
.yarn/*
|
||||||
|
!.yarn/patches
|
||||||
|
!.yarn/plugins
|
||||||
|
!.yarn/releases
|
||||||
|
!.yarn/versions
|
||||||
|
|
||||||
|
# testing
|
||||||
|
/coverage
|
||||||
|
|
||||||
|
# next.js
|
||||||
|
/.next/
|
||||||
|
/out/
|
||||||
|
|
||||||
|
# production
|
||||||
|
/build
|
||||||
|
|
||||||
|
# misc
|
||||||
|
.DS_Store
|
||||||
|
*.pem
|
||||||
|
|
||||||
|
# debug
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
.pnpm-debug.log*
|
||||||
|
|
||||||
|
# env files (can opt-in for committing if needed)
|
||||||
|
.env*
|
||||||
|
|
||||||
|
# vercel
|
||||||
|
.vercel
|
||||||
|
|
||||||
|
# typescript
|
||||||
|
*.tsbuildinfo
|
||||||
|
next-env.d.ts
|
||||||
|
|
86
README.md
Normal file
86
README.md
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
# Block Bitcoin Treasury
|
||||||
|
|
||||||
|
A visualization of Block's Bitcoin treasury holdings.
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
This project is a **Next.js** application. Follow the instructions below to run the application locally.
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- [Node.js](https://nodejs.org/) (ensure you have version 16 or later)
|
||||||
|
- npm (comes with Node.js) or yarn (optional package manager)
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
|
||||||
|
1. Clone the repository if you haven't already:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone <repository-url>
|
||||||
|
cd block-bitcoin-treasury
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Install dependencies:
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
Or, if you're using Yarn:
|
||||||
|
```bash
|
||||||
|
yarn install
|
||||||
|
```
|
||||||
|
|
||||||
|
### Running the Development Server
|
||||||
|
|
||||||
|
To start the development server, run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Or, for Yarn users:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yarn dev
|
||||||
|
```
|
||||||
|
|
||||||
|
This will start the Next.js development server on the default port (3000). Open your browser and visit:
|
||||||
|
|
||||||
|
```
|
||||||
|
http://localhost:3000
|
||||||
|
```
|
||||||
|
|
||||||
|
### Building for Production
|
||||||
|
|
||||||
|
To build the application for production, run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
Or, with Yarn:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yarn build
|
||||||
|
```
|
||||||
|
|
||||||
|
This will create an optimized production build of the app in the `.next` directory. To serve it, use:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm start
|
||||||
|
```
|
||||||
|
|
||||||
|
Or:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yarn start
|
||||||
|
```
|
||||||
|
|
||||||
|
### Available Scripts
|
||||||
|
|
||||||
|
- `npm run dev` - Starts the development server.
|
||||||
|
- `npm run build` - Builds the app for production.
|
||||||
|
- `npm start` - Runs the production server after building.
|
||||||
|
|
||||||
|
### Stopping the Server
|
||||||
|
|
||||||
|
To stop either the development or production server, press `Ctrl+C` in the terminal where the server is running.
|
24
app/components/icons/goose.tsx
Normal file
24
app/components/icons/goose.tsx
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
export const Goose = ({ className = "" }) => {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
className={className}
|
||||||
|
>
|
||||||
|
<g clipPath="url(#clip0_2096_5193)">
|
||||||
|
<path
|
||||||
|
d="M20.9093 19.3861L19.5185 18.2413C18.7624 17.619 18.1189 16.8713 17.6157 16.0313C16.9205 14.8706 15.9599 13.8912 14.8133 13.1735L14.2533 12.8475C14.0614 12.7141 13.9276 12.5062 13.9086 12.2716C13.8963 12.1204 13.9326 11.9852 14.0171 11.8662C14.3087 11.4553 15.896 9.74698 16.1722 9.51845C16.528 9.22442 16.9243 8.97987 17.2921 8.69986C17.3443 8.66 17.3968 8.62035 17.4485 8.57989C17.4503 8.57808 17.4529 8.57668 17.4545 8.57508C17.5725 8.48195 17.6838 8.383 17.7724 8.26563C18.2036 7.76631 18.195 7.3443 18.195 7.3443C18.195 7.3443 18.1954 7.3439 18.1956 7.3437C18.1497 7.23133 17.9847 6.88163 17.6492 6.71759C17.9458 6.71178 18.2805 6.82294 18.4323 6.97156C18.6148 6.68534 18.7328 6.49967 18.9162 6.18762C18.9599 6.11352 18.9831 5.97652 18.8996 5.89981C18.8996 5.89981 18.8992 5.89981 18.8988 5.89981C18.8988 5.89981 18.8988 5.8994 18.8988 5.899C18.8972 5.8974 18.8952 5.8962 18.8936 5.8946C18.892 5.893 18.891 5.89119 18.8892 5.88939C18.8892 5.88939 18.8888 5.88939 18.8884 5.88939C18.8884 5.88939 18.8884 5.88899 18.8884 5.88859C18.885 5.88518 18.8812 5.88258 18.8776 5.87938C18.8754 5.87717 18.8736 5.87457 18.8716 5.87217C18.8692 5.87016 18.8665 5.86836 18.8643 5.86616C18.8609 5.86275 18.8587 5.85855 18.8551 5.85534C18.8551 5.85534 18.8545 5.85514 18.8543 5.85534C18.8543 5.85534 18.8543 5.85494 18.8543 5.85454C18.8527 5.85294 18.8507 5.85174 18.8491 5.85013C18.8475 5.84853 18.8463 5.84653 18.8447 5.84493C18.8447 5.84493 18.8441 5.84473 18.8439 5.84493C18.8439 5.84493 18.8439 5.84453 18.8439 5.84413C18.7672 5.7606 18.6302 5.78384 18.5561 5.8275C18.1503 6.06625 17.7555 6.32322 17.3996 6.54855C17.3996 6.54855 16.9778 6.53973 16.4783 6.97116C16.3607 7.05989 16.2618 7.17125 16.1688 7.28902C16.167 7.29082 16.1654 7.29322 16.164 7.29503C16.1234 7.3465 16.0837 7.39898 16.0441 7.45145C15.7639 7.81939 15.5195 8.21556 15.2255 8.57128C14.9971 8.84768 13.2887 10.4348 12.8777 10.7264C12.7587 10.8109 12.6237 10.8474 12.4723 10.835C12.2379 10.8161 12.0298 10.6821 11.8965 10.4903L11.5704 9.93024C10.8527 8.78318 9.87332 7.82299 8.71264 7.12778C7.87262 6.62466 7.12514 5.98092 6.50264 5.22503L5.35778 3.83421C5.3013 3.76571 5.19314 3.77693 5.15268 3.85585C5.02249 4.10941 4.77393 4.64479 4.58346 5.36483C4.57885 5.38186 4.58286 5.39988 4.59407 5.4135C4.83082 5.69952 5.37901 6.32983 6.03196 6.863C6.07742 6.90005 6.04017 6.97336 5.98369 6.95774C5.42047 6.80432 4.87288 6.55796 4.46308 6.34805C4.42964 6.33103 4.38918 6.35226 4.38437 6.38951C4.32068 6.89985 4.30425 7.46027 4.37155 8.05112C4.37355 8.07035 4.38577 8.08697 4.4036 8.09479C4.87088 8.29808 5.61816 8.59311 6.40269 8.78078C6.45958 8.7944 6.45777 8.87632 6.40029 8.88733C5.78941 9.0023 5.14968 9.02794 4.62973 9.02113C4.59327 9.02073 4.56643 9.05518 4.57625 9.09023C4.6806 9.45896 4.822 9.8339 5.00847 10.2115C5.08559 10.3811 5.16951 10.5475 5.25944 10.7104C5.27486 10.7382 5.3047 10.7548 5.33655 10.7534C5.76577 10.7324 6.28452 10.6871 6.80608 10.595C6.89501 10.5794 6.94268 10.6964 6.86757 10.7466C6.51345 10.9834 6.13571 11.1873 5.7844 11.3551C5.73733 11.3777 5.72211 11.4378 5.75315 11.4797C5.96186 11.7625 6.19139 12.0301 6.44075 12.2794C6.44075 12.2794 7.66853 13.5441 7.70198 13.6432C8.41841 12.9096 9.59612 12.0964 10.8966 11.3864C9.15488 12.8036 8.18387 13.8499 7.69517 14.4444L7.35447 14.9225C7.17742 15.1708 7.02379 15.4346 6.89541 15.7112C6.46579 16.6356 5.75756 18.5051 5.75756 18.5051C5.70328 18.6515 5.74754 18.7959 5.84168 18.89C5.84388 18.8922 5.84609 18.8944 5.84849 18.8964C5.85069 18.8986 5.8527 18.901 5.8549 18.9032C5.94924 18.9976 6.09345 19.0416 6.23986 18.9874C6.23986 18.9874 8.10897 18.2791 9.03371 17.8495C9.31031 17.7211 9.57429 17.5673 9.82245 17.3905L10.349 17.0153C10.6278 16.8166 11.0096 16.8483 11.2517 17.0904L12.4655 18.3042C12.7148 18.5535 12.9824 18.7831 13.2652 18.9918C13.3073 19.0226 13.3672 19.0076 13.3898 18.9605C13.5579 18.6094 13.7618 18.2313 13.9983 17.8774C14.0486 17.8022 14.1657 17.8501 14.1499 17.9388C14.0576 18.4606 14.0127 18.9794 13.9915 19.4084C13.9899 19.44 14.0067 19.4701 14.0345 19.4855C14.1972 19.5756 14.3636 19.6595 14.5335 19.7364C14.911 19.9229 15.2862 20.0645 15.6547 20.1687C15.6897 20.1785 15.7242 20.1516 15.7238 20.1152C15.7168 19.595 15.7424 18.9553 15.8576 18.3446C15.8684 18.2869 15.9503 18.2851 15.9641 18.3422C16.1516 19.127 16.4466 19.8742 16.6501 20.3413C16.6579 20.3591 16.6744 20.3712 16.6938 20.3734C17.2847 20.4407 17.8451 20.4242 18.3554 20.3606C18.3929 20.3559 18.4141 20.3155 18.3969 20.2818C18.187 19.872 17.9406 19.3241 17.7872 18.7612C17.7718 18.7046 17.8449 18.6675 17.8819 18.713C18.4151 19.3659 19.0454 19.9141 19.3314 20.1508C19.345 20.1621 19.3633 20.1659 19.3801 20.1615C20.1003 19.9712 20.6357 19.7226 20.8891 19.5922C20.968 19.5518 20.9792 19.4436 20.9107 19.3871L20.9093 19.3861Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="clip0_2096_5193">
|
||||||
|
<rect width="24" height="24" fill="white" />
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
};
|
31
app/components/layout/footer.tsx
Normal file
31
app/components/layout/footer.tsx
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Goose } from "../icons/goose";
|
||||||
|
|
||||||
|
const Footer = () => {
|
||||||
|
return (
|
||||||
|
<footer className="w-full fixed bottom-0 border-t border-[#1e1e1e] text-white h-[54px]">
|
||||||
|
<div className="flex justify-center items-center h-full">
|
||||||
|
<div className="flex flex-rows">
|
||||||
|
Made with{" "}
|
||||||
|
<a
|
||||||
|
href="https://block.github.io/goose/"
|
||||||
|
target="_blank"
|
||||||
|
className="flex flex-rows hover:underline"
|
||||||
|
>
|
||||||
|
<Goose className="mx-1" /> codename goose
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mx-4">|</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<a href="https://github.com/block/bitcoin-treasury/blob/main/DISCLAIMER.md" target="_blank" className="hover:underline">
|
||||||
|
Disclaimer
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Footer;
|
170
app/components/solari/Flap.tsx
Normal file
170
app/components/solari/Flap.tsx
Normal file
@ -0,0 +1,170 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import React, { useState, useEffect, useRef } from "react";
|
||||||
|
import classNames from "classnames";
|
||||||
|
|
||||||
|
interface FlapProps {
|
||||||
|
value: string;
|
||||||
|
animated?: boolean;
|
||||||
|
final?: boolean;
|
||||||
|
hinge?: boolean;
|
||||||
|
children?: string;
|
||||||
|
bottom?: boolean;
|
||||||
|
half?: boolean;
|
||||||
|
isHovered?: boolean;
|
||||||
|
color?: string; // New color prop
|
||||||
|
hoverDuration?: number; // Duration of hover animation in ms
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Flap = React.memo<FlapProps>(
|
||||||
|
({
|
||||||
|
value,
|
||||||
|
animated,
|
||||||
|
final,
|
||||||
|
hinge,
|
||||||
|
children,
|
||||||
|
bottom,
|
||||||
|
half,
|
||||||
|
isHovered,
|
||||||
|
color,
|
||||||
|
hoverDuration = 300, // Default animation duration
|
||||||
|
}) => {
|
||||||
|
const displayValue = children || value;
|
||||||
|
const [animating, setAnimating] = useState(false);
|
||||||
|
const animationTimer = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
|
||||||
|
// Handle hover animation completion
|
||||||
|
useEffect(() => {
|
||||||
|
// Start animation when hovered
|
||||||
|
if (isHovered && !animating && bottom && half) {
|
||||||
|
setAnimating(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If animation is in progress and mouse leaves, let it complete
|
||||||
|
if (animating && !isHovered) {
|
||||||
|
if (animationTimer.current) {
|
||||||
|
clearTimeout(animationTimer.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set a timer to complete the animation cycle
|
||||||
|
animationTimer.current = setTimeout(() => {
|
||||||
|
setAnimating(false);
|
||||||
|
}, hoverDuration);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If mouse is still hovering after animation completes, keep animating
|
||||||
|
if (animating && isHovered) {
|
||||||
|
if (animationTimer.current) {
|
||||||
|
clearTimeout(animationTimer.current);
|
||||||
|
animationTimer.current = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
return () => {
|
||||||
|
if (animationTimer.current) {
|
||||||
|
clearTimeout(animationTimer.current);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [isHovered, animating, bottom, half, hoverDuration]);
|
||||||
|
|
||||||
|
// Base flap classes
|
||||||
|
const flapBaseClasses = `absolute h-full w-full origin-center z-20 rounded-sm`;
|
||||||
|
|
||||||
|
// Top flap classes
|
||||||
|
const topClasses = classNames(
|
||||||
|
flapBaseClasses,
|
||||||
|
"clip-path-[polygon(0_50%,100%_50%,100%_0,0_0)]", // clip-path for top
|
||||||
|
"shadow-inner-top bg-gradient-to-b from-[rgba(255,255,255,0.03)] from-0% to-transparent to-60%", // 3D effect for top
|
||||||
|
{
|
||||||
|
"animate-flapDownTop z-20": animated && final,
|
||||||
|
"rotate-x-50 opacity-40 z-20": animated && !final,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Bottom flap classes
|
||||||
|
const bottomClasses = classNames(
|
||||||
|
flapBaseClasses,
|
||||||
|
"clip-path-[polygon(0_100%,100%_100%,100%_50%,0_50%)]", // clip-path for bottom
|
||||||
|
"shadow-inner-bottom bg-gradient-to-t from-[rgba(0,0,0,0.07)] from-0% to-transparent to-30%", // 3D effect for bottom
|
||||||
|
"transition-transform duration-200", // Add smooth transition for all states
|
||||||
|
{
|
||||||
|
"animate-flapDownBottom z-20": animated && final,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const bottomHalfClasses = classNames(
|
||||||
|
flapBaseClasses,
|
||||||
|
"clip-path-[polygon(0_120%,100%_120%,100%_50%,0_50%)]", // clip-path for bottom
|
||||||
|
"bg-gradient-to-t from-[rgba(0,0,0,0.07)] from-0% to-transparent to-30%", // 3D effect for bottom
|
||||||
|
"shadow-outer-bottom"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Hinge classes
|
||||||
|
const hingeClasses = classNames(
|
||||||
|
"w-full absolute left-0 top-1/2 -translate-y-1/2 z-30 h-[0.02em] bg-black",
|
||||||
|
"before:content-[''] before:absolute before:left-[20%] before:bg-black before:shadow-[0.5px_0_1px_rgba(0,0,0,0.15)]",
|
||||||
|
"after:content-[''] after:absolute after:left-[80%] after:bg-black after:shadow-[0.5px_0_1px_rgba(0,0,0,0.15)]",
|
||||||
|
{
|
||||||
|
"sm:before:w-[2px] sm:before:h-[16px] sm:after:w-[2px] sm:after:h-[16px] sm:before:top-[-6px] sm:after:top-[-6px]":
|
||||||
|
true, // Default size for larger screens
|
||||||
|
"before:w-[1px] before:h-[8px] after:w-[1px] after:h-[8px] after:top-[-3px] before:top-[-3px]":
|
||||||
|
true, // Smaller size for mobile view
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Base style including color
|
||||||
|
const baseStyle = {
|
||||||
|
backgroundColor: color || "#1a1a1a", // Use provided color or default
|
||||||
|
color: color ? "#ffffff" : "#e1e1e1", // Use white text for colored backgrounds
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{!bottom && (
|
||||||
|
<div style={{ ...baseStyle, zIndex: 30 }} className={topClasses}>
|
||||||
|
{displayValue}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{hinge && <div className={hingeClasses} />}
|
||||||
|
{bottom && !half ? (
|
||||||
|
<div
|
||||||
|
style={{ perspective: "300px" }}
|
||||||
|
className={
|
||||||
|
flapBaseClasses +
|
||||||
|
" z-10 clip-path-[polygon(0_100%,100%_100%,100%_50%,0_50%)]"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div style={baseStyle} className={bottomClasses}>
|
||||||
|
{displayValue}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
{bottom && half ? (
|
||||||
|
<div
|
||||||
|
style={{ perspective: "300px" }}
|
||||||
|
className={
|
||||||
|
flapBaseClasses +
|
||||||
|
" z-10 clip-path-[polygon(-20%_120%,120%_120%,100%_50%,0_50%)]"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
...baseStyle,
|
||||||
|
animationDirection: "alternate",
|
||||||
|
transform:
|
||||||
|
bottom && half && (isHovered || animating)
|
||||||
|
? "rotateX(65deg)"
|
||||||
|
: "initial",
|
||||||
|
transition: `transform ${hoverDuration / 1000}s ease-in-out`,
|
||||||
|
}}
|
||||||
|
className={bottomHalfClasses}
|
||||||
|
>
|
||||||
|
{displayValue}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
94
app/components/solari/FlapDigit.tsx
Normal file
94
app/components/solari/FlapDigit.tsx
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import React, { useMemo, useState } from "react";
|
||||||
|
import { Flap } from "./Flap";
|
||||||
|
|
||||||
|
interface FlapDigitProps {
|
||||||
|
className?: string;
|
||||||
|
css?: React.CSSProperties;
|
||||||
|
value?: string;
|
||||||
|
prevValue?: string;
|
||||||
|
final?: boolean;
|
||||||
|
mode?: string | null;
|
||||||
|
[key: string]: any; // For rest props
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FlapDigit = React.memo<FlapDigitProps>(
|
||||||
|
({
|
||||||
|
className,
|
||||||
|
css,
|
||||||
|
value = "",
|
||||||
|
prevValue = "",
|
||||||
|
final = false,
|
||||||
|
mode = null,
|
||||||
|
...restProps
|
||||||
|
}) => {
|
||||||
|
// Add state to track mouse hover
|
||||||
|
const [isHovered, setIsHovered] = useState(false);
|
||||||
|
|
||||||
|
// Memoize the container style
|
||||||
|
const containerStyle = useMemo(
|
||||||
|
() => ({
|
||||||
|
...css,
|
||||||
|
boxShadow: "0 1px 2px rgba(0, 0, 0, 0.15)",
|
||||||
|
}),
|
||||||
|
[css]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`text-[#e1e1e1] bg-[#1a1a1a] relative inline-block h-[1em] font-mono text-3xl xs:text-4xl sm:text-5xl md:text-6xl lg:text-7xl lg:leading-[66px] border border-black leading-none text-center w-[1.3ch] rounded-sm ${
|
||||||
|
className || ""
|
||||||
|
}`}
|
||||||
|
style={containerStyle}
|
||||||
|
data-kind="digit"
|
||||||
|
data-mode={mode}
|
||||||
|
onMouseEnter={() => setIsHovered(true)}
|
||||||
|
onMouseLeave={() => setIsHovered(false)}
|
||||||
|
>
|
||||||
|
<Flap value={value} {...restProps}>
|
||||||
|
{value}
|
||||||
|
</Flap>
|
||||||
|
<Flap bottom value={prevValue} {...restProps}>
|
||||||
|
{prevValue}
|
||||||
|
</Flap>
|
||||||
|
<Flap
|
||||||
|
key={`top-${prevValue}`}
|
||||||
|
animated
|
||||||
|
final={final}
|
||||||
|
value={prevValue}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{prevValue}
|
||||||
|
</Flap>
|
||||||
|
{final && (
|
||||||
|
<>
|
||||||
|
{/* <Flap
|
||||||
|
key={`bottom-${value}`}
|
||||||
|
bottom
|
||||||
|
animated
|
||||||
|
final
|
||||||
|
value={value}
|
||||||
|
isHovered={isHovered}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{value}
|
||||||
|
</Flap> */}
|
||||||
|
<Flap
|
||||||
|
key={`bottom-${value}`}
|
||||||
|
bottom
|
||||||
|
half
|
||||||
|
animated
|
||||||
|
final
|
||||||
|
value={value}
|
||||||
|
isHovered={isHovered}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{value}
|
||||||
|
</Flap>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
148
app/components/solari/FlapDisplay.tsx
Normal file
148
app/components/solari/FlapDisplay.tsx
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import React, {
|
||||||
|
useEffect,
|
||||||
|
useState,
|
||||||
|
ReactNode,
|
||||||
|
useCallback,
|
||||||
|
memo,
|
||||||
|
} from "react";
|
||||||
|
import { FlapStack } from "./FlapStack";
|
||||||
|
import { Presets } from "./Presets";
|
||||||
|
|
||||||
|
enum Modes {
|
||||||
|
Numeric = "num",
|
||||||
|
Alphanumeric = "alpha",
|
||||||
|
Words = "words",
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RenderProps {
|
||||||
|
id?: string;
|
||||||
|
className?: string;
|
||||||
|
css?: React.CSSProperties;
|
||||||
|
children: ReactNode;
|
||||||
|
[key: string]: any; // For rest props
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FlapDisplayProps {
|
||||||
|
id?: string;
|
||||||
|
className?: string;
|
||||||
|
css?: React.CSSProperties;
|
||||||
|
value: string;
|
||||||
|
chars?: string;
|
||||||
|
words?: string[];
|
||||||
|
length?: number;
|
||||||
|
padChar?: string;
|
||||||
|
padMode?: "auto" | "start" | "end";
|
||||||
|
timing?: number;
|
||||||
|
hinge?: boolean;
|
||||||
|
color?: string; // New color property
|
||||||
|
render?: (props: RenderProps) => ReactNode;
|
||||||
|
[key: string]: any; // For rest props
|
||||||
|
}
|
||||||
|
|
||||||
|
const splitChars = (v: string | number): string[] =>
|
||||||
|
String(v)
|
||||||
|
.split("")
|
||||||
|
.map((c) => c.toUpperCase());
|
||||||
|
|
||||||
|
const padValue = (
|
||||||
|
v: string,
|
||||||
|
length?: number,
|
||||||
|
padChar: string = " ",
|
||||||
|
padStart: boolean = false
|
||||||
|
): string => {
|
||||||
|
if (!length) return v;
|
||||||
|
const trimmed = v.slice(0, length);
|
||||||
|
return padStart
|
||||||
|
? String(trimmed).padStart(length, padChar)
|
||||||
|
: String(trimmed).padEnd(length, padChar);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FlapDisplay = memo<FlapDisplayProps>(
|
||||||
|
({
|
||||||
|
id,
|
||||||
|
className,
|
||||||
|
css,
|
||||||
|
value,
|
||||||
|
chars = Presets.NUM,
|
||||||
|
words,
|
||||||
|
length,
|
||||||
|
padChar = " ",
|
||||||
|
padMode = "auto",
|
||||||
|
timing = 40,
|
||||||
|
hinge = true,
|
||||||
|
color, // Add color to destructured props
|
||||||
|
render,
|
||||||
|
...restProps
|
||||||
|
}) => {
|
||||||
|
const [stack, setStack] = useState<string[]>([]);
|
||||||
|
const [mode, setMode] = useState<Modes>(Modes.Numeric);
|
||||||
|
const [digits, setDigits] = useState<string[]>([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (words && words.length) {
|
||||||
|
setStack(words);
|
||||||
|
setMode(Modes.Words);
|
||||||
|
} else {
|
||||||
|
setStack(splitChars(chars));
|
||||||
|
setMode(chars.match(/[a-z]/i) ? Modes.Alphanumeric : Modes.Numeric);
|
||||||
|
}
|
||||||
|
}, [chars, words]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (words && words.length) {
|
||||||
|
setDigits([value]);
|
||||||
|
} else {
|
||||||
|
const padStart =
|
||||||
|
padMode === "auto"
|
||||||
|
? !!value.match(/^[0-9.,+-]*$/)
|
||||||
|
: padMode === "start";
|
||||||
|
setDigits(splitChars(padValue(value, length, padChar, padStart)));
|
||||||
|
}
|
||||||
|
}, [value, chars, words, length, padChar, padMode]);
|
||||||
|
|
||||||
|
const renderFlapStack = useCallback(() => {
|
||||||
|
return digits.map((digit, i) => (
|
||||||
|
<FlapStack
|
||||||
|
key={`${i}-${digit}`}
|
||||||
|
stack={stack}
|
||||||
|
value={digit}
|
||||||
|
mode={mode}
|
||||||
|
timing={timing}
|
||||||
|
hinge={hinge}
|
||||||
|
color={color} // Pass color to FlapStack
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
}, [digits, stack, mode, timing, hinge, color, restProps]);
|
||||||
|
|
||||||
|
const containerClassName = className || "";
|
||||||
|
|
||||||
|
if (render) {
|
||||||
|
return render({
|
||||||
|
id,
|
||||||
|
className: containerClassName,
|
||||||
|
css,
|
||||||
|
...restProps,
|
||||||
|
children: renderFlapStack(),
|
||||||
|
}) as React.ReactElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
id={id}
|
||||||
|
className={`${containerClassName} flex relative w-full overflow-x-auto overflow-y-hidden justify-center gap-1`}
|
||||||
|
style={{
|
||||||
|
...css,
|
||||||
|
transform: "perspective(1000px)",
|
||||||
|
transition: "transform 0.1s ease-out",
|
||||||
|
}}
|
||||||
|
aria-hidden="true"
|
||||||
|
aria-label={value}
|
||||||
|
>
|
||||||
|
{renderFlapStack()}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
74
app/components/solari/FlapStack.tsx
Normal file
74
app/components/solari/FlapStack.tsx
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { FlapDigit } from './FlapDigit';
|
||||||
|
|
||||||
|
interface CursorState {
|
||||||
|
current: number;
|
||||||
|
previous: number;
|
||||||
|
target: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FlapStackProps {
|
||||||
|
stack: string[];
|
||||||
|
value: string;
|
||||||
|
timing: number;
|
||||||
|
[key: string]: any; // For rest props
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set these three values as one state var
|
||||||
|
// to avoid in-between render states
|
||||||
|
const InitialCursor: CursorState = {
|
||||||
|
current: -1,
|
||||||
|
previous: -1,
|
||||||
|
target: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FlapStack = React.memo<FlapStackProps>(({ stack, value, timing, ...restProps }) => {
|
||||||
|
const [cursor, setCursor] = useState<CursorState>(InitialCursor);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setCursor(InitialCursor);
|
||||||
|
}, [stack]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const target = Math.max(stack.indexOf(value), 0);
|
||||||
|
|
||||||
|
const increment = (prevState: CursorState) => {
|
||||||
|
const { current } = prevState;
|
||||||
|
const previous = current;
|
||||||
|
const nextCurrent = current >= stack.length - 1 ? 0 : current + 1;
|
||||||
|
|
||||||
|
return {
|
||||||
|
current: nextCurrent,
|
||||||
|
previous,
|
||||||
|
target,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initial increment
|
||||||
|
setCursor(prevState => increment(prevState));
|
||||||
|
|
||||||
|
const timer = setInterval(() => {
|
||||||
|
setCursor(prevState => {
|
||||||
|
if (prevState.current === target) {
|
||||||
|
clearInterval(timer);
|
||||||
|
return prevState;
|
||||||
|
}
|
||||||
|
return increment(prevState);
|
||||||
|
});
|
||||||
|
}, timing);
|
||||||
|
|
||||||
|
return () => clearInterval(timer);
|
||||||
|
}, [stack, value, timing]);
|
||||||
|
|
||||||
|
const { current, previous, target } = cursor;
|
||||||
|
return (
|
||||||
|
<FlapDigit
|
||||||
|
value={stack[current]}
|
||||||
|
prevValue={stack[previous]}
|
||||||
|
final={current === target}
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
4
app/components/solari/Presets.ts
Normal file
4
app/components/solari/Presets.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export const Presets = {
|
||||||
|
NUM: " 0123456789",
|
||||||
|
ALPHANUM: " JU↑XSO5QYCENM4↓PIWL$BKZF93,HGA2167D8.VR0T",
|
||||||
|
};
|
39
app/components/solari/SolariBoard.tsx
Normal file
39
app/components/solari/SolariBoard.tsx
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import React, { memo } from "react";
|
||||||
|
import { FlapDisplay, Presets } from "./";
|
||||||
|
|
||||||
|
interface BoardRow {
|
||||||
|
value: string;
|
||||||
|
chars?: string;
|
||||||
|
length?: number;
|
||||||
|
hinge?: boolean;
|
||||||
|
color?: string; // New color property for each row
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SolariBoardProps {
|
||||||
|
rows: BoardRow[];
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Memoize the individual FlapDisplay rows
|
||||||
|
const MemoizedFlapDisplay = memo(FlapDisplay);
|
||||||
|
|
||||||
|
export const SolariBoard: React.FC<SolariBoardProps> = memo(
|
||||||
|
({ rows, className }) => {
|
||||||
|
return (
|
||||||
|
<div className={`flex flex-col ${className} gap-1`}>
|
||||||
|
{rows.map((row, index) => (
|
||||||
|
<MemoizedFlapDisplay
|
||||||
|
key={`row-${index}`}
|
||||||
|
chars={row.chars || Presets.ALPHANUM}
|
||||||
|
length={row.length}
|
||||||
|
value={row.value}
|
||||||
|
hinge={row.hinge ?? true}
|
||||||
|
color={row.color} // Pass the color to FlapDisplay
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
2
app/components/solari/index.ts
Normal file
2
app/components/solari/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export { FlapDisplay } from './FlapDisplay';
|
||||||
|
export { Presets } from './Presets';
|
78
app/components/solari/useDisplayLength.ts
Normal file
78
app/components/solari/useDisplayLength.ts
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
import { useState, useEffect, useMemo } from "react";
|
||||||
|
import _ from "lodash";
|
||||||
|
|
||||||
|
const DIGIT_WIDTH = 1.7; // width in ch units
|
||||||
|
const GAP_WIDTH = 1; // gap in pixels
|
||||||
|
const MIN_LENGTH = 15;
|
||||||
|
|
||||||
|
// Memoized font size calculation
|
||||||
|
const getFontSize = (width: number): number => {
|
||||||
|
if (width < 480) return 30; // text-3xl (1.875rem * 16)
|
||||||
|
if (width < 640) return 36; // text-4xl (2.25rem * 16)
|
||||||
|
if (width < 768) return 48; // text-5xl (3rem * 16)
|
||||||
|
if (width < 1024) return 60; // text-6xl (3.75rem * 16)
|
||||||
|
return 72; // text-7xl (4.5rem * 16)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Calculate display length based on window width
|
||||||
|
const calculateLength = (windowWidth: number): number => {
|
||||||
|
const fontSize = getFontSize(windowWidth);
|
||||||
|
|
||||||
|
// Calculate how many digits can fit
|
||||||
|
const digitWidthPx = DIGIT_WIDTH * (fontSize * 0.5);
|
||||||
|
const totalGapWidth = GAP_WIDTH;
|
||||||
|
|
||||||
|
// Calculate max digits that can fit
|
||||||
|
const maxDigits = Math.ceil(windowWidth / (digitWidthPx + totalGapWidth)) - 4;
|
||||||
|
|
||||||
|
// Ensure we don't go below minimum length
|
||||||
|
return Math.max(maxDigits, MIN_LENGTH);
|
||||||
|
};
|
||||||
|
|
||||||
|
export function useDisplayLength() {
|
||||||
|
// Initialize with minimum length
|
||||||
|
const [displayLength, setDisplayLength] = useState(MIN_LENGTH);
|
||||||
|
const [isClient, setIsClient] = useState(false);
|
||||||
|
|
||||||
|
// Set isClient to true on mount
|
||||||
|
useEffect(() => {
|
||||||
|
setIsClient(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Memoize the debounced update function
|
||||||
|
const debouncedUpdate = useMemo(
|
||||||
|
() =>
|
||||||
|
_.debounce((width: number) => {
|
||||||
|
const newLength = calculateLength(width);
|
||||||
|
setDisplayLength(newLength);
|
||||||
|
}, 100),
|
||||||
|
[] // Empty deps since this function never needs to change
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isClient) return;
|
||||||
|
|
||||||
|
// Function to handle resize
|
||||||
|
const handleResize = () => {
|
||||||
|
const width = window.innerWidth;
|
||||||
|
const newLength = calculateLength(width);
|
||||||
|
setDisplayLength(newLength);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initial calculation
|
||||||
|
handleResize();
|
||||||
|
|
||||||
|
// Add event listener with debounced updates
|
||||||
|
window.addEventListener("resize", () => debouncedUpdate(window.innerWidth));
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("resize", () =>
|
||||||
|
debouncedUpdate(window.innerWidth)
|
||||||
|
);
|
||||||
|
debouncedUpdate.cancel();
|
||||||
|
};
|
||||||
|
}, [isClient, debouncedUpdate]); // Include isClient and debouncedUpdate in deps array
|
||||||
|
|
||||||
|
return displayLength;
|
||||||
|
}
|
BIN
app/favicon.ico
Normal file
BIN
app/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 25 KiB |
29
app/layout.tsx
Normal file
29
app/layout.tsx
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import type { Metadata } from "next";
|
||||||
|
import { ThemeProvider } from "next-themes";
|
||||||
|
import "./styles/main.css";
|
||||||
|
import Footer from "./components/layout/footer";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Bitcoin Holdings",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function RootLayout({
|
||||||
|
children,
|
||||||
|
}: Readonly<{
|
||||||
|
children: React.ReactNode;
|
||||||
|
}>) {
|
||||||
|
return (
|
||||||
|
<html lang="en" suppressHydrationWarning>
|
||||||
|
<body
|
||||||
|
className={`font-mono bg-black text-textStandard relative antialiased`}
|
||||||
|
>
|
||||||
|
<ThemeProvider attribute="data-theme">
|
||||||
|
<div className="h-[calc(100vh-54px)] flex justify-center items-center">
|
||||||
|
<main className="h-full">{children}</main>
|
||||||
|
</div>
|
||||||
|
<Footer />
|
||||||
|
</ThemeProvider>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
);
|
||||||
|
}
|
227
app/page.tsx
Normal file
227
app/page.tsx
Normal file
@ -0,0 +1,227 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { SolariBoard } from "./components/solari/SolariBoard";
|
||||||
|
import { useState, useEffect, useMemo, useRef, Suspense } from "react";
|
||||||
|
import { useSearchParams, useRouter } from "next/navigation";
|
||||||
|
// import { useDisplayLength } from "./components/useDisplayLength";
|
||||||
|
|
||||||
|
function formatCurrency(number: number, locale = "en-US", currency = "USD") {
|
||||||
|
const formatter = new Intl.NumberFormat(locale, {
|
||||||
|
style: "currency",
|
||||||
|
currency: currency,
|
||||||
|
maximumFractionDigits: 2,
|
||||||
|
notation: "standard",
|
||||||
|
});
|
||||||
|
return formatter.format(number).replace("$", "USD ");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initial loading rows - defined outside component to avoid recreation
|
||||||
|
const getLoadingRows = (displayLength: number) => [
|
||||||
|
{ value: "", length: displayLength },
|
||||||
|
{ value: "", length: displayLength },
|
||||||
|
{ value: "", length: displayLength },
|
||||||
|
{ value: "", length: displayLength },
|
||||||
|
{ value: " Loading...", length: displayLength },
|
||||||
|
{ value: "", length: displayLength },
|
||||||
|
{ value: "", length: displayLength },
|
||||||
|
{ value: "", length: displayLength },
|
||||||
|
{ value: "", length: displayLength },
|
||||||
|
];
|
||||||
|
|
||||||
|
function HomeContent() {
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
const router = useRouter();
|
||||||
|
// const displayLength = useDisplayLength();
|
||||||
|
const displayLength = 20; // Fallback to a fixed length for simplicity
|
||||||
|
|
||||||
|
const [bitcoinPrice, setBitcoinPrice] = useState(0);
|
||||||
|
const previousPriceRef = useRef(0);
|
||||||
|
const [priceDirection, setPriceDirection] = useState<string | null>(null);
|
||||||
|
const [holding] = useState(8485);
|
||||||
|
const [holdingValue, setHoldingValue] = useState(0);
|
||||||
|
const [currentRowIndex, setCurrentRowIndex] = useState(-1);
|
||||||
|
const [ticker, setTicker] = useState(searchParams.get("ticker") || "XYZ");
|
||||||
|
const [inputError, setInputError] = useState<string | null>(null);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [countdown, setCountdown] = useState(20);
|
||||||
|
const [isFetching, setIsFetching] = useState(false);
|
||||||
|
|
||||||
|
// Initialize loading rows immediately
|
||||||
|
const loadingBoardRows = useMemo(
|
||||||
|
() => getLoadingRows(displayLength),
|
||||||
|
[displayLength]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Update holding value when Bitcoin price changes
|
||||||
|
useEffect(() => {
|
||||||
|
setHoldingValue(bitcoinPrice * holding);
|
||||||
|
}, [bitcoinPrice, holding]);
|
||||||
|
|
||||||
|
// Format the display values
|
||||||
|
const displayValue = error
|
||||||
|
? "Error"
|
||||||
|
: `${formatCurrency(bitcoinPrice).toString()}${
|
||||||
|
priceDirection ? ` ${priceDirection}` : ""
|
||||||
|
}`;
|
||||||
|
|
||||||
|
const holdingDisplay = error ? "Error" : formatCurrency(holdingValue);
|
||||||
|
|
||||||
|
// Define the final board rows
|
||||||
|
const finalBoardRows = useMemo(
|
||||||
|
() => [
|
||||||
|
{ value: "", length: displayLength },
|
||||||
|
{ value: ` ${ticker}`, length: displayLength },
|
||||||
|
{ value: "", length: displayLength },
|
||||||
|
{ value: " TOTAL HOLDING", length: displayLength },
|
||||||
|
{ value: ` ${holdingDisplay}`, length: displayLength },
|
||||||
|
{ value: "", length: displayLength },
|
||||||
|
{ value: " BTC PRICE", length: displayLength },
|
||||||
|
{ value: ` ${displayValue}`, length: displayLength },
|
||||||
|
{ value: "", length: displayLength },
|
||||||
|
],
|
||||||
|
[ticker, holdingDisplay, displayValue, displayLength]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Current board rows based on loading state and animation progress
|
||||||
|
const currentBoardRows = useMemo(() => {
|
||||||
|
if (currentRowIndex === -1) {
|
||||||
|
return loadingBoardRows;
|
||||||
|
}
|
||||||
|
|
||||||
|
return loadingBoardRows.map((row, index) => {
|
||||||
|
if (index <= currentRowIndex) {
|
||||||
|
return finalBoardRows[index];
|
||||||
|
}
|
||||||
|
return row;
|
||||||
|
});
|
||||||
|
}, [loadingBoardRows, finalBoardRows, currentRowIndex]);
|
||||||
|
|
||||||
|
// Handle the row-by-row animation
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isFetching && currentRowIndex === -1) {
|
||||||
|
// Start the row animation after data is loaded
|
||||||
|
const animateRows = () => {
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
setCurrentRowIndex((prev) => {
|
||||||
|
if (prev >= finalBoardRows.length - 1) {
|
||||||
|
clearInterval(interval);
|
||||||
|
return prev;
|
||||||
|
}
|
||||||
|
return prev + 1;
|
||||||
|
});
|
||||||
|
}, 300); // Adjust timing between each row update
|
||||||
|
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Small delay before starting the animation
|
||||||
|
setTimeout(animateRows, 1000);
|
||||||
|
}
|
||||||
|
}, [isFetching, currentRowIndex, finalBoardRows.length]);
|
||||||
|
|
||||||
|
// Fetch Bitcoin price and manage countdown
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchBitcoinPrice = async () => {
|
||||||
|
setIsFetching(true);
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
"https://pricing.bitcoin.block.xyz/current-price"
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`API error: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
const newPrice = parseFloat(data["amount"]);
|
||||||
|
|
||||||
|
// Check if this is not the first fetch
|
||||||
|
if (!isFetching) {
|
||||||
|
// Compare with previous price to determine direction
|
||||||
|
if (newPrice > previousPriceRef.current) {
|
||||||
|
setPriceDirection("↑");
|
||||||
|
} else if (newPrice < previousPriceRef.current) {
|
||||||
|
setPriceDirection("↓");
|
||||||
|
} else {
|
||||||
|
setPriceDirection(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the direction indicator after 5 seconds (increased from 2 seconds)
|
||||||
|
if (newPrice !== previousPriceRef.current) {
|
||||||
|
setTimeout(() => {
|
||||||
|
setPriceDirection(null);
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Set initial price without showing direction
|
||||||
|
setPriceDirection(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update prices
|
||||||
|
const oldPrice = previousPriceRef.current;
|
||||||
|
previousPriceRef.current = newPrice;
|
||||||
|
setBitcoinPrice(newPrice);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to fetch Bitcoin price:", err);
|
||||||
|
setError("Failed to fetch Bitcoin price");
|
||||||
|
}
|
||||||
|
setIsFetching(false);
|
||||||
|
setCountdown(20);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Fetch immediately on load
|
||||||
|
fetchBitcoinPrice();
|
||||||
|
|
||||||
|
// Set up countdown interval
|
||||||
|
const countdownInterval = setInterval(() => {
|
||||||
|
setCountdown((prev) => {
|
||||||
|
if (prev <= 1) {
|
||||||
|
fetchBitcoinPrice(); // Fetch when countdown reaches 0
|
||||||
|
return 20; // Reset to 20 seconds
|
||||||
|
}
|
||||||
|
return prev - 1;
|
||||||
|
});
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
// Clean up intervals on component unmount
|
||||||
|
return () => {
|
||||||
|
clearInterval(countdownInterval);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full h-full font-mono flex flex-col justify-center items-center">
|
||||||
|
<div className="relative p-4 rounded-lg bg-[#0e0d0d]">
|
||||||
|
<SolariBoard rows={currentBoardRows} className="relative" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-rows w-full justify-between opacity-0 transition-opacity duration-300 animate-fadeIn">
|
||||||
|
{/* Status indicator */}
|
||||||
|
<div className="flex items-center justify-center gap-2 text-zinc-400 mt-2 sm:mt-4">
|
||||||
|
<div
|
||||||
|
className={`w-1.5 sm:w-2 h-1.5 sm:h-2 rounded-full ${
|
||||||
|
isFetching
|
||||||
|
? "animate-pulse bg-yellow-500"
|
||||||
|
: "animate-pulse bg-green-500"
|
||||||
|
}`}
|
||||||
|
></div>
|
||||||
|
<div className="text-xs sm:text-sm">
|
||||||
|
{isFetching
|
||||||
|
? "Fetching..."
|
||||||
|
: `Fetching latest in ${countdown} second${
|
||||||
|
countdown > 1 ? "s" : ""
|
||||||
|
}`}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Home() {
|
||||||
|
return (
|
||||||
|
<Suspense fallback={<div>Loading...</div>}>
|
||||||
|
<HomeContent />
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
}
|
155
app/styles/main.css
Normal file
155
app/styles/main.css
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
:root {
|
||||||
|
/* custom slate */
|
||||||
|
--slate: #393838;
|
||||||
|
|
||||||
|
/* block */
|
||||||
|
--block-teal: #13bbaf;
|
||||||
|
--block-orange: #ff4f00;
|
||||||
|
|
||||||
|
/* start arcade colors */
|
||||||
|
--constant-white: #ffffff;
|
||||||
|
--constant-black: #000000;
|
||||||
|
--grey-10: #101010;
|
||||||
|
--grey-20: #1e1e1e;
|
||||||
|
--grey-50: #666666;
|
||||||
|
--grey-60: #959595;
|
||||||
|
--grey-80: #cccccc;
|
||||||
|
--grey-85: #dadada;
|
||||||
|
--grey-90: #e8e8e8;
|
||||||
|
--grey-95: #f0f0f0;
|
||||||
|
--dark-grey-15: #1a1a1a;
|
||||||
|
--dark-grey-25: #232323;
|
||||||
|
--dark-grey-30: #2a2a2a;
|
||||||
|
--dark-grey-40: #333333;
|
||||||
|
--dark-grey-45: #595959;
|
||||||
|
--dark-grey-60: #878787;
|
||||||
|
--dark-grey-90: #e1e1e1;
|
||||||
|
|
||||||
|
--background-app: var(--constant-white);
|
||||||
|
--background-prominent: var(--grey-80);
|
||||||
|
--background-standard: var(--grey-90);
|
||||||
|
--background-subtle: var(--grey-95);
|
||||||
|
|
||||||
|
--border-divider: var(--grey-90);
|
||||||
|
--border-inverse: var(--constant-white);
|
||||||
|
--border-prominent: var(--grey-10);
|
||||||
|
--border-standard: var(--grey-60);
|
||||||
|
--border-subtle: var(--grey-90);
|
||||||
|
|
||||||
|
--icon-disabled: var(--grey-60);
|
||||||
|
--icon-extra-subtle: var(--grey-60);
|
||||||
|
--icon-inverse: var(--constant-white);
|
||||||
|
--icon-prominent: var(--grey-10);
|
||||||
|
--icon-standard: var(--grey-20);
|
||||||
|
--icon-subtle: var(--grey-50);
|
||||||
|
|
||||||
|
--text-placeholder: var(--grey-60);
|
||||||
|
--text-prominent: var(--grey-10);
|
||||||
|
--text-standard: var(--grey-20);
|
||||||
|
--text-subtle: var(--grey-50);
|
||||||
|
|
||||||
|
&.dark {
|
||||||
|
--background-app: var(--constant-black);
|
||||||
|
--background-prominent: var(--dark-grey-40);
|
||||||
|
--background-standard: var(--dark-grey-25);
|
||||||
|
--background-subtle: var(--dark-grey-15);
|
||||||
|
|
||||||
|
--border-divider: var(--dark-grey-25);
|
||||||
|
--border-inverse: var(--constant-black);
|
||||||
|
--border-prominent: var(--constant-white);
|
||||||
|
--border-standard: var(--dark-grey-45);
|
||||||
|
--border-subtle: var(--dark-grey-25);
|
||||||
|
|
||||||
|
--icon-disabled: var(--dark-grey-45);
|
||||||
|
--icon-extra-subtle: var(--dark-grey-45);
|
||||||
|
--icon-inverse: var(--constant-black);
|
||||||
|
--icon-prominent: var(--constant-white);
|
||||||
|
--icon-standard: var(--dark-grey-90);
|
||||||
|
--icon-subtle: var(--dark-grey-60);
|
||||||
|
|
||||||
|
--text-placeholder: var(--dark-grey-45);
|
||||||
|
--text-prominent: var(--constant-white);
|
||||||
|
--text-standard: var(--dark-grey-90);
|
||||||
|
--text-subtle: var(--dark-grey-60);
|
||||||
|
}
|
||||||
|
/* end arcade colors */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "Cash Sans";
|
||||||
|
src: url(https://cash-f.squarecdn.com/static/fonts/cashsans/woff2/CashSans-Regular.woff2)
|
||||||
|
format("woff2"),
|
||||||
|
url(https://cash-f.squarecdn.com/static/fonts/cashsans/woff/CashSans-Regular.woff)
|
||||||
|
format("woff");
|
||||||
|
font-weight: 400;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "Cash Sans Mono";
|
||||||
|
src: url(https://cash-f.squarecdn.com/static/fonts/cashsans/woff2/CashSansMono-Regular.woff2)
|
||||||
|
format("woff2"),
|
||||||
|
url(https://cash-f.squarecdn.com/static/fonts/cashsans/woff/CashSansMono-Regular.woff)
|
||||||
|
format("woff");
|
||||||
|
font-weight: 400;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* flap overrides */
|
||||||
|
|
||||||
|
.perspective-1000 {
|
||||||
|
perspective: 1000px;
|
||||||
|
transform-style: preserve-3d;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Loading bar animation */
|
||||||
|
@keyframes loading {
|
||||||
|
0% {
|
||||||
|
width: 0;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
width: 60%;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
90% {
|
||||||
|
width: 90%;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
width: 100%;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-bar {
|
||||||
|
animation: loading 1s cubic-bezier(0.4, 0, 0.2, 1) forwards;
|
||||||
|
width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fade in animation */
|
||||||
|
.transition-opacity {
|
||||||
|
transition-property: opacity;
|
||||||
|
}
|
||||||
|
|
||||||
|
.duration-1000 {
|
||||||
|
transition-duration: 1000ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ease-in-out {
|
||||||
|
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.opacity-0 {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opacity-100 {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
6
next.config.js
Normal file
6
next.config.js
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
/** @type {import('next').NextConfig} */
|
||||||
|
const nextConfig = {
|
||||||
|
/* config options here */
|
||||||
|
};
|
||||||
|
|
||||||
|
export default nextConfig;
|
7
next.config.ts
Normal file
7
next.config.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import type { NextConfig } from "next";
|
||||||
|
|
||||||
|
const nextConfig: NextConfig = {
|
||||||
|
/* config options here */
|
||||||
|
};
|
||||||
|
|
||||||
|
export default nextConfig;
|
2456
package-lock.json
generated
Normal file
2456
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
31
package.json
Normal file
31
package.json
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"name": "bitcoin-solari",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "next dev",
|
||||||
|
"build": "next build",
|
||||||
|
"start": "next start",
|
||||||
|
"lint": "next lint"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"classnames": "^2.5.1",
|
||||||
|
"lodash": "^4.17.21",
|
||||||
|
"next": "^15.1.3",
|
||||||
|
"next-themes": "^0.4.3",
|
||||||
|
"prop-types": "^15.8.1",
|
||||||
|
"react": "^19.0.0",
|
||||||
|
"react-dom": "^19.0.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/lodash": "^4.17.16",
|
||||||
|
"@types/node": "^20",
|
||||||
|
"@types/react": "^18",
|
||||||
|
"@types/react-dom": "^18",
|
||||||
|
"@types/react-window": "^1.8.8",
|
||||||
|
"postcss": "^8",
|
||||||
|
"tailwindcss": "^3.4.1",
|
||||||
|
"typescript": "^5"
|
||||||
|
}
|
||||||
|
}
|
8
postcss.config.mjs
Normal file
8
postcss.config.mjs
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
/** @type {import('postcss-load-config').Config} */
|
||||||
|
const config = {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
89
tailwind.config.ts
Normal file
89
tailwind.config.ts
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
import type { Config } from "tailwindcss";
|
||||||
|
import plugin from "tailwindcss/plugin";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
darkMode: ["class", '[data-theme="dark"]'],
|
||||||
|
content: ["./app/**/*.{js,ts,jsx,tsx,mdx}"],
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
fontFamily: {
|
||||||
|
sans: ["Cash Sans", "sans-serif"],
|
||||||
|
mono: ["Cash Sans Mono", "monospace"],
|
||||||
|
},
|
||||||
|
colors: {
|
||||||
|
bgApp: "var(--background-app)",
|
||||||
|
bgSubtle: "var(--background-subtle)",
|
||||||
|
bgStandard: "var(--background-standard)",
|
||||||
|
bgProminent: "var(--background-prominent)",
|
||||||
|
|
||||||
|
borderSubtle: "var(--border-subtle)",
|
||||||
|
borderStandard: "var(--border-standard)",
|
||||||
|
|
||||||
|
textProminent: "var(--text-prominent)",
|
||||||
|
textStandard: "var(--text-standard)",
|
||||||
|
textSubtle: "var(--text-subtle)",
|
||||||
|
textPlaceholder: "var(--text-placeholder)",
|
||||||
|
|
||||||
|
iconProminent: "var(--icon-prominent)",
|
||||||
|
iconStandard: "var(--icon-standard)",
|
||||||
|
iconSubtle: "var(--icon-subtle)",
|
||||||
|
iconExtraSubtle: "var(--icon-extra-subtle)",
|
||||||
|
slate: "var(--slate)",
|
||||||
|
blockTeal: "var(--block-teal)",
|
||||||
|
blockOrange: "var(--block-orange)",
|
||||||
|
},
|
||||||
|
keyframes: {
|
||||||
|
appearDown: {
|
||||||
|
"0%": { opacity: "0", transform: "translateY(-8px)" },
|
||||||
|
"100%": { opacity: "1", transform: "translateY(0px)" },
|
||||||
|
},
|
||||||
|
fadeIn: {
|
||||||
|
"0%": { opacity: "0" },
|
||||||
|
"100%": { opacity: "1" },
|
||||||
|
},
|
||||||
|
flapDownTop: {
|
||||||
|
from: { transform: "rotateX(0deg)" },
|
||||||
|
"50%, to": { transform: "rotateX(90deg)" },
|
||||||
|
},
|
||||||
|
flapDownBottom: {
|
||||||
|
"from, 50%": { transform: "rotateX(90deg)" },
|
||||||
|
"90%": { transform: "rotateX(20deg)" },
|
||||||
|
"80%, to": { transform: "rotateX(0deg)" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
animation: {
|
||||||
|
appearDown: "appearDown 250ms ease-in forwards",
|
||||||
|
fadeIn: "fadeIn 250ms ease-in forwards 1s",
|
||||||
|
flapDownTop: "flapDownTop 300ms ease-in forwards",
|
||||||
|
flapDownBottom: "flapDownBottom 300ms ease-out forwards",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
plugin(function ({ addUtilities }) {
|
||||||
|
const newUtilities = {
|
||||||
|
".clip-path-\\[polygon\\(0_50\\%\\,100\\%_50\\%\\,100\\%_0\\,0_0\\)\\]":
|
||||||
|
{
|
||||||
|
clipPath: "polygon(0 50%, 100% 50%, 100% 0, 0 0)",
|
||||||
|
},
|
||||||
|
".clip-path-\\[polygon\\(0_100\\%\\,100\\%_100\\%\\,100\\%_50\\%\\,0_50\\%\\)\\]":
|
||||||
|
{
|
||||||
|
clipPath: "polygon(0 100%, 100% 100%, 100% 50%, 0 50%)",
|
||||||
|
},
|
||||||
|
".rotate-x-50": {
|
||||||
|
transform: "rotateX(50deg)",
|
||||||
|
},
|
||||||
|
".shadow-inner-top": {
|
||||||
|
boxShadow: "inset 0 -3px 5px -4px rgba(0, 0, 0, 0.2)",
|
||||||
|
},
|
||||||
|
".shadow-inner-bottom": {
|
||||||
|
boxShadow: "inset 0 3px 5px -4px rgba(0, 0, 0, 0.2)",
|
||||||
|
},
|
||||||
|
".shadow-outer-bottom": {
|
||||||
|
boxShadow: "0px 6px 6px 0px rgba(0, 0, 0, 0.8)",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
addUtilities(newUtilities);
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
} satisfies Config;
|
28
tsconfig.json
Normal file
28
tsconfig.json
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2017",
|
||||||
|
"lib": ["dom", "dom.iterable", "esnext"],
|
||||||
|
"allowJs": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strict": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"module": "esnext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"jsx": "preserve",
|
||||||
|
"incremental": true,
|
||||||
|
"plugins": [
|
||||||
|
{
|
||||||
|
"name": "next"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./app/*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||||
|
"exclude": ["node_modules"]
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user