39 Commits

Author SHA1 Message Date
Spedon
d808782afd docs: update documentation (#303) 2024-03-17 16:23:43 +08:00
Sped0n
5327d7c585 ci: update bundled artifacts [skip ci] 2024-03-17 08:15:57 +00:00
Spedon
d7e7fc68ba refactor: improve top position calculation in stylesheets (#302)
* refactor: improve top position calculation in stylesheets

- Update the top position calculation in the `_collection.scss` file

* refactor: tweak it harder

* Revert "refactor: tweak it harder"

This reverts commit b7bf7e50f8.
2024-03-17 16:15:13 +08:00
dependabot[bot]
7b637637a1 build(deps-dev): bump vite-plugin-solid from 2.10.0 to 2.10.2 (#300)
Bumps [vite-plugin-solid](https://github.com/solidjs/vite-plugin-solid) from 2.10.0 to 2.10.2.
- [Release notes](https://github.com/solidjs/vite-plugin-solid/releases)
- [Changelog](https://github.com/solidjs/vite-plugin-solid/blob/main/CHANGELOG.md)
- [Commits](https://github.com/solidjs/vite-plugin-solid/commits)

---
updated-dependencies:
- dependency-name: vite-plugin-solid
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-16 21:38:24 +08:00
Sped0n
4a2ef258ba ci: update bundled artifacts [skip ci] 2024-03-16 13:38:04 +00:00
dependabot[bot]
b2c1c96f56 build(deps): bump solid-js from 1.8.14 to 1.8.15 (#290)
Bumps [solid-js](https://github.com/solidjs/solid) from 1.8.14 to 1.8.15.
- [Release notes](https://github.com/solidjs/solid/releases)
- [Changelog](https://github.com/solidjs/solid/blob/main/CHANGELOG.md)
- [Commits](https://github.com/solidjs/solid/commits)

---
updated-dependencies:
- dependency-name: solid-js
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-16 21:37:22 +08:00
Sped0n
6d98671856 ci: update bundled artifacts [skip ci] 2024-03-16 13:36:35 +00:00
dependabot[bot]
0b7605b3ff build(deps): bump swiper from 11.0.6 to 11.0.7 (#294)
Bumps [swiper](https://github.com/nolimits4web/Swiper) from 11.0.6 to 11.0.7.
- [Release notes](https://github.com/nolimits4web/Swiper/releases)
- [Changelog](https://github.com/nolimits4web/swiper/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nolimits4web/Swiper/compare/v11.0.6...v11.0.7)

---
updated-dependencies:
- dependency-name: swiper
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-16 21:35:58 +08:00
dependabot[bot]
c9ca9c682a build(deps-dev): bump vite from 5.1.2 to 5.1.6 (#296)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 5.1.2 to 5.1.6.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v5.1.6/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-16 21:35:27 +08:00
Sped0n
417a00b9c4 ci: update bundled artifacts [skip ci] 2024-03-16 13:33:45 +00:00
dependabot[bot]
d7a4345f53 build(deps): bump tiny-invariant from 1.3.1 to 1.3.3 (#287)
Bumps [tiny-invariant](https://github.com/alexreardon/tiny-invariant) from 1.3.1 to 1.3.3.
- [Release notes](https://github.com/alexreardon/tiny-invariant/releases)
- [Commits](https://github.com/alexreardon/tiny-invariant/compare/v1.3.1...v1.3.3)

---
updated-dependencies:
- dependency-name: tiny-invariant
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-16 21:33:10 +08:00
dependabot[bot]
2e47be5635 build(deps-dev): bump eslint from 8.56.0 to 8.57.0 (#291)
Bumps [eslint](https://github.com/eslint/eslint) from 8.56.0 to 8.57.0.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md)
- [Commits](https://github.com/eslint/eslint/compare/v8.56.0...v8.57.0)

---
updated-dependencies:
- dependency-name: eslint
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-16 21:32:46 +08:00
dependabot[bot]
0e6fc9384f build(deps-dev): bump terser from 5.27.0 to 5.29.2 (#301)
Bumps [terser](https://github.com/terser/terser) from 5.27.0 to 5.29.2.
- [Changelog](https://github.com/terser/terser/blob/master/CHANGELOG.md)
- [Commits](https://github.com/terser/terser/compare/v5.27.0...v5.29.2)

---
updated-dependencies:
- dependency-name: terser
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-16 21:32:17 +08:00
dependabot[bot]
81eba485d4 build(deps-dev): bump typescript from 5.3.3 to 5.4.2 (#297)
Bumps [typescript](https://github.com/Microsoft/TypeScript) from 5.3.3 to 5.4.2.
- [Release notes](https://github.com/Microsoft/TypeScript/releases)
- [Changelog](https://github.com/microsoft/TypeScript/blob/main/azure-pipelines.release.yml)
- [Commits](https://github.com/Microsoft/TypeScript/compare/v5.3.3...v5.4.2)

---
updated-dependencies:
- dependency-name: typescript
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-16 21:31:01 +08:00
Spedon
b1553a9a8b fix: fix hugo module version bug (#299)
* docs: Update getStarted.md

* fix: update module version in example site config

* chore: Update index.md

* revert
2024-03-16 21:30:40 +08:00
Spedon
66735f4c65 fix: Update go.mod (#298) 2024-03-16 21:10:25 +08:00
Spedon
3ff49b1106 docs: update doc for version 2 (#286)
* feat: update issue template

* docs: update doc for v2.0.0
2024-02-23 19:12:15 +08:00
Spedon
633f6a40e2 ci: update actions to resolve github's complain about deprecated Node.js v16 env (#285)
* ci: update the action to utilize Node.js version 20

* ci: update key for Hugo action to include 'resources' folder
2024-02-23 16:13:46 +08:00
Sped0n
1ff94546e0 ci: update bundled artifacts [skip ci] 2024-02-23 08:00:15 +00:00
Spedon
e081e139fc refactor: reduce amount of createEffect and improve imports (#284)
* refactor: update import syntax

* feat: add GalleryImage component for simplicity

* refactor: replace createEffect in GalleryNav with createMemo

* refactor: refactor Gallery component logic and improve imports
2024-02-23 15:59:32 +08:00
Spedon
875113448b refactor: migrate part of the sass compilation to vite (#283)
* refactor: migrate part of the sass compilation to vite

now `bundled` option is deprecated

* fix: update build script

* chore: add a “type” field to the package.json file to resolve Vite’s complaints about CommonJS modules.
2024-02-22 23:44:16 +08:00
Sped0n
53a44776de ci: update bundled artifacts [skip ci] 2024-02-21 17:49:42 +00:00
Sped0n
ecdaebb6cd refactor: don't render unneeded elements on raw info page 2024-02-22 01:48:48 +08:00
Sped0n
1a02360214 ci: manually update bundled js 2024-02-22 01:32:46 +08:00
Spedon
febbd7d45d feat: migrate to Solid.js (#282)
* refactor: change hires loader function name

* feat: add loading transition animation and improve performance

* refactor: refactor gallery creation and update functions

* feat: update dependencies, configuration, and input file for solidjs

- Update dependencies in package.json
- Modify the input file in rollup.config.mjs
- Update tsconfig.json with new configuration options

* feat: update ESLint config for TypeScript and Solid integration

- Add `plugin:solid/typescript` to the ESLint config
- Add `prettier`, `@typescript-eslint`, and `solid` plugins to the ESLint config
- Remove the `overrides` and `plugins` properties from the ESLint config
- Modify the `memberSyntaxSortOrder` property in the ESLint config

* feat: update build scripts and configuration for Vite

* GitButler Integration Commit

This is an integration commit for the virtual branches that GitButler is tracking.

Due to GitButler managing multiple virtual branches, you cannot switch back and
forth between git branches and virtual branches easily. 

If you switch to another branch, GitButler will need to be reinitialized.
If you commit on this branch, GitButler will throw it away.

Here are the branches that are currently applied:
 - solid (refs/gitbutler/solid)
   branch head: dc6860991c
   - .eslintrc.json
   - assets/ts/main.tsx
   - assets/ts/desktop/stage.tsx
   - static/bundled/js/main.js
   - rollup.config.mjs
   - pnpm-lock.yaml
   - vite.config.ts
   - package.json
   - tsconfig.json
   - assets/ts/globalState.ts
   - assets/ts/mobile/collection.ts
   - assets/ts/container.ts
   - assets/ts/mobile/init.ts
   - assets/ts/mobile/gallery.ts
   - assets/ts/desktop/customCursor.ts
   - assets/ts/mobile/state.ts
   - assets/ts/globalUtils.ts
   - static/bundled/js/zXhbFx.js
   - static/bundled/js/EY5BO_.js
   - static/bundled/js/bBHMTk.js
   - assets/ts/nav.tsx
   - assets/ts/utils.ts
   - assets/ts/desktop/stageNav.ts
   - assets/ts/mobile/utils.ts
   - assets/ts/main.ts
   - assets/ts/desktop/state.ts
   - assets/ts/desktop/init.ts
   - assets/ts/state.tsx
   - assets/ts/desktop/utils.ts
   - assets/ts/nav.ts
   - assets/ts/resources.ts
   - assets/ts/desktop/stage.ts
   - static/bundled/js/GAHquF.js

Your previous branch was: refs/heads/solid

The sha for that commit was: dc6860991c

For more information about what we're doing here, check out our docs:
https://docs.gitbutler.com/features/virtual-branches/integration-branch

* refactor: remove .hide class from _base.scss file

* feat: migrate to Solid.js

* refactor: change i18n loading text with trailing dots

* fix: fix broken pnpm lock file

* chore: update eslint configuration for better code organization

- Update the eslint plugins array by removing newlines and maintaining plugins order
- Disable the rule "@typescript-eslint/non-nullable-type-assertion-style"
- Change the configuration of "sort-imports" rule

* feat: add tiny-invariant and eslint-plugin-solid to deps

* refactor: fix multiple eslint warnings

---------

Co-authored-by: GitButler <gitbutler@gitbutler.com>
2024-02-22 01:18:29 +08:00
Sped0n
1acf24a519 ci: update bundled artifacts [skip ci] 2024-02-20 20:17:00 +00:00
dependabot[bot]
e2afe91131 build(deps): bump rollup from 4.9.6 to 4.12.0 (#281)
Bumps [rollup](https://github.com/rollup/rollup) from 4.9.6 to 4.12.0.
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v4.9.6...v4.12.0)

---
updated-dependencies:
- dependency-name: rollup
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-21 04:15:25 +08:00
Sped0n
3e51b96825 ci: update bundled artifacts [skip ci] 2024-02-11 06:23:22 +00:00
Spedon
c84b4cf234 refactor: better event listener cleanup (#279)
* refactor: change hires loader function name

* feat: add loading transition animation and improve performance

* refactor: refactor gallery creation and update functions

* feat: create createDivWithClass utility function

* feat: refactor abort signal handling in event listener and promise chain

- Add functionality to set up an abort controller for cleanup
- Add an event listener to abort the controller when necessary
- Modify event listener to include the abort signal
- Modify promise chain to include the abort signal
2024-02-11 14:22:48 +08:00
Spedon
997207fa90 feat: remove source image in public to prevent leakage (#278)
* refactor: change hires loader function name

* feat: add loading transition animation and improve performance

* feat: hide image source in exampleSite

* docs: update docs for publishResources
2024-02-06 23:40:46 +08:00
Sped0n
f7d2c7962c ci: update bundled artifacts [skip ci] 2024-02-06 15:13:23 +00:00
Spedon
0812a5a6b8 feat: loading transition (#277)
* refactor: change hires loader function name

* feat: add loading transition animation and improve performance

* refactor: refactor mutation handling in desktop codebase

- Modify the `initStage` function in `assets/ts/desktop/stage.ts`:
  - Change the `onMutation` callback to accept a single mutation instead of an array of mutations.
  - Update the conditions inside the callback to use `hold` instead of `skip`.
- Modify the `onMutation` function in `assets/ts/desktop/utils.ts`:
  - Change the `callback` parameter to `trigger`.
  - Update the implementation of the function to iterate over each mutation and check if it triggers the `trigger` function. If it does, disconnect the observer and break the loop.

* style: refactor state section and remove unnecessary code

- Remove the declaration of `lastIndex` on line 21
- Add a comment block for the state section
- Add a declaration of `lastIndex` for the state section

* refactor: refactor mobile collection and intersection functions

- Modify the `initCollection` function in `assets/ts/mobile/collection.ts`
- Remove the nested loop in the `initCollection` function
- Modify the `onIntersection` function in `assets/ts/mobile/utils.ts`
- Replace the callback parameter with a trigger parameter in the `onIntersection` function
- Remove the nested loop in the `onIntersection` function

* refactor: refactor Watchable class constructor to include lazy parameter

- Add a second parameter `lazy` in the constructor of the `Watchable` class in `globalUtils.ts`
- Set the default value of `lazy` to `true` in the constructor
- Add a condition to check if `e` is equal to `this.obj` and `this.lazy` is `true` to return in `watch`
- Delete the previous constructor definition in the `Watchable` class in `globalUtils.ts`

* fix: set state's lazy param to false

* refactor: refactor third party lib import
2024-02-06 23:12:44 +08:00
dependabot[bot]
7fd971eb13 build(deps-dev): bump @typescript-eslint/eslint-plugin (#275)
Bumps [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) from 6.20.0 to 6.21.0.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v6.21.0/packages/eslint-plugin)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/eslint-plugin"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-06 22:18:13 +08:00
Sped0n
881b0b6490 ci: update commit message for bundled artifacts 2024-02-06 22:17:41 +08:00
Sped0n
50d7b14133 ci: update bundled artifacts 2024-02-06 14:13:56 +00:00
dependabot[bot]
872d23ad13 build(deps-dev): bump @typescript-eslint/parser from 6.20.0 to 6.21.0 (#274)
Bumps [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) from 6.20.0 to 6.21.0.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/parser/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v6.21.0/packages/parser)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/parser"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-06 22:13:14 +08:00
dependabot[bot]
eeca83a74b build(deps-dev): bump prettier from 3.2.4 to 3.2.5 (#273)
Bumps [prettier](https://github.com/prettier/prettier) from 3.2.4 to 3.2.5.
- [Release notes](https://github.com/prettier/prettier/releases)
- [Changelog](https://github.com/prettier/prettier/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prettier/prettier/compare/3.2.4...3.2.5)

---
updated-dependencies:
- dependency-name: prettier
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-06 22:12:58 +08:00
dependabot[bot]
3170f5b65a build(deps): bump swiper from 11.0.5 to 11.0.6 (#272)
Bumps [swiper](https://github.com/nolimits4web/Swiper) from 11.0.5 to 11.0.6.
- [Release notes](https://github.com/nolimits4web/Swiper/releases)
- [Changelog](https://github.com/nolimits4web/swiper/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nolimits4web/Swiper/compare/v11.0.5...v11.0.6)

---
updated-dependencies:
- dependency-name: swiper
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-06 22:12:31 +08:00
Spedon
bf1c5e21bc ci: prefine github action (#276)
* ci: migrate bundled artifacts update from pull request to direct commit

* ci: update lint workflow to include code formatting and auto-commit

* chore: update .prettierignore to include yaml files

- Remove the exclusion of `*.yaml` and `*.yml` files from being formatted by Prettier in the `.prettierignore` file.

* ci: update lint workflow conditions

* ci: format code

---------

Co-authored-by: Sped0n <Sped0n@users.noreply.github.com>
2024-02-06 22:03:50 +08:00
77 changed files with 2946 additions and 1919 deletions

View File

@@ -8,10 +8,11 @@
"prettier",
"eslint:recommended",
"plugin:prettier/recommended",
"plugin:@typescript-eslint/recommended"
"plugin:@typescript-eslint/recommended",
"plugin:solid/typescript"
],
"overrides": [],
"plugins": ["prettier", "@typescript-eslint"],
"parser": "@typescript-eslint/parser",
"plugins": ["prettier", "@typescript-eslint", "solid"],
"parserOptions": {
"ecmaVersion": "latest",
"project": "./tsconfig.json",
@@ -22,13 +23,12 @@
"arrow-body-style": "off",
"prefer-arrow-callback": "off",
"import/no-cycle": "error",
"@typescript-eslint/non-nullable-type-assertion-style": "off",
"sort-imports": [
"error",
{
"ignoreCase": false,
"ignoreDeclarationSort": true,
"ignoreMemberSort": false,
"ignoreMemberSort": true,
"memberSyntaxSortOrder": ["none", "all", "multiple", "single"],
"allowSeparatedGroups": true
}
@@ -37,7 +37,15 @@
"import/order": [
"error",
{
"groups": ["builtin", "external", "internal", "parent", "sibling", "index"],
"groups": [
"builtin",
"external",
"internal",
"parent",
"sibling",
"index",
"unknown"
],
"newlines-between": "always",
"alphabetize": {
"order": "asc",

View File

@@ -1,33 +0,0 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Environment (please complete the following information):**
- OS: [e.g. macOS]
- Browser [e.g. chrome, safari]
- Hugo Version [e.g. v0.114.0 extended]
**Additional context**
Add any other context about the problem here.

115
.github/ISSUE_TEMPLATE/bug_report.yaml vendored Normal file
View File

@@ -0,0 +1,115 @@
name: Bug report
description: Create a bug report
labels:
- 'T: Bug'
- 'S: Untriaged'
body:
- type: markdown
attributes:
value: |
Please finish verify steps which list in the end first before create bug report
- type: textarea
id: reproduce
attributes:
label: Step to reproduce
description: |
Please write down the reproduction steps here and include the error log. If necessary, please provide screenshots or recordings.
placeholder: |
1.
2.
3.
[Screen recording]
validations:
required: true
- type: textarea
id: expected
attributes:
label: Expected behaviour
description: |
Describe what should happened here
placeholder: |
It should be ...
validations:
required: true
- type: textarea
id: actual
attributes:
label: Actual Behaviour
description: |
Describe what actually happened here, screenshots is better
placeholder: |
Actually it ...
[Screenshots]
validations:
required: true
- type: textarea
id: env
attributes:
label: Logs
description: |
CLI output or browser log.
placeholder: |
pnpm run server
> bridget@v1.0.0 server /Users/spedon/eden/hugo/bridget
> run-p vite:server hugo:server
validations:
required: false
- type: textarea
id: more
attributes:
label: Addition details
description: |
Describe addition details here
placeholder: |
Additional details and attachments
validations:
required: false
- type: input
id: hugo-version
attributes:
label: Hugo version
description: You can get version output with hugo --version
placeholder: v0.114.0
validations:
required: true
- type: input
id: bridget-version
attributes:
label: Bridget version
description: Release version or commit SHA
placeholder: v1.0.1 or 633f6a40e202a023471c58c09f05a92ec2130c93
validations:
required: true
- type: input
id: system
attributes:
label: OS version
description: OS + version code
placeholder: Windows 11, macOS 14
validations:
required: true
- type: checkboxes
id: check
attributes:
label: Verify steps
description: |
Please ensure you have obtained all needed options
options:
- label: Pull request is welcome. Check this if you want to start a pull request
required: false
- label: I have searched on [Issue Tracker](https://github.com/Sped0n/bridget/issues), No duplicate or related open issue has been found
required: true
- label: Ensure there is only one bug report in this issue. Please make mutiply issue for mutiply bugs
required: true

5
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@@ -0,0 +1,5 @@
blank_issues_enabled: false
contact_links:
- name: All other questions
url: https://github.com/Sped0n/bridget/discussions
about: Turn to discussions

View File

@@ -1,19 +0,0 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

View File

@@ -0,0 +1,60 @@
name: Feature request
description: Suggest an idea
labels:
- 'T: Feature'
- 'S: Untriaged'
body:
- type: markdown
attributes:
value: |
Please finish verify steps which list in the end first before suggest an idea
- type: textarea
id: request
attributes:
label: Requirement
description: |
Ddescribe what you need here.
placeholder: |
I want ABC feature ...
validations:
required: true
- type: textarea
id: impl
attributes:
label: Suggested implements
description: |
Describe your suggested implements here, It's recommend to add a photo if you are making a UI feature request.
placeholder: |
I recommend add ABC feature to DEF...
Photos (if exists)
validations:
required: true
- type: textarea
id: more
attributes:
label: Addition details
description: |
Describe addition details here
placeholder: |
Additional details and attachments
validations:
required: false
- type: checkboxes
id: check
attributes:
label: Verify steps
description: |
Please ensure you have obtained all needed options
options:
- label: Pull request is welcome. Check this if you want to start a pull request
required: false
- label: I have searched on [Issue Tracker](https://github.com/Sped0n/bridget/issues), No duplicate or related open issue has been found
required: true
- label: Ensure there is only one feature request in this issue. Please make mutiply issue for mutiply feature request
required: true

View File

@@ -18,7 +18,7 @@ jobs:
outputs:
any_changed: ${{ steps.changed-files-specific.outputs.any_changed }}
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
with:
fetch-depth: 0
@@ -45,16 +45,18 @@ jobs:
id: version
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
token: ${{ secrets.PAT }}
- name: Setup Hugo
uses: peaceiris/actions-hugo@v2
uses: peaceiris/actions-hugo@v2.6.0
with:
hugo-version: '0.114.0'
extended: true
- name: Setup pnpm
uses: pnpm/action-setup@v2
uses: pnpm/action-setup@v3
with:
version: 8
@@ -63,17 +65,17 @@ jobs:
run: echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
- name: Setup pnpm cache
uses: actions/cache@v3
uses: actions/cache@v4
with:
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: ${{ runner.os }}-pnpm-store-
- name: Setup hugo cache
uses: actions/cache@v3
uses: actions/cache@v4
with:
path: ./exampleSite/resources
key: ${{ runner.os }}-hugo-${{ hashFiles('./exampleSite') }}
key: ${{ runner.os }}-hugo-${{ hashFiles('./exampleSite/resources') }}
restore-keys: ${{ runner.os }}-hugo-
- name: Install dependencies
@@ -84,10 +86,6 @@ jobs:
- name: Push artifacts
if: ${{ (github.event_name == 'push' || github.event.pull_request.merged == true) && needs.filter.outputs.any_changed == 'true' }}
uses: peter-evans/create-pull-request@v5
uses: stefanzweifel/git-auto-commit-action@v5
with:
token: ${{ secrets.PAT }}
title: "ci: update bundled artifacts"
commit-message: "ci: update bundled artifacts"
branch: update-artifacts-${{ steps.version.outputs.builddate }}
base: main
commit_message: 'ci: update bundled artifacts [skip ci]'

View File

@@ -17,10 +17,12 @@ jobs:
name: Lint
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
token: ${{ secrets.PAT }}
- name: Setup pnpm
uses: pnpm/action-setup@v2
uses: pnpm/action-setup@v3
with:
version: 8
@@ -29,7 +31,7 @@ jobs:
run: echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
- name: Setup pnpm cache
uses: actions/cache@v3
uses: actions/cache@v4
with:
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
@@ -38,5 +40,18 @@ jobs:
- name: Install dependencies
run: pnpm install
- name: Lint
- name: Lint Check
continue-on-error: true
id: check
run: pnpm run lint:check
- name: Format manually
id: format
if: ${{ steps.check.outcome == 'failure' }}
run: pnpm run lint
- name: Commit
if: ${{ steps.format.outcome == 'success' }}
uses: stefanzweifel/git-auto-commit-action@v5
with:
commit_message: 'ci: format code'

View File

@@ -1,6 +1,5 @@
node_modules
static
exmapleSite
*.yaml
*.yml
single.json
pnpm-lock.yaml

View File

@@ -2,9 +2,9 @@
![GitHub Workflow Status (with event)](https://img.shields.io/github/actions/workflow/status/Sped0n/bridget/build.yml?logo=github) ![GitHub deployments](https://img.shields.io/github/deployments/Sped0n/bridget/Production?logo=vercel&label=deploy)
Bridget is a minimal [Hugo](https://gohugo.io) theme designed for photographers / visual artists.
Bridget is a minimal [Hugo](https://gohugo.io) theme for photographers/visual artists, powered by [Solid.js](https://www.solidjs.com).
Its based on the https://github.com/tylermcrobert/bridget-pictures-www.
Based on the https://github.com/tylermcrobert/bridget-pictures-www.
![thumbnail](images/tn.jpg)
@@ -18,10 +18,9 @@ Head to this [documentation](https://github.com/Sped0n/bridget/blob/main/doc/get
## Features
- **Blazingly fast**: 99/100 on mobile and 100/100 on desktop in [Google PageSpeed Insights](https://developers.google.com/speed/pagespeed/insights)
- JS **dynamic loading** (powered by ES6 syntax)
- JS **code splitting** by [rollup.js](https://rollupjs.org)
- **Blazingly fast**: 100/100 on both desktop and mobile in [Google PageSpeed Insights](https://developers.google.com/speed/pagespeed/insights)
- Powered by **[Solid.js](https://www.solidjs.com)**, a declarative, efficient, and flexible JavaScript library for building user interfaces
- JS **dynamic loading** (powered by ESM)
- Image **Preloading**/**Lazy loading**
- **Dynamic resolution** based on view mode
- Multiple **analytics** services supported
@@ -46,3 +45,4 @@ Bridget supports the following languages:
- https://github.com/tylermcrobert/bridget-pictures-www
- https://www.youtube.com/watch?v=Jt3A2lNN2aE
- https://github.com/d4cho/bridget-pictures-clone
- https://www.solidjs.com/tutorial

View File

@@ -17,7 +17,3 @@ a,
button {
cursor: pointer;
}
.hide {
display: none;
}

View File

@@ -3,12 +3,12 @@
flex-direction: column;
gap: 20vh;
padding-top: 50vh;
padding-top: calc(var(--window-height) * 0.4);
margin-top: calc(var(--nav-height) * -1);
img {
position: sticky;
top: 50vh;
top: calc(var(--window-height) * 0.4);
width: 60vw;
height: 20vh;
@@ -19,7 +19,7 @@
align-self: center;
&:last-child {
margin-bottom: 20vh;
margin-bottom: calc(var(--window-height) * 0.35);
}
}
}

View File

@@ -1,29 +0,0 @@
import { Watchable } from './globalUtils'
export const scrollable = new Watchable<boolean>(true)
export let container: Container
/**
* interfaces
*/
export interface Container extends HTMLDivElement {
dataset: {
next: string
close: string
prev: string
loading: string
}
}
export function initContainer(): void {
container = document.getElementsByClassName('container').item(0) as Container
scrollable.addWatcher((o) => {
if (o) {
container.classList.remove('disableScroll')
} else {
container.classList.add('disableScroll')
}
})
}

View File

@@ -1,48 +0,0 @@
import { container } from '../container'
import { active } from './state'
/**
* variables
*/
const cursor = document.createElement('div')
const cursorInner = document.createElement('div')
/**
* main functions
*/
function onMouse(e: MouseEvent): void {
const x = e.clientX
const y = e.clientY
cursor.style.transform = `translate3d(${x}px, ${y}px, 0)`
}
export function setCustomCursor(text: string): void {
cursorInner.innerText = text
}
/**
* init
*/
export function initCustomCursor(): void {
// cursor class name
cursor.className = 'cursor'
// cursor inner class name
cursorInner.className = 'cursorInner'
// append cursor inner to cursor
cursor.append(cursorInner)
// append cursor to main
container.append(cursor)
// bind mousemove event to window
window.addEventListener('mousemove', onMouse, { passive: true })
// add active callback
active.addWatcher((o) => {
if (o) {
cursor.classList.add('active')
} else {
cursor.classList.remove('active')
}
})
}

View File

@@ -0,0 +1,52 @@
import { createSignal, onCleanup, onMount, type Accessor, type JSX } from 'solid-js'
export default function CustomCursor(props: {
children?: JSX.Element
active: Accessor<boolean>
cursorText: Accessor<string>
isOpen: Accessor<boolean>
}): JSX.Element {
// types
interface XY {
x: number
y: number
}
// variables
let controller: AbortController | undefined
// states
const [xy, setXy] = createSignal<XY>({ x: 0, y: 0 })
// helper functions
const onMouse: (e: MouseEvent) => void = (e) => {
const { clientX, clientY } = e
setXy({ x: clientX, y: clientY })
}
// effects
onMount(() => {
controller = new AbortController()
const abortSignal = controller.signal
window.addEventListener('mousemove', onMouse, {
passive: true,
signal: abortSignal
})
})
onCleanup(() => {
controller?.abort()
})
return (
<>
<div
class="cursor"
classList={{ active: props.active() }}
style={{ transform: `translate3d(${xy().x}px, ${xy().y}px, 0)` }}
>
<div class="cursorInner">{props.cursorText()}</div>
</div>
</>
)
}

View File

@@ -1,15 +0,0 @@
import { type ImageJSON } from '../resources'
import { initCustomCursor } from './customCursor'
import { initStage } from './stage'
import { initStageNav } from './stageNav'
/**
* main functions
*/
export function initDesktop(ijs: ImageJSON[]): void {
initCustomCursor()
initStage(ijs)
initStageNav()
}

View File

@@ -0,0 +1,91 @@
// eslint-disable-next-line sort-imports
import { Show, createMemo, createSignal, type JSX } from 'solid-js'
import type { ImageJSON } from '../resources'
import type { Vector } from '../utils'
import CustomCursor from './customCursor'
import Nav from './nav'
import Stage from './stage'
import StageNav from './stageNav'
/**
* interfaces and types
*/
export interface DesktopImage extends HTMLImageElement {
dataset: {
hiUrl: string
hiImgH: string
hiImgW: string
loUrl: string
loImgH: string
loImgW: string
}
}
export interface HistoryItem {
i: number
x: number
y: number
}
/**
* components
*/
export default function Desktop(props: {
children?: JSX.Element
ijs: ImageJSON[]
prevText: string
closeText: string
nextText: string
loadingText: string
}): JSX.Element {
const [cordHist, setCordHist] = createSignal<HistoryItem[]>([])
const [isLoading, setIsLoading] = createSignal(false)
const [isOpen, setIsOpen] = createSignal(false)
const [isAnimating, setIsAnimating] = createSignal(false)
const [hoverText, setHoverText] = createSignal('')
const [navVector, setNavVector] = createSignal<Vector>('none')
const active = createMemo(() => isOpen() && !isAnimating())
const cursorText = createMemo(() => (isLoading() ? props.loadingText : hoverText()))
return (
<>
<Nav />
<Show when={props.ijs.length > 0}>
<Stage
ijs={props.ijs}
setIsLoading={setIsLoading}
isOpen={isOpen}
setIsOpen={setIsOpen}
isAnimating={isAnimating}
setIsAnimating={setIsAnimating}
cordHist={cordHist}
setCordHist={setCordHist}
navVector={navVector}
setNavVector={setNavVector}
/>
<Show when={isOpen()}>
<CustomCursor cursorText={cursorText} active={active} isOpen={isOpen} />
<StageNav
prevText={props.prevText}
closeText={props.closeText}
nextText={props.nextText}
loadingText={props.loadingText}
active={active}
isAnimating={isAnimating}
setCordHist={setCordHist}
isOpen={isOpen}
setIsOpen={setIsOpen}
setHoverText={setHoverText}
navVector={navVector}
setNavVector={setNavVector}
/>
</Show>
</Show>
</>
)
}

View File

@@ -1,20 +1,18 @@
import { decThreshold, incThreshold, state } from './globalState'
import { expand } from './globalUtils'
import { createEffect } from 'solid-js'
import { useState } from '../state'
import { expand } from '../utils'
/**
* variables
* constants
*/
// threshold div
const thresholdDiv = document
.getElementsByClassName('threshold')
.item(0) as HTMLDivElement
const thresholdDiv = document.getElementsByClassName('threshold')[0] as HTMLDivElement
// threshold nums span
const thresholdDispNums = Array.from(
thresholdDiv.getElementsByClassName('num')
) as HTMLSpanElement[]
// threshold buttons
const decButton = thresholdDiv
.getElementsByClassName('dec')
@@ -22,52 +20,24 @@ const decButton = thresholdDiv
const incButton = thresholdDiv
.getElementsByClassName('inc')
.item(0) as HTMLButtonElement
// index div
const indexDiv = document.getElementsByClassName('index').item(0) as HTMLDivElement
// index nums span
const indexDispNums = Array.from(
indexDiv.getElementsByClassName('num')
) as HTMLSpanElement[]
/**
* init
* helper functions
*/
export function initNav(): void {
// add watcher for updating nav text
state.addWatcher((o) => {
updateIndexText(expand(o.index + 1), expand(o.length))
updateThresholdText(expand(o.threshold))
})
// event listeners
decButton.addEventListener(
'click',
() => {
decThreshold()
},
{ passive: true }
)
incButton.addEventListener(
'click',
() => {
incThreshold()
},
{ passive: true }
)
}
// helper
export function updateThresholdText(thresholdValue: string): void {
function updateThresholdText(thresholdValue: string): void {
thresholdDispNums.forEach((e: HTMLSpanElement, i: number) => {
e.innerText = thresholdValue[i]
})
}
export function updateIndexText(indexValue: string, indexLength: string): void {
function updateIndexText(indexValue: string, indexLength: string): void {
indexDispNums.forEach((e: HTMLSpanElement, i: number) => {
if (i < 4) {
e.innerText = indexValue[i]
@@ -76,3 +46,21 @@ export function updateIndexText(indexValue: string, indexLength: string): void {
}
})
}
/**
* Nav component
*/
export default function Nav(): null {
const [state, { incThreshold, decThreshold }] = useState()
createEffect(() => {
updateIndexText(expand(state().index + 1), expand(state().length))
updateThresholdText(expand(state().threshold))
})
decButton.onclick = decThreshold
incButton.onclick = incThreshold
return null
}

View File

@@ -1,368 +0,0 @@
import { type Power3, type gsap } from 'gsap'
import { container } from '../container'
import { incIndex, isAnimating, navigateVector, state } from '../globalState'
import { decrement, increment, loadGsap } from '../globalUtils'
import { type ImageJSON } from '../resources'
import { active, cordHist, isLoading, isOpen } from './state'
// eslint-disable-next-line sort-imports
import { onMutation, type DesktopImage } from './utils'
/**
* variables
*/
let imgs: DesktopImage[] = []
let last = { x: 0, y: 0 }
let _gsap: typeof gsap
let _Power3: typeof Power3
let gsapLoaded = false
/**
* getter
*/
function getTrailElsIndex(): number[] {
return cordHist.get().map((item) => item.i)
}
function getTrailCurrentElsIndex(): number[] {
return getTrailElsIndex().slice(-state.get().trailLength)
}
function getTrailInactiveElsIndex(): number[] {
const trailCurrentElsIndex = getTrailCurrentElsIndex()
return trailCurrentElsIndex.slice(0, trailCurrentElsIndex.length - 1)
}
function getCurrentElIndex(): number {
const trailElsIndex = getTrailElsIndex()
return trailElsIndex[trailElsIndex.length - 1]
}
function getPrevElIndex(): number {
const c = cordHist.get()
const s = state.get()
return decrement(c[c.length - 1].i, s.length)
}
function getNextElIndex(): number {
const c = cordHist.get()
const s = state.get()
return increment(c[c.length - 1].i, s.length)
}
/**
* main functions
*/
// on mouse
function onMouse(e: MouseEvent): void {
if (isOpen.get() || isAnimating.get()) return
if (!gsapLoaded) {
loadLib()
return
}
const cord = { x: e.clientX, y: e.clientY }
const travelDist = Math.hypot(cord.x - last.x, cord.y - last.y)
if (travelDist > state.get().threshold) {
last = cord
incIndex()
const newHist = { i: state.get().index, ...cord }
cordHist.set([...cordHist.get(), newHist].slice(-state.get().length))
}
}
// set image position with gsap (in both stage and navigation)
function setPositions(): void {
const trailElsIndex = getTrailElsIndex()
if (trailElsIndex.length === 0 || !gsapLoaded) return
const elsTrail = getImagesWithIndexArray(trailElsIndex)
_gsap.set(elsTrail, {
x: (i: number) => cordHist.get()[i].x - window.innerWidth / 2,
y: (i: number) => cordHist.get()[i].y - window.innerHeight / 2,
opacity: (i: number) =>
i + 1 + state.get().trailLength <= cordHist.get().length ? 0 : 1,
zIndex: (i: number) => i,
scale: 0.6
})
if (isOpen.get()) {
const elc = getImagesWithIndexArray([getCurrentElIndex()])[0]
elc.classList.add('hide') // hide image to prevent flash
const indexArrayToHires: number[] = []
switch (navigateVector.get()) {
case 'prev':
indexArrayToHires.push(getPrevElIndex())
break
case 'next':
indexArrayToHires.push(getNextElIndex())
break
default:
break
}
hires(getImagesWithIndexArray(indexArrayToHires)) // preload
setLoaderForImage(elc)
_gsap.set(imgs, { opacity: 0 })
_gsap.set(elc, { opacity: 1, x: 0, y: 0, scale: 1 })
} else {
lores(elsTrail)
}
}
// open image into navigation
function expandImage(): void {
if (isAnimating.get()) return
isOpen.set(true)
isAnimating.set(true)
const elcIndex = getCurrentElIndex()
const elc = getImagesWithIndexArray([elcIndex])[0]
// don't hide here because we want a better transition
// elc.classList.add('hide')
hires(getImagesWithIndexArray([elcIndex, getPrevElIndex(), getNextElIndex()]))
setLoaderForImage(elc)
const tl = _gsap.timeline()
const trailInactiveEls = getImagesWithIndexArray(getTrailInactiveElsIndex())
// move down and hide trail inactive
tl.to(trailInactiveEls, {
y: '+=20',
ease: _Power3.easeIn,
stagger: 0.075,
duration: 0.3,
delay: 0.1,
opacity: 0
})
// current move to center
tl.to(elc, {
x: 0,
y: 0,
ease: _Power3.easeInOut,
duration: 0.7,
delay: 0.3
})
// current expand
tl.to(elc, {
delay: 0.1,
scale: 1,
ease: _Power3.easeInOut
})
// finished
tl.then(() => {
isAnimating.set(false)
}).catch((e) => {
console.log(e)
})
}
// close navigation and back to stage
export function minimizeImage(): void {
if (isAnimating.get()) return
isOpen.set(false)
isAnimating.set(true)
navigateVector.set('none') // cleanup
lores(
getImagesWithIndexArray([...getTrailInactiveElsIndex(), ...[getCurrentElIndex()]])
)
const tl = _gsap.timeline()
const elc = getImagesWithIndexArray([getCurrentElIndex()])[0]
const elsTrailInactive = getImagesWithIndexArray(getTrailInactiveElsIndex())
// shrink current
tl.to(elc, {
scale: 0.6,
duration: 0.6,
ease: _Power3.easeInOut
})
// move current to original position
tl.to(elc, {
delay: 0.3,
duration: 0.7,
ease: _Power3.easeInOut,
x: cordHist.get()[cordHist.get().length - 1].x - window.innerWidth / 2,
y: cordHist.get()[cordHist.get().length - 1].y - window.innerHeight / 2
})
// show trail inactive
tl.to(elsTrailInactive, {
y: '-=20',
ease: _Power3.easeOut,
stagger: -0.1,
duration: 0.3,
opacity: 1
})
// finished
tl.then(() => {
isAnimating.set(false)
}).catch((e) => {
console.log(e)
})
}
/**
* init
*/
export function initStage(ijs: ImageJSON[]): void {
// create stage element
createStage(ijs)
// get stage
const stage = document.getElementsByClassName('stage').item(0) as HTMLDivElement
// get image elements
imgs = Array.from(stage.getElementsByTagName('img')) as DesktopImage[]
imgs.forEach((img, i) => {
// preload first 5 images on page load
if (i < 5) {
img.src = img.dataset.loUrl
}
// lores preloader for rest of the images
onMutation(img, (mutations, observer) => {
mutations.every((mutation) => {
// if open or animating, skip
if (isOpen.get() || isAnimating.get()) return true
// if mutation is not about style attribute, skip
if (mutation.attributeName !== 'style') return true
const opacity = parseFloat(img.style.opacity)
// if opacity is not 1, skip
if (opacity !== 1) return true
// preload the i + 5th image
if (i + 5 < imgs.length) {
imgs[i + 5].src = imgs[i + 5].dataset.loUrl
}
// disconnect observer and return false to break the loop
observer.disconnect()
return false
})
})
})
// event listeners
stage.addEventListener(
'click',
() => {
expandImage()
},
{ passive: true }
)
stage.addEventListener(
'keydown',
() => {
expandImage()
},
{ passive: true }
)
window.addEventListener('mousemove', onMouse, { passive: true })
// watchers
isOpen.addWatcher((o) => {
active.set(o && !isAnimating.get())
})
isAnimating.addWatcher((o) => {
active.set(isOpen.get() && !o)
})
cordHist.addWatcher((_) => {
setPositions()
})
// dynamic import
window.addEventListener(
'mousemove',
() => {
loadLib()
},
{ once: true, passive: true }
)
}
/**
* hepler
*/
function createStage(ijs: ImageJSON[]): void {
// create container for images
const stage: HTMLDivElement = document.createElement('div')
stage.className = 'stage'
// append images to container
for (const ij of ijs) {
const e = document.createElement('img') as DesktopImage
e.height = ij.loImgH
e.width = ij.loImgW
// set data attributes
e.dataset.hiUrl = ij.hiUrl
e.dataset.hiImgH = ij.hiImgH.toString()
e.dataset.hiImgW = ij.hiImgW.toString()
e.dataset.loUrl = ij.loUrl
e.dataset.loImgH = ij.loImgH.toString()
e.dataset.loImgW = ij.loImgW.toString()
e.alt = ij.alt
// append
stage.append(e)
}
container.append(stage)
}
function getImagesWithIndexArray(indexArray: number[]): DesktopImage[] {
return indexArray.map((i) => imgs[i])
}
function hires(imgs: DesktopImage[]): void {
imgs.forEach((img) => {
if (img.src === img.dataset.hiUrl) return
img.src = img.dataset.hiUrl
img.height = parseInt(img.dataset.hiImgH)
img.width = parseInt(img.dataset.hiImgW)
})
}
function lores(imgs: DesktopImage[]): void {
imgs.forEach((img) => {
if (img.src === img.dataset.loUrl) return
img.src = img.dataset.loUrl
img.height = parseInt(img.dataset.loImgH)
img.width = parseInt(img.dataset.loImgW)
})
}
function setLoaderForImage(e: HTMLImageElement): void {
if (!e.complete) {
isLoading.set(true)
e.addEventListener(
'load',
() => {
isLoading.set(false)
e.classList.remove('hide')
},
{ once: true, passive: true }
)
e.addEventListener(
'error',
() => {
isLoading.set(false)
},
{ once: true, passive: true }
)
} else {
e.classList.remove('hide')
isLoading.set(false)
}
}
function loadLib(): void {
loadGsap()
.then((g) => {
_gsap = g[0]
_Power3 = g[1]
gsapLoaded = true
})
.catch((e) => {
console.log(e)
})
}

476
assets/ts/desktop/stage.tsx Normal file
View File

@@ -0,0 +1,476 @@
import { type gsap } from 'gsap'
import {
For,
createEffect,
on,
onMount,
type Accessor,
type JSX,
type Setter
} from 'solid-js'
import type { ImageJSON } from '../resources'
import { useState, type State } from '../state'
import { decrement, increment, loadGsap, type Vector } from '../utils'
import type { DesktopImage, HistoryItem } from './layout'
/**
* helper functions
*/
function getTrailElsIndex(cordHistValue: HistoryItem[]): number[] {
return cordHistValue.map((el) => el.i)
}
function getTrailCurrentElsIndex(
cordHistValue: HistoryItem[],
stateValue: State
): number[] {
return getTrailElsIndex(cordHistValue).slice(-stateValue.trailLength)
}
function getTrailInactiveElsIndex(
cordHistValue: HistoryItem[],
stateValue: State
): number[] {
return getTrailCurrentElsIndex(cordHistValue, stateValue).slice(0, -1)
}
function getCurrentElIndex(cordHistValue: HistoryItem[]): number {
return getTrailElsIndex(cordHistValue).slice(-1)[0]
}
function getPrevElIndex(cordHistValue: HistoryItem[], stateValue: State): number {
return decrement(cordHistValue.slice(-1)[0].i, stateValue.length)
}
function getNextElIndex(cordHistValue: HistoryItem[], stateValue: State): number {
return increment(cordHistValue.slice(-1)[0].i, stateValue.length)
}
function getImagesFromIndexes(imgs: DesktopImage[], indexes: number[]): DesktopImage[] {
return indexes.map((i) => imgs[i])
}
function hires(imgs: DesktopImage[]): void {
imgs.forEach((img) => {
if (img.src === img.dataset.hiUrl) return
img.src = img.dataset.hiUrl
img.height = parseInt(img.dataset.hiImgH)
img.width = parseInt(img.dataset.hiImgW)
})
}
function lores(imgs: DesktopImage[]): void {
imgs.forEach((img) => {
if (img.src === img.dataset.loUrl) return
img.src = img.dataset.loUrl
img.height = parseInt(img.dataset.loImgH)
img.width = parseInt(img.dataset.loImgW)
})
}
function onMutation<T extends HTMLElement>(
element: T,
trigger: (arg0: MutationRecord) => boolean,
observeOptions: MutationObserverInit = { attributes: true }
): void {
new MutationObserver((mutations, observer) => {
for (const mutation of mutations) {
if (trigger(mutation)) {
observer.disconnect()
break
}
}
}).observe(element, observeOptions)
}
/**
* Stage component
*/
export default function Stage(props: {
ijs: ImageJSON[]
setIsLoading: Setter<boolean>
isOpen: Accessor<boolean>
setIsOpen: Setter<boolean>
isAnimating: Accessor<boolean>
setIsAnimating: Setter<boolean>
cordHist: Accessor<HistoryItem[]>
setCordHist: Setter<HistoryItem[]>
navVector: Accessor<Vector>
setNavVector: Setter<Vector>
}): JSX.Element {
// variables
let _gsap: typeof gsap
// eslint-disable-next-line solid/reactivity
const imgs: DesktopImage[] = Array<DesktopImage>(props.ijs.length)
let last = { x: 0, y: 0 }
let abortController: AbortController | undefined
// states
let gsapLoaded = false
const [state, { incIndex }] = useState()
const stateLength = state().length
let mounted = false
const onMouse: (e: MouseEvent) => void = (e) => {
if (props.isOpen() || props.isAnimating() || !gsapLoaded || !mounted) return
const cord = { x: e.clientX, y: e.clientY }
const travelDist = Math.hypot(cord.x - last.x, cord.y - last.y)
if (travelDist > state().threshold) {
last = cord
incIndex()
const _state = state()
const newHist = { i: _state.index, ...cord }
props.setCordHist((prev) => [...prev, newHist].slice(-stateLength))
}
}
const onClick: () => void = () => {
!props.isAnimating() && props.setIsOpen(true)
}
const setPosition: () => void = () => {
if (!mounted) return
if (imgs.length === 0) return
const _cordHist = props.cordHist()
const trailElsIndex = getTrailElsIndex(_cordHist)
if (trailElsIndex.length === 0) return
const elsTrail = getImagesFromIndexes(imgs, trailElsIndex)
const _isOpen = props.isOpen()
const _state = state()
_gsap.set(elsTrail, {
x: (i: number) => _cordHist[i].x - window.innerWidth / 2,
y: (i: number) => _cordHist[i].y - window.innerHeight / 2,
opacity: (i: number) =>
Math.max(
(i + 1 + _state.trailLength <= _cordHist.length ? 0 : 1) - (_isOpen ? 1 : 0),
0
),
zIndex: (i: number) => i,
scale: 0.6
})
if (_isOpen) {
const elc = getImagesFromIndexes(imgs, [getCurrentElIndex(_cordHist)])[0]
const indexArrayToHires: number[] = []
const indexArrayToCleanup: number[] = []
switch (props.navVector()) {
case 'prev':
indexArrayToHires.push(getPrevElIndex(_cordHist, _state))
indexArrayToCleanup.push(getNextElIndex(_cordHist, _state))
break
case 'next':
indexArrayToHires.push(getNextElIndex(_cordHist, _state))
indexArrayToCleanup.push(getPrevElIndex(_cordHist, _state))
break
default:
break
}
hires(getImagesFromIndexes(imgs, indexArrayToHires)) // preload
_gsap.set(getImagesFromIndexes(imgs, indexArrayToCleanup), { opacity: 0 })
_gsap.set(elc, { x: 0, y: 0, scale: 1 }) // set current to center
setLoaderForHiresImage(elc) // set loader, if loaded set current opacity to 1
} else {
lores(elsTrail)
}
}
const expandImage: () => Promise<
gsap.core.Omit<gsap.core.Timeline, 'then'>
> = async () => {
// isAnimating is prechecked in isOpen effect
if (!mounted || !gsapLoaded) throw new Error('not mounted or gsap not loaded')
props.setIsAnimating(true)
const _cordHist = props.cordHist()
const _state = state()
const elcIndex = getCurrentElIndex(_cordHist)
const elc = imgs[elcIndex]
// don't hide here because we want a better transition
hires(
getImagesFromIndexes(imgs, [
elcIndex,
getPrevElIndex(_cordHist, _state),
getNextElIndex(_cordHist, _state)
])
)
setLoaderForHiresImage(elc)
const tl = _gsap.timeline()
const trailInactiveEls = getImagesFromIndexes(
imgs,
getTrailInactiveElsIndex(_cordHist, _state)
)
// move down and hide trail inactive
tl.to(trailInactiveEls, {
y: '+=20',
ease: 'power3.in',
stagger: 0.075,
duration: 0.3,
delay: 0.1,
opacity: 0
})
// current move to center
tl.to(elc, {
x: 0,
y: 0,
ease: 'power3.inOut',
duration: 0.7,
delay: 0.3
})
// current expand
tl.to(elc, {
delay: 0.1,
scale: 1,
ease: 'power3.inOut'
})
// finished
// eslint-disable-next-line solid/reactivity
return await tl.then(() => {
props.setIsAnimating(false)
})
}
const minimizeImage: () => Promise<
gsap.core.Omit<gsap.core.Timeline, 'then'>
> = async () => {
if (!mounted || !gsapLoaded) throw new Error('not mounted or gsap not loaded')
props.setIsAnimating(true)
props.setNavVector('none') // cleanup
const _cordHist = props.cordHist()
const _state = state()
const elcIndex = getCurrentElIndex(_cordHist)
const elsTrailInactiveIndexes = getTrailInactiveElsIndex(_cordHist, _state)
lores(getImagesFromIndexes(imgs, [...elsTrailInactiveIndexes, elcIndex]))
const tl = _gsap.timeline()
const elc = getImagesFromIndexes(imgs, [elcIndex])[0]
const elsTrailInactive = getImagesFromIndexes(imgs, elsTrailInactiveIndexes)
// shrink current
tl.to(elc, {
scale: 0.6,
duration: 0.6,
ease: 'power3.inOut'
})
// move current to original position
tl.to(elc, {
delay: 0.3,
duration: 0.7,
ease: 'power3.inOut',
x: _cordHist.slice(-1)[0].x - window.innerWidth / 2,
y: _cordHist.slice(-1)[0].y - window.innerHeight / 2
})
// show trail inactive
tl.to(elsTrailInactive, {
y: '-=20',
ease: 'power3.out',
stagger: -0.1,
duration: 0.3,
opacity: 1
})
// eslint-disable-next-line solid/reactivity
return await tl.then(() => {
props.setIsAnimating(false)
})
}
function setLoaderForHiresImage(img: DesktopImage): void {
if (!mounted || !gsapLoaded) return
if (!img.complete) {
props.setIsLoading(true)
// abort controller for cleanup
const controller = new AbortController()
const abortSignal = controller.signal
// event listeners
img.addEventListener(
'load',
() => {
_gsap
.to(img, { opacity: 1, ease: 'power3.out', duration: 0.5 })
// eslint-disable-next-line solid/reactivity
.then(() => {
props.setIsLoading(false)
})
.catch((e) => {
console.log(e)
})
.finally(() => {
controller.abort()
})
},
{ once: true, passive: true, signal: abortSignal }
)
img.addEventListener(
'error',
() => {
_gsap
.set(img, { opacity: 1 })
// eslint-disable-next-line solid/reactivity
.then(() => {
props.setIsLoading(false)
})
.catch((e) => {
console.log(e)
})
.finally(() => {
controller.abort()
})
},
{ once: true, passive: true, signal: abortSignal }
)
} else {
_gsap
.set(img, { opacity: 1 })
// eslint-disable-next-line solid/reactivity
.then(() => {
props.setIsLoading(false)
})
.catch((e) => {
console.log(e)
})
}
}
onMount(() => {
// preload logic
imgs.forEach((img, i) => {
// preload first 5 images on page load
if (i < 5) {
img.src = img.dataset.loUrl
}
// lores preloader for rest of the images
// eslint-disable-next-line solid/reactivity
onMutation(img, (mutation) => {
// if open or animating, hold
if (props.isOpen() || props.isAnimating()) return false
// if mutation is not about style attribute, hold
if (mutation.attributeName !== 'style') return false
const opacity = parseFloat(img.style.opacity)
// if opacity is not 1, hold
if (opacity !== 1) return false
// preload the i + 5th image, if it exists
if (i + 5 < imgs.length) {
imgs[i + 5].src = imgs[i + 5].dataset.loUrl
}
// triggered
return true
})
})
// load gsap on mousemove
window.addEventListener(
'mousemove',
() => {
loadGsap()
.then((g) => {
_gsap = g
gsapLoaded = true
})
.catch((e) => {
console.log(e)
})
},
{ passive: true, once: true }
)
// event listeners
abortController = new AbortController()
const abortSignal = abortController.signal
window.addEventListener('mousemove', onMouse, {
passive: true,
signal: abortSignal
})
// mounted
mounted = true
})
createEffect(
on(
() => props.cordHist(),
() => {
setPosition()
},
{ defer: true }
)
)
createEffect(
on(
() => props.isOpen(),
async () => {
if (props.isAnimating()) return
if (props.isOpen()) {
// expand image
await expandImage()
.catch(() => {
void 0
})
// eslint-disable-next-line solid/reactivity
.then(() => {
// abort controller for cleanup
abortController?.abort()
})
} else {
// minimize image
await minimizeImage()
.catch(() => {
void 0
})
// eslint-disable-next-line solid/reactivity
.then(() => {
// event listeners and its abort controller
abortController = new AbortController()
const abortSignal = abortController.signal
window.addEventListener('mousemove', onMouse, {
passive: true,
signal: abortSignal
})
// cleanup isLoading
props.setIsLoading(false)
})
}
},
{ defer: true }
)
)
return (
<>
<div class="stage" onClick={onClick} onKeyDown={onClick}>
<For each={props.ijs}>
{(ij, i) => (
<img
ref={imgs[i()]}
height={ij.loImgH}
width={ij.loImgW}
data-hi-url={ij.hiUrl}
data-hi-img-h={ij.hiImgH}
data-hi-img-w={ij.hiImgW}
data-lo-url={ij.loUrl}
data-lo-img-h={ij.loImgH}
data-lo-img-w={ij.loImgW}
alt={ij.alt}
/>
)}
</For>
</div>
</>
)
}

View File

@@ -1,194 +0,0 @@
import { container } from '../container'
import { decIndex, incIndex, isAnimating, navigateVector, state } from '../globalState'
import { decrement, increment } from '../globalUtils'
import { setCustomCursor } from './customCursor'
import { minimizeImage } from './stage'
import { active, cordHist, isLoading, isOpen } from './state'
/**
* types
*/
type NavItem = (typeof navItems)[number]
/**
* variables
*/
const navItems = [
container.dataset.prev,
container.dataset.close,
container.dataset.next
] as const
const loadingText = container.dataset.loading + '...'
let loadedText = ''
/**
* main functions
*/
function handleClick(type: NavItem): void {
if (type === navItems[0]) {
prevImage()
} else if (type === navItems[1]) {
minimizeImage()
} else {
nextImage()
}
}
function handleKey(e: KeyboardEvent): void {
if (isOpen.get() || isAnimating.get()) return
switch (e.key) {
case 'ArrowLeft':
prevImage()
break
case 'Escape':
minimizeImage()
break
case 'ArrowRight':
nextImage()
break
}
}
/**
* init
*/
export function initStageNav(): void {
// isLoading
isLoading.addWatcher((o) => {
if (o) setCustomCursor(loadingText)
else setCustomCursor(loadedText)
})
// navOverlay
const navOverlay = document.createElement('div')
navOverlay.className = 'navOverlay'
for (const [index, navItem] of navItems.entries()) {
const overlay = document.createElement('div')
overlay.className = 'overlay'
const isClose = index === 1
// close
if (isClose) {
overlay.addEventListener(
'click',
() => {
handleCloseClick(navItem)
},
{ passive: true }
)
overlay.addEventListener(
'keydown',
() => {
handleCloseClick(navItem)
},
{ passive: true }
)
overlay.addEventListener(
'mouseover',
() => {
handleCloseHover(navItem)
},
{ passive: true }
)
overlay.addEventListener(
'focus',
() => {
handleCloseHover(navItem)
},
{ passive: true }
)
}
// prev and next
else {
overlay.addEventListener(
'click',
() => {
handlePNClick(navItem)
},
{ passive: true }
)
overlay.addEventListener(
'keydown',
() => {
handlePNClick(navItem)
},
{ passive: true }
)
overlay.addEventListener(
'mouseover',
() => {
handlePNHover(navItem)
},
{ passive: true }
)
overlay.addEventListener(
'focus',
() => {
handlePNHover(navItem)
},
{ passive: true }
)
}
navOverlay.append(overlay)
}
active.addWatcher(() => {
if (active.get()) {
navOverlay.classList.add('active')
} else {
navOverlay.classList.remove('active')
}
})
container.append(navOverlay)
window.addEventListener('keydown', handleKey, { passive: true })
}
/**
* hepler
*/
function nextImage(): void {
if (isAnimating.get()) return
navigateVector.set('next')
cordHist.set(
cordHist.get().map((item) => {
return { ...item, i: increment(item.i, state.get().length) }
})
)
incIndex()
}
function prevImage(): void {
if (isAnimating.get()) return
navigateVector.set('prev')
cordHist.set(
cordHist.get().map((item) => {
return { ...item, i: decrement(item.i, state.get().length) }
})
)
decIndex()
}
function handleCloseClick(navItem: NavItem): void {
handleClick(navItem)
isLoading.set(false)
}
function handleCloseHover(navItem: NavItem): void {
loadedText = navItem
setCustomCursor(navItem)
}
function handlePNClick(navItem: NavItem): void {
if (!isLoading.get()) handleClick(navItem)
}
function handlePNHover(navItem: NavItem): void {
loadedText = navItem
if (isLoading.get()) setCustomCursor(loadingText)
else setCustomCursor(navItem)
}

View File

@@ -0,0 +1,92 @@
import { For, type Accessor, type JSX, type Setter } from 'solid-js'
import { useState } from '../state'
import { decrement, increment, type Vector } from '../utils'
import type { HistoryItem } from './layout'
export default function StageNav(props: {
children?: JSX.Element
prevText: string
closeText: string
nextText: string
loadingText: string
active: Accessor<boolean>
isAnimating: Accessor<boolean>
setCordHist: Setter<HistoryItem[]>
isOpen: Accessor<boolean>
setIsOpen: Setter<boolean>
setHoverText: Setter<string>
navVector: Accessor<Vector>
setNavVector: Setter<Vector>
}): JSX.Element {
// types
type NavItem = (typeof navItems)[number]
// variables
// eslint-disable-next-line solid/reactivity
const navItems = [props.prevText, props.closeText, props.nextText] as const
// states
const [state, { incIndex, decIndex }] = useState()
const stateLength = state().length
const prevImage: () => void = () => {
props.setNavVector('prev')
props.setCordHist((c) =>
c.map((item) => {
return { ...item, i: decrement(item.i, stateLength) }
})
)
decIndex()
}
const closeImage: () => void = () => {
props.setIsOpen(false)
}
const nextImage: () => void = () => {
props.setNavVector('next')
props.setCordHist((c) =>
c.map((item) => {
return { ...item, i: increment(item.i, stateLength) }
})
)
incIndex()
}
const handleClick: (item: NavItem) => void = (item) => {
if (!props.isOpen() || props.isAnimating()) return
if (item === navItems[0]) prevImage()
else if (item === navItems[1]) closeImage()
else nextImage()
}
const handleKey: (e: KeyboardEvent) => void = (e) => {
if (!props.isOpen() || props.isAnimating()) return
if (e.key === 'ArrowLeft') prevImage()
else if (e.key === 'Escape') closeImage()
else if (e.key === 'ArrowRight') nextImage()
}
return (
<>
<div class="navOverlay" classList={{ active: props.active() }}>
<For each={navItems}>
{(item) => (
<div
class="overlay"
onClick={() => {
handleClick(item)
}}
onKeyDown={handleKey}
onFocus={() => props.setHoverText(item)}
onMouseOver={() => props.setHoverText(item)}
/>
)}
</For>
</div>
</>
)
}

View File

@@ -1,20 +0,0 @@
import { Watchable } from '../globalUtils'
/**
* types
*/
export interface HistoryItem {
i: number
x: number
y: number
}
/**
* variables
*/
export const cordHist = new Watchable<HistoryItem[]>([])
export const isOpen = new Watchable<boolean>(false)
export const active = new Watchable<boolean>(false)
export const isLoading = new Watchable<boolean>(false)

View File

@@ -1,28 +0,0 @@
/**
* interfaces
*/
export interface DesktopImage extends HTMLImageElement {
dataset: {
hiUrl: string
hiImgH: string
hiImgW: string
loUrl: string
loImgH: string
loImgW: string
}
}
/**
* utils
*/
export function onMutation<T extends HTMLElement>(
element: T,
callback: (arg0: MutationRecord[], arg1: MutationObserver) => void,
observeOptions: MutationObserverInit = { attributes: true }
): void {
new MutationObserver((mutations, observer) => {
callback(mutations, observer)
}).observe(element, observeOptions)
}

View File

@@ -1,91 +0,0 @@
import {
Watchable,
decrement,
getThresholdSessionIndex,
increment
} from './globalUtils'
/**
* types
*/
export type State = typeof defaultState
export type NavVec = 'next' | 'none' | 'prev'
/**
* variables
*/
const thresholds = [
{ threshold: 20, trailLength: 20 },
{ threshold: 40, trailLength: 10 },
{ threshold: 80, trailLength: 5 },
{ threshold: 140, trailLength: 5 },
{ threshold: 200, trailLength: 5 }
]
const defaultState = {
index: -1,
length: 0,
threshold: thresholds[getThresholdSessionIndex()].threshold,
trailLength: thresholds[getThresholdSessionIndex()].trailLength
}
export const state = new Watchable<State>(defaultState)
export const isAnimating = new Watchable<boolean>(false)
export const navigateVector = new Watchable<NavVec>('none')
/**
* main functions
*/
export function initState(length: number): void {
const s = state.get()
s.length = length
updateThreshold(s, 0)
state.set(s)
}
export function setIndex(index: number): void {
const s = state.get()
s.index = index
state.set(s)
}
export function incIndex(): void {
const s = state.get()
s.index = increment(s.index, s.length)
state.set(s)
}
export function decIndex(): void {
const s = state.get()
s.index = decrement(s.index, s.length)
state.set(s)
}
export function incThreshold(): void {
let s = state.get()
s = updateThreshold(s, 1)
state.set(s)
}
export function decThreshold(): void {
let s = state.get()
s = updateThreshold(s, -1)
state.set(s)
}
/**
* helper
*/
function updateThreshold(state: State, inc: number): State {
const i = thresholds.findIndex((t) => state.threshold === t.threshold) + inc
// out of bounds
if (i < 0 || i >= thresholds.length) return state
// storage the index so we can restore it even if we go to another page
sessionStorage.setItem('thresholdsIndex', i.toString())
const newItems = thresholds[i]
return { ...state, ...newItems }
}

View File

@@ -1,54 +0,0 @@
import { initContainer } from './container'
import { initState } from './globalState'
import { initNav } from './nav'
import { initResources } from './resources'
// this is the main entry point for the app
document.addEventListener('DOMContentLoaded', () => {
main().catch((e) => {
console.log(e)
})
})
/**
* main functions
*/
async function main(): Promise<void> {
initContainer()
const ijs = await initResources()
initState(ijs.length)
initNav()
if (ijs.length === 0) {
return
}
// NOTE: it seems firefox and chromnium don't like top layer await
// so we are using import then instead
if (!isMobile()) {
await import('./desktop/init')
.then((d) => {
d.initDesktop(ijs)
})
.catch((e) => {
console.log(e)
})
} else {
await import('./mobile/init')
.then((m) => {
m.initMobile(ijs)
})
.catch((e) => {
console.log(e)
})
}
}
/**
* hepler
*/
function isMobile(): boolean {
return window.matchMedia('(hover: none)').matches
}

83
assets/ts/main.tsx Normal file
View File

@@ -0,0 +1,83 @@
import {
Match,
Show,
Switch,
createEffect,
createResource,
createSignal,
lazy,
type JSX
} from 'solid-js'
import { render } from 'solid-js/web'
import { getImageJSON } from './resources'
import { StateProvider } from './state'
import '../scss/style.scss'
/**
* interfaces
*/
export interface Container extends HTMLDivElement {
dataset: {
next: string
close: string
prev: string
loading: string
}
}
// container
const container = document.getElementsByClassName('container')[0] as Container
// lazy components
const Desktop = lazy(async () => await import('./desktop/layout'))
const Mobile = lazy(async () => await import('./mobile/layout'))
function Main(): JSX.Element {
// variables
const [ijs] = createResource(getImageJSON)
const isMobile = window.matchMedia('(hover: none)').matches
// states
const [scrollable, setScollable] = createSignal(true)
createEffect(() => {
if (scrollable()) {
container.classList.remove('disableScroll')
} else {
container.classList.add('disableScroll')
}
})
return (
<>
<Show when={ijs.state === 'ready'}>
<StateProvider length={ijs()?.length ?? 0}>
<Switch fallback={<div>Error</div>}>
<Match when={isMobile}>
<Mobile
ijs={ijs() ?? []}
closeText={container.dataset.close}
loadingText={container.dataset.loading}
setScrollable={setScollable}
/>
</Match>
<Match when={!isMobile}>
<Desktop
ijs={ijs() ?? []}
prevText={container.dataset.prev}
closeText={container.dataset.close}
nextText={container.dataset.next}
loadingText={container.dataset.loading}
/>
</Match>
</Switch>
</StateProvider>
</Show>
</>
)
}
render(() => <Main />, container)

View File

@@ -1,106 +0,0 @@
import { container } from '../container'
import { setIndex } from '../globalState'
import { type ImageJSON } from '../resources'
import { slideUp } from './gallery'
import { mounted } from './state'
// eslint-disable-next-line sort-imports
import { getRandom, onIntersection, type MobileImage } from './utils'
/**
* variables
*/
export let imgs: MobileImage[] = []
/**
* main functions
*/
function handleClick(i: number): void {
setIndex(i)
slideUp()
}
/**
* init
*/
export function initCollection(ijs: ImageJSON[]): void {
createCollection(ijs)
// get container
const collection = document
.getElementsByClassName('collection')
.item(0) as HTMLDivElement
// add watcher
mounted.addWatcher((o) => {
if (o) {
collection.classList.remove('hidden')
} else {
collection.classList.add('hidden')
}
})
// get image elements
imgs = Array.from(collection.getElementsByTagName('img')) as MobileImage[]
// add event listeners
imgs.forEach((img, i) => {
// preload first 5 images on page load
if (i < 5) {
img.src = img.dataset.src
}
// event listeners
img.addEventListener(
'click',
() => {
handleClick(i)
},
{ passive: true }
)
img.addEventListener(
'keydown',
() => {
handleClick(i)
},
{ passive: true }
)
// preload
onIntersection(img, (entries, observer) => {
entries.every((entry) => {
// no intersection, skip
if (entry.intersectionRatio <= 0) return true
// preload the i + 5th image
if (i + 5 < imgs.length) {
imgs[i + 5].src = imgs[i + 5].dataset.src
}
// disconnect observer and return false to break the loop
observer.disconnect()
return false
})
})
})
}
/**
* helper
*/
function createCollection(ijs: ImageJSON[]): void {
// create container for images
const _collection: HTMLDivElement = document.createElement('div')
_collection.className = 'collection'
// append images to container
for (const [i, ij] of ijs.entries()) {
// random x and y
const x = i !== 0 ? getRandom(-25, 25) : 0
const y = i !== 0 ? getRandom(-30, 30) : 0
// element
const e = document.createElement('img') as MobileImage
e.dataset.src = ij.loUrl
e.height = ij.loImgH
e.width = ij.loImgW
e.alt = ij.alt
e.style.transform = `translate3d(${x}%, ${y - 50}%, 0)`
_collection.append(e)
}
container.append(_collection)
}

View File

@@ -0,0 +1,133 @@
import {
For,
createEffect,
on,
onMount,
type Accessor,
type JSX,
type Setter
} from 'solid-js'
import type { ImageJSON } from '../resources'
import { useState } from '../state'
import type { MobileImage } from './layout'
function getRandom(min: number, max: number): number {
return Math.floor(Math.random() * (max - min + 1)) + min
}
function onIntersection<T extends HTMLElement>(
element: T,
trigger: (arg0: IntersectionObserverEntry) => boolean
): void {
new IntersectionObserver((entries, observer) => {
for (const entry of entries) {
if (trigger(entry)) {
observer.disconnect()
break
}
}
}).observe(element)
}
export default function Collection(props: {
children?: JSX.Element
ijs: ImageJSON[]
isAnimating: Accessor<boolean>
isOpen: Accessor<boolean>
setIsOpen: Setter<boolean>
}): JSX.Element {
// variables
// eslint-disable-next-line solid/reactivity
const imgs: MobileImage[] = Array<MobileImage>(props.ijs.length)
// states
const [state, { setIndex }] = useState()
// helper functions
const handleClick: (i: number) => void = (i) => {
if (props.isAnimating()) return
setIndex(i)
props.setIsOpen(true)
}
const scrollToActive: () => void = () => {
imgs[state().index].scrollIntoView({ behavior: 'auto', block: 'center' })
}
// effects
onMount(() => {
imgs.forEach((img, i) => {
// preload first 5 images on page load
if (i < 5) {
img.src = img.dataset.src
}
// event listeners
img.addEventListener(
'click',
() => {
handleClick(i)
},
{ passive: true }
)
img.addEventListener(
'keydown',
() => {
handleClick(i)
},
{ passive: true }
)
// preload
onIntersection(img, (entry) => {
// no intersection, hold
if (entry.intersectionRatio <= 0) return false
// preload the i + 5th image, if it exists
if (i + 5 < imgs.length) {
imgs[i + 5].src = imgs[i + 5].dataset.src
}
// triggered
return true
})
})
})
createEffect(
on(
() => {
props.isOpen()
},
() => {
if (!props.isOpen()) scrollToActive() // scroll to active when closed
},
{ defer: true }
)
)
return (
<>
<div class="collection">
<For each={props.ijs}>
{(ij, i) => (
<img
ref={imgs[i()]}
height={ij.loImgH}
width={ij.loImgW}
data-src={ij.loUrl}
alt={ij.alt}
style={{
transform: `translate3d(${i() !== 0 ? getRandom(-25, 25) : 0}%, ${i() !== 0 ? getRandom(-35, 35) : 0}%, 0)`
}}
onClick={() => {
handleClick(i())
}}
onKeyDown={() => {
handleClick(i())
}}
/>
)}
</For>
</div>
</>
)
}

View File

@@ -1,310 +0,0 @@
import { type Power3, type gsap } from 'gsap'
import { type Swiper } from 'swiper'
import { container, scrollable } from '../container'
import { isAnimating, navigateVector, setIndex, state } from '../globalState'
import { expand, loadGsap, removeDuplicates } from '../globalUtils'
import { type ImageJSON } from '../resources'
import { mounted } from './state'
// eslint-disable-next-line sort-imports
import { capitalizeFirstLetter, loadSwiper, type MobileImage } from './utils'
/**
* variables
*/
let swiperNode: HTMLDivElement
let gallery: HTMLDivElement
let curtain: HTMLDivElement
let swiper: Swiper
let lastIndex = -1
let indexDispNums: HTMLSpanElement[] = []
let galleryImages: MobileImage[] = []
let collectionImages: MobileImage[] = []
let _Swiper: typeof Swiper
let _gsap: typeof gsap
let _Power3: typeof Power3
let libLoaded = false
/**
* main functions
*/
export function slideUp(): void {
if (isAnimating.get() || !libLoaded) return
isAnimating.set(true)
_gsap.to(curtain, {
opacity: 1,
duration: 1
})
_gsap.to(gallery, {
y: 0,
ease: _Power3.easeInOut,
duration: 1,
delay: 0.4
})
setTimeout(() => {
// cleanup
scrollable.set(false)
isAnimating.set(false)
}, 1400)
}
function slideDown(): void {
if (isAnimating.get()) return
isAnimating.set(true)
scrollToActive()
_gsap.to(gallery, {
y: '100%',
ease: _Power3.easeInOut,
duration: 1
})
_gsap.to(curtain, {
opacity: 0,
duration: 1.2,
delay: 0.4
})
setTimeout(() => {
// cleanup
scrollable.set(true)
isAnimating.set(false)
lastIndex = -1
}, 1600)
}
/**
* init
*/
export function initGallery(ijs: ImageJSON[]): void {
// create gallery
createGallery(ijs)
// get elements
indexDispNums = Array.from(
document.getElementsByClassName('nav').item(0)?.getElementsByClassName('num') ?? []
) as HTMLSpanElement[]
swiperNode = document.getElementsByClassName('galleryInner').item(0) as HTMLDivElement
gallery = document.getElementsByClassName('gallery').item(0) as HTMLDivElement
curtain = document.getElementsByClassName('curtain').item(0) as HTMLDivElement
galleryImages = Array.from(gallery.getElementsByTagName('img')) as MobileImage[]
collectionImages = Array.from(
document
.getElementsByClassName('collection')
.item(0)
?.getElementsByTagName('img') ?? []
) as MobileImage[]
// state watcher
state.addWatcher((o) => {
if (o.index === lastIndex)
return // change slide only when index is changed
else if (lastIndex === -1)
navigateVector.set('none') // lastIndex before set
else if (o.index < lastIndex)
navigateVector.set('prev') // set navigate vector for galleryLoadImages
else if (o.index > lastIndex)
navigateVector.set('next') // set navigate vector for galleryLoadImages
else navigateVector.set('none') // default
changeSlide(o.index) // change slide to new index
updateIndexText() // update index text
lastIndex = o.index // update last index
})
// mounted watcher
mounted.addWatcher((o) => {
if (!o) return
scrollable.set(true)
})
// dynamic import
window.addEventListener(
'touchstart',
() => {
loadGsap()
.then((g) => {
_gsap = g[0]
_Power3 = g[1]
})
.catch((e) => {
console.log(e)
})
loadSwiper()
.then((s) => {
_Swiper = s
swiper = new _Swiper(swiperNode, { spaceBetween: 20 })
swiper.on('slideChange', ({ realIndex }) => {
setIndex(realIndex)
})
})
.catch((e) => {
console.log(e)
})
libLoaded = true
},
{ once: true, passive: true }
)
// mounted
mounted.set(true)
}
/**
* helper
*/
function changeSlide(slide: number): void {
galleryLoadImages()
swiper.slideTo(slide, 0)
}
function scrollToActive(): void {
collectionImages[state.get().index].scrollIntoView({
block: 'center',
behavior: 'auto'
})
}
function updateIndexText(): void {
const indexValue: string = expand(state.get().index + 1)
const indexLength: string = expand(state.get().length)
indexDispNums.forEach((e: HTMLSpanElement, i: number) => {
if (i < 4) {
e.innerText = indexValue[i]
} else {
e.innerText = indexLength[i - 4]
}
})
}
function galleryLoadImages(): void {
let activeImagesIndex: number[] = []
const currentIndex = state.get().index
const nextIndex = Math.min(currentIndex + 1, state.get().length - 1)
const prevIndex = Math.max(currentIndex - 1, 0)
switch (navigateVector.get()) {
case 'next':
activeImagesIndex = [nextIndex]
break
case 'prev':
activeImagesIndex = [prevIndex]
break
case 'none':
activeImagesIndex = [currentIndex, nextIndex, prevIndex]
break
}
removeDuplicates(activeImagesIndex).forEach((i) => {
const e = galleryImages[i]
if (e.src === e.dataset.src) return // already loaded
e.src = e.dataset.src
})
}
function createGallery(ijs: ImageJSON[]): void {
/**
* gallery
* |- galleryInner
* |- swiper-wrapper
* |- swiper-slide
* |- img
* |- swiper-slide
* |- img
* |- ...
* |- nav
* |- index
* |- close
*/
// swiper wrapper
const _swiperWrapper = document.createElement('div')
_swiperWrapper.className = 'swiper-wrapper'
// loading text
const loadingText = container.dataset.loading + '...'
for (const ij of ijs) {
// swiper slide
const _swiperSlide = document.createElement('div')
_swiperSlide.className = 'swiper-slide'
// loading indicator
const l = document.createElement('div')
l.className = 'loadingText'
l.innerText = loadingText
// img
const e = document.createElement('img') as MobileImage
e.dataset.src = ij.hiUrl
e.height = ij.hiImgH
e.width = ij.hiImgW
e.alt = ij.alt
e.classList.add('hide')
// load event
e.addEventListener(
'load',
() => {
e.classList.remove('hide')
l.classList.add('hide')
},
{ once: true, passive: true }
)
// parent container
const p = document.createElement('div')
p.className = 'slideContainer'
// append
p.append(e)
p.append(l)
_swiperSlide.append(p)
_swiperWrapper.append(_swiperSlide)
}
// swiper node
const _swiperNode = document.createElement('div')
_swiperNode.className = 'galleryInner'
_swiperNode.append(_swiperWrapper)
// index
const _index = document.createElement('div')
_index.insertAdjacentHTML(
'afterbegin',
`<span class="num"></span><span class="num"></span><span class="num"></span><span class="num"></span>
<span>/</span>
<span class="num"></span><span class="num"></span><span class="num"></span><span class="num"></span>`
)
// close
const _close = document.createElement('div')
_close.innerText = capitalizeFirstLetter(container.dataset.close)
_close.addEventListener(
'click',
() => {
slideDown()
},
{ passive: true }
)
_close.addEventListener(
'keydown',
() => {
slideDown()
},
{ passive: true }
)
// nav
const _navDiv = document.createElement('div')
_navDiv.className = 'nav'
_navDiv.append(_index, _close)
// gallery
const _gallery = document.createElement('div')
_gallery.className = 'gallery'
_gallery.append(_swiperNode)
_gallery.append(_navDiv)
/**
* curtain
*/
const _curtain = document.createElement('div')
_curtain.className = 'curtain'
/**
* container
* |- gallery
* |- curtain
*/
container.append(_gallery, _curtain)
}

View File

@@ -0,0 +1,242 @@
import { type gsap } from 'gsap'
import {
createEffect,
createSignal,
For,
on,
onMount,
Show,
type Accessor,
type JSX,
type Setter
} from 'solid-js'
import { createStore } from 'solid-js/store'
import { type Swiper } from 'swiper'
import invariant from 'tiny-invariant'
import { type ImageJSON } from '../resources'
import { useState } from '../state'
import { loadGsap, type Vector } from '../utils'
import GalleryImage from './galleryImage'
import GalleryNav, { capitalizeFirstLetter } from './galleryNav'
function removeDuplicates<T>(arr: T[]): T[] {
if (arr.length < 2) return arr // optimization
return [...new Set(arr)]
}
async function loadSwiper(): Promise<typeof Swiper> {
const s = await import('swiper')
return s.Swiper
}
export default function Gallery(props: {
children?: JSX.Element
ijs: ImageJSON[]
closeText: string
loadingText: string
isAnimating: Accessor<boolean>
setIsAnimating: Setter<boolean>
isOpen: Accessor<boolean>
setIsOpen: Setter<boolean>
setScrollable: Setter<boolean>
}): JSX.Element {
// variables
let _gsap: typeof gsap
let _swiper: Swiper
let curtain: HTMLDivElement | undefined
let gallery: HTMLDivElement | undefined
let galleryInner: HTMLDivElement | undefined
// eslint-disable-next-line solid/reactivity
const _loadingText = capitalizeFirstLetter(props.loadingText)
// states
let lastIndex = -1
let mounted = false
let navigateVector: Vector = 'none'
const [state, { setIndex }] = useState()
const [libLoaded, setLibLoaded] = createSignal(false)
// eslint-disable-next-line solid/reactivity
const [loads, setLoads] = createStore(Array<boolean>(props.ijs.length).fill(false))
// helper functions
const slideUp: () => void = () => {
// isAnimating is prechecked in isOpen effect
if (!libLoaded() || !mounted) return
props.setIsAnimating(true)
invariant(curtain, 'curtain is not defined')
invariant(gallery, 'gallery is not defined')
_gsap.to(curtain, {
opacity: 1,
duration: 1
})
_gsap.to(gallery, {
y: 0,
ease: 'power3.inOut',
duration: 1,
delay: 0.4
})
setTimeout(() => {
props.setScrollable(false)
props.setIsAnimating(false)
}, 1200)
}
const slideDown: () => void = () => {
// isAnimating is prechecked in isOpen effect
props.setIsAnimating(true)
invariant(gallery, 'curtain is not defined')
invariant(curtain, 'gallery is not defined')
_gsap.to(gallery, {
y: '100%',
ease: 'power3.inOut',
duration: 1
})
_gsap.to(curtain, {
opacity: 0,
duration: 1.2,
delay: 0.4
})
setTimeout(() => {
// cleanup
props.setScrollable(true)
props.setIsAnimating(false)
lastIndex = -1
}, 1400)
}
const galleryLoadImages: () => void = () => {
let activeImagesIndex: number[] = []
const _state = state()
const currentIndex = _state.index
const nextIndex = Math.min(currentIndex + 1, _state.length - 1)
const prevIndex = Math.max(currentIndex - 1, 0)
switch (navigateVector) {
case 'next':
activeImagesIndex = [nextIndex]
break
case 'prev':
activeImagesIndex = [prevIndex]
break
case 'none':
activeImagesIndex = [currentIndex, nextIndex, prevIndex]
break
}
setLoads(removeDuplicates(activeImagesIndex), true)
}
const changeSlide: (slide: number) => void = (slide) => {
// we are already in the gallery, don't need to
// check mounted or libLoaded
galleryLoadImages()
_swiper.slideTo(slide, 0)
}
// effects
onMount(() => {
window.addEventListener(
'touchstart',
() => {
loadGsap()
.then((g) => {
_gsap = g
})
.catch((e) => {
console.log(e)
})
loadSwiper()
.then((S) => {
invariant(galleryInner, 'galleryInner is not defined')
_swiper = new S(galleryInner, { spaceBetween: 20 })
_swiper.on('slideChange', ({ realIndex }) => {
setIndex(realIndex)
})
})
.catch((e) => {
console.log(e)
})
setLibLoaded(true)
},
{ once: true, passive: true }
)
mounted = true
})
createEffect(
on(
() => {
state()
},
() => {
const i = state().index
if (i === lastIndex)
return // change slide only when index is changed
else if (lastIndex === -1)
navigateVector = 'none' // lastIndex before set
else if (i < lastIndex)
navigateVector = 'prev' // set navigate vector for galleryLoadImages
else if (i > lastIndex)
navigateVector = 'next' // set navigate vector for galleryLoadImages
else navigateVector = 'none' // default
changeSlide(i) // change slide to new index
lastIndex = i // update last index
}
)
)
createEffect(
on(
() => {
props.isOpen()
},
() => {
if (props.isAnimating()) return
if (props.isOpen()) slideUp()
else slideDown()
},
{ defer: true }
)
)
return (
<>
<div ref={gallery} class="gallery">
<div ref={galleryInner} class="galleryInner">
<div class="swiper-wrapper">
<Show when={libLoaded()}>
<For each={props.ijs}>
{(ij, i) => (
<div class="swiper-slide">
<GalleryImage
load={loads[i()]}
ij={ij}
loadingText={_loadingText}
/>
</div>
)}
</For>
</Show>
</div>
</div>
<GalleryNav
closeText={props.closeText}
isAnimating={props.isAnimating}
setIsOpen={props.setIsOpen}
/>
</div>
<div ref={curtain} class="curtain" />
</>
)
}

View File

@@ -0,0 +1,69 @@
import { onMount, type JSX } from 'solid-js'
import invariant from 'tiny-invariant'
import type { ImageJSON } from '../resources'
import { useState } from '../state'
import { loadGsap } from '../utils'
export default function GalleryImage(props: {
children?: JSX.Element
load: boolean
ij: ImageJSON
loadingText: string
}): JSX.Element {
let img: HTMLImageElement | undefined
let loadingDiv: HTMLDivElement | undefined
let _gsap: typeof gsap
const [state] = useState()
onMount(() => {
loadGsap()
.then((g) => {
_gsap = g
})
.catch((e) => {
console.log(e)
})
img?.addEventListener(
'load',
() => {
invariant(img, 'ref must be defined')
invariant(loadingDiv, 'loadingDiv must be defined')
if (state().index !== props.ij.index) {
_gsap.set(img, { opacity: 1 })
_gsap.set(loadingDiv, { opacity: 0 })
} else {
_gsap.to(img, {
opacity: 1,
delay: 0.5,
duration: 0.5,
ease: 'power3.out'
})
_gsap.to(loadingDiv, { opacity: 0, duration: 0.5, ease: 'power3.in' })
}
},
{ once: true, passive: true }
)
})
return (
<>
<div class="slideContainer">
<img
ref={img}
{...(props.load && { src: props.ij.hiUrl })}
height={props.ij.hiImgH}
width={props.ij.hiImgW}
data-src={props.ij.hiUrl}
alt={props.ij.alt}
style={{ opacity: 0 }}
/>
<div ref={loadingDiv} class="loadingText">
{props.loadingText}
</div>
</div>
</>
)
}

View File

@@ -0,0 +1,46 @@
import { createMemo, type Accessor, type JSX, type Setter } from 'solid-js'
import { useState } from '../state'
import { expand } from '../utils'
export function capitalizeFirstLetter(str: string): string {
return str.charAt(0).toUpperCase() + str.slice(1)
}
export default function GalleryNav(props: {
children?: JSX.Element
closeText: string
isAnimating: Accessor<boolean>
setIsOpen: Setter<boolean>
}): JSX.Element {
// states
const [state] = useState()
const indexValue = createMemo(() => expand(state().index + 1))
const indexLength = createMemo(() => expand(state().length))
const onClick: () => void = () => {
if (props.isAnimating()) return
props.setIsOpen(false)
}
return (
<>
<div class="nav">
<div>
<span class="num">{indexValue()[0]}</span>
<span class="num">{indexValue()[1]}</span>
<span class="num">{indexValue()[2]}</span>
<span class="num">{indexValue()[3]}</span>
<span>/</span>
<span class="num">{indexLength()[0]}</span>
<span class="num">{indexLength()[1]}</span>
<span class="num">{indexLength()[2]}</span>
<span class="num">{indexLength()[3]}</span>
</div>
<div onClick={onClick} onKeyDown={onClick}>
{capitalizeFirstLetter(props.closeText)}
</div>
</div>
</>
)
}

View File

@@ -1,9 +0,0 @@
import { type ImageJSON } from '../resources'
import { initCollection } from './collection'
import { initGallery } from './gallery'
export function initMobile(ijs: ImageJSON[]): void {
initCollection(ijs)
initGallery(ijs)
}

View File

@@ -0,0 +1,52 @@
import { Show, createSignal, type JSX, type Setter } from 'solid-js'
import type { ImageJSON } from '../resources'
import Collection from './collection'
import Gallery from './gallery'
/**
* interfaces
*/
export interface MobileImage extends HTMLImageElement {
dataset: {
src: string
index: string
}
}
export default function Mobile(props: {
children?: JSX.Element
ijs: ImageJSON[]
closeText: string
loadingText: string
setScrollable: Setter<boolean>
}): JSX.Element {
// states
const [isOpen, setIsOpen] = createSignal(false)
const [isAnimating, setIsAnimating] = createSignal(false)
return (
<>
<Show when={props.ijs.length > 0}>
<Collection
ijs={props.ijs}
isAnimating={isAnimating}
isOpen={isOpen}
setIsOpen={setIsOpen}
/>
<Gallery
ijs={props.ijs}
closeText={props.closeText}
loadingText={props.loadingText}
isAnimating={isAnimating}
setIsAnimating={setIsAnimating}
isOpen={isOpen}
setIsOpen={setIsOpen}
setScrollable={props.setScrollable}
/>
</Show>
</>
)
}

View File

@@ -1,3 +0,0 @@
import { Watchable } from '../globalUtils'
export const mounted = new Watchable<boolean>(false)

View File

@@ -1,37 +0,0 @@
import { type Swiper } from 'swiper'
/**
* interfaces
*/
export interface MobileImage extends HTMLImageElement {
dataset: {
src: string
}
}
/**
* utils
*/
export function getRandom(min: number, max: number): number {
return Math.floor(Math.random() * (max - min + 1)) + min
}
export function onIntersection<T extends HTMLElement>(
element: T,
callback: (arg0: IntersectionObserverEntry[], arg1: IntersectionObserver) => void
): void {
new IntersectionObserver((entries, observer) => {
callback(entries, observer)
}).observe(element)
}
export function capitalizeFirstLetter(str: string): string {
return str.charAt(0).toUpperCase() + str.slice(1)
}
export async function loadSwiper(): Promise<typeof Swiper> {
const s = await import('swiper')
return s.Swiper
}

View File

@@ -10,7 +10,7 @@ export interface ImageJSON {
hiImgW: number
}
export async function initResources(): Promise<ImageJSON[]> {
export async function getImageJSON(): Promise<ImageJSON[]> {
if (document.title.split(' | ')[0] === '404') {
return [] // no images on 404 page
}

136
assets/ts/state.tsx Normal file
View File

@@ -0,0 +1,136 @@
import {
createContext,
createSignal,
useContext,
type Accessor,
type JSX,
type Setter
} from 'solid-js'
import invariant from 'tiny-invariant'
import { decrement, getThresholdSessionIndex, increment } from './utils'
/**
* interfaces and types
*/
export interface ThresholdRelated {
threshold: number
trailLength: number
}
export interface State {
index: number
length: number
threshold: number
trailLength: number
}
export type StateContextType = readonly [
Accessor<State>,
{
readonly setIndex: (index: number) => void
readonly incIndex: () => void
readonly decIndex: () => void
readonly incThreshold: () => void
readonly decThreshold: () => void
}
]
/**
* constants
*/
const thresholds: ThresholdRelated[] = [
{ threshold: 20, trailLength: 20 },
{ threshold: 40, trailLength: 10 },
{ threshold: 80, trailLength: 5 },
{ threshold: 140, trailLength: 5 },
{ threshold: 200, trailLength: 5 }
]
const makeStateContext: (
state: Accessor<State>,
setState: Setter<State>
) => StateContextType = (state: Accessor<State>, setState: Setter<State>) => {
return [
state,
{
setIndex: (index: number) => {
setState((s) => {
return { ...s, index }
})
},
incIndex: () => {
setState((s) => {
return { ...s, index: increment(s.index, s.length) }
})
},
decIndex: () => {
setState((s) => {
return { ...s, index: decrement(s.index, s.length) }
})
},
incThreshold: () => {
setState((s) => {
return { ...s, ...updateThreshold(s.threshold, thresholds, 1) }
})
},
decThreshold: () => {
setState((s) => {
return { ...s, ...updateThreshold(s.threshold, thresholds, -1) }
})
}
}
] as const
}
const StateContext = createContext<StateContextType>()
/**
* helper functions
*/
function updateThreshold(
currentThreshold: number,
thresholds: ThresholdRelated[],
stride: number
): ThresholdRelated {
const i = thresholds.findIndex((t) => t.threshold === currentThreshold) + stride
if (i < 0 || i >= thresholds.length) return thresholds[i - stride]
// storage the index so we can restore it even if we go to another page
sessionStorage.setItem('thresholdsIndex', i.toString())
return thresholds[i]
}
/**
* StateProvider
*/
export function StateProvider(props: {
children?: JSX.Element
length: number
}): JSX.Element {
const defaultState: State = {
index: -1,
// eslint-disable-next-line solid/reactivity
length: props.length,
threshold: thresholds[getThresholdSessionIndex()].threshold,
trailLength: thresholds[getThresholdSessionIndex()].trailLength
}
const [state, setState] = createSignal(defaultState)
// eslint-disable-next-line solid/reactivity
const contextValue = makeStateContext(state, setState)
return (
<StateContext.Provider value={contextValue}>{props.children}</StateContext.Provider>
)
}
/**
* use context
*/
export function useState(): StateContextType {
const uc = useContext(StateContext)
invariant(uc, 'undefined context')
return uc
}

View File

@@ -1,4 +1,10 @@
import { type Power3, type gsap } from 'gsap'
import { type gsap } from 'gsap'
/**
* types
*/
export type Vector = 'prev' | 'next' | 'none'
/**
* utils
@@ -16,9 +22,9 @@ export function expand(num: number): string {
return ('0000' + num.toString()).slice(-4)
}
export async function loadGsap(): Promise<[typeof gsap, typeof Power3]> {
export async function loadGsap(): Promise<typeof gsap> {
const g = await import('gsap')
return [g.gsap, g.Power3]
return g.gsap
}
export function getThresholdSessionIndex(): number {
@@ -31,27 +37,3 @@ export function removeDuplicates<T>(arr: T[]): T[] {
if (arr.length < 2) return arr // optimization
return [...new Set(arr)]
}
/**
* custom "reactive" object
*/
export class Watchable<T> {
constructor(private obj: T) {}
private readonly watchers: Array<(arg0: T) => void> = []
get(): T {
return this.obj
}
set(e: T): void {
this.obj = e
this.watchers.forEach((watcher) => {
watcher(this.obj)
})
}
addWatcher(watcher: (arg0: T) => void): void {
this.watchers.push(watcher)
}
}

View File

@@ -60,7 +60,7 @@ Then import the theme as a dependency adding the following line to the `module`
# config/_default/hugo.toml
[module]
[[module.imports]]
path = "github.com/Sped0n/bridget"
path = "github.com/Sped0n/bridget/v2"
```
If you want to upgrade the theme, just run:
@@ -112,6 +112,8 @@ menu:
identifier: Erwitt
title: Erwitt
unifiedAlt: '© Elliott Erwitt'
_build:
publishResources: false
---
```
@@ -129,6 +131,8 @@ unifiedAlt: '© Elliott Erwitt'
- `unifiedAlt` is **optional**, If you left it empty, the alt attribute of the image will default to its file name; if it is set, the alt attributes of all images will be unified to the value you have set;
- `publishResources` is **optional but recommended**, setting it to false will hide unprocessed images in the `public` directory. By default, Hugos value for this option is true, which can potentially result in source image leakage.
- If this is a **showcase** page, simply place the images in the same directory as index.md.
- If this is an **information** page, you can continue writing the information you want to display in index.md.
@@ -158,9 +162,9 @@ We will focus on introducing the part about `theme as module`, detailed comments
```toml
# theme as module
[module]
replacements = "github.com/Sped0n/bridget -> ../.."
replacements = "github.com/Sped0n/bridget/v2 -> ../.."
[[module.imports]]
path = "github.com/Sped0n/bridget"
path = "github.com/Sped0n/bridget/v2"
```
- If you have _installation with Git_
@@ -178,14 +182,6 @@ path = "github.com/Sped0n/bridget"
Detailed description in the comments.
> ⚠️⚠️⚠️
>
> Only thing that you need to pay **extra attention** is the [`bundled`](https://github.com/Sped0n/bridget/blob/1e2f1fadde9c16989eef1ab771f2ac8463dec5a4/exampleSite/config/_default/params.toml#L6) option, please read the corresponding doc and set it as your need.
>
> For users who have installation with module, please always set this option to `true`, unless you know what you are doing.
>
> Or you might get the error related to `node_modules/swiper/swiper.scss`.
### `sitemap.toml`
https://gohugo.io/templates/sitemap-template/#configuration
@@ -193,23 +189,9 @@ https://gohugo.io/templates/sitemap-template/#configuration
## Customization (AKA for developer)
> Before heading to this section, please make sure you have **installation with Git**.
>
> You can use any package manager you want (npm/pnpm/yarn/bun).
### Option 1: _it just works_ way
> If you want to modify js/ts file, please use option 2.
1. Use hugo create a site and move the bridget theme into the theme directory.
2. Run `npm install` in the _bridget theme root dir_, not _your hugo site root dir_.
3. After the command is done, copy the `node_modules` dir from _bridget theme root dir_ to _your hugo site root dir_.
4. In _your hugo site root dir_, write/modify configuration files according to your needs, remember to set `bundled` option to `false`, so hugo will not use prebuilt css file.
5. Run `hugo server` in _your hugo site root dir_, and you are good to go.
### Option 2: recommended way
1. Use hugo create a site and move the bridget theme into the theme directory.
2. Run `npm install` in the _bridget theme root dir_, not _your hugo site root dir_.
3. Run `npm run dev` in the _bridget theme root dir_, we will use content in exampleSite to debug.
4. Make your customization.
5. After modification, run `npm run build` in the _bridget theme root dir_ to build artifacts.
6. In _your hugo site root dir_, write/modify configuration files according to your needs, remember to set `bundled` option to `true`, so hugo will use the artifacts you built in step 5.
7. Run `hugo server` in _your hugo site root dir_, and you are good to go.
- run `pnpm install` to install neceessary dependencies.
- run `pnpm run dev` to host a dev server.
- when youre ready, run `pnpm run build` to update artifacts.

View File

@@ -22,6 +22,6 @@ defaultContentLanguage = 'en'
# theme as module
[module]
replacements = "github.com/Sped0n/bridget -> ../.." # deploy with local dir WARN: delete this line if you want to deploy with git
replacements = "github.com/Sped0n/bridget/v2 -> ../.." # deploy with local dir WARN: delete this line if you want to deploy with git
[[module.imports]]
path = "github.com/Sped0n/bridget" # deploy with git (recommended) WARN: you should also set `bundled` to true in params.toml !!!
path = "github.com/Sped0n/bridget/v2" # deploy with git (recommended) WARN: you should also set `bundled` to true in params.toml !!!

View File

@@ -1,9 +1,5 @@
# description of the site (will be placed in meta)
description = "Bridget is a minimal Hugo theme designed for photographers/visual artists."
# use bundled js and css
# * if you want to build the js and css from scratch, set this to false and run `npm install` and `npm run build`
# * tldr: set this to false if you want to develop and edit the js and css
bundled = false
# whether to use favicon resource links
# generate these with https://realfavicongenerator.net

View File

@@ -8,4 +8,6 @@ menu:
identifier: Erwitt
title: Erwitt
unifiedAlt: '© Elliott Erwitt'
_build:
publishResources: false
---

View File

@@ -8,4 +8,6 @@ menu:
identifier: Gruyaert
title: Gruyaert
unifiedAlt: '© Harry Gruyaert'
_build:
publishResources: false
---

View File

@@ -8,11 +8,13 @@ menu:
identifier: Info
title: Info
unifiedAlt: ''
_build:
publishResources: false
---
Bridget is a _minimal_ Hugo theme designed for photographers/visual artists.
Bridget is a _minimal_ Hugo theme designed for photographers/visual artists, powered by <u>[Solid.js](https://www.solidjs.com)</u>.
The inspiration for this theme came from a video by <u>[Hyperlexed](https://www.youtube.com/@Hyperplexed)</u>, which can be found <u>[here](https://www.youtube.com/watch?v=Jt3A2lNN2aE)</u>. Initially, it was developed using no third-party dependencies. However, after website designer <u>[Tyler McRobert](https://tylermcrobert.com)</u> made the source code publicly available, I realized that I have invented many unnecessary wheels, and this project was modified to porting the original design to hugo while focusing on _performance_.
The inspiration for this theme came from a video by <u>[Hyperlexed](https://www.youtube.com/@Hyperplexed)</u>, which can be found <u>[here](https://www.youtube.com/watch?v=Jt3A2lNN2aE)</u>. Initially, it was developed using no third-party dependencies. However, after website designer <u>[Tyler McRobert](https://tylermcrobert.com)</u> made the source code publicly available, I realized that I have invented many unnecessary wheels, and this project was modified to porting the original design to Hugo while focusing on _performance_.
Once again, great shout out to <u>[Tyler McRobert](https://tylermcrobert.com)</u> for his inspiration to this project.
@@ -20,4 +22,4 @@ Once again, great shout out to <u>[Tyler McRobert](https://tylermcrobert.com)</u
Original site design by <u>[Tyler McRobert](https://tylermcrobert.com)</u>.
&copy; {{< year >}} <u>[Spedon](https://github.com/Sped0n)</u> | Powered by [Hugo](https://gohugo.io)
&copy; {{< year >}} <u>[Spedon](https://github.com/Sped0n)</u> | Built with Hugo

View File

@@ -8,4 +8,6 @@ menu:
identifier: Webb
title: Webb
unifiedAlt: '© Alex Webb'
_build:
publishResources: false
---

2
go.mod
View File

@@ -1,3 +1,3 @@
module github.com/Sped0n/bridget
module github.com/Sped0n/bridget/v2
go 1.21.3

View File

@@ -9,4 +9,4 @@ other = "schwelle"
[404]
other = "seite nicht gefunden"
[loading]
other = "lade"
other = "lade..."

View File

@@ -9,4 +9,4 @@ other = "threshold"
[404]
other = "page not found"
[loading]
other = "loading"
other = "loading..."

View File

@@ -9,4 +9,4 @@ other = "umbral"
[404]
other = "página no encontrada"
[loading]
other = "cargando"
other = "cargando..."

View File

@@ -9,4 +9,4 @@ other = "seuil"
[404]
other = "page non trouvée"
[loading]
other = "chargement"
other = "chargement..."

View File

@@ -9,4 +9,4 @@ other = "soglia"
[404]
other = "pagina non trovata"
[loading]
other = "caricamento"
other = "caricamento..."

View File

@@ -9,4 +9,4 @@ other = "しきい値"
[404]
other = "ページが見つかりません"
[loading]
other = "読み込み中"
other = "読み込み中..."

View File

@@ -9,4 +9,4 @@ other = "임계값"
[404]
other = "페이지를 찾을 수 없습니다"
[loading]
other = "로딩중"
other = "로딩중..."

View File

@@ -9,4 +9,4 @@ other = "阈值"
[404]
other = "页面不存在"
[loading]
other = "加载中"
other = "加载中..."

View File

@@ -9,4 +9,4 @@ other = "閾值"
[404]
other = "找不到頁面"
[loading]
other = "載入中"
other = "載入中..."

View File

@@ -9,4 +9,4 @@ other = "閾值"
[404]
other = "找不到頁面"
[loading]
other = "載入中"
other = "載入中..."

View File

@@ -9,4 +9,4 @@ other = "阈值"
[404]
other = "页面不存在"
[loading]
other = "加载中"
other = "加载中..."

View File

@@ -9,4 +9,4 @@ other = "閾值"
[404]
other = "找不到頁面"
[loading]
other = "載入中"
other = "載入中..."

View File

@@ -7,16 +7,8 @@
{{- $style = dict "Context" . "ToCSS" $options "Inline" true | merge $style -}}
{{- partial "plugin/style.html" $style -}}
{{/* main style */}}
{{- if (site.Params.bundled | default true) -}}
{{- $style := dict "Link" "/bundled/css/style.min.css" "Defer" true -}}
{{- partial "plugin/style.html" $style -}}
{{- else -}}
{{- $style := dict "Source" "scss/style.scss" "Fingerprint" $fingerprint -}}
{{- $options := dict "targetPath" "css/style.css" "enableSourceMap" true "includePaths" (slice "node_modules") -}}
{{- $style = dict "Context" . "ToCSS" $options "Minify" hugo.IsProduction "Defer" true | merge $style -}}
{{- partial "plugin/style.html" $style -}}
{{- end -}}
{{- $style := dict "Link" "/bundled/css/main.css" "Defer" true -}}
{{- partial "plugin/style.html" $style -}}
{{/* fuck safari */}}
<script>

View File

@@ -1,19 +1,21 @@
{
"name": "bridget",
"version": "v1.0.0",
"type": "module",
"description": "bridget theme source file",
"packageManager": "pnpm@8.10.2",
"private": true,
"sideEffects": false,
"scripts": {
"vite": "vite build --no-watch",
"lint": "eslint . --fix && prettier --write .",
"lint:check": "eslint . && prettier . --check",
"dev": "run-p rollup:dev hugo:dev",
"build": "rm -f ./static/bundled/js/* && run-s rollup:build hugo:build && yes | cp -rf ./exampleSite/public/css/*.css ./static/bundled/css",
"server": "run-p rollup:server hugo:server",
"rollup:build": "rollup -c --environment BUILD:production",
"rollup:server": "rollup -c --watch --environment BUILD:production",
"rollup:dev": "rollup -c --watch --environment BUILD:development",
"dev": "run-p vite:dev hugo:dev",
"build": "run-s vite:build hugo:build",
"server": "run-p vite:server hugo:server",
"vite:build": "vite build --no-watch --minify terser",
"vite:server": "vite build --minify terser",
"vite:dev": "vite build --mode development --minify false",
"hugo:build": "hugo --logLevel info --source=exampleSite --gc",
"hugo:preview": "hugo --logLevel info --source=exampleSite -D --gc",
"hugo:dev": "hugo server --source=exampleSite --gc -D --disableFastRender --watch --logLevel info",
@@ -37,9 +39,9 @@
},
"homepage": "https://github.com/Sped0n/bridget#readme",
"devDependencies": {
"@typescript-eslint/eslint-plugin": "^6.20.0",
"@typescript-eslint/parser": "^6.20.0",
"eslint": "^8.56.0",
"@typescript-eslint/eslint-plugin": "^6.21.0",
"@typescript-eslint/parser": "^6.21.0",
"eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0",
"eslint-config-standard": "^17.1.0",
"eslint-config-standard-with-typescript": "^43.0.1",
@@ -48,18 +50,21 @@
"eslint-plugin-n": "^16.6.2",
"eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-promise": "^6.1.1",
"eslint-plugin-solid": "^0.13.1",
"npm-run-all": "^4.1.5",
"prettier": "3.2.4",
"prettier": "3.2.5",
"prettier-plugin-go-template": "^0.0.15",
"prettier-plugin-organize-imports": "^3.2.4",
"typescript": "^5.3.3"
"sass": "^1.71.1",
"terser": "^5.29.2",
"typescript": "^5.4.2",
"vite": "^5.1.6",
"vite-plugin-solid": "^2.10.2"
},
"dependencies": {
"gsap": "^3.12.5",
"swiper": "^11.0.5",
"rollup": "^4.9.6",
"@rollup/plugin-node-resolve": "^15.2.3",
"@rollup/plugin-terser": "^0.4.4",
"@rollup/plugin-typescript": "^11.1.6"
"solid-js": "^1.8.15",
"swiper": "^11.0.7",
"tiny-invariant": "^1.3.3"
}
}

1428
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,28 +0,0 @@
import resolve from '@rollup/plugin-node-resolve'
import terser from '@rollup/plugin-terser'
import typescript from '@rollup/plugin-typescript'
export default {
input: './assets/ts/main.ts',
output: {
dir: './static/bundled/js',
format: 'es',
chunkFileNames: '[hash:6].js',
compact: true
},
plugins: [
resolve({
moduleDirectories: ['node_modules']
}),
typescript({ tsconfig: './tsconfig.json' }),
process.env.BUILD === 'production' &&
terser({
compress: {
passes: 3
},
output: {
comments: false
}
})
]
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1 +0,0 @@
import{W as e,s as t,n,f as a,l as s,g as c,h as o,r as i,c as d,i as r}from"./main.js";const l=new e(!1);function m(e,t){return Math.floor(Math.random()*(t-e+1))+e}let p,u,g,h,v,E,y,f=-1,N=[],w=[],x=[],L=!1;function B(){r.get()||(r.set(!0),x[t.get().index].scrollIntoView({block:"center",behavior:"auto"}),E.to(u,{y:"100%",ease:y.easeInOut,duration:1}),E.to(g,{opacity:0,duration:1.2,delay:.4}),setTimeout((()=>{a.set(!0),r.set(!1),f=-1}),1600))}let I=[];function T(e){c(e),!r.get()&&L&&(r.set(!0),E.to(g,{opacity:1,duration:1}),E.to(u,{y:0,ease:y.easeInOut,duration:1,delay:.4}),setTimeout((()=>{a.set(!1),r.set(!1)}),1400))}function C(e){(function(e){!function(e){const t=document.createElement("div");t.className="collection";for(const[n,a]of e.entries()){const e=0!==n?m(-25,25):0,s=0!==n?m(-30,30):0,c=document.createElement("img");c.dataset.src=a.loUrl,c.height=a.loImgH,c.width=a.loImgW,c.alt=a.alt,c.style.transform=`translate3d(${e}%, ${s-50}%, 0)`,t.append(c)}d.append(t)}(e);const t=document.getElementsByClassName("collection").item(0);l.addWatcher((e=>{e?t.classList.remove("hidden"):t.classList.add("hidden")})),I=Array.from(t.getElementsByTagName("img")),I.forEach(((e,t)=>{var n,a;t<5&&(e.src=e.dataset.src),e.addEventListener("click",(()=>{T(t)}),{passive:!0}),e.addEventListener("keydown",(()=>{T(t)}),{passive:!0}),n=e,a=(e,n)=>{e.every((e=>e.intersectionRatio<=0||(t+5<I.length&&(I[t+5].src=I[t+5].dataset.src),n.disconnect(),!1)))},new IntersectionObserver(((e,t)=>{a(e,t)})).observe(n)}))})(e),function(e){!function(e){const t=document.createElement("div");t.className="swiper-wrapper";const n=d.dataset.loading+"...";for(const a of e){const e=document.createElement("div");e.className="swiper-slide";const s=document.createElement("div");s.className="loadingText",s.innerText=n;const c=document.createElement("img");c.dataset.src=a.hiUrl,c.height=a.hiImgH,c.width=a.hiImgW,c.alt=a.alt,c.classList.add("hide"),c.addEventListener("load",(()=>{c.classList.remove("hide"),s.classList.add("hide")}),{once:!0,passive:!0});const o=document.createElement("div");o.className="slideContainer",o.append(c),o.append(s),e.append(o),t.append(e)}const a=document.createElement("div");a.className="galleryInner",a.append(t);const s=document.createElement("div");s.insertAdjacentHTML("afterbegin",'<span class="num"></span><span class="num"></span><span class="num"></span><span class="num"></span>\n <span>/</span>\n <span class="num"></span><span class="num"></span><span class="num"></span><span class="num"></span>');const c=document.createElement("div");var o;c.innerText=(o=d.dataset.close).charAt(0).toUpperCase()+o.slice(1),c.addEventListener("click",(()=>{B()}),{passive:!0}),c.addEventListener("keydown",(()=>{B()}),{passive:!0});const i=document.createElement("div");i.className="nav",i.append(s,c);const r=document.createElement("div");r.className="gallery",r.append(a),r.append(i);const l=document.createElement("div");l.className="curtain",d.append(r,l)}(e),N=Array.from(document.getElementsByClassName("nav").item(0)?.getElementsByClassName("num")??[]),p=document.getElementsByClassName("galleryInner").item(0),u=document.getElementsByClassName("gallery").item(0),g=document.getElementsByClassName("curtain").item(0),w=Array.from(u.getElementsByTagName("img")),x=Array.from(document.getElementsByClassName("collection").item(0)?.getElementsByTagName("img")??[]),t.addWatcher((e=>{var a;e.index!==f&&(-1===f?n.set("none"):e.index<f?n.set("prev"):e.index>f?n.set("next"):n.set("none"),a=e.index,function(){let e=[];const a=t.get().index,s=Math.min(a+1,t.get().length-1),c=Math.max(a-1,0);switch(n.get()){case"next":e=[s];break;case"prev":e=[c];break;case"none":e=[a,s,c]}i(e).forEach((e=>{const t=w[e];t.src!==t.dataset.src&&(t.src=t.dataset.src)}))}(),h.slideTo(a,0),function(){const e=o(t.get().index+1),n=o(t.get().length);N.forEach(((t,a)=>{t.innerText=a<4?e[a]:n[a-4]}))}(),f=e.index)})),l.addWatcher((e=>{e&&a.set(!0)})),window.addEventListener("touchstart",(()=>{s().then((e=>{E=e[0],y=e[1]})).catch((e=>{console.log(e)})),async function(){return(await import("./f6JTi3.js")).Swiper}().then((e=>{v=e,h=new v(p,{spaceBetween:20}),h.on("slideChange",(({realIndex:e})=>{c(e)}))})).catch((e=>{console.log(e)})),L=!0}),{once:!0,passive:!0}),l.set(!0)}(e)}export{C as initMobile};

File diff suppressed because one or more lines are too long

View File

@@ -9,7 +9,9 @@
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"moduleResolution": "node"
"moduleResolution": "node",
"jsx": "preserve",
"jsxImportSource": "solid-js"
},
"$schema": "https://json.schemastore.org/tsconfig",
"display": "Recommended"

30
vite.config.ts Normal file
View File

@@ -0,0 +1,30 @@
import { defineConfig } from 'vite'
import solidPlugin from 'vite-plugin-solid'
export default defineConfig({
plugins: [solidPlugin()],
build: {
outDir: './static/bundled',
watch: {
include: 'assets/**'
},
rollupOptions: {
input: './assets/ts/main.tsx',
output: {
format: 'es',
entryFileNames: 'js/[name].js',
chunkFileNames: 'js/[hash:6].js',
assetFileNames: '[ext]/[name].[ext]',
compact: true
}
},
terserOptions: {
compress: {
passes: 3
},
output: {
comments: false
}
}
}
})