git: reset
@@ -0,0 +1,7 @@
|
||||
.DS_Store
|
||||
|
||||
/datasets
|
||||
/datasets2
|
||||
/datasets_*
|
||||
|
||||
TODO.md
|
||||
@@ -0,0 +1,39 @@
|
||||
# Changelog
|
||||
|
||||
## v. 0.1.1 - WIP
|
||||
|
||||
### Parser
|
||||
|
||||
- Fixed overflow in `Price` struct which caused many Realized Caps and Realized Prices to have completely bogus data
|
||||
- Fixed Realized Cap computation which was using rounded prices instead normal ones
|
||||
|
||||
### Server
|
||||
|
||||
- Added the chunk, date and time in the terminal logs
|
||||
|
||||
### App
|
||||
|
||||
- Chart
|
||||
- Added double click option on a legend to toggle the visibility of all other series
|
||||
- Added highlight effect to a legend by darkening the color of all the other series on the chart while hovering it with the mouse
|
||||
- Added an API link in the legend for each dataset where applicable (when isn't generated locally)
|
||||
- Save fullscreen preference in local storage and url
|
||||
- Fixed time range shifting not being the one in url params or saved in local storage
|
||||
- Fixed time range shifting on series toggling via the legend
|
||||
- Fixed time range shifting on fullscreen
|
||||
- Fixed time range shifting on resize of the sidebar
|
||||
- Set default view at first load to last 6 months
|
||||
- Added some padding around the datasets (year 1970 to 2100)
|
||||
- History
|
||||
- Changed background for the sticky dates from blur to a solid color as it didn't appear properly in Firefox
|
||||
- Build
|
||||
- Added lazy loads to have split chunks after build
|
||||
- Removed many libraries and did some things manually instead to improve build size
|
||||
- Strip
|
||||
- Temporarily removed the Home button on the strip bar on desktop as there is no landing page yet
|
||||
- Misc
|
||||
- Removed tracker even though it was a very privacy friendly as it appeared to not be working properly
|
||||
|
||||
### Price
|
||||
|
||||
- Deleted old price datasets and their backups
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 Satonomics
|
||||
|
||||
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.
|
||||
@@ -0,0 +1,59 @@
|
||||
# SATONOMICS
|
||||
|
||||
## Description
|
||||
|
||||
TLDR: FOSS [glassnode](https://glassnode.com).
|
||||
|
||||
Satonomics is an open-source suite of tools that computes, distributes, and displays on-chain data, making it freely available for anyone to use.
|
||||
|
||||
The generated datasets are incredibly diverse and can be used for a wide range of purposes. Whether you're looking to conduct a health check on the network, gain insights into its current or past state, or leverage the data for trading purposes, these tools offer various charts, dashboards (Soon TM), and an extensive API to help you achieve your goals.
|
||||
|
||||
To promote transparency and trust in the network, this project is committed to making on-chain data accessible and verifiable to all, without discrimination and is a great complimentary tool to [mempool.space](https://mempool.space).
|
||||
|
||||
## Instances
|
||||
|
||||
Web App:
|
||||
- [app.satonomics.xyz](https://app.satonomics.xyz)
|
||||
|
||||
API:
|
||||
- [api.satonomics.xyz](https://api.satonomics.xyz)
|
||||
|
||||
## Structure
|
||||
|
||||
- `parser`: The backbone of the project, it does most of the work by parsing and then computing datasets from the timechain.
|
||||
- `server`: A small server which automatically creates routes to access through an API all created datasets.
|
||||
- `app`: A web app which displays the generated datasets in various charts.
|
||||
|
||||
## Git
|
||||
|
||||
- [Repository](https://codeberg.org/satonomics/satonomics)
|
||||
- [Issues](https://gitworkshop.dev/r/naddr1qq99xct5dahx7mtfvdesz9thwden5te0wp6hyurvv4ex2mrp0yhxxmmdqgsfw5dacngjlahye34krvgz7u0yghhjgk7gxzl5ptm9v6n2y3sn03srqsqqqaueek2h03/issues)
|
||||
- [Proposals](https://gitworkshop.dev/r/naddr1qq99xct5dahx7mtfvdesz9thwden5te0wp6hyurvv4ex2mrp0yhxxmmdqgsfw5dacngjlahye34krvgz7u0yghhjgk7gxzl5ptm9v6n2y3sn03srqsqqqaueek2h03/proposals)
|
||||
|
||||
## Goals
|
||||
|
||||
- Be the absolute best on-chain data source and app
|
||||
- Have as many datasets and charts as possible
|
||||
- Be self-hostable on cheap computers
|
||||
- Be runnable on a machine with 8 GB RAM (16 GB RAM is already possible right now)
|
||||
- Still being runnable 10 years from now
|
||||
- By not relying on any third-party dependencies besides price APIs (which are and should be very common and easy to update)
|
||||
- By **NOT** doing address labelling/tagging (which means **NO** exchange or any other individual address tracking), for that please use [mempool.space](https://mempool.space) or any other tool
|
||||
|
||||
## Proof of Work
|
||||
|
||||
Aka: Previous iterations
|
||||
|
||||
The initial idea was totally different yet morphed over time into what it is today: a fully FOSS self-hostable on-chain data generator
|
||||
|
||||
- https://github.com/drgarlic/satonomics
|
||||
- https://github.com/drgarlic/satonomics-parser
|
||||
- https://github.com/drgarlic/satonomics-explorer
|
||||
- https://github.com/drgarlic/satonomics-server
|
||||
- https://github.com/drgarlic/satonomics-app
|
||||
- https://github.com/drgarlic/bitalisys
|
||||
- https://github.com/drgarlic/bitesque-app
|
||||
- https://github.com/drgarlic/bitesque-back
|
||||
- https://github.com/drgarlic/bitesque-front
|
||||
- https://github.com/drgarlic/bitesque-assets
|
||||
- https://github.com/drgarlic/syf
|
||||
@@ -0,0 +1,9 @@
|
||||
node_modules
|
||||
charts
|
||||
dist
|
||||
dev-dist
|
||||
.DS_Store
|
||||
visualizer
|
||||
# Local Netlify folder
|
||||
.netlify
|
||||
.wrangler
|
||||
@@ -0,0 +1,10 @@
|
||||
# Satonomics - App
|
||||
|
||||
## Description
|
||||
|
||||
A web app to view the generated datasets in various charts.
|
||||
|
||||
## Requirements
|
||||
|
||||
- `node`
|
||||
- `pnpm`
|
||||
@@ -0,0 +1 @@
|
||||
/* /index.html
|
||||
@@ -0,0 +1,372 @@
|
||||
<!doctype html>
|
||||
<html lang="en" class="overflow-hidden bg-black text-white">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Satonomics</title>
|
||||
<meta
|
||||
name="description"
|
||||
content="An app to visualize Bitcoin on-chain data"
|
||||
/>
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"
|
||||
/>
|
||||
<link rel="manifest" href="/manifest.webmanifest" />
|
||||
<meta name="theme-color" content="#0c0a09" />
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
sizes="196x196"
|
||||
href="/assets/favicon-196.png"
|
||||
/>
|
||||
<link rel="apple-touch-icon" href="/assets/apple-icon-180.png" />
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-2048-2732.jpg"
|
||||
media="(device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-2732-2048.jpg"
|
||||
media="(device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-1668-2388.jpg"
|
||||
media="(device-width: 834px) and (device-height: 1194px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-2388-1668.jpg"
|
||||
media="(device-width: 834px) and (device-height: 1194px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-1536-2048.jpg"
|
||||
media="(device-width: 768px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-2048-1536.jpg"
|
||||
media="(device-width: 768px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-1488-2266.jpg"
|
||||
media="(device-width: 744px) and (device-height: 1133px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-2266-1488.jpg"
|
||||
media="(device-width: 744px) and (device-height: 1133px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-1640-2360.jpg"
|
||||
media="(device-width: 820px) and (device-height: 1180px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-2360-1640.jpg"
|
||||
media="(device-width: 820px) and (device-height: 1180px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-1668-2224.jpg"
|
||||
media="(device-width: 834px) and (device-height: 1112px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-2224-1668.jpg"
|
||||
media="(device-width: 834px) and (device-height: 1112px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-1620-2160.jpg"
|
||||
media="(device-width: 810px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-2160-1620.jpg"
|
||||
media="(device-width: 810px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-1290-2796.jpg"
|
||||
media="(device-width: 430px) and (device-height: 932px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-2796-1290.jpg"
|
||||
media="(device-width: 430px) and (device-height: 932px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-1179-2556.jpg"
|
||||
media="(device-width: 393px) and (device-height: 852px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-2556-1179.jpg"
|
||||
media="(device-width: 393px) and (device-height: 852px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-1284-2778.jpg"
|
||||
media="(device-width: 428px) and (device-height: 926px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-2778-1284.jpg"
|
||||
media="(device-width: 428px) and (device-height: 926px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-1170-2532.jpg"
|
||||
media="(device-width: 390px) and (device-height: 844px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-2532-1170.jpg"
|
||||
media="(device-width: 390px) and (device-height: 844px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-1125-2436.jpg"
|
||||
media="(device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-2436-1125.jpg"
|
||||
media="(device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-1242-2688.jpg"
|
||||
media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-2688-1242.jpg"
|
||||
media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-828-1792.jpg"
|
||||
media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-1792-828.jpg"
|
||||
media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-1242-2208.jpg"
|
||||
media="(device-width: 414px) and (device-height: 736px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-2208-1242.jpg"
|
||||
media="(device-width: 414px) and (device-height: 736px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-750-1334.jpg"
|
||||
media="(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-1334-750.jpg"
|
||||
media="(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-640-1136.jpg"
|
||||
media="(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-1136-640.jpg"
|
||||
media="(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
|
||||
/>
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-dark-2048-2732.jpg"
|
||||
media="(prefers-color-scheme: dark) and (device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-dark-2732-2048.jpg"
|
||||
media="(prefers-color-scheme: dark) and (device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-dark-1668-2388.jpg"
|
||||
media="(prefers-color-scheme: dark) and (device-width: 834px) and (device-height: 1194px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-dark-2388-1668.jpg"
|
||||
media="(prefers-color-scheme: dark) and (device-width: 834px) and (device-height: 1194px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-dark-1536-2048.jpg"
|
||||
media="(prefers-color-scheme: dark) and (device-width: 768px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-dark-2048-1536.jpg"
|
||||
media="(prefers-color-scheme: dark) and (device-width: 768px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-dark-1488-2266.jpg"
|
||||
media="(prefers-color-scheme: dark) and (device-width: 744px) and (device-height: 1133px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-dark-2266-1488.jpg"
|
||||
media="(prefers-color-scheme: dark) and (device-width: 744px) and (device-height: 1133px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-dark-1640-2360.jpg"
|
||||
media="(prefers-color-scheme: dark) and (device-width: 820px) and (device-height: 1180px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-dark-2360-1640.jpg"
|
||||
media="(prefers-color-scheme: dark) and (device-width: 820px) and (device-height: 1180px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-dark-1668-2224.jpg"
|
||||
media="(prefers-color-scheme: dark) and (device-width: 834px) and (device-height: 1112px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-dark-2224-1668.jpg"
|
||||
media="(prefers-color-scheme: dark) and (device-width: 834px) and (device-height: 1112px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-dark-1620-2160.jpg"
|
||||
media="(prefers-color-scheme: dark) and (device-width: 810px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-dark-2160-1620.jpg"
|
||||
media="(prefers-color-scheme: dark) and (device-width: 810px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-dark-1290-2796.jpg"
|
||||
media="(prefers-color-scheme: dark) and (device-width: 430px) and (device-height: 932px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-dark-2796-1290.jpg"
|
||||
media="(prefers-color-scheme: dark) and (device-width: 430px) and (device-height: 932px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-dark-1179-2556.jpg"
|
||||
media="(prefers-color-scheme: dark) and (device-width: 393px) and (device-height: 852px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-dark-2556-1179.jpg"
|
||||
media="(prefers-color-scheme: dark) and (device-width: 393px) and (device-height: 852px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-dark-1284-2778.jpg"
|
||||
media="(prefers-color-scheme: dark) and (device-width: 428px) and (device-height: 926px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-dark-2778-1284.jpg"
|
||||
media="(prefers-color-scheme: dark) and (device-width: 428px) and (device-height: 926px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-dark-1170-2532.jpg"
|
||||
media="(prefers-color-scheme: dark) and (device-width: 390px) and (device-height: 844px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-dark-2532-1170.jpg"
|
||||
media="(prefers-color-scheme: dark) and (device-width: 390px) and (device-height: 844px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-dark-1125-2436.jpg"
|
||||
media="(prefers-color-scheme: dark) and (device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-dark-2436-1125.jpg"
|
||||
media="(prefers-color-scheme: dark) and (device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-dark-1242-2688.jpg"
|
||||
media="(prefers-color-scheme: dark) and (device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-dark-2688-1242.jpg"
|
||||
media="(prefers-color-scheme: dark) and (device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-dark-828-1792.jpg"
|
||||
media="(prefers-color-scheme: dark) and (device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-dark-1792-828.jpg"
|
||||
media="(prefers-color-scheme: dark) and (device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-dark-1242-2208.jpg"
|
||||
media="(prefers-color-scheme: dark) and (device-width: 414px) and (device-height: 736px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-dark-2208-1242.jpg"
|
||||
media="(prefers-color-scheme: dark) and (device-width: 414px) and (device-height: 736px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-dark-750-1334.jpg"
|
||||
media="(prefers-color-scheme: dark) and (device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-dark-1334-750.jpg"
|
||||
media="(prefers-color-scheme: dark) and (device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-dark-640-1136.jpg"
|
||||
media="(prefers-color-scheme: dark) and (device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-dark-1136-640.jpg"
|
||||
media="(prefers-color-scheme: dark) and (device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
|
||||
/>
|
||||
</head>
|
||||
<body style="font-size: 15px; line-height: 22px">
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
|
||||
<div id="root"></div>
|
||||
|
||||
<script src="/src/index.tsx" type="module"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"name": "satonomics",
|
||||
"description": "Satoshi Economics",
|
||||
"version": "0.1.0",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "($npm_execpath outdated || read -p \"Press enter to ignore...\") && vite --host",
|
||||
"build": "vite build",
|
||||
"check": "tsc --noEmit --skipLibCheck --pretty",
|
||||
"check-watch": "$npm_execpath check --watch",
|
||||
"format": "prettier --write './src'",
|
||||
"prod": "$npm_execpath run build && vite preview --host",
|
||||
"pages-prod": "pnpm build && pnpm wrangler pages deploy ./dist",
|
||||
"pages-dev": "pnpm build && pnpm wrangler pages deploy --branch dev ./dist",
|
||||
"assets": "pnpm pwa-asset-generator ./public/logo/white.svg ./public/assets --index ./index.html --manifest ./public/manifest.webmanifest --icon-only --favicon --background \"linear-gradient(to right bottom, rgb(249, 115, 22), rgb(154, 52, 18))\" --padding \"min(15vh, 15vw)\" --path-override \"/assets\" && pnpm pwa-asset-generator ./public/logo/white.svg ./public/assets --index ./index.html --splash-only --background \"linear-gradient(to right bottom, rgb(249, 115, 22), rgb(154, 52, 18))\" --padding \"min(33vh, 33vw)\" --path-override \"/assets\" && pnpm pwa-asset-generator ./public/logo/white.svg ./public/assets --index ./index.html --splash-only --dark-mode --background \"#0c0a09\" --padding \"min(33vh, 33vw)\" --path-override \"/assets\""
|
||||
},
|
||||
"dependencies": {
|
||||
"@leeoniya/ufuzzy": "^1.0.14",
|
||||
"@solid-primitives/event-listener": "^2.3.3",
|
||||
"@solid-primitives/intersection-observer": "^2.1.6",
|
||||
"@solid-primitives/memo": "^1.3.8",
|
||||
"@solid-primitives/resize-observer": "^2.0.25",
|
||||
"lean-qr": "^2.3.4",
|
||||
"lightweight-charts": "^4.1.6",
|
||||
"solid-js": "^1.8.17"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@ianvs/prettier-plugin-sort-imports": "^4.2.1",
|
||||
"@iconify-json/tabler": "^1.1.114",
|
||||
"@tailwindcss/container-queries": "^0.1.1",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"postcss": "^8.4.38",
|
||||
"prettier": "^3.3.2",
|
||||
"prettier-plugin-tailwindcss": "^0.6.5",
|
||||
"pwa-asset-generator": "^6.3.1",
|
||||
"rollup-plugin-visualizer": "^5.12.0",
|
||||
"tailwindcss": "^3.4.4",
|
||||
"typescript": "^5.5.2",
|
||||
"unplugin-auto-import": "^0.17.6",
|
||||
"unplugin-icons": "^0.19.0",
|
||||
"vite": "^5.3.1",
|
||||
"vite-plugin-pwa": "^0.20.0",
|
||||
"vite-plugin-solid": "^2.10.2",
|
||||
"workbox-window": "^7.1.0",
|
||||
"wrangler": "^3.61.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
/** @type {import("prettier").Options} */
|
||||
export default {
|
||||
plugins: [
|
||||
'@ianvs/prettier-plugin-sort-imports',
|
||||
'prettier-plugin-tailwindcss', // MUST come last
|
||||
],
|
||||
|
||||
tailwindFunctions: ['classList'],
|
||||
|
||||
importOrder: ['<THIRD_PARTY_MODULES>', '', '^/?(~|src)/', '', '^[./]'],
|
||||
}
|
||||
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 8.0 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 8.8 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 7.0 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 8.8 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 7.0 KiB |
|
After Width: | Height: | Size: 8.8 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 4.3 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 40 KiB |
@@ -0,0 +1,17 @@
|
||||
<svg width="100%" height="100%" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;" fill="black">
|
||||
<g transform="matrix(1.14102,0,0,2.63158,-0.849652,5.12904)">
|
||||
<rect x="4.25" y="3.751" width="14.023" height="1.52"/>
|
||||
</g>
|
||||
<g transform="matrix(1.14102,0,0,2.63158,-0.849652,0.129039)">
|
||||
<rect x="4.25" y="3.751" width="14.023" height="1.52"/>
|
||||
</g>
|
||||
<g transform="matrix(1.14102,0,0,2.63158,-0.849652,-4.87096)">
|
||||
<rect x="4.25" y="3.751" width="14.023" height="1.52"/>
|
||||
</g>
|
||||
<g transform="matrix(0.285256,0,0,2.63158,8.78759,-9.87096)">
|
||||
<rect x="4.25" y="3.751" width="14.023" height="1.52"/>
|
||||
</g>
|
||||
<g transform="matrix(0.285256,0,0,2.63158,8.78759,10.129)">
|
||||
<rect x="4.25" y="3.751" width="14.023" height="1.52"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1004 B |
@@ -0,0 +1,17 @@
|
||||
<svg width="100%" height="100%" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;" fill="white">
|
||||
<g transform="matrix(1.14102,0,0,2.63158,-0.849652,5.12904)">
|
||||
<rect x="4.25" y="3.751" width="14.023" height="1.52"/>
|
||||
</g>
|
||||
<g transform="matrix(1.14102,0,0,2.63158,-0.849652,0.129039)">
|
||||
<rect x="4.25" y="3.751" width="14.023" height="1.52"/>
|
||||
</g>
|
||||
<g transform="matrix(1.14102,0,0,2.63158,-0.849652,-4.87096)">
|
||||
<rect x="4.25" y="3.751" width="14.023" height="1.52"/>
|
||||
</g>
|
||||
<g transform="matrix(0.285256,0,0,2.63158,8.78759,-9.87096)">
|
||||
<rect x="4.25" y="3.751" width="14.023" height="1.52"/>
|
||||
</g>
|
||||
<g transform="matrix(0.285256,0,0,2.63158,8.78759,10.129)">
|
||||
<rect x="4.25" y="3.751" width="14.023" height="1.52"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1004 B |
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"name": "Satonomics",
|
||||
"short_name": "Satonomics",
|
||||
"description": "Satoshi Economics",
|
||||
"start_url": "/",
|
||||
"display": "standalone",
|
||||
"theme_color": "#0c0a09",
|
||||
"background_color": "#0c0a09",
|
||||
"lang": "en",
|
||||
"scope": "/",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/assets/manifest-icon-192.maskable.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "any"
|
||||
},
|
||||
{
|
||||
"src": "/assets/manifest-icon-192.maskable.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
},
|
||||
{
|
||||
"src": "/assets/manifest-icon-512.maskable.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "any"
|
||||
},
|
||||
{
|
||||
"src": "/assets/manifest-icon-512.maskable.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
User-agent: *
|
||||
Allow: /
|
||||
@@ -0,0 +1,175 @@
|
||||
import { createRWS } from "/src/solid/rws";
|
||||
|
||||
const texts = [
|
||||
"satonomics",
|
||||
"satonomics",
|
||||
"satonomics",
|
||||
|
||||
"stay humble, stack sats",
|
||||
"21 million",
|
||||
"cold storage",
|
||||
"utxo",
|
||||
"satoshi nakamoto",
|
||||
"hodl",
|
||||
`don't trust, verify`,
|
||||
"zap",
|
||||
"bitcoin",
|
||||
"lightning",
|
||||
"nostr",
|
||||
"freedom tech",
|
||||
"2008/10/31",
|
||||
"2009/01/03",
|
||||
"2010/05/22",
|
||||
"hodl!",
|
||||
"Hal Finney",
|
||||
"Vote for better money",
|
||||
"gradually then suddenly",
|
||||
"timechain",
|
||||
"self custody",
|
||||
"be your own bank",
|
||||
"resistance money",
|
||||
"foss",
|
||||
];
|
||||
|
||||
export const LOCAL_STORAGE_MARQUEE_KEY = "bg-marquee";
|
||||
|
||||
export function Background({
|
||||
marquee: on,
|
||||
focused,
|
||||
}: {
|
||||
marquee: Accessor<boolean>;
|
||||
focused: Accessor<boolean>;
|
||||
}) {
|
||||
createEffect(() => {
|
||||
if (on()) {
|
||||
localStorage.removeItem(LOCAL_STORAGE_MARQUEE_KEY);
|
||||
} else {
|
||||
localStorage.setItem(LOCAL_STORAGE_MARQUEE_KEY, "false");
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<div class="absolute h-full w-full overflow-hidden opacity-[0.0333] will-change-auto">
|
||||
<div class="-m-[2rem] -space-y-1 overflow-hidden md:-m-[1rem]">
|
||||
<Line on={on} focused={focused} />
|
||||
<Line on={on} focused={focused} />
|
||||
<Line on={on} focused={focused} />
|
||||
<Line on={on} focused={focused} />
|
||||
<Line on={on} focused={focused} />
|
||||
<Line on={on} focused={focused} />
|
||||
<Line on={on} focused={focused} />
|
||||
<Line on={on} focused={focused} />
|
||||
<Line on={on} focused={focused} />
|
||||
<Line on={on} focused={focused} />
|
||||
<Line on={on} focused={focused} />
|
||||
<Line on={on} focused={focused} />
|
||||
<Line on={on} focused={focused} />
|
||||
<Line on={on} focused={focused} />
|
||||
<Line on={on} focused={focused} />
|
||||
<Line on={on} focused={focused} />
|
||||
<Line on={on} focused={focused} />
|
||||
<Line on={on} focused={focused} />
|
||||
<Line on={on} focused={focused} />
|
||||
<Line on={on} focused={focused} />
|
||||
<Line on={on} focused={focused} />
|
||||
<Line on={on} focused={focused} />
|
||||
<Line on={on} focused={focused} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="absolute h-full w-full opacity-10 mix-blend-multiply">
|
||||
<Noise />
|
||||
</div>
|
||||
<div class="absolute h-full w-full opacity-10 mix-blend-hard-light">
|
||||
<Noise />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function Line({
|
||||
on,
|
||||
focused,
|
||||
}: {
|
||||
on: Accessor<boolean>;
|
||||
focused: Accessor<boolean>;
|
||||
}) {
|
||||
const shuffled = shuffle([...texts]);
|
||||
shuffled.pop();
|
||||
const joined = shuffled.join(". ");
|
||||
|
||||
return (
|
||||
<div class="select-none whitespace-nowrap">
|
||||
<TextWrapper on={on} focused={focused} joined={joined} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function TextWrapper({
|
||||
joined,
|
||||
on,
|
||||
focused,
|
||||
}: {
|
||||
on: Accessor<boolean>;
|
||||
focused: Accessor<boolean>;
|
||||
joined: string;
|
||||
}) {
|
||||
const seconds = joined.length * 2;
|
||||
|
||||
const wasOnceOn = createRWS(false);
|
||||
|
||||
createEffect(() => {
|
||||
if (!wasOnceOn() && on()) {
|
||||
wasOnceOn.set(true);
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<p
|
||||
class="inline-block px-2 text-[5dvh] font-black uppercase leading-none"
|
||||
style={{
|
||||
...(wasOnceOn()
|
||||
? {
|
||||
animation: `marquee ${seconds}s linear infinite`,
|
||||
"animation-play-state": focused() && on() ? "running" : "paused",
|
||||
}
|
||||
: {}),
|
||||
}}
|
||||
>
|
||||
{joined} {wasOnceOn() ? joined : undefined}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
function shuffle<T>([...arr]: T[]): T[] {
|
||||
let m = arr.length;
|
||||
|
||||
while (m) {
|
||||
const i = Math.floor(Math.random() * m--);
|
||||
[arr[m], arr[i]] = [arr[i], arr[m]];
|
||||
}
|
||||
|
||||
return arr;
|
||||
}
|
||||
|
||||
function Noise() {
|
||||
return (
|
||||
<svg
|
||||
class="size-full"
|
||||
viewBox="0 0 200 200"
|
||||
preserveAspectRatio="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<filter id="noiseFilter">
|
||||
<feTurbulence
|
||||
type="fractalNoise"
|
||||
baseFrequency="3"
|
||||
numOctaves="3"
|
||||
stitchTiles="stitch"
|
||||
/>
|
||||
</filter>
|
||||
|
||||
<rect width="100%" height="100%" filter="url(#noiseFilter)" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
import { createResizeObserver } from "@solid-primitives/resize-observer";
|
||||
|
||||
import { classPropToString } from "/src/solid/classes";
|
||||
import { createRWS } from "/src/solid/rws";
|
||||
|
||||
export function Box({
|
||||
flex = true,
|
||||
absolute,
|
||||
padded = true,
|
||||
children,
|
||||
dark,
|
||||
overflowY,
|
||||
}: {
|
||||
flex?: boolean;
|
||||
absolute?: "top" | "bottom";
|
||||
padded?: boolean;
|
||||
dark?: boolean;
|
||||
overflowY?: boolean;
|
||||
} & ParentProps) {
|
||||
const maybeScrollable = createRWS<HTMLDivElement | undefined>(undefined);
|
||||
const scrollable = createRWS(false);
|
||||
const showLeftArrow = createRWS(false);
|
||||
const showRightArrow = createRWS(false);
|
||||
|
||||
onMount(() => {
|
||||
createResizeObserver(maybeScrollable, (_, el) => {
|
||||
if (el !== maybeScrollable()) {
|
||||
return;
|
||||
}
|
||||
|
||||
scrollable.set(() => el.scrollWidth > el.clientWidth);
|
||||
|
||||
checkArrows();
|
||||
});
|
||||
});
|
||||
|
||||
function checkArrows() {
|
||||
const offset = 20;
|
||||
|
||||
const target = maybeScrollable()!;
|
||||
|
||||
const left = target.scrollLeft;
|
||||
const right = target.scrollWidth - target.scrollLeft - target.clientWidth;
|
||||
|
||||
showLeftArrow.set(() => left > offset);
|
||||
showRightArrow.set(() => right > offset);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
class={classPropToString([
|
||||
"p-2",
|
||||
absolute
|
||||
? [
|
||||
"absolute inset-x-0",
|
||||
absolute === "top"
|
||||
? "top-0"
|
||||
: "pointer-events-none bottom-0 bg-gradient-to-b from-transparent to-black",
|
||||
]
|
||||
: "relative",
|
||||
])}
|
||||
>
|
||||
<div
|
||||
class={classPropToString([
|
||||
"pointer-events-auto relative overflow-hidden rounded-xl border border-orange-200/10 shadow-md",
|
||||
dark
|
||||
? "bg-orange-100/5 backdrop-blur-sm"
|
||||
: "bg-orange-200/10 backdrop-blur-md",
|
||||
])}
|
||||
>
|
||||
<For
|
||||
each={[
|
||||
{
|
||||
showArrow: showLeftArrow,
|
||||
side: "left-0",
|
||||
order: "",
|
||||
buttonPadding: "pl-3 pr-2",
|
||||
iconPadding: "pr-0.5",
|
||||
scrollMultiplier: -1,
|
||||
chevronIcon: IconTablerChevronLeft,
|
||||
gradientDirection: "bg-gradient-to-r",
|
||||
},
|
||||
{
|
||||
showArrow: showRightArrow,
|
||||
side: "right-0",
|
||||
order: "order-2",
|
||||
buttonPadding: "pl-2 pr-3",
|
||||
iconPadding: "pl-0.5",
|
||||
scrollMultiplier: 1,
|
||||
chevronIcon: IconTablerChevronRight,
|
||||
gradientDirection: "bg-gradient-to-l",
|
||||
},
|
||||
]}
|
||||
>
|
||||
{(obj) => (
|
||||
<Show when={scrollable() && obj.showArrow()}>
|
||||
<div
|
||||
class={[
|
||||
obj.side,
|
||||
"pointer-events-none absolute bottom-0 top-0 z-20 flex transition-opacity duration-200 ease-in-out",
|
||||
].join(" ")}
|
||||
>
|
||||
<div
|
||||
class={[
|
||||
obj.order,
|
||||
obj.buttonPadding,
|
||||
"pointer-events-auto hidden h-full items-center bg-black/90 md:flex",
|
||||
].join(" ")}
|
||||
>
|
||||
<button
|
||||
onClick={() => {
|
||||
maybeScrollable()?.scrollBy({
|
||||
left: Math.floor(
|
||||
maybeScrollable()!.clientWidth *
|
||||
obj.scrollMultiplier *
|
||||
0.8,
|
||||
),
|
||||
behavior: "smooth",
|
||||
});
|
||||
}}
|
||||
class="rounded-full border border-orange-200/20 bg-black p-0.5 transition hover:scale-110 active:scale-100"
|
||||
>
|
||||
<Dynamic
|
||||
component={obj.chevronIcon}
|
||||
class={[`size-5 ${obj.iconPadding}`]}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class={[
|
||||
obj.gradientDirection,
|
||||
"h-full w-10 from-black/90 to-transparent",
|
||||
].join(" ")}
|
||||
/>
|
||||
</div>
|
||||
</Show>
|
||||
)}
|
||||
</For>
|
||||
|
||||
<div
|
||||
ref={maybeScrollable.set}
|
||||
onScroll={checkArrows}
|
||||
class={classPropToString([
|
||||
flex && "flex w-full space-x-2",
|
||||
overflowY && "overflow-y-auto",
|
||||
padded && "p-1.5",
|
||||
])}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
export function Button({
|
||||
onClick,
|
||||
children,
|
||||
}: { onClick: VoidFunction } & ParentProps) {
|
||||
return (
|
||||
<button
|
||||
class="group flex w-full flex-1 items-center justify-center rounded-lg px-2 py-1.5 hover:bg-orange-200/20 active:scale-95"
|
||||
onClick={onClick}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
import type { Generate } from "lean-qr";
|
||||
|
||||
import { chartState } from "/src/scripts/lightweightCharts/chart/state";
|
||||
import { setTimeScale } from "/src/scripts/lightweightCharts/chart/time";
|
||||
import { classPropToString } from "/src/solid/classes";
|
||||
import { createRWS } from "/src/solid/rws";
|
||||
|
||||
export function Actions({
|
||||
presets,
|
||||
fullscreen,
|
||||
qrcode,
|
||||
}: {
|
||||
presets: Presets;
|
||||
qrcode: RWS<string>;
|
||||
fullscreen?: RWS<boolean>;
|
||||
}) {
|
||||
const leanQRGenerate = createRWS<Generate | undefined>(undefined);
|
||||
|
||||
onMount(() => {
|
||||
import("lean-qr").then((leanQR) => {
|
||||
leanQRGenerate.set(() => leanQR.generate);
|
||||
});
|
||||
});
|
||||
|
||||
return (
|
||||
<div class="flex space-x-1">
|
||||
<Button
|
||||
icon={() => IconTablerMaximize}
|
||||
onClick={() => {
|
||||
const range = chartState.range;
|
||||
|
||||
fullscreen?.set((b) => !b);
|
||||
|
||||
setTimeScale(range);
|
||||
}}
|
||||
classes="hidden md:block"
|
||||
/>
|
||||
<Button
|
||||
icon={() => IconTablerShare}
|
||||
disabled={() => !leanQRGenerate()}
|
||||
onClick={() => {
|
||||
let generate = leanQRGenerate();
|
||||
|
||||
if (generate) {
|
||||
qrcode.set(() =>
|
||||
generate(document.location.href).toDataURL({
|
||||
on: [0xff, 0xff, 0xff, 0xff],
|
||||
off: [0x00, 0x00, 0x00, 0x00],
|
||||
}),
|
||||
);
|
||||
}
|
||||
}}
|
||||
classes="hidden md:block"
|
||||
/>
|
||||
<Button
|
||||
colors={() =>
|
||||
presets.selected().isFavorite()
|
||||
? "text-amber-500 bg-amber-500/15 hover:bg-amber-500/30"
|
||||
: ""
|
||||
}
|
||||
icon={() =>
|
||||
presets.selected().isFavorite()
|
||||
? IconTablerStarFilled
|
||||
: IconTablerStar
|
||||
}
|
||||
onClick={() => presets.selected().isFavorite.set((b) => !b)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Button({
|
||||
icon,
|
||||
colors,
|
||||
onClick,
|
||||
disabled,
|
||||
classes,
|
||||
}: {
|
||||
icon: () => ValidComponent;
|
||||
colors?: () => string;
|
||||
onClick: VoidFunction;
|
||||
disabled?: () => boolean;
|
||||
classes?: string;
|
||||
}) {
|
||||
return (
|
||||
<button
|
||||
disabled={disabled?.()}
|
||||
class={classPropToString([
|
||||
colors?.() || (disabled?.() ? "" : "hover:bg-orange-200/15"),
|
||||
!disabled?.() && "group",
|
||||
classes,
|
||||
"flex-none rounded-lg p-2 disabled:opacity-50",
|
||||
])}
|
||||
onClick={onClick}
|
||||
>
|
||||
<Dynamic
|
||||
component={icon()}
|
||||
class="size-[1.125rem] group-active:scale-90"
|
||||
/>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { cleanChart } from "/src/scripts/lightweightCharts/chart/clean";
|
||||
import { renderChart } from "/src/scripts/lightweightCharts/chart/render";
|
||||
|
||||
export function Chart({
|
||||
presets,
|
||||
datasets,
|
||||
legendSetter,
|
||||
activeResources,
|
||||
}: {
|
||||
presets: Presets;
|
||||
datasets: Datasets;
|
||||
legendSetter: Setter<PresetLegend>;
|
||||
activeResources: Accessor<Set<ResourceDataset<any, any>>>;
|
||||
}) {
|
||||
onMount(() => {
|
||||
createEffect(() => {
|
||||
const preset = presets.selected();
|
||||
|
||||
untrack(() =>
|
||||
renderChart({
|
||||
datasets,
|
||||
preset,
|
||||
legendSetter,
|
||||
activeResources,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
onCleanup(cleanChart);
|
||||
});
|
||||
|
||||
return <div id="chart" class="h-full w-full cursor-crosshair" />;
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
import { createRWS } from "/src/solid/rws";
|
||||
|
||||
const transparency = "66";
|
||||
|
||||
export function Legend({
|
||||
legend: legendList,
|
||||
}: {
|
||||
legend: Accessor<PresetLegend>;
|
||||
}) {
|
||||
const hovering = createRWS<SeriesLegend | undefined>(undefined);
|
||||
|
||||
let toggle = false;
|
||||
|
||||
return (
|
||||
<div class="flex flex-1 items-center gap-1 overflow-y-auto">
|
||||
<For each={legendList()}>
|
||||
{(legend) => {
|
||||
const initialColors = {} as any;
|
||||
const darkenColors = {} as any;
|
||||
|
||||
Object.entries(legend.series.options()).forEach(([k, v]) => {
|
||||
if (k.toLowerCase().includes("color") && v) {
|
||||
initialColors[k] = v;
|
||||
darkenColors[k] = `${v}${transparency}`;
|
||||
} else if (k === "lastValueVisible" && v) {
|
||||
initialColors[k] = v;
|
||||
darkenColors[k] = !v;
|
||||
}
|
||||
});
|
||||
|
||||
createEffect(() => {
|
||||
if (hovering()) {
|
||||
if (hovering()?.title !== legend.title) {
|
||||
legend.series.applyOptions(darkenColors);
|
||||
}
|
||||
} else {
|
||||
legend.series.applyOptions(initialColors);
|
||||
}
|
||||
});
|
||||
|
||||
let previousClickValueOf: number = 0;
|
||||
|
||||
return (
|
||||
<Show when={!legend.disabled()}>
|
||||
<button
|
||||
onMouseEnter={() => {
|
||||
hovering.set(legend);
|
||||
}}
|
||||
onMouseLeave={() => hovering.set(undefined)}
|
||||
onClick={() => {
|
||||
const currentClickValueOf = new Date().valueOf();
|
||||
|
||||
if (currentClickValueOf - previousClickValueOf > 300) {
|
||||
legend.visible.set((visible) => !visible);
|
||||
} else {
|
||||
legendList().forEach((_legend) => {
|
||||
if (_legend.title != legend.title) {
|
||||
_legend.visible.set(toggle);
|
||||
}
|
||||
});
|
||||
|
||||
legend.visible.set(true);
|
||||
|
||||
toggle = !toggle;
|
||||
}
|
||||
|
||||
previousClickValueOf = currentClickValueOf;
|
||||
}}
|
||||
class="flex flex-none items-center space-x-1.5 rounded-full py-1.5 pl-2 pr-2.5 hover:bg-orange-200/20 active:scale-[0.975]"
|
||||
>
|
||||
<span
|
||||
class="flex size-4 flex-col overflow-hidden rounded-full"
|
||||
style={{
|
||||
opacity: legend.visible() ? 1 : 0.5,
|
||||
}}
|
||||
>
|
||||
<For
|
||||
each={
|
||||
Array.isArray(legend.color())
|
||||
? (legend.color() as string[])
|
||||
: [legend.color() as string]
|
||||
}
|
||||
>
|
||||
{(color) => (
|
||||
<span
|
||||
class="w-full flex-1"
|
||||
style={{
|
||||
"background-color": color,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</For>
|
||||
</span>
|
||||
<span
|
||||
class="text-white decoration-white decoration-wavy decoration-[1.5px]"
|
||||
style={{
|
||||
"text-decoration-line": !legend.visible()
|
||||
? "line-through"
|
||||
: undefined,
|
||||
"--tw-text-opacity": legend.visible() ? 1 : 0.5,
|
||||
}}
|
||||
>
|
||||
{legend.title}
|
||||
</span>
|
||||
<Show when={legend.url}>
|
||||
{(url) => (
|
||||
<a
|
||||
class="-my-0.5 !-mr-1 inline-flex size-6 flex-col overflow-hidden rounded-full border border-orange-200/5 bg-orange-200 bg-opacity-5 p-1 pl-0.5 hover:bg-opacity-30"
|
||||
style={{
|
||||
opacity: legend.visible() ? 1 : 0.5,
|
||||
}}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
// event.preventDefault();
|
||||
}}
|
||||
href={url()}
|
||||
target={
|
||||
url()?.startsWith("/") || url()?.startsWith("http")
|
||||
? "_blank"
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<IconTablerExternalLink />
|
||||
</a>
|
||||
)}
|
||||
</Show>
|
||||
</button>
|
||||
</Show>
|
||||
);
|
||||
}}
|
||||
</For>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import { chartState } from "/src/scripts/lightweightCharts/chart/state";
|
||||
import { GENESIS_DAY } from "/src/scripts/lightweightCharts/chart/whitespace";
|
||||
import { ONE_DAY_IN_MS } from "/src/scripts/utils/time";
|
||||
|
||||
import { Box } from "../../box";
|
||||
|
||||
export function TimeScale() {
|
||||
return (
|
||||
<Box dark padded overflowY>
|
||||
<Button onClick={() => setTimeScale()}>All Time</Button>
|
||||
<Button onClick={() => setTimeScale(7)}>1 Week</Button>
|
||||
<Button onClick={() => setTimeScale(30)}>1 Month</Button>
|
||||
<Button onClick={() => setTimeScale(30 * 6)}>6 Months</Button>
|
||||
<Button
|
||||
onClick={() =>
|
||||
setTimeScale(
|
||||
Math.ceil(
|
||||
(new Date().valueOf() -
|
||||
new Date(`${new Date().getUTCFullYear()}-01-01`).valueOf()) /
|
||||
ONE_DAY_IN_MS,
|
||||
),
|
||||
)
|
||||
}
|
||||
>
|
||||
Year To Date
|
||||
</Button>
|
||||
<Button onClick={() => setTimeScale(365)}>1 Year</Button>
|
||||
<Button onClick={() => setTimeScale(2 * 365)}>2 Years</Button>
|
||||
<Button onClick={() => setTimeScale(4 * 365)}>4 Years</Button>
|
||||
<Button onClick={() => setTimeScale(8 * 365)}>8 Years</Button>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
function Button(props: ParentProps & { onClick: VoidFunction }) {
|
||||
return (
|
||||
<button
|
||||
class="min-w-20 flex-shrink-0 flex-grow whitespace-nowrap rounded-lg px-2 py-1.5 hover:bg-white/20 active:scale-95"
|
||||
onClick={props.onClick}
|
||||
>
|
||||
{props.children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
function setTimeScale(days?: number) {
|
||||
const to = new Date();
|
||||
|
||||
if (days) {
|
||||
const from = new Date();
|
||||
from.setDate(from.getUTCDate() - days);
|
||||
|
||||
chartState.chart?.timeScale().setVisibleRange({
|
||||
from: (from.getTime() / 1000) as Time,
|
||||
to: (to.getTime() / 1000) as Time,
|
||||
});
|
||||
} else {
|
||||
// chartState.chart?.timeScale().fitContent();
|
||||
chartState.chart?.timeScale().setVisibleRange({
|
||||
from: (new Date(
|
||||
// datasets.candlesticks.values()?.[0]?.date || "",
|
||||
GENESIS_DAY,
|
||||
).getTime() / 1000) as Time,
|
||||
to: (to.getTime() / 1000) as Time,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
export function Title({ presets }: { presets: Presets }) {
|
||||
return (
|
||||
<div class="flex flex-1 items-center overflow-y-auto pb-1.5 text-orange-100/50">
|
||||
<div class="flex-1 -space-y-1 whitespace-nowrap px-1 md:mt-0.5 md:-space-y-1.5">
|
||||
<h3 class="text-xs">{`/ ${[...presets.selected().path.map(({ name }) => name), presets.selected().name].join(" / ")}`}</h3>
|
||||
<h1 class="text-lg font-bold text-white md:text-xl">
|
||||
{presets.selected().title}
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import { classPropToString } from "/src/solid/classes";
|
||||
import { createRWS } from "/src/solid/rws";
|
||||
|
||||
import { Box } from "../box";
|
||||
import { Actions } from "./components/actions";
|
||||
import { Legend } from "./components/legend";
|
||||
import { TimeScale } from "./components/timeScale";
|
||||
import { Title } from "./components/title";
|
||||
|
||||
export function ChartFrame({
|
||||
presets,
|
||||
datasets,
|
||||
activeResources,
|
||||
hide,
|
||||
qrcode,
|
||||
standalone,
|
||||
fullscreen,
|
||||
}: {
|
||||
presets: Presets;
|
||||
hide?: Accessor<boolean>;
|
||||
qrcode: RWS<string>;
|
||||
activeResources: Accessor<Set<ResourceDataset<any, any>>>;
|
||||
datasets: Datasets;
|
||||
fullscreen?: RWS<boolean>;
|
||||
standalone: boolean;
|
||||
}) {
|
||||
const legend = createRWS<PresetLegend>([]);
|
||||
|
||||
const Chart = lazy(() =>
|
||||
import("./components/chart").then((d) => ({
|
||||
default: d.Chart,
|
||||
})),
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
class={classPropToString([
|
||||
standalone &&
|
||||
"rounded-2xl border border-orange-200/15 bg-gradient-to-b from-orange-100/5 to-black/10 to-80%",
|
||||
"flex size-full min-h-0 flex-1 flex-col overflow-hidden",
|
||||
])}
|
||||
style={{
|
||||
display: (hide ? !hide() : true) ? undefined : "none",
|
||||
}}
|
||||
>
|
||||
<Box flex={false} dark>
|
||||
<Title presets={presets} />
|
||||
|
||||
<div class="-mx-2 border-t border-orange-200/15" />
|
||||
|
||||
<div class="flex pt-1.5">
|
||||
<Legend legend={legend} />
|
||||
|
||||
<div class="-my-1.5 border-l border-orange-200/15 pr-1.5" />
|
||||
|
||||
<Actions presets={presets} qrcode={qrcode} fullscreen={fullscreen} />
|
||||
</div>
|
||||
</Box>
|
||||
|
||||
<div class="-mt-2 min-h-0 flex-1">
|
||||
<Chart
|
||||
activeResources={activeResources}
|
||||
datasets={datasets}
|
||||
legendSetter={legend.set}
|
||||
presets={presets}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<TimeScale />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
export function Counter({
|
||||
count,
|
||||
name,
|
||||
setRef,
|
||||
}: {
|
||||
count: () => number;
|
||||
name: string;
|
||||
setRef?: Setter<HTMLDivElement | undefined>;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
ref={setRef}
|
||||
class="text-orange-100/75"
|
||||
style={{
|
||||
"border-style": count() ? "dashed" : "none",
|
||||
}}
|
||||
>
|
||||
Counted{" "}
|
||||
<span class="font-medium text-orange-400/75">
|
||||
{count().toLocaleString("en-us")}
|
||||
</span>{" "}
|
||||
{name}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import { Header } from "./header";
|
||||
import { Line } from "./line";
|
||||
import { Number } from "./number";
|
||||
|
||||
export function FavoritesFrame({
|
||||
presets,
|
||||
selectedFrame,
|
||||
}: {
|
||||
presets: Presets;
|
||||
selectedFrame: Accessor<FrameName>;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
class="flex-1 overflow-y-auto"
|
||||
hidden={selectedFrame() !== "Favorites"}
|
||||
>
|
||||
<div class="flex max-h-full min-h-0 flex-1 flex-col gap-4 p-4">
|
||||
<Header title="Favorites">
|
||||
<Number number={() => presets.favorites().length} /> presets marked as
|
||||
favorites.
|
||||
</Header>
|
||||
|
||||
<div class="-mx-4 border-t border-orange-200/10" />
|
||||
|
||||
<div
|
||||
class="space-y-0.5 py-1"
|
||||
style={{
|
||||
display: !presets.favorites().length ? "none" : undefined,
|
||||
}}
|
||||
>
|
||||
<For each={presets.favorites()}>
|
||||
{(preset) => (
|
||||
<Line
|
||||
id={`favorite-${preset.id}`}
|
||||
name={preset.title}
|
||||
onClick={() => presets.select(preset)}
|
||||
active={() => presets.selected() === preset}
|
||||
header={`/ ${[...preset.path.map(({ name }) => name), preset.name].join(" / ")}`}
|
||||
/>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
|
||||
<div class="h-[25dvh] flex-none" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
export function Header({ title, children }: { title: string } & ParentProps) {
|
||||
return (
|
||||
<div>
|
||||
<h3 class="text-lg font-bold md:text-xl">{title}</h3>
|
||||
<p class="text-orange-100/75">{children}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||