Compare commits
192 Commits
add-code-s
...
jazz-react
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
61b346a948 | ||
|
|
c2dfe6a0dd | ||
|
|
81e8910519 | ||
|
|
cce679b031 | ||
|
|
c2dbf45f26 | ||
|
|
04d3a8c677 | ||
|
|
91019f3f85 | ||
|
|
06278c3d97 | ||
|
|
36273b3114 | ||
|
|
51a0a9f23f | ||
|
|
fc24a65264 | ||
|
|
fa0b9003dc | ||
|
|
8a0da655e8 | ||
|
|
fff11daca7 | ||
|
|
fd011d76f4 | ||
|
|
0681a8301e | ||
|
|
221c58f1f7 | ||
|
|
ebca75bf70 | ||
|
|
b5e516f974 | ||
|
|
8fc5a49ca8 | ||
|
|
00054aa9a5 | ||
|
|
6f3c98f704 | ||
|
|
8823957c6e | ||
|
|
47ec0cd8fe | ||
|
|
685031c39c | ||
|
|
e4fd89dbbb | ||
|
|
e0dd0065fb | ||
|
|
98d7d402eb | ||
|
|
57deec451a | ||
|
|
bc8ea23e6d | ||
|
|
d2ad2db930 | ||
|
|
44497fd058 | ||
|
|
a18c187818 | ||
|
|
f49949cc25 | ||
|
|
3cc6aee7e6 | ||
|
|
c80763100e | ||
|
|
6ed75ebb35 | ||
|
|
30d7734124 | ||
|
|
bca8b44189 | ||
|
|
36a069c90f | ||
|
|
757b37e8ed | ||
|
|
46b1163058 | ||
|
|
8c261b0409 | ||
|
|
fdacde57dd | ||
|
|
817dd7dde1 | ||
|
|
6279dd1467 | ||
|
|
abd4b94392 | ||
|
|
68c6ee77c0 | ||
|
|
d2b2812428 | ||
|
|
fe214cc3c2 | ||
|
|
2cca9506ad | ||
|
|
17e69aff8f | ||
|
|
c45331e645 | ||
|
|
4337001526 | ||
|
|
9eef1ec031 | ||
|
|
9d31bbc3aa | ||
|
|
911add3dea | ||
|
|
eed9ad99d8 | ||
|
|
a1d0793e95 | ||
|
|
ac9aad2ff5 | ||
|
|
1ed4ab5a8e | ||
|
|
b03aed69b6 | ||
|
|
1120d84c89 | ||
|
|
4e4671cba2 | ||
|
|
cdca330f1e | ||
|
|
bf8840cfbc | ||
|
|
89d4da8d50 | ||
|
|
74e441d233 | ||
|
|
58e5682e3d | ||
|
|
4915422646 | ||
|
|
2337d66ec5 | ||
|
|
e2c73d3c3d | ||
|
|
e4c0314368 | ||
|
|
228cd42b23 | ||
|
|
1c80492482 | ||
|
|
bfbd5c514b | ||
|
|
520bcfd72a | ||
|
|
566fde6ecc | ||
|
|
c44b5db09a | ||
|
|
477d0aa899 | ||
|
|
60171b477f | ||
|
|
9d4fb4d32b | ||
|
|
d5b9544f6b | ||
|
|
357e6ba59f | ||
|
|
0aca58af1e | ||
|
|
790caf6f0d | ||
|
|
d4fbae2bf3 | ||
|
|
da3bc74fe5 | ||
|
|
a1bd53100b | ||
|
|
46d8b19918 | ||
|
|
8ed31452b9 | ||
|
|
dfb8c3f329 | ||
|
|
de0045e2ee | ||
|
|
7dfb485558 | ||
|
|
81ac9d0dfd | ||
|
|
1430ef6158 | ||
|
|
8759bccc02 | ||
|
|
5dd0e31e2a | ||
|
|
a2c1586d26 | ||
|
|
7000698629 | ||
|
|
f52511721e | ||
|
|
94e84dd5bd | ||
|
|
ee0b1695fd | ||
|
|
e338e9005b | ||
|
|
00f2dea6ab | ||
|
|
4fe3ae89da | ||
|
|
195d703e1f | ||
|
|
a49e3492ec | ||
|
|
a81846241c | ||
|
|
e0d62096af | ||
|
|
77ee9bc529 | ||
|
|
e20c4a2d76 | ||
|
|
675e72aec3 | ||
|
|
8a45e6d642 | ||
|
|
171565a88c | ||
|
|
f8945e99a9 | ||
|
|
830a007a08 | ||
|
|
3072072c60 | ||
|
|
2bb672ea20 | ||
|
|
b6f7c61445 | ||
|
|
a9af4fa0ff | ||
|
|
e37369027e | ||
|
|
47eedc4433 | ||
|
|
3d4c151260 | ||
|
|
eadbb2b8e5 | ||
|
|
12ab0b9ac1 | ||
|
|
a723ad5e92 | ||
|
|
a459193bbd | ||
|
|
65c0eecc62 | ||
|
|
7f63675ee5 | ||
|
|
35f236f17e | ||
|
|
4131754c32 | ||
|
|
8ead3a7ce5 | ||
|
|
2aa055e164 | ||
|
|
2e2e0b8dce | ||
|
|
42897dc211 | ||
|
|
d80a8f35ec | ||
|
|
7a6dcc28b2 | ||
|
|
3f78c011df | ||
|
|
55aa159b56 | ||
|
|
6225427363 | ||
|
|
81d922d490 | ||
|
|
0ba357dab7 | ||
|
|
03b4c3cd12 | ||
|
|
265b2aaf83 | ||
|
|
fbb22f1e9d | ||
|
|
280e1e25e3 | ||
|
|
c7306a0c57 | ||
|
|
4af29dd034 | ||
|
|
4d70862344 | ||
|
|
d40ef934e8 | ||
|
|
7af0b2b938 | ||
|
|
49ee543b5e | ||
|
|
f68be96d86 | ||
|
|
99721a2e56 | ||
|
|
e4b6678538 | ||
|
|
bb443269c3 | ||
|
|
2e97deb894 | ||
|
|
cabec79dde | ||
|
|
6f9afdc2d3 | ||
|
|
3e8d4fdd53 | ||
|
|
35d5e4b39e | ||
|
|
aeb521e6d9 | ||
|
|
5228dc43c9 | ||
|
|
4ba7731dce | ||
|
|
1afe45d2f2 | ||
|
|
7e574191eb | ||
|
|
88a6cc132f | ||
|
|
574509e83a | ||
|
|
f9ce03113e | ||
|
|
b0eda8350a | ||
|
|
9cb5ea332a | ||
|
|
5239a05734 | ||
|
|
4bed591002 | ||
|
|
e4180308ea | ||
|
|
2b48954693 | ||
|
|
49d8287b24 | ||
|
|
b6af85c91e | ||
|
|
308ab80736 | ||
|
|
b759f29ab8 | ||
|
|
b826adabbf | ||
|
|
2b4e839926 | ||
|
|
c7aae14e03 | ||
|
|
263fe7a4b3 | ||
|
|
8049f1d972 | ||
|
|
f3263e60ff | ||
|
|
6d6849707e | ||
|
|
11ac24e3f4 | ||
|
|
7a30f3c025 | ||
|
|
c84f96c436 | ||
|
|
0ecf666c9f | ||
|
|
42e4969f92 |
@@ -1,11 +0,0 @@
|
||||
# EditorConfig is awesome: https://editorconfig.org
|
||||
|
||||
# top-most EditorConfig file
|
||||
root = true
|
||||
|
||||
# Unix-style newlines with a newline ending every file
|
||||
[*]
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
51
.github/workflows/jazz-run.yml
vendored
Normal file
@@ -0,0 +1,51 @@
|
||||
name: Jazz Run Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["main"]
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
continue-on-error: true
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: true
|
||||
|
||||
- name: Enable corepack
|
||||
run: corepack enable
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version-file: '.node-version'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Get pnpm store directory
|
||||
shell: bash
|
||||
run: |
|
||||
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
|
||||
|
||||
- uses: actions/cache@v3
|
||||
name: Setup pnpm cache
|
||||
with:
|
||||
path: ${{ env.STORE_PATH }}
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-store-
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Build jazz-run
|
||||
run: pnpm exec turbo build && chmod +x dist/index.js;
|
||||
working-directory: ./packages/jazz-run
|
||||
|
||||
- name: Run create account
|
||||
run: ./dist/index.js account create --name "Jazz Run CI test"
|
||||
working-directory: ./packages/jazz-run
|
||||
|
||||
22
.github/workflows/monorepo-linting.yml
vendored
@@ -13,29 +13,9 @@ jobs:
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version-file: '.node-version'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Get pnpm store directory
|
||||
shell: bash
|
||||
run: |
|
||||
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
|
||||
|
||||
- uses: actions/cache@v4
|
||||
name: Setup pnpm cache
|
||||
with:
|
||||
path: ${{ env.STORE_PATH }}
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-store-
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Run sherif
|
||||
run: npx sherif@1.0.0
|
||||
|
||||
- name: Run formatter
|
||||
run: pnpm format
|
||||
|
||||
2
.github/workflows/playwright.yml
vendored
@@ -13,7 +13,7 @@ jobs:
|
||||
continue-on-error: true
|
||||
strategy:
|
||||
matrix:
|
||||
project: ["e2e/BinaryCoStream", "e2e/CoValues", "examples/chat", "examples/pets"]
|
||||
project: ["e2e/BinaryCoStream", "e2e/CoValues", "examples/chat", "examples/music-player", "examples/pets"]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
51
.github/workflows/release.yml
vendored
Normal file
@@ -0,0 +1,51 @@
|
||||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
concurrency: ${{ github.workflow }}-${{ github.ref }}
|
||||
|
||||
jobs:
|
||||
release:
|
||||
name: Release
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Repo
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Enable corepack
|
||||
run: corepack enable
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version-file: '.node-version'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Get pnpm store directory
|
||||
shell: bash
|
||||
run: |
|
||||
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
|
||||
|
||||
- uses: actions/cache@v3
|
||||
name: Setup pnpm cache
|
||||
with:
|
||||
path: ${{ env.STORE_PATH }}
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-store-
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Create Release Pull Request or Publish to npm
|
||||
id: changesets
|
||||
uses: changesets/action@v1
|
||||
with:
|
||||
version: pnpm changeset-version
|
||||
publish: pnpm release
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
@@ -1 +0,0 @@
|
||||
{}
|
||||
9
.prettierrc.js
Normal file
@@ -0,0 +1,9 @@
|
||||
/** @type {import("prettier").Config} */
|
||||
const config = {
|
||||
trailingComma: "all",
|
||||
tabWidth: 4,
|
||||
semi: true,
|
||||
singleQuote: false,
|
||||
};
|
||||
|
||||
export default config;
|
||||
131
CODE_OF_CONDUCT.md
Normal file
@@ -0,0 +1,131 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
We as members, contributors, and leaders pledge to make participation in our
|
||||
community a harassment-free experience for everyone, regardless of age, body
|
||||
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||
identity and expression, level of experience, education, socio-economic status,
|
||||
nationality, personal appearance, race, caste, color, religion, or sexual
|
||||
identity and orientation.
|
||||
|
||||
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||
diverse, inclusive, and healthy community.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to a positive environment for our
|
||||
community include:
|
||||
|
||||
- Demonstrating empathy and kindness toward other people
|
||||
- Being respectful of differing opinions, viewpoints, and experiences
|
||||
- Giving and gracefully accepting constructive feedback
|
||||
- Accepting responsibility and apologizing to those affected by our mistakes,
|
||||
and learning from the experience
|
||||
- Focusing on what is best not just for us as individuals, but for the overall
|
||||
community
|
||||
|
||||
Examples of unacceptable behavior include:
|
||||
|
||||
- The use of sexualized language or imagery, and sexual attention or advances of
|
||||
any kind
|
||||
- Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
- Public or private harassment
|
||||
- Publishing others' private information, such as a physical or email address,
|
||||
without their explicit permission
|
||||
- Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Enforcement Responsibilities
|
||||
|
||||
Community leaders are responsible for clarifying and enforcing our standards of
|
||||
acceptable behavior and will take appropriate and fair corrective action in
|
||||
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||
or harmful.
|
||||
|
||||
Community leaders have the right and responsibility to remove, edit, or reject
|
||||
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||
decisions when appropriate.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies within all community spaces, and also applies when
|
||||
an individual is officially representing the community in public spaces.
|
||||
Examples of representing our community include using an official email address,
|
||||
posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported to [the community leaders responsible for enforcement](mailto:hello@gcmp.io).
|
||||
All complaints will be reviewed and investigated promptly and fairly.
|
||||
|
||||
All community leaders are obligated to respect the privacy and security of the
|
||||
reporter of any incident.
|
||||
|
||||
## Enforcement Guidelines
|
||||
|
||||
Community leaders will follow these Community Impact Guidelines in determining
|
||||
the consequences for any action they deem in violation of this Code of Conduct:
|
||||
|
||||
### 1. Correction
|
||||
|
||||
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||
unprofessional or unwelcome in the community.
|
||||
|
||||
**Consequence**: A private, written warning from community leaders, providing
|
||||
clarity around the nature of the violation and an explanation of why the
|
||||
behavior was inappropriate. A public apology may be requested.
|
||||
|
||||
### 2. Warning
|
||||
|
||||
**Community Impact**: A violation through a single incident or series of
|
||||
actions.
|
||||
|
||||
**Consequence**: A warning with consequences for continued behavior. No
|
||||
interaction with the people involved, including unsolicited interaction with
|
||||
those enforcing the Code of Conduct, for a specified period of time. This
|
||||
includes avoiding interactions in community spaces as well as external channels
|
||||
like social media. Violating these terms may lead to a temporary or permanent
|
||||
ban.
|
||||
|
||||
### 3. Temporary Ban
|
||||
|
||||
**Community Impact**: A serious violation of community standards, including
|
||||
sustained inappropriate behavior.
|
||||
|
||||
**Consequence**: A temporary ban from any sort of interaction or public
|
||||
communication with the community for a specified period of time. No public or
|
||||
private interaction with the people involved, including unsolicited interaction
|
||||
with those enforcing the Code of Conduct, is allowed during this period.
|
||||
Violating these terms may lead to a permanent ban.
|
||||
|
||||
### 4. Permanent Ban
|
||||
|
||||
**Community Impact**: Demonstrating a pattern of violation of community
|
||||
standards, including sustained inappropriate behavior, harassment of an
|
||||
individual, or aggression toward or disparagement of classes of individuals.
|
||||
|
||||
**Consequence**: A permanent ban from any sort of public interaction within the
|
||||
community.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||
version 2.1, available at
|
||||
[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
|
||||
|
||||
Community Impact Guidelines were inspired by
|
||||
[Mozilla's code of conduct enforcement ladder][Mozilla CoC].
|
||||
|
||||
For answers to common questions about this code of conduct, see the FAQ at
|
||||
[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
|
||||
[https://www.contributor-covenant.org/translations][translations].
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
|
||||
[Mozilla CoC]: https://github.com/mozilla/diversity
|
||||
[FAQ]: https://www.contributor-covenant.org/faq
|
||||
[translations]: https://www.contributor-covenant.org/translations
|
||||
73
CONTRIBUTING.md
Normal file
@@ -0,0 +1,73 @@
|
||||
# Contribution Guide
|
||||
|
||||
Thank you for considering contributing to Jazz! Jazz is an open-source framework for building local-first apps. We value your time and effort and are excited to collaborate with you. This guide will help you get started with contributing.
|
||||
|
||||
## How to Contribute
|
||||
|
||||
### 1. Reporting Bugs
|
||||
|
||||
If you find a bug, please [open an issue with as much detail as possible](https://github.com/gardencmp/jazz/issues). Include:
|
||||
|
||||
- A clear and descriptive title.
|
||||
- Steps to reproduce the issue.
|
||||
- What you expected to happen.
|
||||
- What actually happened.
|
||||
|
||||
### 2. Suggesting Enhancements
|
||||
|
||||
We welcome all ideas! If you have suggestions, feel free to open an issue marked with the "enhancement" label. Please provide context on why the enhancement would be beneficial and how it could be implemented.
|
||||
|
||||
### 3. Pull Requests
|
||||
|
||||
1. **Fork the repository** and create your feature branch (see [GitHub's guide on forking a repository](https://docs.github.com/en/get-started/quickstart/fork-a-repo) if you're unfamiliar with the process):
|
||||
|
||||
2. **Make your changes**, ensuring that you follow our coding standards (`pnpm format` (prettier) and `pnpm lint` (eslint) will automatically let you know there are issues).
|
||||
|
||||
3. **Commit your changes** with a descriptive commit message.
|
||||
|
||||
4. **Push to your fork** and submit a pull request.
|
||||
|
||||
5. **Describe your pull request**, explaining the problem it solves or the enhancement it adds.
|
||||
|
||||
### 4. Code Style Guidelines
|
||||
|
||||
- We use [Prettier](https://prettier.io/) for formatting. Please ensure your code is formatted before submitting.
|
||||
- Write descriptive comments where necessary.
|
||||
|
||||
### 5. Local Setup
|
||||
|
||||
1. **Clone the repository**:
|
||||
```bash
|
||||
git clone https://github.com/gardencmp/jazz.git
|
||||
```
|
||||
|
||||
2. **Install dependencies**:
|
||||
```bash
|
||||
pnpm install
|
||||
```
|
||||
|
||||
3. **Run tests** to verify everything is working:
|
||||
```bash
|
||||
pnpm test
|
||||
```
|
||||
|
||||
### 6. Testing
|
||||
|
||||
Please write tests for any new features or bug fixes. We use Vitest for unit tests. Make sure all tests pass before submitting a pull request.
|
||||
|
||||
### 7. Communication
|
||||
|
||||
- If you're unsure about anything, feel free to ask questions by opening a discussion, reaching out via issues, or on our [Discord](https://discord.gg/utDMjHYg42).
|
||||
- Be respectful and constructive, this is a welcoming community for everyone.
|
||||
- Please be mindful of GitHub’s [Community Guidelines](https://docs.github.com/en/site-policy/github-terms/github-community-guidelines), which include being kind, avoiding disruptive behavior, and respecting others.
|
||||
|
||||
## Code of Conduct
|
||||
|
||||
Please read and adhere to our [Code of Conduct](./CODE_OF_CONDUCT.md) to ensure a positive experience for all contributors.
|
||||
|
||||
---
|
||||
|
||||
Thank you again for your interest in contributing to Jazz. Your help makes this project better for everyone!
|
||||
|
||||
If you have any questions, don't hesitate to reach out. Let's make something great together!
|
||||
|
||||
@@ -1,5 +1,48 @@
|
||||
# @jazz-e2e/binarycostream
|
||||
|
||||
## 0.0.93
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [cce679b]
|
||||
- jazz-tools@0.8.15
|
||||
- jazz-react@0.8.15
|
||||
|
||||
## 0.0.92
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [36273b3]
|
||||
- jazz-tools@0.8.14
|
||||
- jazz-react@0.8.14
|
||||
|
||||
## 0.0.91
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [fd011d7]
|
||||
- jazz-tools@0.8.13
|
||||
- jazz-react@0.8.13
|
||||
|
||||
## 0.0.90
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [6ed75eb]
|
||||
- Updated dependencies [3cc6aee]
|
||||
- cojson@0.8.12
|
||||
- jazz-react@0.8.12
|
||||
- jazz-tools@0.8.12
|
||||
|
||||
## 0.0.89
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [1ed4ab5]
|
||||
- cojson@0.8.11
|
||||
- jazz-react@0.8.11
|
||||
- jazz-tools@0.8.11
|
||||
|
||||
## 0.0.88
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@jazz-e2e/binarycostream",
|
||||
"private": true,
|
||||
"version": "0.0.88",
|
||||
"version": "0.0.93",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
@@ -14,11 +14,11 @@
|
||||
"*.{js,jsx,mdx,json}": "prettier --write"
|
||||
},
|
||||
"dependencies": {
|
||||
"cojson": "workspace:0.8.5",
|
||||
"cojson": "workspace:0.8.12",
|
||||
"hash-slash": "workspace:0.2.1",
|
||||
"is-ci": "^3.0.1",
|
||||
"jazz-react": "workspace:0.8.7",
|
||||
"jazz-tools": "workspace:0.8.5",
|
||||
"jazz-react": "workspace:0.8.15",
|
||||
"jazz-tools": "workspace:0.8.15",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
},
|
||||
|
||||
@@ -27,10 +27,11 @@ export default defineConfig({
|
||||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||
use: {
|
||||
/* Base URL to use in actions like `await page.goto('/')`. */
|
||||
baseURL: isCI ? "http://localhost:4173/" : "http://localhost:5173",
|
||||
baseURL: "http://localhost:5173/",
|
||||
|
||||
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||
trace: "on-first-retry",
|
||||
permissions: ["clipboard-read", "clipboard-write"],
|
||||
},
|
||||
|
||||
/* Configure projects for major browsers */
|
||||
@@ -42,8 +43,11 @@ export default defineConfig({
|
||||
],
|
||||
|
||||
/* Run your local dev server before starting the tests */
|
||||
webServer: isCI ? {
|
||||
command: "pnpm preview",
|
||||
url: "http://localhost:4173/",
|
||||
} : undefined,
|
||||
webServer: [
|
||||
{
|
||||
command: "pnpm preview --port 5173",
|
||||
url: "http://localhost:5173/",
|
||||
reuseExistingServer: !isCI,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
@@ -1,5 +1,48 @@
|
||||
# @jazz-e2e/covalues
|
||||
|
||||
## 0.0.92
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [cce679b]
|
||||
- jazz-tools@0.8.15
|
||||
- jazz-react@0.8.15
|
||||
|
||||
## 0.0.91
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [36273b3]
|
||||
- jazz-tools@0.8.14
|
||||
- jazz-react@0.8.14
|
||||
|
||||
## 0.0.90
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [fd011d7]
|
||||
- jazz-tools@0.8.13
|
||||
- jazz-react@0.8.13
|
||||
|
||||
## 0.0.89
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [6ed75eb]
|
||||
- Updated dependencies [3cc6aee]
|
||||
- cojson@0.8.12
|
||||
- jazz-react@0.8.12
|
||||
- jazz-tools@0.8.12
|
||||
|
||||
## 0.0.88
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [1ed4ab5]
|
||||
- cojson@0.8.11
|
||||
- jazz-react@0.8.11
|
||||
- jazz-tools@0.8.11
|
||||
|
||||
## 0.0.87
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@jazz-e2e/covalues",
|
||||
"private": true,
|
||||
"version": "0.0.87",
|
||||
"version": "0.0.92",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -27,10 +27,11 @@ export default defineConfig({
|
||||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||
use: {
|
||||
/* Base URL to use in actions like `await page.goto('/')`. */
|
||||
baseURL: isCI ? "http://localhost:4173/" : "http://localhost:5173",
|
||||
baseURL: "http://localhost:5173/",
|
||||
|
||||
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||
trace: "on-first-retry",
|
||||
permissions: ["clipboard-read", "clipboard-write"],
|
||||
},
|
||||
|
||||
/* Configure projects for major browsers */
|
||||
@@ -42,8 +43,11 @@ export default defineConfig({
|
||||
],
|
||||
|
||||
/* Run your local dev server before starting the tests */
|
||||
webServer: isCI ? {
|
||||
command: "pnpm preview",
|
||||
url: "http://localhost:4173/",
|
||||
} : undefined,
|
||||
webServer: [
|
||||
{
|
||||
command: "pnpm preview --port 5173",
|
||||
url: "http://localhost:5173/",
|
||||
reuseExistingServer: !isCI,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
@@ -2,15 +2,20 @@ import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import { AuthAndJazz } from "./jazz";
|
||||
import { TestInput } from "./pages/TestInput";
|
||||
import { RouterProvider, createHashRouter } from "react-router-dom";
|
||||
import { ResumeSyncState } from "./pages/ResumeSyncState";
|
||||
import { RouterProvider, createBrowserRouter } from "react-router-dom";
|
||||
|
||||
const router = createHashRouter([
|
||||
const router = createBrowserRouter([
|
||||
{
|
||||
path: "/",
|
||||
path: "/test-input",
|
||||
element: <TestInput />,
|
||||
},
|
||||
{
|
||||
path: "/test-input",
|
||||
path: "/resume-sync",
|
||||
element: <ResumeSyncState />,
|
||||
},
|
||||
{
|
||||
path: "/",
|
||||
element: <TestInput />,
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -1,25 +1,43 @@
|
||||
import { createJazzReactApp } from "jazz-react";
|
||||
import { ephemeralCredentialsAuth } from "jazz-tools";
|
||||
import { useState } from "react";
|
||||
import { createJazzReactApp, useDemoAuth } from "jazz-react";
|
||||
import { useEffect, useRef } from "react";
|
||||
|
||||
const key = `test-comap@jazz.tools`;
|
||||
|
||||
const localSync = new URLSearchParams(location.search).has("localSync");
|
||||
const url = new URL(window.location.href);
|
||||
const peer =
|
||||
(url.searchParams.get("peer") as `ws://${string}`) ??
|
||||
`wss://cloud.jazz.tools/`;
|
||||
|
||||
const Jazz = createJazzReactApp();
|
||||
|
||||
export const { useAccount, useCoState } = Jazz;
|
||||
|
||||
export function AuthAndJazz({ children }: { children: React.ReactNode }) {
|
||||
const [ephemeralAuth] = useState(ephemeralCredentialsAuth())
|
||||
|
||||
return (
|
||||
<Jazz.Provider auth={ephemeralAuth} peer={
|
||||
localSync
|
||||
? `ws://localhost:4200?key=${key}`
|
||||
: `wss://cloud.jazz.tools/?key=${key}`
|
||||
}>
|
||||
{children}
|
||||
</Jazz.Provider>
|
||||
);
|
||||
function getUserInfo() {
|
||||
return url.searchParams.get("userName") ?? "Mister X";
|
||||
}
|
||||
|
||||
export function AuthAndJazz({ children }: { children: React.ReactNode }) {
|
||||
const [auth, state] = useDemoAuth();
|
||||
|
||||
const signedUp = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (state.state === "ready" && !signedUp.current) {
|
||||
const userName = getUserInfo();
|
||||
|
||||
if (state.existingUsers.includes(userName)) {
|
||||
state.logInAs(userName);
|
||||
} else {
|
||||
state.signUp(userName);
|
||||
}
|
||||
|
||||
signedUp.current = true;
|
||||
}
|
||||
}, [state.state]);
|
||||
|
||||
return (
|
||||
<Jazz.Provider auth={auth} peer={`${peer}?key=${key}`}>
|
||||
{children}
|
||||
</Jazz.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
53
e2e/CoValues/src/pages/ResumeSyncState.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
import { co, CoMap, Group, ID } from "jazz-tools";
|
||||
import { useAccount, useCoState } from "../jazz";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export class ResumeSyncCoMap extends CoMap {
|
||||
value = co.string;
|
||||
}
|
||||
|
||||
function getIdParam() {
|
||||
const url = new URL(window.location.href);
|
||||
return (url.searchParams.get("id") as ID<ResumeSyncCoMap>) ?? undefined;
|
||||
}
|
||||
|
||||
export function ResumeSyncState() {
|
||||
const [id, setId] = useState(getIdParam);
|
||||
const coMap = useCoState(ResumeSyncCoMap, id);
|
||||
const { me } = useAccount();
|
||||
|
||||
useEffect(() => {
|
||||
if (id) {
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.set("id", id);
|
||||
history.pushState({}, "", url.toString());
|
||||
}
|
||||
}, [id])
|
||||
|
||||
useEffect(() => {
|
||||
if (!me || id) return;
|
||||
|
||||
const group = Group.create({ owner: me });
|
||||
|
||||
group.addMember("everyone", "writer");
|
||||
|
||||
setId(ResumeSyncCoMap.create({ value: "" }, { owner: group }).id);
|
||||
}, [me]);
|
||||
|
||||
if (!coMap) return null;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>Resume Sync State</h1>
|
||||
<p data-testid="id">{coMap.id}</p>
|
||||
<label htmlFor="value">Value</label>
|
||||
<input
|
||||
id="value"
|
||||
value={coMap.value ?? ""}
|
||||
onChange={(e) => {
|
||||
coMap.value = e.target.value;
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
47
e2e/CoValues/tests/ResumeSyncState.spec.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
import { setTimeout } from "node:timers/promises";
|
||||
|
||||
test.describe("ResumeSyncState", () => {
|
||||
test.skip("should resume the sync even after a page reload", async ({ page, browser }) => {
|
||||
const context = page.context();
|
||||
|
||||
await page.goto("/resume-sync?userName=SuperMario");
|
||||
|
||||
const id = await page.getByTestId("id").textContent();
|
||||
|
||||
// Sync an initial value
|
||||
await page.getByRole("textbox", { name: "Value" }).fill("Let's go!");
|
||||
await setTimeout(1000);
|
||||
|
||||
await context.setOffline(true);
|
||||
|
||||
// Change the value while offline
|
||||
await page.getByRole("textbox", { name: "Value" }).fill("Mammamia!");
|
||||
|
||||
// Navigate away from the page
|
||||
await page.goto(`about:blank`);
|
||||
|
||||
await setTimeout(1000);
|
||||
await context.setOffline(false);
|
||||
|
||||
// Reload the page but without loading the coValue
|
||||
// await page.goto(`/resume-sync?userName=SuperMario`);
|
||||
await page.goto(`/resume-sync?userName=SuperMario`);
|
||||
|
||||
await setTimeout(1000);
|
||||
|
||||
await expect(page.getByTestId("id")).toBeInViewport();
|
||||
|
||||
// Create a new incognito instance and try to load the coValue
|
||||
const newUserPage = await (await browser.newContext()).newPage();
|
||||
await newUserPage.goto(`/resume-sync?userName=Luigi&id=${id}`);
|
||||
|
||||
await expect(newUserPage.getByTestId("id")).toBeInViewport();
|
||||
|
||||
// The initial user should have synced the value even if the coValue was not loaded
|
||||
// when the user is back online
|
||||
await expect(newUserPage.getByRole("textbox", { name: "Value" })).toHaveValue("Mammamia!", {
|
||||
timeout: 20_000
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,49 @@
|
||||
# jazz-example-book-shelf
|
||||
|
||||
## 0.1.8
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [cce679b]
|
||||
- jazz-tools@0.8.15
|
||||
- jazz-browser-media-images@0.8.15
|
||||
- jazz-react@0.8.15
|
||||
|
||||
## 0.1.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [36273b3]
|
||||
- jazz-tools@0.8.14
|
||||
- jazz-browser-media-images@0.8.14
|
||||
- jazz-react@0.8.14
|
||||
|
||||
## 0.1.6
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [fd011d7]
|
||||
- jazz-tools@0.8.13
|
||||
- jazz-browser-media-images@0.8.13
|
||||
- jazz-react@0.8.13
|
||||
|
||||
## 0.1.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [3cc6aee]
|
||||
- jazz-react@0.8.12
|
||||
- jazz-tools@0.8.12
|
||||
- jazz-browser-media-images@0.8.12
|
||||
|
||||
## 0.1.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react@0.8.11
|
||||
- jazz-tools@0.8.11
|
||||
- jazz-browser-media-images@0.8.11
|
||||
|
||||
## 0.1.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "jazz-example-book-shelf",
|
||||
"version": "0.1.3",
|
||||
"version": "0.1.8",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
@@ -11,9 +11,9 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"clsx": "^2.0.0",
|
||||
"jazz-browser-media-images": "workspace:0.8.7",
|
||||
"jazz-react": "workspace:0.8.7",
|
||||
"jazz-tools": "workspace:0.8.5",
|
||||
"jazz-browser-media-images": "workspace:0.8.15",
|
||||
"jazz-react": "workspace:0.8.15",
|
||||
"jazz-tools": "workspace:0.8.15",
|
||||
"next": "14.2.5",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
|
||||
@@ -1,5 +1,54 @@
|
||||
# jazz-example-chat
|
||||
|
||||
## 0.0.92
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [cce679b]
|
||||
- Updated dependencies [221c58f]
|
||||
- jazz-tools@0.8.15
|
||||
- jazz-react-auth-clerk@0.8.15
|
||||
- jazz-react@0.8.15
|
||||
|
||||
## 0.0.91
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [36273b3]
|
||||
- jazz-tools@0.8.14
|
||||
- jazz-react@0.8.14
|
||||
- jazz-react-auth-clerk@0.8.14
|
||||
|
||||
## 0.0.90
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [fd011d7]
|
||||
- jazz-tools@0.8.13
|
||||
- jazz-react@0.8.13
|
||||
- jazz-react-auth-clerk@0.8.13
|
||||
|
||||
## 0.0.89
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [6ed75eb]
|
||||
- Updated dependencies [3cc6aee]
|
||||
- cojson@0.8.12
|
||||
- jazz-react@0.8.12
|
||||
- jazz-react-auth-clerk@0.8.12
|
||||
- jazz-tools@0.8.12
|
||||
|
||||
## 0.0.88
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [1ed4ab5]
|
||||
- cojson@0.8.11
|
||||
- jazz-react@0.8.11
|
||||
- jazz-react-auth-clerk@0.8.11
|
||||
- jazz-tools@0.8.11
|
||||
|
||||
## 0.0.87
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "jazz-example-chat-clerk",
|
||||
"private": true,
|
||||
"version": "0.0.87",
|
||||
"version": "0.0.92",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
@@ -21,11 +21,11 @@
|
||||
"@radix-ui/react-toast": "^1.1.4",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.0.0",
|
||||
"cojson": "workspace:0.8.5",
|
||||
"cojson": "workspace:0.8.12",
|
||||
"hash-slash": "workspace:0.2.1",
|
||||
"jazz-react": "workspace:0.8.7",
|
||||
"jazz-react-auth-clerk": "workspace:0.8.7",
|
||||
"jazz-tools": "workspace:0.8.5",
|
||||
"jazz-react": "workspace:0.8.15",
|
||||
"jazz-react-auth-clerk": "workspace:0.8.15",
|
||||
"jazz-tools": "workspace:0.8.15",
|
||||
"lucide-react": "^0.274.0",
|
||||
"qrcode": "^1.5.3",
|
||||
"react": "^18.2.0",
|
||||
|
||||
@@ -1,5 +1,54 @@
|
||||
# chat-rn-clerk
|
||||
|
||||
## 1.0.8
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [cce679b]
|
||||
- Updated dependencies [221c58f]
|
||||
- jazz-tools@0.8.15
|
||||
- jazz-react-auth-clerk@0.8.15
|
||||
- jazz-react-native@0.8.15
|
||||
- jazz-react-native-media-images@0.8.11
|
||||
|
||||
## 1.0.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [36273b3]
|
||||
- jazz-tools@0.8.14
|
||||
- jazz-react-auth-clerk@0.8.14
|
||||
- jazz-react-native@0.8.14
|
||||
- jazz-react-native-media-images@0.8.10
|
||||
|
||||
## 1.0.6
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [fd011d7]
|
||||
- jazz-tools@0.8.13
|
||||
- jazz-react-auth-clerk@0.8.13
|
||||
- jazz-react-native@0.8.13
|
||||
- jazz-react-native-media-images@0.8.9
|
||||
|
||||
## 1.0.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react-auth-clerk@0.8.12
|
||||
- jazz-react-native@0.8.12
|
||||
- jazz-tools@0.8.12
|
||||
- jazz-react-native-media-images@0.8.8
|
||||
|
||||
## 1.0.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react-auth-clerk@0.8.11
|
||||
- jazz-react-native@0.8.11
|
||||
- jazz-tools@0.8.11
|
||||
- jazz-react-native-media-images@0.8.7
|
||||
|
||||
## 1.0.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "chat-rn-clerk",
|
||||
"main": "index.js",
|
||||
"version": "1.0.3",
|
||||
"version": "1.0.8",
|
||||
"scripts": {
|
||||
"build": "expo export -p ios",
|
||||
"start": "expo start",
|
||||
|
||||
@@ -1,5 +1,43 @@
|
||||
# chat-rn
|
||||
|
||||
## 1.0.10
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [cce679b]
|
||||
- jazz-tools@0.8.15
|
||||
- jazz-react-native@0.8.15
|
||||
|
||||
## 1.0.9
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [36273b3]
|
||||
- jazz-tools@0.8.14
|
||||
- jazz-react-native@0.8.14
|
||||
|
||||
## 1.0.8
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [fd011d7]
|
||||
- jazz-tools@0.8.13
|
||||
- jazz-react-native@0.8.13
|
||||
|
||||
## 1.0.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react-native@0.8.12
|
||||
- jazz-tools@0.8.12
|
||||
|
||||
## 1.0.6
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react-native@0.8.11
|
||||
- jazz-tools@0.8.11
|
||||
|
||||
## 1.0.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "chat-rn",
|
||||
"version": "1.0.5",
|
||||
"version": "1.0.10",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"build": "expo export -p ios",
|
||||
|
||||
@@ -1,5 +1,48 @@
|
||||
# jazz-example-chat
|
||||
|
||||
## 0.0.94
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [cce679b]
|
||||
- jazz-tools@0.8.15
|
||||
- jazz-react@0.8.15
|
||||
|
||||
## 0.0.93
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [36273b3]
|
||||
- jazz-tools@0.8.14
|
||||
- jazz-react@0.8.14
|
||||
|
||||
## 0.0.92
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [fd011d7]
|
||||
- jazz-tools@0.8.13
|
||||
- jazz-react@0.8.13
|
||||
|
||||
## 0.0.91
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [6ed75eb]
|
||||
- Updated dependencies [3cc6aee]
|
||||
- cojson@0.8.12
|
||||
- jazz-react@0.8.12
|
||||
- jazz-tools@0.8.12
|
||||
|
||||
## 0.0.90
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [1ed4ab5]
|
||||
- cojson@0.8.11
|
||||
- jazz-react@0.8.11
|
||||
- jazz-tools@0.8.11
|
||||
|
||||
## 0.0.89
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "jazz-example-chat",
|
||||
"private": true,
|
||||
"version": "0.0.89",
|
||||
"version": "0.0.94",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
@@ -22,10 +22,10 @@
|
||||
"@radix-ui/react-toast": "^1.1.4",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.0.0",
|
||||
"cojson": "workspace:0.8.5",
|
||||
"cojson": "workspace:0.8.12",
|
||||
"hash-slash": "workspace:0.2.1",
|
||||
"jazz-react": "workspace:0.8.7",
|
||||
"jazz-tools": "workspace:0.8.5",
|
||||
"jazz-react": "workspace:0.8.15",
|
||||
"jazz-tools": "workspace:0.8.15",
|
||||
"lucide-react": "^0.274.0",
|
||||
"qrcode": "^1.5.3",
|
||||
"react": "^18.2.0",
|
||||
|
||||
@@ -27,7 +27,7 @@ export default defineConfig({
|
||||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||
use: {
|
||||
/* Base URL to use in actions like `await page.goto('/')`. */
|
||||
baseURL: isCI ? "http://localhost:4173/" : "http://localhost:5173",
|
||||
baseURL: "http://localhost:5173/",
|
||||
|
||||
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||
trace: "on-first-retry",
|
||||
@@ -43,8 +43,11 @@ export default defineConfig({
|
||||
],
|
||||
|
||||
/* Run your local dev server before starting the tests */
|
||||
webServer: isCI ? {
|
||||
command: "pnpm preview",
|
||||
url: "http://localhost:4173/",
|
||||
} : undefined,
|
||||
webServer: [
|
||||
{
|
||||
command: "pnpm preview --port 5173",
|
||||
url: "http://localhost:5173/",
|
||||
reuseExistingServer: !isCI,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
@@ -1,5 +1,28 @@
|
||||
# jazz-example-inspector
|
||||
|
||||
## 0.0.68
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [e0dd006]
|
||||
- cojson-transport-ws@0.8.13
|
||||
|
||||
## 0.0.67
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [6ed75eb]
|
||||
- cojson@0.8.12
|
||||
- cojson-transport-ws@0.8.12
|
||||
|
||||
## 0.0.66
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [1ed4ab5]
|
||||
- cojson@0.8.11
|
||||
- cojson-transport-ws@0.8.11
|
||||
|
||||
## 0.0.65
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "jazz-inspector",
|
||||
"private": true,
|
||||
"version": "0.0.65",
|
||||
"version": "0.0.68",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
@@ -15,8 +15,8 @@
|
||||
"@radix-ui/react-toast": "^1.1.4",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.0.0",
|
||||
"cojson": "workspace:0.8.5",
|
||||
"cojson-transport-ws": "workspace:0.8.7",
|
||||
"cojson": "workspace:0.8.12",
|
||||
"cojson-transport-ws": "workspace:0.8.13",
|
||||
"hash-slash": "workspace:0.2.1",
|
||||
"lucide-react": "^0.274.0",
|
||||
"qrcode": "^1.5.3",
|
||||
|
||||
7
examples/music-player/.gitignore
vendored
@@ -22,3 +22,10 @@ dist-ssr
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
/test-results/
|
||||
/playwright-report/
|
||||
/blob-report/
|
||||
/playwright/.cache/
|
||||
|
||||
sync-db/
|
||||
@@ -1,5 +1,44 @@
|
||||
# jazz-example-musicplayer
|
||||
|
||||
## 0.0.14
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [cce679b]
|
||||
- jazz-tools@0.8.15
|
||||
- jazz-react@0.8.15
|
||||
|
||||
## 0.0.13
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [36273b3]
|
||||
- jazz-tools@0.8.14
|
||||
- jazz-react@0.8.14
|
||||
|
||||
## 0.0.12
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [fd011d7]
|
||||
- jazz-tools@0.8.13
|
||||
- jazz-react@0.8.13
|
||||
|
||||
## 0.0.11
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [3cc6aee]
|
||||
- jazz-react@0.8.12
|
||||
- jazz-tools@0.8.12
|
||||
|
||||
## 0.0.10
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react@0.8.11
|
||||
- jazz-tools@0.8.11
|
||||
|
||||
## 0.0.9
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
{
|
||||
"name": "jazz-example-music-player",
|
||||
"private": true,
|
||||
"version": "0.0.9",
|
||||
"version": "0.0.14",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
||||
"format": "prettier --write './src/**/*.{ts,tsx}'",
|
||||
"preview": "vite preview"
|
||||
"preview": "vite preview",
|
||||
"test": "playwright test",
|
||||
"test:ui": "playwright test --ui"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{ts,tsx}": "eslint --fix",
|
||||
@@ -20,8 +22,8 @@
|
||||
"@radix-ui/react-toast": "^1.1.4",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.0.0",
|
||||
"jazz-react": "workspace:0.8.7",
|
||||
"jazz-tools": "workspace:0.8.5",
|
||||
"jazz-react": "workspace:0.8.15",
|
||||
"jazz-tools": "workspace:0.8.15",
|
||||
"lucide-react": "^0.274.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
@@ -31,6 +33,7 @@
|
||||
"tailwindcss-animate": "^1.0.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.46.1",
|
||||
"@types/react": "^18.2.19",
|
||||
"@types/react-dom": "^18.2.7",
|
||||
"@typescript-eslint/eslint-plugin": "^6.2.1",
|
||||
|
||||
53
examples/music-player/playwright.config.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { defineConfig, devices } from "@playwright/test";
|
||||
import isCI from "is-ci";
|
||||
|
||||
/**
|
||||
* Read environment variables from file.
|
||||
* https://github.com/motdotla/dotenv
|
||||
*/
|
||||
// import dotenv from 'dotenv';
|
||||
// dotenv.config({ path: path.resolve(__dirname, '.env') });
|
||||
|
||||
/**
|
||||
* See https://playwright.dev/docs/test-configuration.
|
||||
*/
|
||||
export default defineConfig({
|
||||
testDir: "./tests",
|
||||
/* Run tests in files in parallel */
|
||||
fullyParallel: true,
|
||||
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
||||
forbidOnly: isCI,
|
||||
/* Retry on CI only */
|
||||
retries: isCI ? 2 : 0,
|
||||
/* Opt out of parallel tests on CI. */
|
||||
workers: isCI ? 1 : undefined,
|
||||
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||
reporter: "html",
|
||||
|
||||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||
use: {
|
||||
/* Base URL to use in actions like `await page.goto('/')`. */
|
||||
baseURL: "http://localhost:5173/",
|
||||
|
||||
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||
trace: "on-first-retry",
|
||||
permissions: ["clipboard-read", "clipboard-write"],
|
||||
},
|
||||
|
||||
/* Configure projects for major browsers */
|
||||
projects: [
|
||||
{
|
||||
name: "chromium",
|
||||
use: { ...devices["Desktop Chrome"] },
|
||||
},
|
||||
],
|
||||
|
||||
/* Run your local dev server before starting the tests */
|
||||
webServer: [
|
||||
{
|
||||
command: "pnpm preview --port 5173",
|
||||
url: "http://localhost:5173/",
|
||||
reuseExistingServer: !isCI,
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -88,7 +88,7 @@ export class MusicaAccount extends Account {
|
||||
profile = co.ref(Profile);
|
||||
root = co.ref(MusicaAccountRoot);
|
||||
|
||||
/**
|
||||
/**
|
||||
* The account migration is run on account creation and on every log-in.
|
||||
* You can use it to set up the account root and any other initial CoValues you need.
|
||||
*/
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import { createHashRouter, RouterProvider } from "react-router-dom";
|
||||
import { Toaster } from "@/components/ui/toaster";
|
||||
import { Toaster } from "@/components/ui/toaster"
|
||||
import { useMediaPlayer } from "./5_useMediaPlayer";
|
||||
import { HomePage } from "./3_HomePage";
|
||||
import { InvitePage } from "./6_InvitePage";
|
||||
|
||||
@@ -5,16 +5,14 @@ import { usePlayState } from "./lib/audio/usePlayState";
|
||||
import { SidePanel } from "./components/SidePanel";
|
||||
import { FileUploadButton } from "./components/FileUploadButton";
|
||||
import { Button } from "./components/ui/button";
|
||||
import {
|
||||
createNewPlaylist,
|
||||
updatePlaylistTitle,
|
||||
uploadMusicTracks,
|
||||
} from "./4_actions";
|
||||
import { createNewPlaylist, uploadMusicTracks } from "./4_actions";
|
||||
import { useNavigate, useParams } from "react-router";
|
||||
import { ID } from "jazz-tools";
|
||||
import { Playlist } from "./1_schema";
|
||||
import { createInviteLink } from "jazz-react";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import { useToast } from "@/hooks/use-toast"
|
||||
import { PlaylistTitleInput } from "./components/PlaylistTitleInput";
|
||||
import { LogoutButton } from "./components/LogoutButton";
|
||||
|
||||
export function HomePage({ mediaPlayer }: { mediaPlayer: MediaPlayer }) {
|
||||
/**
|
||||
@@ -31,7 +29,7 @@ export function HomePage({ mediaPlayer }: { mediaPlayer: MediaPlayer }) {
|
||||
const navigate = useNavigate();
|
||||
const playState = usePlayState();
|
||||
const isPlaying = playState.value === "play";
|
||||
const { toast } = useToast();
|
||||
const { toast } = useToast()
|
||||
|
||||
async function handleFileLoad(files: FileList) {
|
||||
if (!me) return;
|
||||
@@ -61,14 +59,6 @@ export function HomePage({ mediaPlayer }: { mediaPlayer: MediaPlayer }) {
|
||||
const isPlaylistOwner = playlist?._owner.myRole() === "admin";
|
||||
const isActivePlaylist = playlistId === me?.root.activePlaylist?.id;
|
||||
|
||||
const handlePlaylistTitleChange = (
|
||||
evt: React.ChangeEvent<HTMLInputElement>,
|
||||
) => {
|
||||
if (!playlist) return;
|
||||
|
||||
updatePlaylistTitle(playlist, evt.target.value);
|
||||
};
|
||||
|
||||
const handlePlaylistShareClick = async () => {
|
||||
if (!isPlaylistOwner) return;
|
||||
|
||||
@@ -92,12 +82,7 @@ export function HomePage({ mediaPlayer }: { mediaPlayer: MediaPlayer }) {
|
||||
All tracks
|
||||
</h1>
|
||||
) : (
|
||||
<input
|
||||
value={playlist?.title ?? ""}
|
||||
onChange={handlePlaylistTitleChange}
|
||||
className="text-2xl font-bold text-blue-800 bg-transparent"
|
||||
disabled={!isPlaylistOwner}
|
||||
/>
|
||||
<PlaylistTitleInput playlistId={playlistId} />
|
||||
)}
|
||||
<div className="flex items-center space-x-4">
|
||||
{isRootPlaylist && (
|
||||
@@ -108,7 +93,7 @@ export function HomePage({ mediaPlayer }: { mediaPlayer: MediaPlayer }) {
|
||||
Add file
|
||||
</FileUploadButton>
|
||||
<Button onClick={handleCreatePlaylist}>
|
||||
Create new playlist
|
||||
New playlist
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
@@ -117,6 +102,7 @@ export function HomePage({ mediaPlayer }: { mediaPlayer: MediaPlayer }) {
|
||||
Share playlist
|
||||
</Button>
|
||||
)}
|
||||
<LogoutButton />
|
||||
</div>
|
||||
</div>
|
||||
<ul className="flex flex-col">
|
||||
@@ -131,15 +117,10 @@ export function HomePage({ mediaPlayer }: { mediaPlayer: MediaPlayer }) {
|
||||
}
|
||||
isPlaying={
|
||||
mediaPlayer.activeTrackId ===
|
||||
track.id &&
|
||||
isActivePlaylist &&
|
||||
isPlaying
|
||||
track.id && isActivePlaylist && isPlaying
|
||||
}
|
||||
onClick={() => {
|
||||
mediaPlayer.setActiveTrack(
|
||||
track,
|
||||
playlist,
|
||||
);
|
||||
mediaPlayer.setActiveTrack(track, playlist);
|
||||
}}
|
||||
showAddToPlaylist={isRootPlaylist}
|
||||
/>
|
||||
|
||||
@@ -92,8 +92,7 @@ export async function addTrackToPlaylist(
|
||||
) {
|
||||
if (!account) return;
|
||||
|
||||
if (playlist.tracks?.some((t) => t?._refs.sourceTrack.id === track.id))
|
||||
return;
|
||||
if (playlist.tracks?.some((t) => t?._refs.sourceTrack.id === track.id)) return;
|
||||
|
||||
/**
|
||||
* Since musicTracks are created as private values (see uploadMusicTracks)
|
||||
@@ -138,13 +137,10 @@ export async function updateMusicTrackTitle(track: MusicTrack, title: string) {
|
||||
track.title = title;
|
||||
}
|
||||
|
||||
export async function updateActivePlaylist(
|
||||
playlist: Playlist,
|
||||
me: MusicaAccount,
|
||||
) {
|
||||
export async function updateActivePlaylist(playlist: Playlist, me: MusicaAccount) {
|
||||
me.root!.activePlaylist = playlist ?? me.root!.rootPlaylist;
|
||||
}
|
||||
|
||||
export async function updateActiveTrack(track: MusicTrack, me: MusicaAccount) {
|
||||
me.root!.activeTrack = track;
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@ export function useMediaPlayer() {
|
||||
|
||||
const [loading, setLoading] = useState<ID<MusicTrack> | null>(null);
|
||||
|
||||
const activeTrackId = me?.root?._refs.activeTrack?.id;
|
||||
const activeTrackId = me?.root?._refs.activeTrack?.id
|
||||
|
||||
// Reference used to avoid out-of-order track loads
|
||||
const lastLoadedTrackId = useRef<ID<MusicTrack> | null>(null);
|
||||
@@ -71,7 +71,10 @@ export function useMediaPlayer() {
|
||||
async function setActiveTrack(track: MusicTrack, playlist?: Playlist) {
|
||||
if (!me?.root) return;
|
||||
|
||||
if (activeTrackId === track.id && lastLoadedTrackId.current !== null) {
|
||||
if (
|
||||
activeTrackId === track.id &&
|
||||
lastLoadedTrackId.current !== null
|
||||
) {
|
||||
playState.toggle();
|
||||
return;
|
||||
}
|
||||
|
||||
12
examples/music-player/src/components/LogoutButton.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
import { Button } from "./ui/button";
|
||||
import { useAccount } from "@/2_main";
|
||||
|
||||
export function LogoutButton() {
|
||||
const { logOut } = useAccount();
|
||||
|
||||
return (
|
||||
<Button onClick={logOut}>
|
||||
Logout
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
@@ -7,11 +7,11 @@ import {
|
||||
DropdownMenuItem,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { MoreHorizontal } from "lucide-react";
|
||||
import { ChangeEvent } from "react";
|
||||
import { Button } from "./ui/button";
|
||||
import { useAccount, useCoState } from "@/2_main";
|
||||
import { addTrackToPlaylist, updateMusicTrackTitle } from "@/4_actions";
|
||||
import { addTrackToPlaylist } from "@/4_actions";
|
||||
import { ID } from "jazz-tools";
|
||||
import { MusicTrackTitleInput } from "./MusicTrackTitleInput";
|
||||
|
||||
export function MusicTrackRow({
|
||||
trackId,
|
||||
@@ -28,12 +28,6 @@ export function MusicTrackRow({
|
||||
}) {
|
||||
const track = useCoState(MusicTrack, trackId);
|
||||
|
||||
function handleTrackTitleChange(evt: ChangeEvent<HTMLInputElement>) {
|
||||
if (!track) return;
|
||||
|
||||
updateMusicTrackTitle(track, evt.target.value);
|
||||
}
|
||||
|
||||
const { me } = useAccount({
|
||||
root: {
|
||||
playlists: [{}],
|
||||
@@ -65,6 +59,7 @@ export function MusicTrackRow({
|
||||
!isPlaying && "group-hover:bg-slate-300 rounded-full",
|
||||
)}
|
||||
onClick={handleTrackClick}
|
||||
aria-label={`${isPlaying ? "Pause" : "Play"} ${track?.title}`}
|
||||
>
|
||||
{isLoading ? (
|
||||
<div className="animate-spin">߷</div>
|
||||
@@ -74,25 +69,12 @@ export function MusicTrackRow({
|
||||
"▶️"
|
||||
)}
|
||||
</button>
|
||||
<div
|
||||
className="relative flex-grow"
|
||||
onClick={(evt) => evt.stopPropagation()}
|
||||
>
|
||||
<input
|
||||
className="absolute w-full h-full left-0 bg-transparent px-1"
|
||||
value={track?.title}
|
||||
onChange={handleTrackTitleChange}
|
||||
spellCheck="false"
|
||||
/>
|
||||
<span className="opacity-0 px-1 w-fit pointer-events-none whitespace-pre">
|
||||
{track?.title}
|
||||
</span>
|
||||
</div>
|
||||
<MusicTrackTitleInput trackId={trackId} />
|
||||
<div onClick={(evt) => evt.stopPropagation()}>
|
||||
{showAddToPlaylist && (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" className="h-8 w-8 p-0">
|
||||
<Button variant="ghost" className="h-8 w-8 p-0" aria-label={`Open ${track?.title} menu`}>
|
||||
<span className="sr-only">Open menu</span>
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
</Button>
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import { MusicTrack } from "@/1_schema";
|
||||
import { useCoState } from "@/2_main";
|
||||
import { updateMusicTrackTitle } from "@/4_actions";
|
||||
import { ID } from "jazz-tools";
|
||||
import { ChangeEvent, useState } from "react";
|
||||
|
||||
export function MusicTrackTitleInput({
|
||||
trackId,
|
||||
}: {
|
||||
trackId: ID<MusicTrack> | undefined;
|
||||
}) {
|
||||
const track = useCoState(MusicTrack, trackId);
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [localTrackTitle, setLocalTrackTitle] = useState("");
|
||||
|
||||
function handleTitleChange(evt: ChangeEvent<HTMLInputElement>) {
|
||||
setLocalTrackTitle(evt.target.value);
|
||||
}
|
||||
|
||||
function handleFoucsIn() {
|
||||
setIsEditing(true);
|
||||
setLocalTrackTitle(track?.title ?? "");
|
||||
}
|
||||
|
||||
function handleFocusOut() {
|
||||
setIsEditing(false);
|
||||
setLocalTrackTitle("");
|
||||
track && updateMusicTrackTitle(track, localTrackTitle);
|
||||
}
|
||||
|
||||
const inputValue = isEditing ? localTrackTitle : track?.title ?? "";
|
||||
|
||||
return (
|
||||
<div
|
||||
className="relative flex-grow"
|
||||
onClick={(evt) => evt.stopPropagation()}
|
||||
>
|
||||
<input
|
||||
className="absolute w-full h-full left-0 bg-transparent px-1"
|
||||
value={inputValue}
|
||||
onChange={handleTitleChange}
|
||||
spellCheck="false"
|
||||
onFocus={handleFoucsIn}
|
||||
onBlur={handleFocusOut}
|
||||
aria-label={`Edit track title: ${track?.title}`}
|
||||
/>
|
||||
<span className="opacity-0 px-1 w-fit pointer-events-none whitespace-pre">
|
||||
{inputValue}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -25,7 +25,7 @@ export function PlayerControls({ mediaPlayer }: { mediaPlayer: MediaPlayer }) {
|
||||
});
|
||||
|
||||
const activeTrack = useCoState(MusicTrack, mediaPlayer.activeTrackId, {
|
||||
waveform: {},
|
||||
waveform: {}
|
||||
});
|
||||
|
||||
if (!activeTrack) return null;
|
||||
@@ -39,12 +39,14 @@ export function PlayerControls({ mediaPlayer }: { mediaPlayer: MediaPlayer }) {
|
||||
<button
|
||||
onClick={mediaPlayer.playPrevTrack}
|
||||
className="text-blue-600 hover:text-blue-800"
|
||||
aria-label="Previous track"
|
||||
>
|
||||
<SkipBack size={20} />
|
||||
</button>
|
||||
<button
|
||||
onClick={playState.toggle}
|
||||
className="w-[42px] h-[42px] flex items-center justify-center bg-blue-600 rounded-full text-white hover:bg-blue-700"
|
||||
aria-label={isPlaying ? "Pause active track" : "Play active track"}
|
||||
>
|
||||
{isPlaying ? (
|
||||
<Pause size={24} fill="currentColor" />
|
||||
@@ -55,21 +57,20 @@ export function PlayerControls({ mediaPlayer }: { mediaPlayer: MediaPlayer }) {
|
||||
<button
|
||||
onClick={mediaPlayer.playNextTrack}
|
||||
className="text-blue-600 hover:text-blue-800"
|
||||
aria-label="Next track"
|
||||
>
|
||||
<SkipForward size={20} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className=" sm:hidden md:flex flex-col flex-shrink-1 items-center w-[75%]">
|
||||
<div className=" sm:hidden md:flex flex-col flex-shrink-1 items-center w-[75%]">
|
||||
<Waveform track={activeTrack} height={30} />
|
||||
</div>
|
||||
<div className="flex flex-col items-end gap-1 text-right min-w-fit w-[25%]">
|
||||
<h4 className="font-medium text-blue-800">
|
||||
{activeTrackTitle}
|
||||
</h4>
|
||||
<p className="text-sm text-gray-600">
|
||||
{activePlaylist?.title || "All tracks"}
|
||||
</p>
|
||||
<p className="text-sm text-gray-600">{activePlaylist?.title || "All tracks"}</p>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
|
||||
45
examples/music-player/src/components/PlaylistTitleInput.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
import { Playlist } from "@/1_schema";
|
||||
import { useCoState } from "@/2_main";
|
||||
import {
|
||||
updatePlaylistTitle
|
||||
} from "@/4_actions";
|
||||
import { ID } from "jazz-tools";
|
||||
import { ChangeEvent, useState } from "react";
|
||||
|
||||
export function PlaylistTitleInput({
|
||||
playlistId,
|
||||
}: {
|
||||
playlistId: ID<Playlist> | undefined;
|
||||
}) {
|
||||
const playlist = useCoState(Playlist, playlistId);
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [localPlaylistTitle, setLocalPlaylistTitle] = useState("");
|
||||
|
||||
function handleTitleChange(evt: ChangeEvent<HTMLInputElement>) {
|
||||
setLocalPlaylistTitle(evt.target.value);
|
||||
}
|
||||
|
||||
function handleFoucsIn() {
|
||||
setIsEditing(true);
|
||||
setLocalPlaylistTitle(playlist?.title ?? "");
|
||||
}
|
||||
|
||||
function handleFocusOut() {
|
||||
setIsEditing(false);
|
||||
setLocalPlaylistTitle("");
|
||||
playlist && updatePlaylistTitle(playlist, localPlaylistTitle);
|
||||
}
|
||||
|
||||
const inputValue = isEditing ? localPlaylistTitle : playlist?.title ?? "";
|
||||
|
||||
return (
|
||||
<input
|
||||
value={inputValue}
|
||||
onChange={handleTitleChange}
|
||||
className="text-2xl font-bold text-blue-800 bg-transparent"
|
||||
onFocus={handleFoucsIn}
|
||||
onBlur={handleFocusOut}
|
||||
aria-label={`Playlist title`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -10,7 +10,9 @@ export function SidePanel() {
|
||||
},
|
||||
});
|
||||
|
||||
function handleAllTracksClick(evt: React.MouseEvent<HTMLAnchorElement>) {
|
||||
function handleAllTracksClick(
|
||||
evt: React.MouseEvent<HTMLAnchorElement>,
|
||||
) {
|
||||
evt.preventDefault();
|
||||
navigate(`/`);
|
||||
}
|
||||
|
||||
@@ -53,6 +53,7 @@ export function Waveform(props: { track: MusicTrack; height: number }) {
|
||||
style={{
|
||||
height: height * value,
|
||||
}}
|
||||
aria-label={`Seek to ${(i / barCount) * duration} seconds`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -1,58 +1,57 @@
|
||||
import * as React from "react";
|
||||
import { Slot } from "@radix-ui/react-slot";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import * as React from "react"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"bg-primary text-primary-foreground hover:bg-primary/90",
|
||||
destructive:
|
||||
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
||||
outline:
|
||||
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default: "h-10 px-4 py-2",
|
||||
sm: "h-9 rounded-md px-3",
|
||||
lg: "h-11 rounded-md px-8",
|
||||
icon: "h-10 w-10",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||
destructive:
|
||||
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
||||
outline:
|
||||
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default: "h-10 px-4 py-2",
|
||||
sm: "h-9 rounded-md px-3",
|
||||
lg: "h-11 rounded-md px-8",
|
||||
icon: "h-10 w-10",
|
||||
},
|
||||
},
|
||||
);
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
export interface ButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> {
|
||||
asChild?: boolean;
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> {
|
||||
asChild?: boolean
|
||||
}
|
||||
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "button";
|
||||
return (
|
||||
<Comp
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
Button.displayName = "Button";
|
||||
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "button"
|
||||
return (
|
||||
<Comp
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
Button.displayName = "Button"
|
||||
|
||||
// eslint-disable-next-line react-refresh/only-export-components
|
||||
export { Button, buttonVariants };
|
||||
export { Button, buttonVariants }
|
||||
|
||||
@@ -1,201 +1,198 @@
|
||||
import * as React from "react";
|
||||
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
|
||||
import { Check, ChevronRight, Circle } from "lucide-react";
|
||||
import * as React from "react"
|
||||
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
|
||||
import { Check, ChevronRight, Circle } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const DropdownMenu = DropdownMenuPrimitive.Root;
|
||||
const DropdownMenu = DropdownMenuPrimitive.Root
|
||||
|
||||
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
|
||||
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
|
||||
|
||||
const DropdownMenuGroup = DropdownMenuPrimitive.Group;
|
||||
const DropdownMenuGroup = DropdownMenuPrimitive.Group
|
||||
|
||||
const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
|
||||
const DropdownMenuPortal = DropdownMenuPrimitive.Portal
|
||||
|
||||
const DropdownMenuSub = DropdownMenuPrimitive.Sub;
|
||||
const DropdownMenuSub = DropdownMenuPrimitive.Sub
|
||||
|
||||
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
|
||||
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
|
||||
|
||||
const DropdownMenuSubTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
|
||||
inset?: boolean;
|
||||
}
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
|
||||
inset?: boolean
|
||||
}
|
||||
>(({ className, inset, children, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.SubTrigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent",
|
||||
inset && "pl-8",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronRight className="ml-auto h-4 w-4" />
|
||||
</DropdownMenuPrimitive.SubTrigger>
|
||||
));
|
||||
<DropdownMenuPrimitive.SubTrigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent",
|
||||
inset && "pl-8",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronRight className="ml-auto h-4 w-4" />
|
||||
</DropdownMenuPrimitive.SubTrigger>
|
||||
))
|
||||
DropdownMenuSubTrigger.displayName =
|
||||
DropdownMenuPrimitive.SubTrigger.displayName;
|
||||
DropdownMenuPrimitive.SubTrigger.displayName
|
||||
|
||||
const DropdownMenuSubContent = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.SubContent
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
<DropdownMenuPrimitive.SubContent
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DropdownMenuSubContent.displayName =
|
||||
DropdownMenuPrimitive.SubContent.displayName;
|
||||
DropdownMenuPrimitive.SubContent.displayName
|
||||
|
||||
const DropdownMenuContent = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
|
||||
>(({ className, sideOffset = 4, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Portal>
|
||||
<DropdownMenuPrimitive.Content
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</DropdownMenuPrimitive.Portal>
|
||||
));
|
||||
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
|
||||
<DropdownMenuPrimitive.Portal>
|
||||
<DropdownMenuPrimitive.Content
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</DropdownMenuPrimitive.Portal>
|
||||
))
|
||||
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
|
||||
|
||||
const DropdownMenuItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
|
||||
inset?: boolean;
|
||||
}
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
|
||||
inset?: boolean
|
||||
}
|
||||
>(({ className, inset, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
inset && "pl-8",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
|
||||
<DropdownMenuPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
inset && "pl-8",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
|
||||
|
||||
const DropdownMenuCheckboxItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
|
||||
>(({ className, children, checked, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.CheckboxItem
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className,
|
||||
)}
|
||||
checked={checked}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<Check className="h-4 w-4" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.CheckboxItem>
|
||||
));
|
||||
<DropdownMenuPrimitive.CheckboxItem
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className
|
||||
)}
|
||||
checked={checked}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<Check className="h-4 w-4" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.CheckboxItem>
|
||||
))
|
||||
DropdownMenuCheckboxItem.displayName =
|
||||
DropdownMenuPrimitive.CheckboxItem.displayName;
|
||||
DropdownMenuPrimitive.CheckboxItem.displayName
|
||||
|
||||
const DropdownMenuRadioItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.RadioItem
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<Circle className="h-2 w-2 fill-current" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.RadioItem>
|
||||
));
|
||||
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
|
||||
<DropdownMenuPrimitive.RadioItem
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<Circle className="h-2 w-2 fill-current" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.RadioItem>
|
||||
))
|
||||
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
|
||||
|
||||
const DropdownMenuLabel = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
|
||||
inset?: boolean;
|
||||
}
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
|
||||
inset?: boolean
|
||||
}
|
||||
>(({ className, inset, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Label
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"px-2 py-1.5 text-sm font-semibold",
|
||||
inset && "pl-8",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
|
||||
<DropdownMenuPrimitive.Label
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"px-2 py-1.5 text-sm font-semibold",
|
||||
inset && "pl-8",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
|
||||
|
||||
const DropdownMenuSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
|
||||
<DropdownMenuPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
|
||||
|
||||
const DropdownMenuShortcut = ({
|
||||
className,
|
||||
...props
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
"ml-auto text-xs tracking-widest opacity-60",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
DropdownMenuShortcut.displayName = "DropdownMenuShortcut";
|
||||
return (
|
||||
<span
|
||||
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
|
||||
|
||||
export {
|
||||
DropdownMenu,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuShortcut,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuPortal,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuRadioGroup,
|
||||
};
|
||||
DropdownMenu,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuShortcut,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuPortal,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuRadioGroup,
|
||||
}
|
||||
|
||||
@@ -1,127 +1,127 @@
|
||||
import * as React from "react";
|
||||
import * as ToastPrimitives from "@radix-ui/react-toast";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import { X } from "lucide-react";
|
||||
import * as React from "react"
|
||||
import * as ToastPrimitives from "@radix-ui/react-toast"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import { X } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const ToastProvider = ToastPrimitives.Provider;
|
||||
const ToastProvider = ToastPrimitives.Provider
|
||||
|
||||
const ToastViewport = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Viewport>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>
|
||||
React.ElementRef<typeof ToastPrimitives.Viewport>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ToastPrimitives.Viewport
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
ToastViewport.displayName = ToastPrimitives.Viewport.displayName;
|
||||
<ToastPrimitives.Viewport
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
ToastViewport.displayName = ToastPrimitives.Viewport.displayName
|
||||
|
||||
const toastVariants = cva(
|
||||
"group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "border bg-background text-foreground",
|
||||
destructive:
|
||||
"destructive group border-destructive bg-destructive text-destructive-foreground",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
"group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "border bg-background text-foreground",
|
||||
destructive:
|
||||
"destructive group border-destructive bg-destructive text-destructive-foreground",
|
||||
},
|
||||
},
|
||||
);
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
const Toast = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &
|
||||
VariantProps<typeof toastVariants>
|
||||
React.ElementRef<typeof ToastPrimitives.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &
|
||||
VariantProps<typeof toastVariants>
|
||||
>(({ className, variant, ...props }, ref) => {
|
||||
return (
|
||||
<ToastPrimitives.Root
|
||||
ref={ref}
|
||||
className={cn(toastVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
Toast.displayName = ToastPrimitives.Root.displayName;
|
||||
return (
|
||||
<ToastPrimitives.Root
|
||||
ref={ref}
|
||||
className={cn(toastVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
Toast.displayName = ToastPrimitives.Root.displayName
|
||||
|
||||
const ToastAction = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Action>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>
|
||||
React.ElementRef<typeof ToastPrimitives.Action>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ToastPrimitives.Action
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium ring-offset-background transition-colors hover:bg-secondary focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
ToastAction.displayName = ToastPrimitives.Action.displayName;
|
||||
<ToastPrimitives.Action
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium ring-offset-background transition-colors hover:bg-secondary focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
ToastAction.displayName = ToastPrimitives.Action.displayName
|
||||
|
||||
const ToastClose = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Close>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>
|
||||
React.ElementRef<typeof ToastPrimitives.Close>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ToastPrimitives.Close
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"absolute right-2 top-2 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-2 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600",
|
||||
className,
|
||||
)}
|
||||
toast-close=""
|
||||
{...props}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</ToastPrimitives.Close>
|
||||
));
|
||||
ToastClose.displayName = ToastPrimitives.Close.displayName;
|
||||
<ToastPrimitives.Close
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"absolute right-2 top-2 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-2 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600",
|
||||
className
|
||||
)}
|
||||
toast-close=""
|
||||
{...props}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</ToastPrimitives.Close>
|
||||
))
|
||||
ToastClose.displayName = ToastPrimitives.Close.displayName
|
||||
|
||||
const ToastTitle = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>
|
||||
React.ElementRef<typeof ToastPrimitives.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ToastPrimitives.Title
|
||||
ref={ref}
|
||||
className={cn("text-sm font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
ToastTitle.displayName = ToastPrimitives.Title.displayName;
|
||||
<ToastPrimitives.Title
|
||||
ref={ref}
|
||||
className={cn("text-sm font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
ToastTitle.displayName = ToastPrimitives.Title.displayName
|
||||
|
||||
const ToastDescription = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
|
||||
React.ElementRef<typeof ToastPrimitives.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ToastPrimitives.Description
|
||||
ref={ref}
|
||||
className={cn("text-sm opacity-90", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
ToastDescription.displayName = ToastPrimitives.Description.displayName;
|
||||
<ToastPrimitives.Description
|
||||
ref={ref}
|
||||
className={cn("text-sm opacity-90", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
ToastDescription.displayName = ToastPrimitives.Description.displayName
|
||||
|
||||
type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>;
|
||||
type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>
|
||||
|
||||
type ToastActionElement = React.ReactElement<typeof ToastAction>;
|
||||
type ToastActionElement = React.ReactElement<typeof ToastAction>
|
||||
|
||||
export {
|
||||
type ToastProps,
|
||||
type ToastActionElement,
|
||||
ToastProvider,
|
||||
ToastViewport,
|
||||
Toast,
|
||||
ToastTitle,
|
||||
ToastDescription,
|
||||
ToastClose,
|
||||
ToastAction,
|
||||
};
|
||||
type ToastProps,
|
||||
type ToastActionElement,
|
||||
ToastProvider,
|
||||
ToastViewport,
|
||||
Toast,
|
||||
ToastTitle,
|
||||
ToastDescription,
|
||||
ToastClose,
|
||||
ToastAction,
|
||||
}
|
||||
|
||||
@@ -1,41 +1,33 @@
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import { useToast } from "@/hooks/use-toast"
|
||||
import {
|
||||
Toast,
|
||||
ToastClose,
|
||||
ToastDescription,
|
||||
ToastProvider,
|
||||
ToastTitle,
|
||||
ToastViewport,
|
||||
} from "@/components/ui/toast";
|
||||
Toast,
|
||||
ToastClose,
|
||||
ToastDescription,
|
||||
ToastProvider,
|
||||
ToastTitle,
|
||||
ToastViewport,
|
||||
} from "@/components/ui/toast"
|
||||
|
||||
export function Toaster() {
|
||||
const { toasts } = useToast();
|
||||
const { toasts } = useToast()
|
||||
|
||||
return (
|
||||
<ToastProvider>
|
||||
{toasts.map(function ({
|
||||
id,
|
||||
title,
|
||||
description,
|
||||
action,
|
||||
...props
|
||||
}) {
|
||||
return (
|
||||
<Toast key={id} {...props}>
|
||||
<div className="grid gap-1">
|
||||
{title && <ToastTitle>{title}</ToastTitle>}
|
||||
{description && (
|
||||
<ToastDescription>
|
||||
{description}
|
||||
</ToastDescription>
|
||||
)}
|
||||
</div>
|
||||
{action}
|
||||
<ToastClose />
|
||||
</Toast>
|
||||
);
|
||||
})}
|
||||
<ToastViewport />
|
||||
</ToastProvider>
|
||||
);
|
||||
return (
|
||||
<ToastProvider>
|
||||
{toasts.map(function ({ id, title, description, action, ...props }) {
|
||||
return (
|
||||
<Toast key={id} {...props}>
|
||||
<div className="grid gap-1">
|
||||
{title && <ToastTitle>{title}</ToastTitle>}
|
||||
{description && (
|
||||
<ToastDescription>{description}</ToastDescription>
|
||||
)}
|
||||
</div>
|
||||
{action}
|
||||
<ToastClose />
|
||||
</Toast>
|
||||
)
|
||||
})}
|
||||
<ToastViewport />
|
||||
</ToastProvider>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,189 +1,191 @@
|
||||
import * as React from "react";
|
||||
import * as React from "react"
|
||||
|
||||
import type { ToastActionElement, ToastProps } from "@/components/ui/toast";
|
||||
import type {
|
||||
ToastActionElement,
|
||||
ToastProps,
|
||||
} from "@/components/ui/toast"
|
||||
|
||||
const TOAST_LIMIT = 1;
|
||||
const TOAST_REMOVE_DELAY = 1000000;
|
||||
const TOAST_LIMIT = 1
|
||||
const TOAST_REMOVE_DELAY = 1000000
|
||||
|
||||
type ToasterToast = ToastProps & {
|
||||
id: string;
|
||||
title?: React.ReactNode;
|
||||
description?: React.ReactNode;
|
||||
action?: ToastActionElement;
|
||||
};
|
||||
id: string
|
||||
title?: React.ReactNode
|
||||
description?: React.ReactNode
|
||||
action?: ToastActionElement
|
||||
}
|
||||
|
||||
const actionTypes = {
|
||||
ADD_TOAST: "ADD_TOAST",
|
||||
UPDATE_TOAST: "UPDATE_TOAST",
|
||||
DISMISS_TOAST: "DISMISS_TOAST",
|
||||
REMOVE_TOAST: "REMOVE_TOAST",
|
||||
} as const;
|
||||
ADD_TOAST: "ADD_TOAST",
|
||||
UPDATE_TOAST: "UPDATE_TOAST",
|
||||
DISMISS_TOAST: "DISMISS_TOAST",
|
||||
REMOVE_TOAST: "REMOVE_TOAST",
|
||||
} as const
|
||||
|
||||
let count = 0;
|
||||
let count = 0
|
||||
|
||||
function genId() {
|
||||
count = (count + 1) % Number.MAX_SAFE_INTEGER;
|
||||
return count.toString();
|
||||
count = (count + 1) % Number.MAX_SAFE_INTEGER
|
||||
return count.toString()
|
||||
}
|
||||
|
||||
type ActionType = typeof actionTypes;
|
||||
type ActionType = typeof actionTypes
|
||||
|
||||
type Action =
|
||||
| {
|
||||
type: ActionType["ADD_TOAST"];
|
||||
toast: ToasterToast;
|
||||
}
|
||||
| {
|
||||
type: ActionType["UPDATE_TOAST"];
|
||||
toast: Partial<ToasterToast>;
|
||||
}
|
||||
| {
|
||||
type: ActionType["DISMISS_TOAST"];
|
||||
toastId?: ToasterToast["id"];
|
||||
}
|
||||
| {
|
||||
type: ActionType["REMOVE_TOAST"];
|
||||
toastId?: ToasterToast["id"];
|
||||
};
|
||||
| {
|
||||
type: ActionType["ADD_TOAST"]
|
||||
toast: ToasterToast
|
||||
}
|
||||
| {
|
||||
type: ActionType["UPDATE_TOAST"]
|
||||
toast: Partial<ToasterToast>
|
||||
}
|
||||
| {
|
||||
type: ActionType["DISMISS_TOAST"]
|
||||
toastId?: ToasterToast["id"]
|
||||
}
|
||||
| {
|
||||
type: ActionType["REMOVE_TOAST"]
|
||||
toastId?: ToasterToast["id"]
|
||||
}
|
||||
|
||||
interface State {
|
||||
toasts: ToasterToast[];
|
||||
toasts: ToasterToast[]
|
||||
}
|
||||
|
||||
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>();
|
||||
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>()
|
||||
|
||||
const addToRemoveQueue = (toastId: string) => {
|
||||
if (toastTimeouts.has(toastId)) {
|
||||
return;
|
||||
}
|
||||
if (toastTimeouts.has(toastId)) {
|
||||
return
|
||||
}
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
toastTimeouts.delete(toastId);
|
||||
dispatch({
|
||||
type: "REMOVE_TOAST",
|
||||
toastId: toastId,
|
||||
});
|
||||
}, TOAST_REMOVE_DELAY);
|
||||
const timeout = setTimeout(() => {
|
||||
toastTimeouts.delete(toastId)
|
||||
dispatch({
|
||||
type: "REMOVE_TOAST",
|
||||
toastId: toastId,
|
||||
})
|
||||
}, TOAST_REMOVE_DELAY)
|
||||
|
||||
toastTimeouts.set(toastId, timeout);
|
||||
};
|
||||
|
||||
export const reducer = (state: State, action: Action): State => {
|
||||
switch (action.type) {
|
||||
case "ADD_TOAST":
|
||||
return {
|
||||
...state,
|
||||
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
|
||||
};
|
||||
|
||||
case "UPDATE_TOAST":
|
||||
return {
|
||||
...state,
|
||||
toasts: state.toasts.map((t) =>
|
||||
t.id === action.toast.id ? { ...t, ...action.toast } : t,
|
||||
),
|
||||
};
|
||||
|
||||
case "DISMISS_TOAST": {
|
||||
const { toastId } = action;
|
||||
|
||||
// ! Side effects ! - This could be extracted into a dismissToast() action,
|
||||
// but I'll keep it here for simplicity
|
||||
if (toastId) {
|
||||
addToRemoveQueue(toastId);
|
||||
} else {
|
||||
state.toasts.forEach((toast) => {
|
||||
addToRemoveQueue(toast.id);
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
toasts: state.toasts.map((t) =>
|
||||
t.id === toastId || toastId === undefined
|
||||
? {
|
||||
...t,
|
||||
open: false,
|
||||
}
|
||||
: t,
|
||||
),
|
||||
};
|
||||
}
|
||||
case "REMOVE_TOAST":
|
||||
if (action.toastId === undefined) {
|
||||
return {
|
||||
...state,
|
||||
toasts: [],
|
||||
};
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
toasts: state.toasts.filter((t) => t.id !== action.toastId),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const listeners: Array<(state: State) => void> = [];
|
||||
|
||||
let memoryState: State = { toasts: [] };
|
||||
|
||||
function dispatch(action: Action) {
|
||||
memoryState = reducer(memoryState, action);
|
||||
listeners.forEach((listener) => {
|
||||
listener(memoryState);
|
||||
});
|
||||
toastTimeouts.set(toastId, timeout)
|
||||
}
|
||||
|
||||
type Toast = Omit<ToasterToast, "id">;
|
||||
export const reducer = (state: State, action: Action): State => {
|
||||
switch (action.type) {
|
||||
case "ADD_TOAST":
|
||||
return {
|
||||
...state,
|
||||
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
|
||||
}
|
||||
|
||||
case "UPDATE_TOAST":
|
||||
return {
|
||||
...state,
|
||||
toasts: state.toasts.map((t) =>
|
||||
t.id === action.toast.id ? { ...t, ...action.toast } : t
|
||||
),
|
||||
}
|
||||
|
||||
case "DISMISS_TOAST": {
|
||||
const { toastId } = action
|
||||
|
||||
// ! Side effects ! - This could be extracted into a dismissToast() action,
|
||||
// but I'll keep it here for simplicity
|
||||
if (toastId) {
|
||||
addToRemoveQueue(toastId)
|
||||
} else {
|
||||
state.toasts.forEach((toast) => {
|
||||
addToRemoveQueue(toast.id)
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
toasts: state.toasts.map((t) =>
|
||||
t.id === toastId || toastId === undefined
|
||||
? {
|
||||
...t,
|
||||
open: false,
|
||||
}
|
||||
: t
|
||||
),
|
||||
}
|
||||
}
|
||||
case "REMOVE_TOAST":
|
||||
if (action.toastId === undefined) {
|
||||
return {
|
||||
...state,
|
||||
toasts: [],
|
||||
}
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
toasts: state.toasts.filter((t) => t.id !== action.toastId),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const listeners: Array<(state: State) => void> = []
|
||||
|
||||
let memoryState: State = { toasts: [] }
|
||||
|
||||
function dispatch(action: Action) {
|
||||
memoryState = reducer(memoryState, action)
|
||||
listeners.forEach((listener) => {
|
||||
listener(memoryState)
|
||||
})
|
||||
}
|
||||
|
||||
type Toast = Omit<ToasterToast, "id">
|
||||
|
||||
function toast({ ...props }: Toast) {
|
||||
const id = genId();
|
||||
|
||||
const update = (props: ToasterToast) =>
|
||||
dispatch({
|
||||
type: "UPDATE_TOAST",
|
||||
toast: { ...props, id },
|
||||
});
|
||||
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id });
|
||||
const id = genId()
|
||||
|
||||
const update = (props: ToasterToast) =>
|
||||
dispatch({
|
||||
type: "ADD_TOAST",
|
||||
toast: {
|
||||
...props,
|
||||
id,
|
||||
open: true,
|
||||
onOpenChange: (open) => {
|
||||
if (!open) dismiss();
|
||||
},
|
||||
},
|
||||
});
|
||||
type: "UPDATE_TOAST",
|
||||
toast: { ...props, id },
|
||||
})
|
||||
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
|
||||
|
||||
return {
|
||||
id: id,
|
||||
dismiss,
|
||||
update,
|
||||
};
|
||||
dispatch({
|
||||
type: "ADD_TOAST",
|
||||
toast: {
|
||||
...props,
|
||||
id,
|
||||
open: true,
|
||||
onOpenChange: (open) => {
|
||||
if (!open) dismiss()
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
return {
|
||||
id: id,
|
||||
dismiss,
|
||||
update,
|
||||
}
|
||||
}
|
||||
|
||||
function useToast() {
|
||||
const [state, setState] = React.useState<State>(memoryState);
|
||||
const [state, setState] = React.useState<State>(memoryState)
|
||||
|
||||
React.useEffect(() => {
|
||||
listeners.push(setState);
|
||||
return () => {
|
||||
const index = listeners.indexOf(setState);
|
||||
if (index > -1) {
|
||||
listeners.splice(index, 1);
|
||||
}
|
||||
};
|
||||
}, [state]);
|
||||
React.useEffect(() => {
|
||||
listeners.push(setState)
|
||||
return () => {
|
||||
const index = listeners.indexOf(setState)
|
||||
if (index > -1) {
|
||||
listeners.splice(index, 1)
|
||||
}
|
||||
}
|
||||
}, [state])
|
||||
|
||||
return {
|
||||
...state,
|
||||
toast,
|
||||
dismiss: (toastId?: string) =>
|
||||
dispatch({ type: "DISMISS_TOAST", toastId }),
|
||||
};
|
||||
return {
|
||||
...state,
|
||||
toast,
|
||||
dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
|
||||
}
|
||||
}
|
||||
|
||||
export { useToast, toast };
|
||||
export { useToast, toast }
|
||||
|
||||
@@ -2,34 +2,31 @@ import { useLayoutEffect, useState } from "react";
|
||||
import { useAudioManager } from "./AudioManager";
|
||||
|
||||
export function usePlayerCurrentTime() {
|
||||
const audioManager = useAudioManager();
|
||||
const [value, setValue] = useState<number>(0);
|
||||
const audioManager = useAudioManager();
|
||||
const [value, setValue] = useState<number>(0);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
setValue(audioManager.mediaElement.currentTime);
|
||||
useLayoutEffect(() => {
|
||||
setValue(audioManager.mediaElement.currentTime);
|
||||
|
||||
const onTimeUpdate = () => {
|
||||
setValue(audioManager.mediaElement.currentTime);
|
||||
};
|
||||
const onTimeUpdate = () => {
|
||||
setValue(audioManager.mediaElement.currentTime);
|
||||
};
|
||||
|
||||
audioManager.mediaElement.addEventListener("timeupdate", onTimeUpdate);
|
||||
audioManager.mediaElement.addEventListener("timeupdate", onTimeUpdate);
|
||||
|
||||
return () => {
|
||||
audioManager.mediaElement.removeEventListener(
|
||||
"timeupdate",
|
||||
onTimeUpdate,
|
||||
);
|
||||
};
|
||||
}, [audioManager]);
|
||||
return () => {
|
||||
audioManager.mediaElement.removeEventListener("timeupdate", onTimeUpdate);
|
||||
};
|
||||
}, [audioManager]);
|
||||
|
||||
function setCurrentTime(time: number) {
|
||||
if (audioManager.mediaElement.paused) audioManager.play();
|
||||
function setCurrentTime(time: number) {
|
||||
if (audioManager.mediaElement.paused) audioManager.play();
|
||||
|
||||
audioManager.mediaElement.currentTime = time;
|
||||
}
|
||||
audioManager.mediaElement.currentTime = time;
|
||||
}
|
||||
|
||||
return {
|
||||
value,
|
||||
setValue: setCurrentTime,
|
||||
};
|
||||
return {
|
||||
value,
|
||||
setValue: setCurrentTime,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { type ClassValue, clsx } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { type ClassValue, clsx } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
|
||||
44
examples/music-player/tests/createAndShare.spec.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { test } from "@playwright/test";
|
||||
import { LoginPage } from "./pages/LoginPage";
|
||||
import { HomePage } from "./pages/HomePage";
|
||||
|
||||
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
test("create a new playlist and share", async ({ page }) => {
|
||||
const loginPage = new LoginPage(page);
|
||||
|
||||
await loginPage.goto();
|
||||
await loginPage.fillUsername("S. Mario");
|
||||
await loginPage.signup();
|
||||
|
||||
const homePage = new HomePage(page);
|
||||
|
||||
// The example song should be loaded
|
||||
await homePage.expectMusicTrack("Example song");
|
||||
await homePage.editTrackTitle("Example song", "Super Mario World");
|
||||
|
||||
await homePage.createPlaylist();
|
||||
await homePage.editPlaylistTitle("Save the princess");
|
||||
|
||||
await homePage.navigateToPlaylist("All tracks");
|
||||
await homePage.addTrackToPlaylist("Super Mario World", "Save the princess");
|
||||
|
||||
await homePage.navigateToPlaylist("Save the princess");
|
||||
await homePage.expectMusicTrack("Super Mario World");
|
||||
|
||||
const url = await homePage.getShareLink();
|
||||
|
||||
await sleep(4000); // Wait for the sync to complete
|
||||
|
||||
await homePage.logout();
|
||||
|
||||
await loginPage.goto();
|
||||
await loginPage.fillUsername("Luigi");
|
||||
await loginPage.signup();
|
||||
|
||||
await page.goto(url);
|
||||
|
||||
await homePage.expectMusicTrack("Super Mario World");
|
||||
await homePage.playMusicTrack("Super Mario World");
|
||||
await homePage.expectActiveTrackPlaying();
|
||||
});
|
||||
102
examples/music-player/tests/pages/HomePage.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import { Page, expect } from "@playwright/test";
|
||||
|
||||
export class HomePage {
|
||||
constructor(public page: Page) {}
|
||||
|
||||
newPlaylistButton = this.page.getByRole("button", {
|
||||
name: "New Playlist",
|
||||
});
|
||||
|
||||
playlistTitleInput = this.page.getByRole("textbox", {
|
||||
name: "Playlist title",
|
||||
});
|
||||
|
||||
logoutButton = this.page.getByRole("button", {
|
||||
name: "Logout",
|
||||
});
|
||||
|
||||
async expectActiveTrackPlaying() {
|
||||
await expect(
|
||||
this.page.getByRole("button", {
|
||||
name: `Pause active track`,
|
||||
}),
|
||||
|
||||
).toBeVisible({
|
||||
timeout: 10_000,
|
||||
});
|
||||
}
|
||||
|
||||
async expectMusicTrack(trackName: string) {
|
||||
await expect(
|
||||
this.page.getByRole("button", {
|
||||
name: `Play ${trackName}`,
|
||||
}),
|
||||
).toBeVisible();
|
||||
}
|
||||
|
||||
async playMusicTrack(trackName: string) {
|
||||
await this.page
|
||||
.getByRole("button", {
|
||||
name: `Play ${trackName}`,
|
||||
})
|
||||
.click();
|
||||
}
|
||||
|
||||
async editTrackTitle(trackTitle: string, newTitle: string) {
|
||||
await this.page
|
||||
.getByRole("textbox", {
|
||||
name: `Edit track title: ${trackTitle}`,
|
||||
})
|
||||
.fill(newTitle);
|
||||
}
|
||||
|
||||
async createPlaylist() {
|
||||
await this.newPlaylistButton.click();
|
||||
}
|
||||
|
||||
async editPlaylistTitle(playlistTitle: string) {
|
||||
await this.playlistTitleInput.fill(playlistTitle);
|
||||
}
|
||||
|
||||
async navigateToPlaylist(playlistTitle: string) {
|
||||
await this.page
|
||||
.getByRole("link", {
|
||||
name: playlistTitle,
|
||||
})
|
||||
.click();
|
||||
}
|
||||
|
||||
async getShareLink() {
|
||||
await this.page
|
||||
.getByRole("button", {
|
||||
name: "Share playlist",
|
||||
})
|
||||
.click();
|
||||
|
||||
const inviteUrl = await this.page.evaluate(() =>
|
||||
navigator.clipboard.readText(),
|
||||
);
|
||||
|
||||
expect(inviteUrl).toBeTruthy();
|
||||
|
||||
return inviteUrl;
|
||||
}
|
||||
|
||||
async addTrackToPlaylist(trackTitle: string, playlistTitle: string) {
|
||||
await this.page
|
||||
.getByRole("button", {
|
||||
name: `Open ${trackTitle} menu`,
|
||||
})
|
||||
.click();
|
||||
|
||||
await this.page
|
||||
.getByRole("menuitem", {
|
||||
name: `Add to ${playlistTitle}`,
|
||||
})
|
||||
.click();
|
||||
}
|
||||
|
||||
async logout() {
|
||||
await this.logoutButton.click();
|
||||
}
|
||||
}
|
||||
40
examples/music-player/tests/pages/LoginPage.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { Page, Locator, expect } from "@playwright/test";
|
||||
|
||||
export class LoginPage {
|
||||
readonly page: Page;
|
||||
readonly usernameInput: Locator;
|
||||
readonly signupButton: Locator;
|
||||
|
||||
constructor(page: Page) {
|
||||
this.page = page;
|
||||
this.usernameInput = page.getByRole("textbox");
|
||||
this.signupButton = page.getByRole("button", {
|
||||
name: "Sign up",
|
||||
});
|
||||
}
|
||||
|
||||
async goto() {
|
||||
this.page.goto("/");
|
||||
}
|
||||
|
||||
async fillUsername(value: string) {
|
||||
await this.usernameInput.clear();
|
||||
await this.usernameInput.fill(value);
|
||||
}
|
||||
|
||||
async loginAs(value: string) {
|
||||
await this.page
|
||||
.getByRole("button", {
|
||||
name: value,
|
||||
})
|
||||
.click();
|
||||
}
|
||||
|
||||
async signup() {
|
||||
await this.signupButton.click();
|
||||
}
|
||||
|
||||
async expectLoaded() {
|
||||
await expect(this.signupButton).toBeVisible();
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,44 @@
|
||||
# jazz-password-manager
|
||||
|
||||
## 0.0.13
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [cce679b]
|
||||
- jazz-tools@0.8.15
|
||||
- jazz-react@0.8.15
|
||||
|
||||
## 0.0.12
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [36273b3]
|
||||
- jazz-tools@0.8.14
|
||||
- jazz-react@0.8.14
|
||||
|
||||
## 0.0.11
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [fd011d7]
|
||||
- jazz-tools@0.8.13
|
||||
- jazz-react@0.8.13
|
||||
|
||||
## 0.0.10
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [3cc6aee]
|
||||
- jazz-react@0.8.12
|
||||
- jazz-tools@0.8.12
|
||||
|
||||
## 0.0.9
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react@0.8.11
|
||||
- jazz-tools@0.8.11
|
||||
|
||||
## 0.0.8
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "jazz-password-manager",
|
||||
"private": true,
|
||||
"version": "0.0.8",
|
||||
"version": "0.0.13",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
@@ -11,8 +11,8 @@
|
||||
"clean-install": "rm -rf node_modules pnpm-lock.yaml && pnpm install"
|
||||
},
|
||||
"dependencies": {
|
||||
"jazz-react": "workspace:0.8.7",
|
||||
"jazz-tools": "workspace:0.8.5",
|
||||
"jazz-react": "workspace:0.8.15",
|
||||
"jazz-tools": "workspace:0.8.15",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-hook-form": "^7.41.5",
|
||||
|
||||
@@ -1,5 +1,49 @@
|
||||
# jazz-example-pets
|
||||
|
||||
## 0.0.111
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [cce679b]
|
||||
- jazz-tools@0.8.15
|
||||
- jazz-browser-media-images@0.8.15
|
||||
- jazz-react@0.8.15
|
||||
|
||||
## 0.0.110
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [36273b3]
|
||||
- jazz-tools@0.8.14
|
||||
- jazz-browser-media-images@0.8.14
|
||||
- jazz-react@0.8.14
|
||||
|
||||
## 0.0.109
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [fd011d7]
|
||||
- jazz-tools@0.8.13
|
||||
- jazz-browser-media-images@0.8.13
|
||||
- jazz-react@0.8.13
|
||||
|
||||
## 0.0.108
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [3cc6aee]
|
||||
- jazz-react@0.8.12
|
||||
- jazz-tools@0.8.12
|
||||
- jazz-browser-media-images@0.8.12
|
||||
|
||||
## 0.0.107
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react@0.8.11
|
||||
- jazz-tools@0.8.11
|
||||
- jazz-browser-media-images@0.8.11
|
||||
|
||||
## 0.0.106
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "jazz-example-pets",
|
||||
"private": true,
|
||||
"version": "0.0.106",
|
||||
"version": "0.0.111",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
@@ -23,9 +23,9 @@
|
||||
"@radix-ui/react-toast": "^1.1.4",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.0.0",
|
||||
"jazz-browser-media-images": "workspace:0.8.7",
|
||||
"jazz-react": "workspace:0.8.7",
|
||||
"jazz-tools": "workspace:0.8.5",
|
||||
"jazz-browser-media-images": "workspace:0.8.15",
|
||||
"jazz-react": "workspace:0.8.15",
|
||||
"jazz-tools": "workspace:0.8.15",
|
||||
"lucide-react": "^0.274.0",
|
||||
"qrcode": "^1.5.3",
|
||||
"react": "^18.2.0",
|
||||
@@ -50,7 +50,7 @@
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.3",
|
||||
"is-ci": "^3.0.1",
|
||||
"jazz-run": "workspace:0.8.7",
|
||||
"jazz-run": "workspace:0.8.15",
|
||||
"postcss": "^8.4.27",
|
||||
"tailwindcss": "3.3.2",
|
||||
"typescript": "^5.3.3",
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import {
|
||||
Link,
|
||||
RouterProvider,
|
||||
createHashRouter,
|
||||
useNavigate,
|
||||
} from "react-router-dom";
|
||||
import { Link, RouterProvider, createHashRouter, useNavigate } from "react-router-dom";
|
||||
import "./index.css";
|
||||
|
||||
import { createJazzReactApp, DemoAuthBasicUI, useDemoAuth } from "jazz-react";
|
||||
@@ -43,7 +38,10 @@ function JazzAndAuth({ children }: { children: React.ReactNode }) {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Jazz.Provider auth={auth} peer={peer}>
|
||||
<Jazz.Provider
|
||||
auth={auth}
|
||||
peer={peer}
|
||||
>
|
||||
{children}
|
||||
</Jazz.Provider>
|
||||
<DemoAuthBasicUI appName={appName} state={authState} />
|
||||
@@ -98,7 +96,9 @@ export default function App() {
|
||||
<RouterProvider router={router} />
|
||||
|
||||
<Button
|
||||
onClick={() => router.navigate("/").then(() => logOut())}
|
||||
onClick={() =>
|
||||
router.navigate("/").then(() => logOut())
|
||||
}
|
||||
variant="outline"
|
||||
>
|
||||
Log out
|
||||
@@ -114,7 +114,7 @@ function AcceptInvite() {
|
||||
onAccept: (petPostID) => navigate("/pet/" + petPostID),
|
||||
});
|
||||
|
||||
return <p>Accepting invite...</p>;
|
||||
return <p>Accepting invite...</p>
|
||||
}
|
||||
|
||||
export function PostOverview() {
|
||||
|
||||
@@ -1,5 +1,44 @@
|
||||
# jazz-example-todo
|
||||
|
||||
## 0.0.110
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [cce679b]
|
||||
- jazz-tools@0.8.15
|
||||
- jazz-react@0.8.15
|
||||
|
||||
## 0.0.109
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [36273b3]
|
||||
- jazz-tools@0.8.14
|
||||
- jazz-react@0.8.14
|
||||
|
||||
## 0.0.108
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [fd011d7]
|
||||
- jazz-tools@0.8.13
|
||||
- jazz-react@0.8.13
|
||||
|
||||
## 0.0.107
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [3cc6aee]
|
||||
- jazz-react@0.8.12
|
||||
- jazz-tools@0.8.12
|
||||
|
||||
## 0.0.106
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react@0.8.11
|
||||
- jazz-tools@0.8.11
|
||||
|
||||
## 0.0.105
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "jazz-example-todo",
|
||||
"private": true,
|
||||
"version": "0.0.105",
|
||||
"version": "0.0.110",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
@@ -20,8 +20,8 @@
|
||||
"@radix-ui/react-toast": "^1.1.4",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.0.0",
|
||||
"jazz-react": "workspace:0.8.7",
|
||||
"jazz-tools": "workspace:0.8.5",
|
||||
"jazz-react": "workspace:0.8.15",
|
||||
"jazz-tools": "workspace:0.8.15",
|
||||
"lucide-react": "^0.274.0",
|
||||
"qrcode": "^1.5.3",
|
||||
"react": "^18.2.0",
|
||||
|
||||
@@ -6,11 +6,7 @@ import {
|
||||
} from "react-router-dom";
|
||||
import "./index.css";
|
||||
|
||||
import {
|
||||
createJazzReactApp,
|
||||
PasskeyAuthBasicUI,
|
||||
usePasskeyAuth,
|
||||
} from "jazz-react";
|
||||
import { createJazzReactApp, PasskeyAuthBasicUI, usePasskeyAuth } from "jazz-react";
|
||||
|
||||
import {
|
||||
Button,
|
||||
@@ -66,9 +62,8 @@ ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||
<App />
|
||||
</JazzAndAuth>
|
||||
</div>
|
||||
</ThemeProvider>
|
||||
,
|
||||
</React.StrictMode>,
|
||||
</ThemeProvider>,
|
||||
</React.StrictMode>
|
||||
);
|
||||
|
||||
/**
|
||||
@@ -110,7 +105,9 @@ export default function App() {
|
||||
<RouterProvider router={router} />
|
||||
|
||||
<Button
|
||||
onClick={() => router.navigate("/").then(logOut)}
|
||||
onClick={() =>
|
||||
router.navigate("/").then(logOut)
|
||||
}
|
||||
variant="outline"
|
||||
>
|
||||
Log out
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-react": "^0.436.0",
|
||||
"next": "14.2.7",
|
||||
"next-themes": "^0.2.1",
|
||||
"react": "^18",
|
||||
"react-dom": "^18",
|
||||
"tailwind-merge": "^1.14.0",
|
||||
|
||||
@@ -28,7 +28,7 @@ export function Button(props: ButtonProps) {
|
||||
primary:
|
||||
"bg-blue border-blue text-white font-medium bg-blue hover:bg-blue-800 hover:border-blue-800",
|
||||
secondary:
|
||||
"text-stone-900 border border-stone-200 dark:border-stone-800 font-medium hover:border-stone-300 hover:dark:border-stone-700 dark:text-white",
|
||||
"text-stone-900 border font-medium hover:border-stone-300 hover:dark:border-stone-700 dark:text-white",
|
||||
};
|
||||
|
||||
const classNames = clsx(
|
||||
|
||||
@@ -6,12 +6,10 @@ export function CodeRef({ children }: { children: React.ReactNode }) {
|
||||
className={clsx(
|
||||
"font-mono",
|
||||
"text-[0.9em]",
|
||||
"px-1 py-0.5",
|
||||
"rounded-sm",
|
||||
"border",
|
||||
"text-stone-800 dark:text-stone-200",
|
||||
"px-2 py-1",
|
||||
"rounded",
|
||||
"text-stone-900 dark:text-stone-200",
|
||||
"bg-stone-100 dark:bg-stone-800",
|
||||
"border-stone-200 dark:border-stone-900",
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -6,7 +6,7 @@ export function GridCard(props: { children: ReactNode; className?: string }) {
|
||||
<div
|
||||
className={clsx(
|
||||
"col-span-2 p-4 [&>h4]:mt-0 [&>h3]:mt-0 [&>:last-child]:mb-0",
|
||||
"border border-stone-200 dark:border-stone-900 rounded-xl shadow-sm",
|
||||
"border rounded-xl shadow-sm",
|
||||
props.className,
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -30,7 +30,7 @@ export function H2({ children, className }: HeadingProps) {
|
||||
className,
|
||||
"font-display",
|
||||
"text-stone-950 dark:text-white",
|
||||
"text-2xl",
|
||||
"text-2xl md:text-4xl",
|
||||
"mb-2",
|
||||
"font-semibold",
|
||||
"tracking-tight",
|
||||
@@ -48,7 +48,7 @@ export function H3({ children, className }: HeadingProps) {
|
||||
className,
|
||||
"font-display",
|
||||
"text-stone-950 dark:text-white",
|
||||
"text-xl",
|
||||
"text-xl md:text-2xl",
|
||||
"mb-2",
|
||||
"font-semibold",
|
||||
"tracking-tight",
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
export function P({ children }: { children: React.ReactNode }) {
|
||||
return <p className="mb-4">{children}</p>;
|
||||
import clsx from "clsx";
|
||||
|
||||
export function P({
|
||||
children,
|
||||
className,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}) {
|
||||
return <p className={clsx(className, "mb-4")}>{children}</p>;
|
||||
}
|
||||
|
||||
@@ -84,7 +84,15 @@ export function CodeGroup({
|
||||
|
||||
return (
|
||||
<div className="group relative">
|
||||
<pre ref={textRef}>{children}</pre>
|
||||
<pre
|
||||
className={clsx(
|
||||
"border p-0 bg-stone-50 dark:bg-stone-900",
|
||||
"text-black dark:text-white",
|
||||
)}
|
||||
ref={textRef}
|
||||
>
|
||||
{children}
|
||||
</pre>
|
||||
|
||||
{code ? <CopyButton size={size} code={code} /> : <></>}
|
||||
</div>
|
||||
@@ -15,7 +15,7 @@ export function Input(props: Props) {
|
||||
const inputClassName = clsx(
|
||||
"w-full rounded-md border px-3.5 py-2 shadow-sm",
|
||||
"font-medium text-stone-900",
|
||||
"dark:border-stone-900 dark:text-white",
|
||||
"dark:text-white",
|
||||
);
|
||||
|
||||
const containerClassName = clsx("grid gap-1", className);
|
||||
|
||||
@@ -5,17 +5,20 @@ export function LabelledFeatureIcon({
|
||||
label,
|
||||
icon: Icon,
|
||||
explanation,
|
||||
className,
|
||||
}: {
|
||||
label: string;
|
||||
icon: LucideIcon;
|
||||
explanation: React.ReactNode;
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
className,
|
||||
"p-4 flex flex-col gap-3",
|
||||
"not-prose text-base",
|
||||
"border border-stone-200 dark:border-stone-900 rounded-xl",
|
||||
"border rounded-xl",
|
||||
)}
|
||||
>
|
||||
<div>
|
||||
|
||||
@@ -1,23 +1,32 @@
|
||||
import clsx from "clsx";
|
||||
import { ReactNode } from "react";
|
||||
|
||||
export function Prose(props: { children: ReactNode; className?: string }) {
|
||||
export function Prose({
|
||||
children,
|
||||
className,
|
||||
size = "md",
|
||||
}: {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
size?: "sm" | "md" | "lg";
|
||||
}) {
|
||||
const sizeClassName = {
|
||||
sm: "prose-sm",
|
||||
md: "",
|
||||
lg: "prose-xl",
|
||||
}[size];
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
"max-w-4xl prose-stone dark:prose-invert",
|
||||
"prose-headings:font-display",
|
||||
"lg:prose-h1:text-5xl prose-h1:font-medium prose-h1:tracking-tight",
|
||||
"lg:prose-h2:text-3xl prose-h2:font-medium prose-h2:tracking-tight",
|
||||
"prose-p:leading-snug",
|
||||
"prose-strong:font-medium",
|
||||
"prose-code:font-normal prose-code:before:content-none prose-code:after:content-none prose-code:bg-stone-100 prose-code:dark:bg-stone-900 prose-code:p-1 prose-code:rounded",
|
||||
"prose-pre:border prose-pre:p-0 prose-pre:bg-stone-50 prose-pre:dark:bg-stone-900 dark:prose-pre:border-stone-800",
|
||||
"prose-pre:text-black dark:prose-pre:text-white",
|
||||
props.className || "prose",
|
||||
className,
|
||||
"prose",
|
||||
sizeClassName,
|
||||
"dark:prose-invert",
|
||||
"prose-code:dark:bg-stone-900",
|
||||
)}
|
||||
>
|
||||
{props.children}
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ function H2Sub({ children }: { children: React.ReactNode }) {
|
||||
"text-lg lg:text-xl",
|
||||
"leading-snug",
|
||||
"tracking-tight",
|
||||
"max-w-4xl",
|
||||
"max-w-2xl",
|
||||
"text-stone-700 dark:text-stone-500",
|
||||
)}
|
||||
>
|
||||
@@ -21,12 +21,14 @@ function H2Sub({ children }: { children: React.ReactNode }) {
|
||||
export function SectionHeader({
|
||||
title,
|
||||
slogan,
|
||||
className,
|
||||
}: {
|
||||
title: ReactNode;
|
||||
slogan: ReactNode;
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<hgroup className="mb-5">
|
||||
<hgroup className={clsx(className, "mb-5")}>
|
||||
<H2>{title}</H2>
|
||||
<H2Sub>{slogan}</H2Sub>
|
||||
</hgroup>
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
export function Testimonial({
|
||||
children,
|
||||
name,
|
||||
role,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
name: string;
|
||||
role: string;
|
||||
}) {
|
||||
return (
|
||||
<figure className="max-w-2xl">
|
||||
<svg
|
||||
className="size-8 text-blue"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 32 32"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M9.352 4C4.456 7.456 1 13.12 1 19.36c0 5.088 3.072 8.064 6.624 8.064 3.36 0 5.856-2.688 5.856-5.856 0-3.168-2.208-5.472-5.088-5.472-.576 0-1.344.096-1.536.192.48-3.264 3.552-7.104 6.624-9.024L9.352 4zm16.512 0c-4.8 3.456-8.256 9.12-8.256 15.36 0 5.088 3.072 8.064 6.624 8.064 3.264 0 5.856-2.688 5.856-5.856 0-3.168-2.304-5.472-5.184-5.472-.576 0-1.248.096-1.44.192.48-3.264 3.456-7.104 6.528-9.024L25.864 4z"></path>
|
||||
</svg>
|
||||
<blockquote className="mt-6 text-balance font-medium leading-8 text-stone-900 sm:text-lg dark:text-white">
|
||||
{children}
|
||||
</blockquote>
|
||||
<figcaption className="mt-6 flex items-center gap-x-6">
|
||||
<div className="text-sm leading-6 sm:text-base">
|
||||
<div className="font-semibold text-stone-900 dark:text-white">
|
||||
{name}
|
||||
</div>
|
||||
<div className="mt-0.5 text-stone-600 dark:text-stone-500">
|
||||
{role}
|
||||
</div>
|
||||
</div>
|
||||
</figcaption>
|
||||
</figure>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import { ThemeProvider as NextThemesProvider, useTheme } from "next-themes";
|
||||
import { type ThemeProviderProps } from "next-themes/dist/types";
|
||||
import { useEffect } from "react";
|
||||
|
||||
function ThemeWatcher() {
|
||||
let { resolvedTheme, setTheme } = useTheme();
|
||||
|
||||
useEffect(() => {
|
||||
let media = window.matchMedia("(prefers-color-scheme: dark)");
|
||||
|
||||
function onMediaChange() {
|
||||
let systemTheme = media.matches ? "dark" : "light";
|
||||
if (resolvedTheme === systemTheme) {
|
||||
setTheme("system");
|
||||
}
|
||||
}
|
||||
|
||||
onMediaChange();
|
||||
media.addEventListener("change", onMediaChange);
|
||||
|
||||
return () => {
|
||||
media.removeEventListener("change", onMediaChange);
|
||||
};
|
||||
}, [resolvedTheme, setTheme]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
|
||||
return (
|
||||
<NextThemesProvider {...props}>
|
||||
<ThemeWatcher />
|
||||
{children}
|
||||
</NextThemesProvider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTheme } from "next-themes";
|
||||
import { MoonIcon, SunIcon } from "lucide-react";
|
||||
import clsx from "clsx";
|
||||
|
||||
export function ThemeToggle({ className }: { className?: string }) {
|
||||
let { resolvedTheme, setTheme } = useTheme();
|
||||
let otherTheme = resolvedTheme === "dark" ? "light" : "dark";
|
||||
let [mounted, setMounted] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className={clsx(
|
||||
className,
|
||||
"md:p-2 md:rounded-full md:border",
|
||||
"text-stone-400 hover:text-stone-900 dark:text-stone-400 dark:hover:text-white",
|
||||
"md:hover:bg-stone-200 md:dark:hover:bg-stone-900",
|
||||
"transition-colors",
|
||||
)}
|
||||
aria-label={
|
||||
mounted ? `Switch to ${otherTheme} theme` : "Toggle theme"
|
||||
}
|
||||
onClick={() => setTheme(otherTheme)}
|
||||
>
|
||||
<MoonIcon
|
||||
size={24}
|
||||
strokeWidth={1.5}
|
||||
className="size-5 md:size-6 stroke-stone-900 dark:hidden"
|
||||
/>
|
||||
<SunIcon
|
||||
size={24}
|
||||
strokeWidth={1.5}
|
||||
className="size-5 md:size-6 hidden stroke-white dark:block"
|
||||
/>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import clsx from "clsx";
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { ReactNode } from "react";
|
||||
import { ThemeToggle } from "../molecules/ThemeToggle";
|
||||
|
||||
type FooterSection = {
|
||||
title: string;
|
||||
@@ -20,20 +21,38 @@ type FooterProps = {
|
||||
sections: FooterSection[];
|
||||
};
|
||||
|
||||
function Copyright({
|
||||
className,
|
||||
companyName,
|
||||
}: {
|
||||
companyName: string;
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<p className={clsx(className, "text-sm")}>
|
||||
© {new Date().getFullYear()} {companyName}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
export function Footer({ logo, companyName, sections }: FooterProps) {
|
||||
return (
|
||||
<footer className="flex z-10 mt-10 min-h-[15rem] border-t bg-stone-100 dark:bg-stone-925 text-stone-600 dark:text-stone-400 w-full justify-center dark:border-stone-900">
|
||||
<div className="p-8 container w-full grid grid-cols-3 md:grid-cols-4 lg:grid-cols-7 gap-8 max-sm:mb-12">
|
||||
<div className="col-span-full md:col-span-1 sm:row-start-4 md:row-start-auto lg:col-span-2 md:row-span-2 md:flex-1 flex flex-row md:flex-col max-sm:mt-4 justify-between max-sm:items-start gap-2 text-sm min-w-[10rem]">
|
||||
<footer className="w-full border-t bg-stone-100 mt-12 md:mt-20 dark:bg-stone-925">
|
||||
<div className="container py-8 md:py-16 grid gap-y-8 grid-cols-12">
|
||||
<div className="flex flex-col justify-between col-span-full md:col-span-4">
|
||||
{logo}
|
||||
<p className="max-sm:text-right">
|
||||
© {new Date().getFullYear()}
|
||||
<br />
|
||||
{companyName}
|
||||
</p>
|
||||
|
||||
<Copyright
|
||||
className="hidden md:block"
|
||||
companyName={companyName}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{sections.map((section, index) => (
|
||||
<div key={index} className="flex flex-col gap-2 text-sm">
|
||||
<div
|
||||
key={index}
|
||||
className="flex flex-col gap-2 text-sm col-span-4 sm:col-span-4 md:col-span-2"
|
||||
>
|
||||
<h2 className="font-medium">{section.title}</h2>
|
||||
{section.links.map((link, linkIndex) => (
|
||||
<FooterLink
|
||||
@@ -46,6 +65,15 @@ export function Footer({ logo, companyName, sections }: FooterProps) {
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
|
||||
<div className="hidden md:flex justify-end items-end md:col-span-2">
|
||||
<ThemeToggle />
|
||||
</div>
|
||||
|
||||
<Copyright
|
||||
className="col-span-full md:hidden"
|
||||
companyName={companyName}
|
||||
/>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
|
||||
@@ -2,10 +2,11 @@
|
||||
|
||||
import { MenuIcon, XIcon } from "lucide-react";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { ReactNode, useLayoutEffect, useRef, useState } from "react";
|
||||
import { ReactNode, useEffect, useLayoutEffect, useRef, useState } from "react";
|
||||
import { BreadCrumb } from "../molecules/Breadcrumb";
|
||||
import clsx from "clsx";
|
||||
import Link from "next/link";
|
||||
import { ThemeToggle } from "../molecules/ThemeToggle";
|
||||
|
||||
export function Nav({
|
||||
mainLogo,
|
||||
@@ -34,16 +35,18 @@ export function Nav({
|
||||
|
||||
const pathname = usePathname();
|
||||
|
||||
useEffect(() => {
|
||||
setMenuOpen(false);
|
||||
}, [pathname]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<nav
|
||||
className={[
|
||||
clsx(
|
||||
"hidden md:flex sticky left-0 right-0 top-0 max-sm:bottom-0 w-full justify-center",
|
||||
"bg-white dark:bg-stone-950 border-b max-sm:border-t border-stone-200 dark:border-stone-900",
|
||||
"max-h-none overflow-hidden transition[max-height] duration-300 ease-in-out",
|
||||
"hidden md:flex sticky left-0 right-0 top-0 w-full justify-center",
|
||||
"bg-white dark:bg-stone-950 border-b",
|
||||
"z-50",
|
||||
menuOpen ? "h-[100dvh]" : "h-16",
|
||||
),
|
||||
].join(" ")}
|
||||
>
|
||||
@@ -90,6 +93,7 @@ export function Nav({
|
||||
setMenuOpen((o) => !o);
|
||||
setSearchOpen(false);
|
||||
}}
|
||||
aria-label="Open menu"
|
||||
>
|
||||
<MenuIcon />
|
||||
<BreadCrumb items={items} />
|
||||
@@ -108,7 +112,7 @@ export function Nav({
|
||||
<nav
|
||||
className={clsx(
|
||||
"md:hidden fixed flex flex-col bottom-4 right-4 z-50",
|
||||
"bg-stone-50 dark:bg-stone-925 border border-stone-100 dark:border-stone-900 dark:outline dark:outline-1 dark:outline-black/60 rounded-lg shadow-lg",
|
||||
"bg-stone-50 dark:bg-stone-925 border rounded-lg shadow-lg",
|
||||
menuOpen || searchOpen ? "left-4" : "",
|
||||
)}
|
||||
>
|
||||
@@ -118,7 +122,7 @@ export function Nav({
|
||||
" px-2 pb-2",
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center w-full border-b border-stone-100 dark:border-stone-900">
|
||||
<div className="flex items-center w-full border-b">
|
||||
<NavLinkLogo
|
||||
prominent
|
||||
href="/"
|
||||
@@ -141,7 +145,7 @@ export function Nav({
|
||||
</div>
|
||||
|
||||
{pathname.startsWith("/docs") && docNav && (
|
||||
<div className="max-h-[calc(100dvh-15rem)] p-4 border-b border-stone-100 dark:border-stone-900 overflow-x-auto">
|
||||
<div className="max-h-[calc(100dvh-15rem)] p-4 border-b overflow-x-auto">
|
||||
{docNav}
|
||||
</div>
|
||||
)}
|
||||
@@ -162,7 +166,7 @@ export function Nav({
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex gap-4 justify-end border-b border-stone-100 dark:border-stone-900">
|
||||
<div className="flex gap-4 justify-end border-b">
|
||||
{items
|
||||
.filter((item) => !("icon" in item))
|
||||
.slice(3)
|
||||
@@ -179,12 +183,12 @@ export function Nav({
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center self-stretch justify-end">
|
||||
<div className="flex items-center self-stretch justify-between">
|
||||
{/* <input
|
||||
type="text"
|
||||
className={clsx(
|
||||
menuOpen || searchOpen ? "" : "hidden",
|
||||
"ml-2 border border-stone-200 dark:border-stone-900 px-2 py-1 rounded w-full"
|
||||
"ml-2 border px-2 py-1 rounded w-full"
|
||||
)}
|
||||
placeholder="Search docs..."
|
||||
ref={searchRef}
|
||||
@@ -202,12 +206,16 @@ export function Nav({
|
||||
>
|
||||
<SearchIcon className="" />
|
||||
</button> */}
|
||||
{(menuOpen || searchOpen) && (
|
||||
<ThemeToggle className="p-3" />
|
||||
)}
|
||||
<button
|
||||
className="flex gap-2 p-3 rounded-xl items-center"
|
||||
onMouseDown={() => {
|
||||
setMenuOpen((o) => !o);
|
||||
setSearchOpen(false);
|
||||
}}
|
||||
aria-label="Close menu"
|
||||
>
|
||||
{menuOpen || searchOpen ? (
|
||||
<XIcon />
|
||||
|
||||
@@ -1,45 +1,3 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
/* .manrope-bold {
|
||||
font-family: "Manrope", sans-serif;
|
||||
font-optical-sizing: auto;
|
||||
font-weight: 500;
|
||||
font-style: normal;
|
||||
} */
|
||||
|
||||
:root {
|
||||
--foreground-rgb: 0, 0, 0;
|
||||
--background-start-rgb: 214, 219, 220;
|
||||
--background-end-rgb: 255, 255, 255;
|
||||
|
||||
font-family: "Manrope", sans-serif;
|
||||
font-optical-sizing: auto;
|
||||
font-weight: 300;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--foreground-rgb: 255, 255, 255;
|
||||
--background-start-rgb: 0, 0, 0;
|
||||
--background-end-rgb: 0, 0, 0;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
color: rgb(var(--foreground-rgb));
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
transparent,
|
||||
rgb(var(--background-end-rgb))
|
||||
)
|
||||
rgb(var(--background-start-rgb));
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
.text-balance {
|
||||
text-wrap: balance;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,6 +51,7 @@ export default function RootLayout({
|
||||
commitMono.variable,
|
||||
inter.className,
|
||||
"h-full",
|
||||
"bg-white text-stone-700 dark:text-stone-400 dark:bg-stone-950",
|
||||
].join(" ")}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -1,9 +1,72 @@
|
||||
import { Prose } from "@components/molecules/Prose";
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<main className="container h-full flex flex-col gap-8 py-8 lg:py-16">
|
||||
<main className="container flex flex-col gap-8 py-8 lg:py-16">
|
||||
<h1 className="text-2xl font-semibold font-display">
|
||||
Jazz Design System
|
||||
</h1>
|
||||
|
||||
<h2>Typography (Prose)</h2>
|
||||
|
||||
<div className="grid gap-4">
|
||||
<div>
|
||||
Heading 1
|
||||
<Prose className="p-3 border">
|
||||
<h1>Ship top-tier apps at high tempo</h1>
|
||||
</Prose>
|
||||
</div>
|
||||
<div>
|
||||
Heading 2
|
||||
<Prose className="p-3 border">
|
||||
<h2>Ship top-tier apps at high tempo</h2>
|
||||
</Prose>
|
||||
</div>
|
||||
<div>
|
||||
Heading 3
|
||||
<Prose className="p-3 border">
|
||||
<h3>Ship top-tier apps at high tempo</h3>
|
||||
</Prose>
|
||||
</div>
|
||||
<div>
|
||||
Heading 4
|
||||
<Prose className="p-3 border">
|
||||
<h4>Ship top-tier apps at high tempo</h4>
|
||||
</Prose>
|
||||
</div>
|
||||
<div>
|
||||
Paragraph
|
||||
<Prose className="p-3 border">
|
||||
<p>
|
||||
<strong>
|
||||
Jazz is a framework for building local-first
|
||||
apps
|
||||
</strong>{" "}
|
||||
— an architecture that lets companies like Figma and
|
||||
Linear play in a league of their own.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Open source. Self-host or use Jazz Cloud for
|
||||
zero-config magic.
|
||||
</p>
|
||||
</Prose>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
Link
|
||||
<Prose className="p-3 border">
|
||||
This is a <a href="https://jazz.tools">link</a>
|
||||
</Prose>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
Code
|
||||
<Prose className="p-3 border">
|
||||
This is a one-line <code>piece of code</code>
|
||||
</Prose>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,28 @@ import tailwindCSSAnimate from "tailwindcss-animate";
|
||||
const colors = require("tailwindcss/colors")
|
||||
const plugin = require("tailwindcss/plugin")
|
||||
|
||||
const stonePalette = {
|
||||
"50": "oklch(0.988281 0.002 75)",
|
||||
"75": "oklch(0.980563 0.002 75)",
|
||||
"100": "oklch(0.964844 0.002 75)",
|
||||
"200": "oklch(0.917969 0.002 75)",
|
||||
"300": "oklch(0.853516 0.002 75)",
|
||||
"400": "oklch(0.789063 0.002 75)",
|
||||
"500": "oklch(0.726563 0.002 75)",
|
||||
"600": "oklch(0.613281 0.002 75)",
|
||||
"700": "oklch(0.523438 0.002 75)",
|
||||
"800": "oklch(0.412109 0.002 75)",
|
||||
"900": "oklch(0.302734 0.002 75)",
|
||||
"925": "oklch(0.220000 0.002 75)",
|
||||
"950": "oklch(0.193359 0.002 75)",
|
||||
}
|
||||
|
||||
const stonePaletteWithAlpha = {...stonePalette};
|
||||
|
||||
Object.keys(stonePalette).forEach(key => {
|
||||
stonePaletteWithAlpha[key] = stonePaletteWithAlpha[key].replace(")", "/ <alpha-value>)")
|
||||
})
|
||||
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
const config = {
|
||||
content: [
|
||||
@@ -14,21 +36,7 @@ const config = {
|
||||
theme: {
|
||||
colors: {
|
||||
...harmonyPalette,
|
||||
stone: {
|
||||
"50": "oklch(0.988281 0.002 75 / <alpha-value>)",
|
||||
"75": "oklch(0.980563 0.002 75 / <alpha-value>)",
|
||||
"100": "oklch(0.964844 0.002 75 / <alpha-value>)",
|
||||
"200": "oklch(0.917969 0.002 75 / <alpha-value>)",
|
||||
"300": "oklch(0.853516 0.002 75 / <alpha-value>)",
|
||||
"400": "oklch(0.789063 0.002 75 / <alpha-value>)",
|
||||
"500": "oklch(0.726563 0.002 75 / <alpha-value>)",
|
||||
"600": "oklch(0.613281 0.002 75 / <alpha-value>)",
|
||||
"700": "oklch(0.523438 0.002 75 / <alpha-value>)",
|
||||
"800": "oklch(0.412109 0.002 75 / <alpha-value>)",
|
||||
"900": "oklch(0.302734 0.002 75 / <alpha-value>)",
|
||||
"925": "oklch(0.220000 0.002 75 / <alpha-value>)",
|
||||
"950": "oklch(0.193359 0.002 75 / <alpha-value>)",
|
||||
},
|
||||
stone: stonePaletteWithAlpha,
|
||||
blue: {
|
||||
...colors.indigo,
|
||||
"500": "#5870F1",
|
||||
@@ -38,15 +46,15 @@ const config = {
|
||||
"900": "#12046A",
|
||||
DEFAULT: "#3313F7",
|
||||
},
|
||||
green: colors.green,
|
||||
red: colors.red,
|
||||
},
|
||||
extend: {
|
||||
fontFamily: {
|
||||
display: ["var(--font-manrope)"],
|
||||
mono: ["var(--font-commit-mono)"],
|
||||
handwritten: ["var(--font-sriracha)"],
|
||||
},
|
||||
fontSize: {
|
||||
|
||||
'2xs': ['0.75rem', { lineHeight: '1.25rem' }],
|
||||
},
|
||||
// shadcn-ui
|
||||
@@ -85,11 +93,6 @@ const config = {
|
||||
foreground: "hsl(var(--card-foreground))",
|
||||
},
|
||||
},
|
||||
borderRadius: {
|
||||
lg: "var(--radius)",
|
||||
md: "calc(var(--radius) - 2px)",
|
||||
sm: "calc(var(--radius) - 4px)",
|
||||
},
|
||||
keyframes: {
|
||||
"accordion-down": {
|
||||
from: { height: "0" },
|
||||
@@ -119,12 +122,107 @@ const config = {
|
||||
md: "960px",
|
||||
lg: "1276px",
|
||||
},
|
||||
typography: (theme) => ({
|
||||
DEFAULT: {
|
||||
css: {
|
||||
"--tw-prose-body": stonePalette[700],
|
||||
"--tw-prose-headings": stonePalette[900],
|
||||
"--tw-prose-bold": stonePalette[900],
|
||||
"--tw-prose-invert-bold": theme("colors.white"),
|
||||
"--tw-prose-invert-body": stonePalette[400],
|
||||
"--tw-prose-invert-headings": theme("colors.white"),
|
||||
"--tw-prose-code": stonePalette[900],
|
||||
"--tw-prose-invert-code": stonePalette[50],
|
||||
"--tw-prose-links": theme("colors.blue.DEFAULT"),
|
||||
"--tw-prose-invert-links": theme("colors.blue.500"),
|
||||
maxWidth: null,
|
||||
strong: {
|
||||
color: "var(--tw-prose-bold)",
|
||||
fontWeight: theme("fontWeight.medium"),
|
||||
},
|
||||
b: {
|
||||
color: "var(--tw-prose-bold)",
|
||||
fontWeight: theme("fontWeight.medium"),
|
||||
},
|
||||
a: {
|
||||
fontWeight: theme("fontWeight.normal"),
|
||||
textUnderlineOffset: "4px",
|
||||
},
|
||||
h1: {
|
||||
fontFamily: theme("fontFamily.display"),
|
||||
letterSpacing: theme("letterSpacing.tight"),
|
||||
fontWeight: theme("fontWeight.semibold"),
|
||||
fontSize: theme("fontSize.4xl"),
|
||||
},
|
||||
h2: {
|
||||
fontFamily: theme("fontFamily.display"),
|
||||
letterSpacing: theme("letterSpacing.tight"),
|
||||
fontWeight: theme("fontWeight.semibold"),
|
||||
fontSize: theme("fontSize.3xl"),
|
||||
},
|
||||
h3: {
|
||||
fontFamily: theme("fontFamily.display"),
|
||||
letterSpacing: theme("letterSpacing.tight"),
|
||||
fontWeight: theme("fontWeight.semibold"),
|
||||
fontSize: theme("fontSize.2xl"),
|
||||
},
|
||||
h4: {
|
||||
fontFamily: theme("fontFamily.display"),
|
||||
letterSpacing: theme("letterSpacing.tight"),
|
||||
fontWeight: theme("fontWeight.semibold"),
|
||||
fontSize: theme("fontSize.xl"),
|
||||
},
|
||||
'code::before': {
|
||||
content: 'none',
|
||||
},
|
||||
'code::after': {
|
||||
content: 'none',
|
||||
},
|
||||
code: {
|
||||
backgroundColor: stonePalette[100],
|
||||
padding: "0.15rem 0.25rem",
|
||||
borderRadius: "2px",
|
||||
whiteSpace: "nowrap",
|
||||
},
|
||||
p: {
|
||||
marginBottom: theme("spacing.3"),
|
||||
marginTop: theme("spacing.3"),
|
||||
}
|
||||
}
|
||||
},
|
||||
xl: {
|
||||
css: {
|
||||
p: {
|
||||
marginBottom: theme("spacing.3"),
|
||||
marginTop: theme("spacing.3"),
|
||||
}
|
||||
},
|
||||
}
|
||||
}),
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
tailwindCSSAnimate,
|
||||
typography(),
|
||||
plugin(({ addVariant }) => addVariant("label", "& :is(label)")),
|
||||
plugin(({ addUtilities }) => addUtilities({
|
||||
".text-reset, .text-reset:hover, .text-reset:focus": {
|
||||
color: "inherit",
|
||||
textDecoration: "none",
|
||||
},
|
||||
})),
|
||||
plugin(({ addBase }) => addBase({
|
||||
":root": {
|
||||
"--gcmp-border-color": stonePalette[200],
|
||||
"--gcmp-invert-border-color": stonePalette[900],
|
||||
},
|
||||
"*": {
|
||||
borderColor: "var(--gcmp-border-color)",
|
||||
},
|
||||
".dark *": {
|
||||
borderColor: "var(--gcmp-invert-border-color)",
|
||||
},
|
||||
}))
|
||||
],
|
||||
};
|
||||
export default config;
|
||||
|
||||
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 3.8 KiB |
@@ -65,10 +65,6 @@
|
||||
--ring: 24 5.7% 82.9%;
|
||||
}
|
||||
|
||||
* {
|
||||
@apply border-border;
|
||||
}
|
||||
|
||||
.overlay-close {
|
||||
@apply bg-black;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import "./globals.css";
|
||||
import type { Metadata } from "next";
|
||||
import { ThemeProvider } from "@/components/themeProvider";
|
||||
import { ThemeProvider } from "gcmp-design-system/src/app/components/molecules/ThemeProvider";
|
||||
|
||||
import { Inter, Manrope } from "next/font/google";
|
||||
import localFont from "next/font/local";
|
||||
@@ -8,6 +8,7 @@ import localFont from "next/font/local";
|
||||
import { SpeedInsights } from "@vercel/speed-insights/next";
|
||||
import { Analytics } from "@vercel/analytics/react";
|
||||
import { GcmpNav } from "@/components/Nav";
|
||||
import { ThemeToggle } from "gcmp-design-system/src/app/components/molecules/ThemeToggle";
|
||||
|
||||
// If loading a variable font, you don't need to specify the font weight
|
||||
const manrope = Manrope({
|
||||
@@ -76,13 +77,13 @@ export default function RootLayout({
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<html lang="en" className="h-full">
|
||||
<body
|
||||
className={[
|
||||
manrope.variable,
|
||||
commitMono.variable,
|
||||
inter.className,
|
||||
"flex flex-col items-center",
|
||||
"min-h-full flex flex-col items-center",
|
||||
"bg-white text-stone-700 dark:text-stone-400 dark:bg-stone-950",
|
||||
].join(" ")}
|
||||
>
|
||||
@@ -95,11 +96,13 @@ export default function RootLayout({
|
||||
disableTransitionOnChange
|
||||
>
|
||||
<GcmpNav />
|
||||
<main className="flex flex-1 flex-col w-full">
|
||||
<main className="flex-1 w-full">
|
||||
{children}
|
||||
</main>
|
||||
<footer className="py-8 md:py-16 text-sm">
|
||||
<footer className="py-8 text-sm flex justify-between gap-3 w-full container mt-12 md:mt-20">
|
||||
<p>©2024 Garden Computing, Inc.</p>
|
||||
|
||||
<ThemeToggle className="hidden md:block"/>
|
||||
</footer>
|
||||
</ThemeProvider>
|
||||
</body>
|
||||
|
||||
@@ -53,7 +53,7 @@ pre.twoslash data-lsp::before {
|
||||
position: absolute;
|
||||
transform: translate(0, 1.2rem);
|
||||
max-width: 30rem;
|
||||
@apply text-xs px-1.5 py-1 rounded border border-stone-200 dark:border-stone-900 shadow-lg overflow-hidden whitespace-pre-wrap text-stone-700 bg-stone-50 dark:text-stone-200 dark:bg-stone-950;
|
||||
@apply text-xs px-1.5 py-1 rounded border shadow-lg overflow-hidden whitespace-pre-wrap text-stone-700 bg-stone-50 dark:text-stone-200 dark:bg-stone-950;
|
||||
text-align: left;
|
||||
z-index: 100;
|
||||
opacity: 0;
|
||||
@@ -79,7 +79,7 @@ pre.twoslash data-lsp:hover::before {
|
||||
}
|
||||
|
||||
pre .code-container {
|
||||
@apply overflow-auto p-2 pl-0 bg-white dark:bg-stone-925 rounded-b-xl text-xs h-full dark:border-stone-900;
|
||||
@apply overflow-auto p-2 pl-0 bg-white dark:bg-stone-925 rounded-b-xl text-xs h-full;
|
||||
}
|
||||
/* The try button */
|
||||
pre .code-container > a {
|
||||
|
||||
@@ -1,42 +1,187 @@
|
||||
import { H1, H2 } from "gcmp-design-system/src/app/components/atoms/Headings";
|
||||
import { MailIcon } from "lucide-react";
|
||||
import { GlobeIcon, LucideIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { SiGithub, SiTwitter } from "@icons-pack/react-simple-icons";
|
||||
import { SectionHeader } from "gcmp-design-system/src/app/components/molecules/SectionHeader";
|
||||
import {
|
||||
SiBluesky,
|
||||
SiGithub,
|
||||
SiGitlab,
|
||||
SiLinkedin,
|
||||
SiX,
|
||||
} from "@icons-pack/react-simple-icons";
|
||||
import { HeroHeader } from "gcmp-design-system/src/app/components/molecules/HeroHeader";
|
||||
|
||||
export const metadata = {
|
||||
title: "Team",
|
||||
};
|
||||
interface TeamMember {
|
||||
name: string;
|
||||
titles: string[];
|
||||
image?: string;
|
||||
location: string;
|
||||
x?: string;
|
||||
github?: string;
|
||||
gitlab?: string;
|
||||
website?: string;
|
||||
linkedin?: string;
|
||||
bluesky?: string;
|
||||
}
|
||||
|
||||
export default function TeamPage() {
|
||||
const team: Array<TeamMember> = [
|
||||
{
|
||||
name: "Anselm Eickhoff",
|
||||
titles: ["Founder"],
|
||||
image: "anselm.jpg",
|
||||
location: "Canterbury, UK ",
|
||||
x: "anselm_io",
|
||||
github: "aeplay",
|
||||
website: "http://anselm.io",
|
||||
bluesky: "anselm.io",
|
||||
},
|
||||
{
|
||||
name: "Andrei Popa",
|
||||
titles: ["Full-Stack Dev", "Infra"],
|
||||
image: "andrei.jpeg",
|
||||
location: "Bucharest, Romania ",
|
||||
x: "elitepax",
|
||||
github: "pax-k",
|
||||
},
|
||||
{
|
||||
name: "Guido D'Orsi",
|
||||
titles: ["Frontend Dev", "React Performance"],
|
||||
image: "guido.jpeg",
|
||||
location: "Piano di Sorrento, Italy ",
|
||||
github: "gdorsi",
|
||||
},
|
||||
{
|
||||
name: "Trisha Lim",
|
||||
titles: ["Frontend Dev", "Design", "Marketing"],
|
||||
image: "trisha.png",
|
||||
location: "Lisbon, Portugal ",
|
||||
github: "trishalim",
|
||||
website: "https://trishalim.com",
|
||||
},
|
||||
{
|
||||
name: "Benjamin Leveritt",
|
||||
titles: ["Full-Stack Dev"],
|
||||
image: "benjamin.jpg",
|
||||
location: "Portsmouth, UK ",
|
||||
github: "bensleveritt",
|
||||
},
|
||||
{
|
||||
name: "Marina Orlova",
|
||||
titles: ["Full-Stack Dev"],
|
||||
location: "Tarragona, Spain ",
|
||||
gitlab: "marinaorlova",
|
||||
linkedin: "marina-orlova-52a34394",
|
||||
// github: "marinoska",
|
||||
image: "marina.jpeg",
|
||||
},
|
||||
];
|
||||
|
||||
function SocialLink({
|
||||
link,
|
||||
label,
|
||||
icon: Icon,
|
||||
}: {
|
||||
label: string;
|
||||
link: string;
|
||||
icon: LucideIcon;
|
||||
}) {
|
||||
return (
|
||||
<div className="container">
|
||||
<HeroHeader title="Team" slogan="" />
|
||||
<Link href={link} className="p-1 -m-1">
|
||||
<Icon size={16} />
|
||||
<span className="sr-only">{label}</span>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
<div className="grid gap-1">
|
||||
<H2>Anselm Eickhoff (Founder)</H2>
|
||||
<Link
|
||||
href="mailto:anselm@gcmp.io"
|
||||
className="flex gap-1 items-center"
|
||||
aria-label="Email address"
|
||||
>
|
||||
<MailIcon height="1em" /> anselm@gcmp.io
|
||||
</Link>
|
||||
<Link
|
||||
href="https://x.com/anselm_io"
|
||||
className="flex gap-1 items-center"
|
||||
aria-label="Twitter link"
|
||||
>
|
||||
<SiTwitter height="1em" /> anselm_io
|
||||
</Link>
|
||||
<Link
|
||||
href="https://github.com/aeplay"
|
||||
className="flex gap-1 items-center"
|
||||
aria-label="GitHub link"
|
||||
>
|
||||
<SiGithub height="1em" /> aeplay
|
||||
</Link>
|
||||
function Person({ person }: { person: TeamMember }) {
|
||||
const imageClassName = "size-24 shadow rounded-md bg-stone-100 sm:size-28 ";
|
||||
return (
|
||||
<div className="flex items-center gap-5">
|
||||
{person.image ? (
|
||||
<img src={`/team/${person.image}`} className={imageClassName} />
|
||||
) : (
|
||||
<span className={imageClassName}>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="w-full pt-5 h-full text-stone-300 dark:text-stone-700"
|
||||
width="1em"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M4 22a8 8 0 1 1 16 0zm8-9c-3.315 0-6-2.685-6-6s2.685-6 6-6s6 2.685 6 6s-2.685 6-6 6"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
)}
|
||||
<div className="flex flex-col gap-2.5">
|
||||
<h3 className="text-lg leading-none font-semibold tracking-tight text-stone-900 dark:text-white">
|
||||
{person.name}
|
||||
</h3>
|
||||
<p className="text-sm leading-none text-gray-600 dark:text-stone-400">
|
||||
{person.titles.join(", ")}
|
||||
</p>
|
||||
<p className="text-sm leading-none text-gray-600 dark:text-stone-400">
|
||||
{person.location}
|
||||
</p>
|
||||
|
||||
<div className="flex gap-2 mt-0.5">
|
||||
{person.website && (
|
||||
<SocialLink
|
||||
link={person.website}
|
||||
icon={GlobeIcon}
|
||||
label="Website"
|
||||
/>
|
||||
)}
|
||||
{person.x && (
|
||||
<SocialLink
|
||||
link={`https://x.com/${person.x}`}
|
||||
icon={SiX}
|
||||
label="X profile"
|
||||
/>
|
||||
)}
|
||||
{person.bluesky && (
|
||||
<SocialLink
|
||||
link={`https://bsky.app/profile/${person.bluesky}`}
|
||||
icon={SiBluesky}
|
||||
label="Bluesky profile"
|
||||
/>
|
||||
)}
|
||||
{person.gitlab && (
|
||||
<SocialLink
|
||||
link={`https://gitlab.com/${person.gitlab}`}
|
||||
icon={SiGitlab}
|
||||
label="Gitlab profile"
|
||||
/>
|
||||
)}
|
||||
{person.linkedin && (
|
||||
<SocialLink
|
||||
link={`https://www.linkedin.com/in/${person.linkedin}`}
|
||||
icon={SiLinkedin}
|
||||
label="Linkedin profile"
|
||||
/>
|
||||
)}
|
||||
{person.github && (
|
||||
<SocialLink
|
||||
link={`https://github.com/${person.github}`}
|
||||
label="Github profile"
|
||||
icon={SiGithub}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function TeamPage() {
|
||||
return (
|
||||
<div className="container">
|
||||
<HeroHeader title="Meet the team" slogan="" />
|
||||
|
||||
<div className="grid gap-5 sm:grid-cols-2 lg:grid-cols-3 lg:gap-10">
|
||||
{team.map((person) => (
|
||||
<Person key={person.name} person={person} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import { ThemeProvider as NextThemesProvider } from "next-themes";
|
||||
import { type ThemeProviderProps } from "next-themes/dist/types";
|
||||
|
||||
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
|
||||
return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as TestPost from "@/components/blog/posts/test.mdx";
|
||||
|
||||
export const posts: typeof TestPost[] = [];
|
||||
export const posts: (typeof TestPost)[] = [];
|
||||
|
||||
export const getPostBySlug = (slug: string) => {
|
||||
return posts.find((post) => post.meta.slug === slug);
|
||||
|
||||
@@ -30,7 +30,6 @@
|
||||
"mdast-util-mdx": "^3.0.0",
|
||||
"micromark-extension-mdxjs": "^3.0.0",
|
||||
"next": "14.2.15",
|
||||
"next-themes": "^0.2.1",
|
||||
"react": "^18",
|
||||
"react-dom": "^18",
|
||||
"resend": "^4.0.0",
|
||||
|
||||
BIN
homepage/gcmp/public/team/andrei.jpeg
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
homepage/gcmp/public/team/anselm.jpg
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
homepage/gcmp/public/team/benjamin.jpg
Normal file
|
After Width: | Height: | Size: 42 KiB |
BIN
homepage/gcmp/public/team/guido.jpeg
Normal file
|
After Width: | Height: | Size: 9.7 KiB |
BIN
homepage/gcmp/public/team/marina.jpeg
Normal file
|
After Width: | Height: | Size: 35 KiB |
BIN
homepage/gcmp/public/team/trisha.png
Normal file
|
After Width: | Height: | Size: 186 KiB |