mirror of
https://github.com/Sped0n/bridget.git
synced 2026-04-16 02:59:31 -07:00
Compare commits
162 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b7ed5a2834 | ||
|
|
5ba7d77c07 | ||
|
|
af82026d1a | ||
|
|
b5ef661e1c | ||
|
|
883eec1a3b | ||
|
|
b027cd03cf | ||
|
|
aa34979bb4 | ||
|
|
08bbfaa3ba | ||
|
|
b8c6ce2b04 | ||
|
|
27083c0336 | ||
|
|
082458b2aa | ||
|
|
1c05eb2633 | ||
|
|
9016cfb035 | ||
|
|
d9b4100d17 | ||
|
|
082f5f4961 | ||
|
|
9d91becbd5 | ||
|
|
2e51b7eb89 | ||
|
|
7dd8c2242c | ||
|
|
2a10e4944e | ||
|
|
7893586d24 | ||
|
|
5a51f83654 | ||
|
|
e4d5ac4389 | ||
|
|
ea3d58760b | ||
|
|
4812cdb191 | ||
|
|
b93b8d3ac6 | ||
|
|
a7bc6b2df5 | ||
|
|
1cfbc8ac28 | ||
|
|
c4cea2648e | ||
|
|
26bdddc5ff | ||
|
|
437bbf17e0 | ||
|
|
2a715327f6 | ||
|
|
c111de15b1 | ||
|
|
1b9826f582 | ||
|
|
6a3ce498a9 | ||
|
|
8d48e6347e | ||
|
|
4599a5dfc2 | ||
|
|
7536288baa | ||
|
|
e12c32388b | ||
|
|
4198a5fa90 | ||
|
|
089e9b285a | ||
|
|
60e19fed00 | ||
|
|
626433e67d | ||
|
|
cb5080ce41 | ||
|
|
e2f8317669 | ||
|
|
5d9e32f62b | ||
|
|
b96ecd6042 | ||
|
|
5b7ec62106 | ||
|
|
3ca4a0d803 | ||
|
|
a8d8802d9f | ||
|
|
28782217f1 | ||
|
|
cfcda29524 | ||
|
|
fb498971c7 | ||
|
|
9dbb3cb624 | ||
|
|
129f26dd54 | ||
|
|
d1f9b843c3 | ||
|
|
a7b5ec45ed | ||
|
|
5d82276734 | ||
|
|
b0c4fa8ea7 | ||
|
|
a6f983de5d | ||
|
|
3bc232638a | ||
|
|
67944df12f | ||
|
|
e82fe6cab2 | ||
|
|
819df6b2ed | ||
|
|
dfef87ca55 | ||
|
|
477b6d748a | ||
|
|
31a59c5e9e | ||
|
|
e8cdd12151 | ||
|
|
a8bc17ca12 | ||
|
|
4d04fe1945 | ||
|
|
bafd2aa3b3 | ||
|
|
acf50d10d7 | ||
|
|
30a6a3bd23 | ||
|
|
d808782afd | ||
|
|
5327d7c585 | ||
|
|
d7e7fc68ba | ||
|
|
7b637637a1 | ||
|
|
4a2ef258ba | ||
|
|
b2c1c96f56 | ||
|
|
6d98671856 | ||
|
|
0b7605b3ff | ||
|
|
c9ca9c682a | ||
|
|
417a00b9c4 | ||
|
|
d7a4345f53 | ||
|
|
2e47be5635 | ||
|
|
0e6fc9384f | ||
|
|
81eba485d4 | ||
|
|
b1553a9a8b | ||
|
|
66735f4c65 | ||
|
|
3ff49b1106 | ||
|
|
633f6a40e2 | ||
|
|
1ff94546e0 | ||
|
|
e081e139fc | ||
|
|
875113448b | ||
|
|
53a44776de | ||
|
|
ecdaebb6cd | ||
|
|
1a02360214 | ||
|
|
febbd7d45d | ||
|
|
1acf24a519 | ||
|
|
e2afe91131 | ||
|
|
3e51b96825 | ||
|
|
c84b4cf234 | ||
|
|
997207fa90 | ||
|
|
f7d2c7962c | ||
|
|
0812a5a6b8 | ||
|
|
7fd971eb13 | ||
|
|
881b0b6490 | ||
|
|
50d7b14133 | ||
|
|
872d23ad13 | ||
|
|
eeca83a74b | ||
|
|
3170f5b65a | ||
|
|
bf1c5e21bc | ||
|
|
d08e2c92b8 | ||
|
|
ba07636f8f | ||
|
|
a98c6a4a60 | ||
|
|
c1414bbfc5 | ||
|
|
22b81a8e1d | ||
|
|
8432540bde | ||
|
|
bb056d9c4f | ||
|
|
6bf10c103f | ||
|
|
1b1aea5047 | ||
|
|
5132e36e87 | ||
|
|
964c1802d3 | ||
|
|
0af4e20720 | ||
|
|
1f65b08b56 | ||
|
|
9bfaac25f5 | ||
|
|
794b5c0ea6 | ||
|
|
2fd34c2f7f | ||
|
|
44d7da48e3 | ||
|
|
49e9f904e2 | ||
|
|
1a3fade5fc | ||
|
|
4e4c32384a | ||
|
|
88b518b773 | ||
|
|
4eece4684a | ||
|
|
4f9b5ff311 | ||
|
|
c123fd45be | ||
|
|
09506d2d4e | ||
|
|
08350cfb9f | ||
|
|
cf2f36d232 | ||
|
|
480df04e55 | ||
|
|
ae96a07793 | ||
|
|
17ef30c18b | ||
|
|
0717ce1051 | ||
|
|
9fa1b718b8 | ||
|
|
8c6b38bb49 | ||
|
|
80c784262b | ||
|
|
ae1a08eb82 | ||
|
|
ff1a76eef8 | ||
|
|
d9452ca8d2 | ||
|
|
3fef127666 | ||
|
|
ed3b08dce2 | ||
|
|
8ba41fc32c | ||
|
|
0f537630e8 | ||
|
|
3f60289381 | ||
|
|
a3c2310375 | ||
|
|
b9de1b9c70 | ||
|
|
ac5fb33f41 | ||
|
|
ba044a2147 | ||
|
|
b34a85fa92 | ||
|
|
7b0cd6aae7 | ||
|
|
bc04ef37bb | ||
|
|
a5807f7625 | ||
|
|
0c16bab065 |
@@ -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",
|
||||
|
||||
33
.github/ISSUE_TEMPLATE/bug_report.md
vendored
33
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -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
115
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
Normal 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
5
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal 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
|
||||
19
.github/ISSUE_TEMPLATE/feature_request.md
vendored
19
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -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.
|
||||
60
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
Normal file
60
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
Normal 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
|
||||
2
.github/dependabot.yml
vendored
2
.github/dependabot.yml
vendored
@@ -8,5 +8,5 @@ updates:
|
||||
- package-ecosystem: 'npm' # See documentation for possible values
|
||||
directory: '/' # Location of package manifests
|
||||
schedule:
|
||||
interval: 'daily'
|
||||
interval: 'weekly'
|
||||
open-pull-requests-limit: 1000
|
||||
|
||||
26
.github/workflows/build.yml
vendored
26
.github/workflows/build.yml
vendored
@@ -18,13 +18,13 @@ jobs:
|
||||
outputs:
|
||||
any_changed: ${{ steps.changed-files-specific.outputs.any_changed }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Get changed files in scope
|
||||
id: changed-files-specific
|
||||
uses: tj-actions/changed-files@v40
|
||||
uses: tj-actions/changed-files@v41
|
||||
with:
|
||||
files: |
|
||||
package.json
|
||||
@@ -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: Update bundled artifacts
|
||||
commit-message: Update bundled artifacts
|
||||
branch: update-artifacts-${{ steps.version.outputs.builddate }}
|
||||
base: main
|
||||
commit_message: 'ci: update bundled artifacts [skip ci]'
|
||||
|
||||
23
.github/workflows/eslint.yml
vendored
23
.github/workflows/eslint.yml
vendored
@@ -17,10 +17,12 @@ jobs:
|
||||
name: Lint
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ secrets.PAT || github.token }}
|
||||
|
||||
- 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'
|
||||
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -21,4 +21,7 @@ $RECYCLE.BIN/
|
||||
|
||||
# Hugo
|
||||
.hugo_build.lock
|
||||
jsconfig.json
|
||||
jsconfig.json
|
||||
|
||||
# css map
|
||||
*.css.map
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
node_modules
|
||||
static
|
||||
exmapleSite
|
||||
*.yaml
|
||||
*.yml
|
||||
single.json
|
||||
pnpm-lock.yaml
|
||||
|
||||
@@ -60,7 +60,7 @@ representative at an online or offline event.
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported to the community leaders responsible for enforcement at
|
||||
hi@sped0nwen.com.
|
||||
hi@sped0n.com.
|
||||
All complaints will be reviewed and investigated promptly and fairly.
|
||||
|
||||
All community leaders are obligated to respect the privacy and security of the
|
||||
|
||||
17
README.md
17
README.md
@@ -2,15 +2,15 @@
|
||||
|
||||
 
|
||||
|
||||
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).
|
||||
|
||||
It’s based on the https://github.com/tylermcrobert/bridget-pictures-www.
|
||||
Based on the https://github.com/tylermcrobert/bridget-pictures-www.
|
||||
|
||||

|
||||
|
||||
## [Demo Site](https://bridget-demo.sped0nwen.com)
|
||||
## [Demo Site](https://bridget-demo.sped0n.com)
|
||||
|
||||
To see this theme in action, here is a live [demo site](https://bridget-demo.sped0nwen.com) which is rendered with **Bridget** theme.
|
||||
To see this theme in action, here is a live [demo site](https://bridget-demo.sped0n.com) which is rendered with **Bridget** theme.
|
||||
|
||||
## Getting Started
|
||||
|
||||
@@ -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
|
||||
@@ -39,6 +38,7 @@ Bridget supports the following languages:
|
||||
- Deutsch
|
||||
- Spanish
|
||||
- Italian
|
||||
- Tamil
|
||||
- [Contribute with a new language](https://github.com/Sped0n/bridget/pulls)
|
||||
|
||||
## Credits
|
||||
@@ -46,3 +46,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
|
||||
|
||||
@@ -55,3 +55,11 @@
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Noto Sans';
|
||||
src: url('/lib/fonts/NotoSansTamil-Regular.woff2') format('woff2');
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
.info {
|
||||
article {
|
||||
padding: var(--space-standard);
|
||||
max-width: 25em;
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
}
|
||||
|
||||
@media (max-width: $tablet), (hover: none) {
|
||||
.info {
|
||||
article {
|
||||
margin-top: var(--nav-height);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,23 @@
|
||||
.container {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
z-index: 0;
|
||||
$tablet: map-get($breakpoints, 'tablet') - 1;
|
||||
|
||||
width: 100vw;
|
||||
height: var(--window-height);
|
||||
@media (max-width: $tablet), (hover: none) {
|
||||
.container {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
z-index: 0;
|
||||
|
||||
overflow-y: scroll;
|
||||
overflow-x: hidden;
|
||||
background: white;
|
||||
width: 100vw;
|
||||
height: var(--window-height);
|
||||
|
||||
overscroll-behavior: none;
|
||||
-webkit-overflow-scrolling: none;
|
||||
}
|
||||
|
||||
.disableScroll {
|
||||
pointer-events: none;
|
||||
overflow-y: scroll;
|
||||
overflow-x: hidden;
|
||||
background: white;
|
||||
|
||||
overscroll-behavior: none;
|
||||
-webkit-overflow-scrolling: none;
|
||||
}
|
||||
|
||||
.disableScroll {
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,18 +16,30 @@
|
||||
.galleryInner {
|
||||
flex: 1;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.swiper-slide {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
.swiper-slide {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.loadingText {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate3d(-50%, -50%, 0);
|
||||
}
|
||||
|
||||
.slideContainer {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.nav {
|
||||
|
||||
@@ -9,3 +9,4 @@
|
||||
|
||||
@import '_partial/nav';
|
||||
@import '_partial/article';
|
||||
@import '_partial/container';
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
import { scrollable } from './mobile/scroll'
|
||||
|
||||
export let container: HTMLDivElement
|
||||
|
||||
export function initContainer(): void {
|
||||
container = document.getElementsByClassName('container').item(0) as HTMLDivElement
|
||||
scrollable.addWatcher((o) => {
|
||||
if (o) {
|
||||
container.classList.remove('disableScroll')
|
||||
} else {
|
||||
container.classList.add('disableScroll')
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
import { container } from '../container'
|
||||
|
||||
import { active } from './stage'
|
||||
|
||||
/**
|
||||
* 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')
|
||||
}
|
||||
})
|
||||
}
|
||||
52
assets/ts/desktop/customCursor.tsx
Normal file
52
assets/ts/desktop/customCursor.tsx
Normal 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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
import { type ImageJSON } from '../resources'
|
||||
|
||||
import { initCustomCursor } from './customCursor'
|
||||
import { initStage } from './stage'
|
||||
import { initStageNav } from './stageNav'
|
||||
|
||||
export function initDesktop(ijs: ImageJSON[]): void {
|
||||
initCustomCursor()
|
||||
initStage(ijs)
|
||||
initStageNav()
|
||||
}
|
||||
91
assets/ts/desktop/layout.tsx
Normal file
91
assets/ts/desktop/layout.tsx
Normal 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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
66
assets/ts/desktop/nav.tsx
Normal file
66
assets/ts/desktop/nav.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
import { createEffect } from 'solid-js'
|
||||
|
||||
import { useState } from '../state'
|
||||
import { expand } from '../utils'
|
||||
|
||||
/**
|
||||
* constants
|
||||
*/
|
||||
|
||||
// threshold div
|
||||
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')
|
||||
.item(0) as HTMLButtonElement
|
||||
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[]
|
||||
|
||||
/**
|
||||
* helper functions
|
||||
*/
|
||||
|
||||
function updateThresholdText(thresholdValue: string): void {
|
||||
thresholdDispNums.forEach((e: HTMLSpanElement, i: number) => {
|
||||
e.innerText = thresholdValue[i]
|
||||
})
|
||||
}
|
||||
|
||||
function updateIndexText(indexValue: string, indexLength: string): void {
|
||||
indexDispNums.forEach((e: HTMLSpanElement, i: number) => {
|
||||
if (i < 4) {
|
||||
e.innerText = indexValue[i]
|
||||
} else {
|
||||
e.innerText = indexLength[i - 4]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
@@ -1,294 +0,0 @@
|
||||
import { type Power3, type gsap } from 'gsap'
|
||||
|
||||
import { container } from '../container'
|
||||
import { type ImageJSON } from '../resources'
|
||||
import { incIndex, state } from '../state'
|
||||
import { Watchable, decrement, increment, loadGsap } from '../utils'
|
||||
|
||||
/**
|
||||
* types
|
||||
*/
|
||||
|
||||
export interface HistoryItem {
|
||||
i: number
|
||||
x: number
|
||||
y: number
|
||||
}
|
||||
|
||||
/**
|
||||
* variables
|
||||
*/
|
||||
|
||||
let imgs: HTMLImageElement[] = []
|
||||
let last = { x: 0, y: 0 }
|
||||
export const cordHist = new Watchable<HistoryItem[]>([])
|
||||
export const isOpen = new Watchable<boolean>(false)
|
||||
export const isAnimating = new Watchable<boolean>(false)
|
||||
export const active = new Watchable<boolean>(false)
|
||||
|
||||
let _gsap: typeof gsap
|
||||
let _Power3: typeof Power3
|
||||
|
||||
let gsapLoaded = false
|
||||
|
||||
/**
|
||||
* getter
|
||||
*/
|
||||
|
||||
function getElTrail(): HTMLImageElement[] {
|
||||
return cordHist.get().map((item) => imgs[item.i])
|
||||
}
|
||||
|
||||
function getElTrailCurrent(): HTMLImageElement[] {
|
||||
return getElTrail().slice(-state.get().trailLength)
|
||||
}
|
||||
|
||||
function getElTrailInactive(): HTMLImageElement[] {
|
||||
const elTrailCurrent = getElTrailCurrent()
|
||||
return elTrailCurrent.slice(0, elTrailCurrent.length - 1)
|
||||
}
|
||||
|
||||
function getElCurrent(): HTMLImageElement {
|
||||
const elTrail = getElTrail()
|
||||
return elTrail[elTrail.length - 1]
|
||||
}
|
||||
|
||||
function getElNextSeven(): HTMLImageElement[] {
|
||||
const c = cordHist.get()
|
||||
const s = state.get()
|
||||
const c0 = c.length > 0 ? c[c.length - 1].i : s.index
|
||||
const els = []
|
||||
for (let i = 0; i < 7; i++) {
|
||||
els.push(imgs[increment(c0 + i, s.length)])
|
||||
}
|
||||
return els
|
||||
}
|
||||
|
||||
function getElPrev(): HTMLImageElement {
|
||||
const c = cordHist.get()
|
||||
const s = state.get()
|
||||
return imgs[decrement(c[c.length - 1].i, s.length)]
|
||||
}
|
||||
|
||||
function getElNext(): HTMLImageElement {
|
||||
const c = cordHist.get()
|
||||
const s = state.get()
|
||||
return imgs[increment(c[c.length - 1].i, s.length)]
|
||||
}
|
||||
|
||||
/**
|
||||
* main functions
|
||||
*/
|
||||
|
||||
// on mouse
|
||||
function onMouse(e: MouseEvent): void {
|
||||
if (isOpen.get() || isAnimating.get() || !gsapLoaded) 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
|
||||
function setPositions(): void {
|
||||
const elTrail = getElTrail()
|
||||
if (elTrail.length === 0 || !gsapLoaded) return
|
||||
|
||||
// preload
|
||||
lores(getElNextSeven())
|
||||
|
||||
_gsap.set(elTrail, {
|
||||
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()) {
|
||||
lores(getElTrail())
|
||||
hires([getElCurrent(), getElPrev(), getElNext()])
|
||||
_gsap.set(imgs, { opacity: 0 })
|
||||
_gsap.set(getElCurrent(), { opacity: 1, x: 0, y: 0, scale: 1 })
|
||||
}
|
||||
}
|
||||
|
||||
// open image into navigation
|
||||
function expandImage(): void {
|
||||
if (isAnimating.get() || !gsapLoaded) return
|
||||
|
||||
isOpen.set(true)
|
||||
isAnimating.set(true)
|
||||
|
||||
hires([getElCurrent(), getElPrev(), getElNext()])
|
||||
|
||||
const tl = _gsap.timeline()
|
||||
// move down and hide trail inactive
|
||||
tl.to(getElTrailInactive(), {
|
||||
y: '+=20',
|
||||
ease: _Power3.easeIn,
|
||||
stagger: 0.075,
|
||||
duration: 0.3,
|
||||
delay: 0.1,
|
||||
opacity: 0
|
||||
})
|
||||
// current move to center
|
||||
tl.to(getElCurrent(), {
|
||||
x: 0,
|
||||
y: 0,
|
||||
ease: _Power3.easeInOut,
|
||||
duration: 0.7,
|
||||
delay: 0.3
|
||||
})
|
||||
// current expand
|
||||
tl.to(getElCurrent(), {
|
||||
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() || !gsapLoaded) return
|
||||
|
||||
isOpen.set(false)
|
||||
isAnimating.set(true)
|
||||
|
||||
lores([getElCurrent()])
|
||||
lores(getElTrailInactive())
|
||||
|
||||
const tl = _gsap.timeline()
|
||||
// shrink current
|
||||
tl.to(getElCurrent(), {
|
||||
scale: 0.6,
|
||||
duration: 0.6,
|
||||
ease: _Power3.easeInOut
|
||||
})
|
||||
// move current to original position
|
||||
tl.to(getElCurrent(), {
|
||||
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(getElTrailInactive(), {
|
||||
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'))
|
||||
// event listeners
|
||||
stage.addEventListener('click', () => {
|
||||
expandImage()
|
||||
})
|
||||
stage.addEventListener('keydown', () => {
|
||||
expandImage()
|
||||
})
|
||||
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()
|
||||
})
|
||||
// preload
|
||||
lores(getElNextSeven())
|
||||
// dynamic import
|
||||
window.addEventListener(
|
||||
'mousemove',
|
||||
() => {
|
||||
loadGsap()
|
||||
.then((g) => {
|
||||
_gsap = g[0]
|
||||
_Power3 = g[1]
|
||||
gsapLoaded = true
|
||||
})
|
||||
.catch((e) => {
|
||||
console.log(e)
|
||||
})
|
||||
},
|
||||
{ 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')
|
||||
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
|
||||
stage.append(e)
|
||||
}
|
||||
container.append(stage)
|
||||
}
|
||||
|
||||
function hires(imgs: HTMLImageElement[]): void {
|
||||
imgs.forEach((img) => {
|
||||
img.src = img.dataset.hiUrl as string
|
||||
img.height = parseInt(img.dataset.hiImgH as string)
|
||||
img.width = parseInt(img.dataset.hiImgW as string)
|
||||
})
|
||||
}
|
||||
|
||||
function lores(imgs: HTMLImageElement[]): void {
|
||||
imgs.forEach((img) => {
|
||||
img.src = img.dataset.loUrl as string
|
||||
img.height = parseInt(img.dataset.loImgH as string)
|
||||
img.width = parseInt(img.dataset.loImgW as string)
|
||||
})
|
||||
}
|
||||
476
assets/ts/desktop/stage.tsx
Normal file
476
assets/ts/desktop/stage.tsx
Normal 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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -1,129 +0,0 @@
|
||||
import { container } from '../container'
|
||||
import { decIndex, incIndex, state } from '../state'
|
||||
import { decrement, increment } from '../utils'
|
||||
|
||||
import { setCustomCursor } from './customCursor'
|
||||
import { active, cordHist, isAnimating, isOpen, minimizeImage } from './stage'
|
||||
|
||||
/**
|
||||
* types
|
||||
*/
|
||||
|
||||
type NavItem = (typeof navItems)[number]
|
||||
|
||||
/**
|
||||
* variables
|
||||
*/
|
||||
|
||||
const mainDiv = document.getElementById('main') as HTMLDivElement
|
||||
const navItems = [
|
||||
mainDiv.getAttribute('prevText') as string,
|
||||
mainDiv.getAttribute('closeText') as string,
|
||||
mainDiv.getAttribute('nextText') as string
|
||||
] as const
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
const navOverlay = document.createElement('div')
|
||||
navOverlay.className = 'navOverlay'
|
||||
for (const navItem of navItems) {
|
||||
const overlay = document.createElement('div')
|
||||
overlay.className = 'overlay'
|
||||
overlay.addEventListener(
|
||||
'click',
|
||||
() => {
|
||||
handleClick(navItem)
|
||||
},
|
||||
{ passive: true }
|
||||
)
|
||||
overlay.addEventListener(
|
||||
'keydown',
|
||||
() => {
|
||||
handleClick(navItem)
|
||||
},
|
||||
{ passive: true }
|
||||
)
|
||||
overlay.addEventListener(
|
||||
'mouseover',
|
||||
() => {
|
||||
setCustomCursor(navItem)
|
||||
},
|
||||
{ passive: true }
|
||||
)
|
||||
overlay.addEventListener(
|
||||
'focus',
|
||||
() => {
|
||||
setCustomCursor(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
|
||||
cordHist.set(
|
||||
cordHist.get().map((item) => {
|
||||
return { ...item, i: increment(item.i, state.get().length) }
|
||||
})
|
||||
)
|
||||
|
||||
incIndex()
|
||||
}
|
||||
|
||||
function prevImage(): void {
|
||||
if (isAnimating.get()) return
|
||||
cordHist.set(
|
||||
cordHist.get().map((item) => {
|
||||
return { ...item, i: decrement(item.i, state.get().length) }
|
||||
})
|
||||
)
|
||||
|
||||
decIndex()
|
||||
}
|
||||
106
assets/ts/desktop/stageNav.tsx
Normal file
106
assets/ts/desktop/stageNav.tsx
Normal file
@@ -0,0 +1,106 @@
|
||||
import { For, createEffect, 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
|
||||
let controller: AbortController | undefined
|
||||
// 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()
|
||||
}
|
||||
|
||||
createEffect(() => {
|
||||
if (props.isOpen()) {
|
||||
controller = new AbortController()
|
||||
const abortSignal = controller.signal
|
||||
window.addEventListener('keydown', handleKey, {
|
||||
passive: true,
|
||||
signal: abortSignal
|
||||
})
|
||||
} else {
|
||||
controller?.abort()
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
<div class="navOverlay" classList={{ active: props.active() }}>
|
||||
<For each={navItems}>
|
||||
{(item) => (
|
||||
<div
|
||||
class="overlay"
|
||||
onClick={() => {
|
||||
handleClick(item)
|
||||
}}
|
||||
onFocus={() => props.setHoverText(item)}
|
||||
onMouseOver={() => props.setHoverText(item)}
|
||||
tabIndex="-1"
|
||||
/>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
import { initContainer } from './container'
|
||||
import { initNav } from './nav'
|
||||
import { initResources } from './resources'
|
||||
import { initState } from './state'
|
||||
import { isMobile } from './utils'
|
||||
|
||||
initContainer()
|
||||
const ijs = await initResources()
|
||||
initState(ijs.length)
|
||||
initNav()
|
||||
|
||||
// NOTE: it seems firefox and chromnium don't like top layer await
|
||||
// so we are using import then instead
|
||||
if (ijs.length > 0) {
|
||||
if (!isMobile()) {
|
||||
import('./desktop/init')
|
||||
.then((d) => {
|
||||
d.initDesktop(ijs)
|
||||
})
|
||||
.catch((e) => {
|
||||
console.log(e)
|
||||
})
|
||||
} else {
|
||||
import('./mobile/init')
|
||||
.then((m) => {
|
||||
m.initMobile(ijs)
|
||||
})
|
||||
.catch((e) => {
|
||||
console.log(e)
|
||||
})
|
||||
}
|
||||
}
|
||||
85
assets/ts/main.tsx
Normal file
85
assets/ts/main.tsx
Normal file
@@ -0,0 +1,85 @@
|
||||
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 &&
|
||||
!window.navigator.userAgent.includes('Win')
|
||||
|
||||
// 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)
|
||||
@@ -1,94 +0,0 @@
|
||||
import { container } from '../container'
|
||||
import { type ImageJSON } from '../resources'
|
||||
import { setIndex } from '../state'
|
||||
import { getRandom, onVisible } from '../utils'
|
||||
|
||||
import { slideUp } from './gallery'
|
||||
import { mounted } from './mounted'
|
||||
|
||||
/**
|
||||
* variables
|
||||
*/
|
||||
|
||||
export let imgs: HTMLImageElement[] = []
|
||||
|
||||
/**
|
||||
* 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'))
|
||||
// add event listeners
|
||||
imgs.forEach((img, i) => {
|
||||
img.addEventListener(
|
||||
'click',
|
||||
() => {
|
||||
handleClick(i)
|
||||
},
|
||||
{ passive: true }
|
||||
)
|
||||
img.addEventListener(
|
||||
'keydown',
|
||||
() => {
|
||||
handleClick(i)
|
||||
},
|
||||
{ passive: true }
|
||||
)
|
||||
// preload
|
||||
onVisible(img, () => {
|
||||
for (let _i = 0; _i < 5; _i++) {
|
||||
const n = i + _i
|
||||
if (n < 0 || n > imgs.length - 1) continue
|
||||
imgs[n].src = imgs[n].dataset.src as string
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 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')
|
||||
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)
|
||||
}
|
||||
133
assets/ts/mobile/collection.tsx
Normal file
133
assets/ts/mobile/collection.tsx
Normal 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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -1,276 +0,0 @@
|
||||
import { type Power3, type gsap } from 'gsap'
|
||||
import { type Swiper } from 'swiper'
|
||||
|
||||
import { container } from '../container'
|
||||
import { type ImageJSON } from '../resources'
|
||||
import { setIndex, state } from '../state'
|
||||
import {
|
||||
Watchable,
|
||||
capitalizeFirstLetter,
|
||||
expand,
|
||||
loadGsap,
|
||||
loadSwiper
|
||||
} from '../utils'
|
||||
|
||||
import { mounted } from './mounted'
|
||||
import { scrollable } from './scroll'
|
||||
|
||||
/**
|
||||
* variables
|
||||
*/
|
||||
|
||||
let swiperNode: HTMLDivElement
|
||||
let gallery: HTMLDivElement
|
||||
let curtain: HTMLDivElement
|
||||
let swiper: Swiper
|
||||
const isAnimating = new Watchable<boolean>(false)
|
||||
let lastIndex = -1
|
||||
let indexDispNums: HTMLSpanElement[] = []
|
||||
let galleryImages: HTMLImageElement[] = []
|
||||
let collectionImages: HTMLImageElement[] = []
|
||||
|
||||
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)
|
||||
|
||||
// load active image
|
||||
loadImages()
|
||||
|
||||
_gsap.to(curtain, {
|
||||
opacity: 1,
|
||||
duration: 1
|
||||
})
|
||||
|
||||
_gsap.to(gallery, {
|
||||
y: 0,
|
||||
ease: _Power3.easeInOut,
|
||||
duration: 1,
|
||||
delay: 0.4
|
||||
})
|
||||
|
||||
setTimeout(() => {
|
||||
scrollable.set(false)
|
||||
isAnimating.set(false)
|
||||
}, 1200)
|
||||
}
|
||||
|
||||
function slideDown(): void {
|
||||
scrollable.set(true)
|
||||
scrollToActive()
|
||||
|
||||
_gsap.to(gallery, {
|
||||
y: '100%',
|
||||
ease: _Power3.easeInOut,
|
||||
duration: 1
|
||||
})
|
||||
|
||||
_gsap.to(curtain, {
|
||||
opacity: 0,
|
||||
duration: 1.2,
|
||||
delay: 0.4
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 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'))
|
||||
collectionImages = Array.from(
|
||||
document
|
||||
.getElementsByClassName('collection')
|
||||
.item(0)
|
||||
?.getElementsByTagName('img') ?? []
|
||||
)
|
||||
// state watcher
|
||||
state.addWatcher(() => {
|
||||
const s = state.get()
|
||||
// change slide only when index is changed
|
||||
if (s.index === lastIndex) return
|
||||
changeSlide(s.index)
|
||||
updateIndexText()
|
||||
lastIndex = s.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 {
|
||||
loadImages()
|
||||
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 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'
|
||||
// swiper slide
|
||||
for (const ij of ijs) {
|
||||
const _swiperSlide = document.createElement('div')
|
||||
_swiperSlide.className = 'swiper-slide'
|
||||
// img
|
||||
const e = document.createElement('img')
|
||||
e.dataset.src = ij.hiUrl
|
||||
e.height = ij.hiImgH
|
||||
e.width = ij.hiImgW
|
||||
e.alt = ij.alt
|
||||
// append
|
||||
_swiperSlide.append(e)
|
||||
_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')
|
||||
const str: string = document
|
||||
.getElementById('main')
|
||||
?.getAttribute('closeText') as string
|
||||
_close.innerText = capitalizeFirstLetter(str)
|
||||
_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)
|
||||
}
|
||||
|
||||
function loadImages(): void {
|
||||
const activeImages: HTMLImageElement[] = []
|
||||
// load current, next, prev image
|
||||
activeImages.push(galleryImages[swiper.activeIndex])
|
||||
activeImages.push(
|
||||
galleryImages[Math.min(swiper.activeIndex + 1, swiper.slides.length)]
|
||||
)
|
||||
activeImages.push(galleryImages[Math.max(swiper.activeIndex - 1, 0)])
|
||||
for (const e of activeImages) {
|
||||
e.src = e.dataset.src as string
|
||||
}
|
||||
}
|
||||
242
assets/ts/mobile/gallery.tsx
Normal file
242
assets/ts/mobile/gallery.tsx
Normal 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" />
|
||||
</>
|
||||
)
|
||||
}
|
||||
69
assets/ts/mobile/galleryImage.tsx
Normal file
69
assets/ts/mobile/galleryImage.tsx
Normal 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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
46
assets/ts/mobile/galleryNav.tsx
Normal file
46
assets/ts/mobile/galleryNav.tsx
Normal 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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
52
assets/ts/mobile/layout.tsx
Normal file
52
assets/ts/mobile/layout.tsx
Normal 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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
import { Watchable } from '../utils'
|
||||
|
||||
export const mounted = new Watchable<boolean>(false)
|
||||
@@ -1,3 +0,0 @@
|
||||
import { Watchable } from '../utils'
|
||||
|
||||
export const scrollable = new Watchable<boolean>(true)
|
||||
104
assets/ts/nav.ts
104
assets/ts/nav.ts
@@ -1,104 +0,0 @@
|
||||
import { decThreshold, incThreshold, state } from './state'
|
||||
import { expand } from './utils'
|
||||
|
||||
/**
|
||||
* variables
|
||||
*/
|
||||
|
||||
// threshold div
|
||||
const thresholdDiv = document
|
||||
.getElementsByClassName('threshold')
|
||||
.item(0) as HTMLDivElement
|
||||
|
||||
// threshold nums span
|
||||
const thresholdDispNums = Array.from(
|
||||
thresholdDiv.getElementsByClassName('num')
|
||||
) as HTMLSpanElement[]
|
||||
|
||||
// threshold buttons
|
||||
const decButton = thresholdDiv
|
||||
.getElementsByClassName('dec')
|
||||
.item(0) as HTMLButtonElement
|
||||
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[]
|
||||
|
||||
// links div
|
||||
const linksDiv = document.getElementsByClassName('links').item(0) as HTMLDivElement
|
||||
|
||||
// links
|
||||
const links = Array.from(linksDiv.getElementsByClassName('link')) as HTMLAnchorElement[]
|
||||
|
||||
// current link index
|
||||
const currentLinkIndex = document
|
||||
.getElementById('main')
|
||||
?.getAttribute('currentMenuItemIndex') as string
|
||||
|
||||
// set current link
|
||||
for (const [index, link] of links.entries()) {
|
||||
if (index === parseInt(currentLinkIndex)) {
|
||||
// set current link style
|
||||
link.classList.add('current')
|
||||
// set current link title (only if not home)
|
||||
if (index !== 0) document.title = link.innerText + ' | ' + document.title
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* init
|
||||
*/
|
||||
|
||||
export function initNav(): void {
|
||||
const s = state.get()
|
||||
// init threshold text
|
||||
updateThresholdText(expand(s.threshold))
|
||||
// init index text
|
||||
updateIndexText(expand(s.index + 1), expand(s.length))
|
||||
// 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 {
|
||||
thresholdDispNums.forEach((e: HTMLSpanElement, i: number) => {
|
||||
e.innerText = thresholdValue[i]
|
||||
})
|
||||
}
|
||||
|
||||
export function updateIndexText(indexValue: string, indexLength: string): void {
|
||||
indexDispNums.forEach((e: HTMLSpanElement, i: number) => {
|
||||
if (i < 4) {
|
||||
e.innerText = indexValue[i]
|
||||
} else {
|
||||
e.innerText = indexLength[i - 4]
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -10,7 +10,10 @@ 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
|
||||
}
|
||||
try {
|
||||
const response = await fetch(`${window.location.href}index.json`, {
|
||||
headers: {
|
||||
|
||||
@@ -1,89 +0,0 @@
|
||||
import { Watchable, decrement, increment } from './utils'
|
||||
|
||||
/**
|
||||
* types
|
||||
*/
|
||||
|
||||
export type State = typeof defaultState
|
||||
|
||||
/**
|
||||
* 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)
|
||||
|
||||
/**
|
||||
* 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 }
|
||||
}
|
||||
|
||||
function getThresholdSessionIndex(): number {
|
||||
const s = sessionStorage.getItem('thresholdsIndex')
|
||||
if (s === null) return 2
|
||||
return parseInt(s)
|
||||
}
|
||||
136
assets/ts/state.tsx
Normal file
136
assets/ts/state.tsx
Normal 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
|
||||
}
|
||||
@@ -1,8 +1,13 @@
|
||||
import { type Power3, type gsap } from 'gsap'
|
||||
import { type Swiper } from 'swiper'
|
||||
import { type gsap } from 'gsap'
|
||||
|
||||
/**
|
||||
* custom helpers
|
||||
* types
|
||||
*/
|
||||
|
||||
export type Vector = 'prev' | 'next' | 'none'
|
||||
|
||||
/**
|
||||
* utils
|
||||
*/
|
||||
|
||||
export function increment(num: number, length: number): number {
|
||||
@@ -17,62 +22,18 @@ export function expand(num: number): string {
|
||||
return ('0000' + num.toString()).slice(-4)
|
||||
}
|
||||
|
||||
export function isMobile(): boolean {
|
||||
return window.matchMedia('(hover: none)').matches
|
||||
}
|
||||
|
||||
export function getRandom(min: number, max: number): number {
|
||||
return Math.floor(Math.random() * (max - min + 1)) + min
|
||||
}
|
||||
|
||||
export function onVisible<T extends Element>(
|
||||
element: T,
|
||||
callback: (arg0: T) => void
|
||||
): void {
|
||||
new IntersectionObserver((entries, observer) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.intersectionRatio > 0) {
|
||||
callback(element)
|
||||
observer.disconnect()
|
||||
}
|
||||
})
|
||||
}).observe(element)
|
||||
}
|
||||
|
||||
export function capitalizeFirstLetter(str: string): string {
|
||||
return str.charAt(0).toUpperCase() + str.slice(1)
|
||||
}
|
||||
|
||||
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 async function loadSwiper(): Promise<typeof Swiper> {
|
||||
const s = await import('swiper')
|
||||
return s.Swiper
|
||||
export function getThresholdSessionIndex(): number {
|
||||
const s = sessionStorage.getItem('thresholdsIndex')
|
||||
if (s === null) return 2
|
||||
return parseInt(s)
|
||||
}
|
||||
|
||||
/**
|
||||
* custom types
|
||||
*/
|
||||
|
||||
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)
|
||||
}
|
||||
export function removeDuplicates<T>(arr: T[]): T[] {
|
||||
if (arr.length < 2) return arr // optimization
|
||||
return [...new Set(arr)]
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ The minimum required Hugo version can be seen in the [`theme.toml`](https://gith
|
||||
|
||||
## Installation
|
||||
|
||||
### Git
|
||||
### Git (for adavanced user)
|
||||
|
||||
On the main branch, you can find the theme's latest source code. To use the latest version, you can clone the repository to `themes/bridget` by running the following command in the root directory of your Hugo site:
|
||||
|
||||
@@ -36,11 +36,13 @@ If you are already using Git for your site, you can add the theme as a submodule
|
||||
git submodule add https://github.com/Sped0n/bridget themes/bridget
|
||||
```
|
||||
|
||||
After cloning/downloading theme files to the directory, if you want to customize the theme, please run `pnpm install` or `npm install` first.
|
||||
> ⚠️⚠️⚠️
|
||||
>
|
||||
> Please refer to the config section for the following content.
|
||||
|
||||
### Module (recommended)
|
||||
|
||||
> If you want to modify the theme, use Git installation instead.
|
||||
> If you want to have some customizations, use Git installation instead.
|
||||
|
||||
This theme is also available as a [Hugo module](https://gohugo.io/hugo-modules/). Run the following command in the root directory of your Hugo site:
|
||||
|
||||
@@ -48,7 +50,7 @@ First turn your site into a Hugo module (in case you haven't done it yet):
|
||||
|
||||
```shell
|
||||
hugo mod init github.com/me/my-new-site
|
||||
# or whatever you like, it doesn’t necessarily have to be a GitHub link.
|
||||
# or whatever you like, it doesn’t necessarily have to be a GitHub repo link.
|
||||
hugo mod init blablabla
|
||||
```
|
||||
|
||||
@@ -58,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:
|
||||
@@ -67,6 +69,10 @@ If you want to upgrade the theme, just run:
|
||||
hugo mod get -u
|
||||
```
|
||||
|
||||
> ⚠️⚠️⚠️
|
||||
>
|
||||
> Please refer to the config section for the following content.
|
||||
|
||||
## Content Management
|
||||
|
||||
The content is where the pictures/text is stored, while the static refers to the website icons.
|
||||
@@ -106,6 +112,8 @@ menu:
|
||||
identifier: Erwitt
|
||||
title: Erwitt
|
||||
unifiedAlt: '© Elliott Erwitt'
|
||||
_build:
|
||||
publishResources: false
|
||||
---
|
||||
```
|
||||
|
||||
@@ -123,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, Hugo’s 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.
|
||||
@@ -152,13 +162,17 @@ 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 want to <u>modify the theme</u> or you have <u>installation with Git</u>, please **keep the `replacements` configuration** and change the path after the arrow to the location of your local theme file (relative path only).
|
||||
- If you have <u>installation with Module</u>, **remove the `replacements` configuration**.
|
||||
- If you have _installation with Git_
|
||||
|
||||
- `replacement`: replace the _path after the arrow_(`../..`) with the location of your local theme file (⚠️⚠️⚠️**relative path to hugo site theme directory only([official doc](https://gohugo.io/hugo-modules/configuration/#module-configuration-top-level))**, example: `bridget`)
|
||||
- `path`: no change
|
||||
|
||||
- If you have _installation with Module_, **remove the `replacements` configuration**.
|
||||
|
||||
### `markup.toml`
|
||||
|
||||
@@ -168,10 +182,16 @@ 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.
|
||||
|
||||
### `sitemap.toml`
|
||||
|
||||
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).
|
||||
|
||||
- run `pnpm install` to install neceessary dependencies.
|
||||
- run `pnpm run dev` to host a dev server.
|
||||
- when you’re ready, run `pnpm run build` to update artifacts.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# timeout
|
||||
timeout = "1200s"
|
||||
# your website url
|
||||
baseURL = 'https://www.example.com/'
|
||||
baseURL = 'https://bridget-demo.sped0n.com'
|
||||
# website title
|
||||
title = 'Bridget'
|
||||
# don't touch this
|
||||
@@ -18,10 +18,11 @@ enableRobotsTXT = true
|
||||
# * zh-hk zh-tw zh-mo (powered by Noto Sans TC)
|
||||
# * ja (powered by Noto Sans JP)
|
||||
# * ko (powered by Noto Sans KR)
|
||||
# * ta (powered by Noto Sans TA)
|
||||
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 (relative to hugo site theme 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 !!!
|
||||
|
||||
@@ -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
|
||||
description = "Bridget is a minimal Hugo theme designed for photographers/visual artists."
|
||||
|
||||
# whether to use favicon resource links
|
||||
# generate these with https://realfavicongenerator.net
|
||||
@@ -51,9 +47,9 @@ id = ""
|
||||
# Umami Analytics
|
||||
[analytics.umami]
|
||||
data_website_id = "44a4a42d-ec8e-44c9-a38c-7533929e9845"
|
||||
src = "https://umami.sped0nwen.com/script.js"
|
||||
src = "https://umami.sped0n.com/script.js"
|
||||
data_host_url = ""
|
||||
data_domains = "bridget-demo.sped0nwen.com"
|
||||
data_domains = "bridget-demo.sped0n.com"
|
||||
# Plausible Analytics
|
||||
[analytics.plausible]
|
||||
data_domain = ""
|
||||
|
||||
@@ -8,4 +8,6 @@ menu:
|
||||
identifier: Erwitt
|
||||
title: Erwitt
|
||||
unifiedAlt: '© Elliott Erwitt'
|
||||
_build:
|
||||
publishResources: false
|
||||
---
|
||||
|
||||
@@ -8,4 +8,6 @@ menu:
|
||||
identifier: Gruyaert
|
||||
title: Gruyaert
|
||||
unifiedAlt: '© Harry Gruyaert'
|
||||
_build:
|
||||
publishResources: false
|
||||
---
|
||||
|
||||
@@ -8,16 +8,18 @@ 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 designed using pure TypeScript and CSS. However, after website designer <u>[Tyler McRobert](https://tylermcrobert.com)</u> made the source code publicly available, the whole project was modified to mimic the original design. The animations in Bridget are achieved using gsap and swiper. In essence, it is more like a porting of the original design into Hugo.
|
||||
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.
|
||||
|
||||
[Repo ↗](https://github.com/Sped0n/bridget)
|
||||
[GitHub Repo ↗](https://github.com/Sped0n/bridget)
|
||||
|
||||
Original site design by <u>[Tyler McRobert](https://tylermcrobert.com)</u>.
|
||||
|
||||
© {{< year >}} <u>[Spedon](https://github.com/Sped0n)</u> | Powered by [Hugo](https://gohugo.io)
|
||||
© {{< year >}} <u>[Spedon](https://github.com/Sped0n)</u> | Built with Hugo
|
||||
|
||||
@@ -8,4 +8,6 @@ menu:
|
||||
identifier: Webb
|
||||
title: Webb
|
||||
unifiedAlt: '© Alex Webb'
|
||||
_build:
|
||||
publishResources: false
|
||||
---
|
||||
|
||||
2
go.mod
2
go.mod
@@ -1,3 +1,3 @@
|
||||
module github.com/Sped0n/bridget
|
||||
module github.com/Sped0n/bridget/v2
|
||||
|
||||
go 1.21.3
|
||||
|
||||
@@ -8,3 +8,5 @@ other = "schließen"
|
||||
other = "schwelle"
|
||||
[404]
|
||||
other = "seite nicht gefunden"
|
||||
[loading]
|
||||
other = "lade..."
|
||||
|
||||
@@ -8,3 +8,5 @@ other = "close"
|
||||
other = "threshold"
|
||||
[404]
|
||||
other = "page not found"
|
||||
[loading]
|
||||
other = "loading..."
|
||||
|
||||
@@ -8,3 +8,5 @@ other = "cerrar"
|
||||
other = "umbral"
|
||||
[404]
|
||||
other = "página no encontrada"
|
||||
[loading]
|
||||
other = "cargando..."
|
||||
|
||||
@@ -8,3 +8,5 @@ other = "fermer"
|
||||
other = "seuil"
|
||||
[404]
|
||||
other = "page non trouvée"
|
||||
[loading]
|
||||
other = "chargement..."
|
||||
|
||||
@@ -8,3 +8,5 @@ other = "chiudi"
|
||||
other = "soglia"
|
||||
[404]
|
||||
other = "pagina non trovata"
|
||||
[loading]
|
||||
other = "caricamento..."
|
||||
|
||||
@@ -8,3 +8,5 @@ other = "閉じる"
|
||||
other = "しきい値"
|
||||
[404]
|
||||
other = "ページが見つかりません"
|
||||
[loading]
|
||||
other = "読み込み中..."
|
||||
|
||||
@@ -8,3 +8,5 @@ other = "닫기"
|
||||
other = "임계값"
|
||||
[404]
|
||||
other = "페이지를 찾을 수 없습니다"
|
||||
[loading]
|
||||
other = "로딩중..."
|
||||
|
||||
12
i18n/ta.toml
Normal file
12
i18n/ta.toml
Normal file
@@ -0,0 +1,12 @@
|
||||
[next]
|
||||
other = "அடுத்தது"
|
||||
[prev]
|
||||
other = "முந்தையது"
|
||||
[close]
|
||||
other = "வெளியே செல்ல"
|
||||
[threshold]
|
||||
other = "எல்லை"
|
||||
[404]
|
||||
other = "பக்கம் காணப்படவில்லை"
|
||||
[loading]
|
||||
other = "பதிவேற்றம் நடக்கிறது..."
|
||||
@@ -8,3 +8,5 @@ other = "关闭"
|
||||
other = "阈值"
|
||||
[404]
|
||||
other = "页面不存在"
|
||||
[loading]
|
||||
other = "加载中..."
|
||||
|
||||
@@ -8,3 +8,5 @@ other = "關閉"
|
||||
other = "閾值"
|
||||
[404]
|
||||
other = "找不到頁面"
|
||||
[loading]
|
||||
other = "載入中..."
|
||||
|
||||
@@ -8,3 +8,5 @@ other = "關閉"
|
||||
other = "閾值"
|
||||
[404]
|
||||
other = "找不到頁面"
|
||||
[loading]
|
||||
other = "載入中..."
|
||||
|
||||
@@ -8,3 +8,5 @@ other = "关闭"
|
||||
other = "阈值"
|
||||
[404]
|
||||
other = "页面不存在"
|
||||
[loading]
|
||||
other = "加载中..."
|
||||
|
||||
@@ -8,3 +8,5 @@ other = "關閉"
|
||||
other = "閾值"
|
||||
[404]
|
||||
other = "找不到頁面"
|
||||
[loading]
|
||||
other = "載入中..."
|
||||
|
||||
@@ -1,15 +1,10 @@
|
||||
{{- define "main" -}}
|
||||
{{- $params := .Scratch.Get "params" -}}
|
||||
{{- $currentPage := . -}}
|
||||
{{- with partial "function/currentMenuItem.html" . -}}
|
||||
<script>document.getElementById("main").setAttribute("currentMenuItemIndex", "{{- (sub .ID 1) -}}")</script>
|
||||
{{- end -}}
|
||||
<div class="container">
|
||||
{{- partial "nav.html" . -}}
|
||||
<article>
|
||||
<p class="error">⛝ <u>404</u> {{- i18n 404 -}} ⛝</p>
|
||||
<p class="error">⛝ <u>404</u> {{- i18n 404 -}} ⛝</p>
|
||||
<p class="error">⛝ <u>404</u> {{- i18n 404 -}} ⛝</p>
|
||||
</article>
|
||||
</div>
|
||||
<article class="info">
|
||||
<p class="error">⛝ <u>404</u> {{- i18n 404 -}} ⛝</p>
|
||||
<p class="error">⛝ <u>404</u> {{- i18n 404 -}} ⛝</p>
|
||||
<p class="error">⛝ <u>404</u> {{- i18n 404 -}} ⛝</p>
|
||||
</article>
|
||||
{{- end -}}
|
||||
|
||||
@@ -3,11 +3,10 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>{{ site.Title }}</title>
|
||||
{{- partial "head/link.html" -}}
|
||||
{{- partial "head/meta.html" -}}
|
||||
{{- partial "head/seo.html" -}}
|
||||
{{- partial "head/favicon.html" -}}
|
||||
{{- partial "head/link.html" . -}}
|
||||
{{- partial "head/meta.html" . -}}
|
||||
{{- partial "head/seo.html" . -}}
|
||||
{{- partial "head/favicon.html" . -}}
|
||||
</head>
|
||||
<body lang="{{- site.LanguageCode -}}">
|
||||
<div id="main">
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
{{- define "main" -}}
|
||||
{{- $params := .Scratch.Get "params" -}}
|
||||
{{- $currentPage := . -}}
|
||||
{{- with partial "function/currentMenuItem.html" . -}}
|
||||
<script>
|
||||
document.getElementById("main").setAttribute("currentMenuItemIndex", "{{- (sub .ID 1) -}}")
|
||||
document.getElementById("main").setAttribute("nextText", "{{- i18n "next" -}}")
|
||||
document.getElementById("main").setAttribute("prevText", "{{- i18n "prev" -}}")
|
||||
document.getElementById("main").setAttribute("closeText", "{{- i18n "close" -}}")
|
||||
</script>
|
||||
{{- end -}}
|
||||
<div class="container">
|
||||
<div
|
||||
class="container"
|
||||
data-next="{{- i18n "next" -}}"
|
||||
data-prev="{{- i18n "prev" -}}"
|
||||
data-close="{{- i18n "close" -}}"
|
||||
data-loading="{{- i18n "loading" -}}"
|
||||
>
|
||||
{{- with .Content -}}
|
||||
<article>
|
||||
{{- . -}}
|
||||
</article>
|
||||
{{- end -}}
|
||||
|
||||
{{- partial "nav.html" . -}}
|
||||
</div>
|
||||
<article class="info">
|
||||
{{ .Content }}
|
||||
</article>
|
||||
{{- end -}}
|
||||
|
||||
@@ -1,15 +1,9 @@
|
||||
{{- $Page := . -}}
|
||||
{{- $Path := "" -}}
|
||||
{{- $context := . -}}
|
||||
{{- $params := .Page.Params | merge .Site.Params.Page -}}
|
||||
|
||||
{{- with partial "function/currentMenuItem.html" . -}}
|
||||
{{- $Path = .DirName -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- $gallery := site.GetPage $Path -}}
|
||||
{{- with $gallery.Resources.ByType "image" -}}
|
||||
{{- with partial "function/getImageSlice.html" . -}}
|
||||
{{- $index := len . -}}
|
||||
{{- $Page.Scratch.Add "img" slice -}}
|
||||
{{- $context.Scratch.Add "img" slice -}}
|
||||
{{- range sort . "Name" "desc" -}}
|
||||
{{- $image := . -}}
|
||||
{{- $index = sub $index 1 -}}
|
||||
@@ -19,7 +13,7 @@
|
||||
{{- end -}}
|
||||
{{- $lores := .Resize (site.Params.loResOpt | default "700x webp Lanczos q60") -}}
|
||||
{{- $hires := .Resize (site.Params.hiResOpt | default "2000x webp Lanczos q75") -}}
|
||||
{{- $Page.Scratch.Add "img" (dict
|
||||
{{- $context.Scratch.Add "img" (dict
|
||||
"index" (int $index)
|
||||
"alt" (string $alt)
|
||||
"loUrl" (string $lores.RelPermalink)
|
||||
@@ -31,7 +25,7 @@
|
||||
)
|
||||
-}}
|
||||
{{- end -}}
|
||||
{{ $Page.Scratch.Get "img" | jsonify }}
|
||||
{{ $context.Scratch.Get "img" | jsonify }}
|
||||
{{- else -}}
|
||||
[]
|
||||
{{- end -}}
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
{{- $currentPage := . -}}
|
||||
|
||||
{{- $dirName := "" -}}
|
||||
{{- $id := -1 -}}
|
||||
{{- $identifier := "" -}}
|
||||
{{- $title := "404" -}}
|
||||
{{- $weight := -1 -}}
|
||||
|
||||
{{- range site.Menus.main -}}
|
||||
{{ $menu_item_url := .URL | relLangURL }}
|
||||
{{ $page_url:= $currentPage.RelPermalink | relLangURL }}
|
||||
{{ if eq $menu_item_url $page_url }}
|
||||
{{- $dirName = .Identifier -}}
|
||||
{{- $id = .Weight -}}
|
||||
{{- $identifier = .Identifier -}}
|
||||
{{- $title = .Title -}}
|
||||
{{- $weight = .Weight -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- return (dict "DirName" $dirName "ID" $id) -}}
|
||||
{{- return (dict "Identifier" $identifier "Title" $title "Weight" $weight) -}}
|
||||
|
||||
10
layouts/partials/function/getImageSlice.html
Normal file
10
layouts/partials/function/getImageSlice.html
Normal file
@@ -0,0 +1,10 @@
|
||||
{{- $context := . -}}
|
||||
{{- $Path := "" -}}
|
||||
{{- $params := .Page.Params | merge .Site.Params.Page -}}
|
||||
|
||||
{{- with partial "function/currentMenuItem.html" . -}}
|
||||
{{- $Path = .Identifier -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- $gallery := site.GetPage $Path -}}
|
||||
{{- return $gallery.Resources.ByType "image" -}}
|
||||
@@ -7,16 +7,18 @@
|
||||
{{- $style = dict "Context" . "ToCSS" $options "Inline" true | merge $style -}}
|
||||
{{- partial "plugin/style.html" $style -}}
|
||||
|
||||
{{/* main style */}}
|
||||
{{- if site.Params.bundled -}}
|
||||
{{- $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>
|
||||
function z() {
|
||||
const r = document.querySelector(':root')
|
||||
r.style.setProperty('--window-height', `${window.innerHeight}px`)
|
||||
}
|
||||
z()
|
||||
window.addEventListener('resize', z, { passive: true })
|
||||
</script>
|
||||
|
||||
{{/* main js */}}
|
||||
{{- $script := dict "Link" "/bundled/js/main.js" "Defer" true "Esm" true -}}
|
||||
@@ -58,4 +60,12 @@
|
||||
as="font"
|
||||
crossorigin
|
||||
/>
|
||||
{{- else if (partial "function/langCode.html" (slice "ta")) -}}
|
||||
<link rel="preload" href="/lib/fonts/NotoSans-Regular.woff2" as="font" crossorigin />
|
||||
<link
|
||||
rel="preload"
|
||||
href="/lib/fonts/NotoSansTamil-Regular.woff2"
|
||||
as="font"
|
||||
crossorigin
|
||||
/>
|
||||
{{- end -}}
|
||||
|
||||
@@ -1,9 +1,22 @@
|
||||
<meta name="Description" content="{{- site.Params.description -}}" />
|
||||
<meta
|
||||
name="application-name"
|
||||
content="{{- .Site.Params.app.title | default site.Title -}}"
|
||||
/>
|
||||
<meta
|
||||
name="apple-mobile-web-app-title"
|
||||
content="{{- .Site.Params.app.title | default site.Title -}}"
|
||||
/>
|
||||
{{/* Title */}}
|
||||
{{- $page_title := "" -}}
|
||||
{{- with partial "function/currentMenuItem.html" . -}}
|
||||
{{ $page_title = printf "%s" site.Title | printf "%s%s" " | " | printf "%s%s" .Title | printf "%s" }}
|
||||
{{- end -}}
|
||||
<title>{{ $page_title }}</title>
|
||||
|
||||
{{/* Basic */}}
|
||||
<meta name="Description" content="{{ site.Params.description }}" />
|
||||
<meta name="application-name" content="{{ $page_title }}" />
|
||||
<meta name="apple-mobile-web-app-title" content="{{ $page_title }}" />
|
||||
|
||||
{{/* Opengraph */}}
|
||||
<meta property="og:title" content="{{ $page_title }}" />
|
||||
<meta name="twitter:title" content="{{ $page_title }}" />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:url" content="{{ .Permalink }}" />
|
||||
<meta property="og:description" content="{{ site.Params.description }}" />
|
||||
<meta name="twitter:description" content="{{ site.Params.description }}" />
|
||||
|
||||
{{/* Generator */}}
|
||||
{{ hugo.Generator }}
|
||||
|
||||
@@ -4,19 +4,39 @@
|
||||
</div>
|
||||
<div class="links">
|
||||
{{- $index := 0 -}}
|
||||
{{- $currentIndex := -1 -}}
|
||||
{{- with partial "function/currentMenuItem.html" . -}}
|
||||
{{- $currentIndex = sub .Weight 1 -}}
|
||||
{{- end -}}
|
||||
{{- $menus := .Site.Menus.main -}}
|
||||
{{- $len := len $menus }}
|
||||
{{- range $menus -}}
|
||||
{{- $url := .URL | relURL -}}
|
||||
{{- if eq (add $index 1) $len -}}
|
||||
<a href="{{- .URL | relURL -}}" class="link">{{- .Title -}}</a>
|
||||
<a
|
||||
href="{{- .URL | relURL -}}"
|
||||
class="link{{- if eq $index $currentIndex -}}
|
||||
{{- printf " current" -}}
|
||||
{{- end -}}"
|
||||
>{{- .Title -}}</a
|
||||
>
|
||||
{{- else -}}
|
||||
<a href="{{- .URL | relURL -}}" class="link">{{- .Title -}}</a>, 
|
||||
<a
|
||||
href="{{- .URL | relURL -}}"
|
||||
class="link{{- if eq $index $currentIndex -}}
|
||||
{{- printf " current" -}}
|
||||
{{- end -}}"
|
||||
>{{- .Title -}}</a
|
||||
>, 
|
||||
{{- end -}}
|
||||
{{- $index = add $index 1 -}}
|
||||
{{- end -}}
|
||||
</div>
|
||||
<div class="threshold">
|
||||
{{- $length := 0 -}}
|
||||
{{- with partial "function/getImageSlice.html" . -}}
|
||||
{{- $length = len . -}}
|
||||
{{- end -}}
|
||||
<span>{{- i18n "threshold" | strings.FirstUpper -}}:</span>
|
||||
<span>
|
||||
<button class="dec">-</button>
|
||||
@@ -26,10 +46,43 @@
|
||||
</span>
|
||||
</div>
|
||||
<div class="index">
|
||||
<span class="num"></span><span class="num"></span><span class="num"></span
|
||||
><span class="num"></span>
|
||||
<span class="num">0</span><span class="num">0</span><span class="num">0</span
|
||||
><span class="num">0</span>
|
||||
<span>/</span>
|
||||
<span class="num"></span><span class="num"></span><span class="num"></span
|
||||
><span class="num"></span>
|
||||
<span class="num">{{- math.Floor (div $length 1000) -}}</span
|
||||
><span class="num">{{- mod (math.Floor (div $length 100)) 10 -}}</span
|
||||
><span class="num">{{- mod (math.Floor (div $length 10)) 10 -}}</span
|
||||
><span class="num">{{- mod $length 10 -}}</span>
|
||||
</div>
|
||||
</nav>
|
||||
{{- /* for threshold */ -}}
|
||||
<script>
|
||||
i = 2
|
||||
const s = sessionStorage.getItem('thresholdsIndex')
|
||||
if (s !== null) {
|
||||
i = parseInt(s)
|
||||
}
|
||||
var t = ''
|
||||
switch (i) {
|
||||
case 0:
|
||||
t = '0020'
|
||||
break
|
||||
case 1:
|
||||
t = '0040'
|
||||
break
|
||||
case 2:
|
||||
t = '0080'
|
||||
break
|
||||
case 3:
|
||||
t = '0140'
|
||||
break
|
||||
case 4:
|
||||
t = '0200'
|
||||
break
|
||||
}
|
||||
Array.from(
|
||||
document.getElementsByClassName('threshold').item(0).getElementsByClassName('num')
|
||||
).forEach((e, i) => {
|
||||
e.innerText = t[i]
|
||||
})
|
||||
</script>
|
||||
|
||||
55
package.json
55
package.json
@@ -1,19 +1,21 @@
|
||||
{
|
||||
"name": "bridget",
|
||||
"version": "v0.0.4",
|
||||
"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/* ./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,29 +39,32 @@
|
||||
},
|
||||
"homepage": "https://github.com/Sped0n/bridget#readme",
|
||||
"devDependencies": {
|
||||
"@typescript-eslint/eslint-plugin": "^6.14.0",
|
||||
"@typescript-eslint/parser": "^6.14.0",
|
||||
"eslint": "^8.55.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": "^42.0.0",
|
||||
"eslint-import-resolver-typescript": "^3.6.1",
|
||||
"eslint-plugin-import": "^2.29.0",
|
||||
"eslint-plugin-n": "^16.4.0",
|
||||
"eslint-plugin-prettier": "^5.0.1",
|
||||
"eslint-plugin-promise": "^6.1.1",
|
||||
"eslint-config-standard-with-typescript": "^43.0.1",
|
||||
"eslint-import-resolver-typescript": "^3.6.3",
|
||||
"eslint-plugin-import": "^2.31.0",
|
||||
"eslint-plugin-n": "^16.6.2",
|
||||
"eslint-plugin-prettier": "^5.2.1",
|
||||
"eslint-plugin-promise": "^6.6.0",
|
||||
"eslint-plugin-solid": "^0.14.5",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"prettier": "3.1.1",
|
||||
"prettier": "3.3.3",
|
||||
"prettier-plugin-go-template": "^0.0.15",
|
||||
"prettier-plugin-organize-imports": "^3.2.4",
|
||||
"typescript": "^5.3.3"
|
||||
"prettier-plugin-organize-imports": "^4.1.0",
|
||||
"sass": "^1.83.1",
|
||||
"terser": "^5.37.0",
|
||||
"typescript": "^5.7.2",
|
||||
"vite": "^5.4.2",
|
||||
"vite-plugin-solid": "^2.10.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"gsap": "^3.12.3",
|
||||
"swiper": "^11.0.5",
|
||||
"rollup": "^4.9.0",
|
||||
"@rollup/plugin-node-resolve": "^15.2.3",
|
||||
"@rollup/plugin-terser": "^0.4.4",
|
||||
"@rollup/plugin-typescript": "^11.1.5"
|
||||
"gsap": "^3.12.5",
|
||||
"solid-js": "^1.9.4",
|
||||
"swiper": "^11.1.15",
|
||||
"tiny-invariant": "^1.3.3"
|
||||
}
|
||||
}
|
||||
|
||||
2925
pnpm-lock.yaml
generated
2925
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
}
|
||||
})
|
||||
]
|
||||
}
|
||||
1
static/bundled/css/main.css
Normal file
1
static/bundled/css/main.css
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
static/bundled/css/style.min.css
vendored
1
static/bundled/css/style.min.css
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
static/bundled/js/BPtQtF.js
Normal file
1
static/bundled/js/BPtQtF.js
Normal file
File diff suppressed because one or more lines are too long
1
static/bundled/js/BSNAnE.js
Normal file
1
static/bundled/js/BSNAnE.js
Normal file
File diff suppressed because one or more lines are too long
1
static/bundled/js/CqsBem.js
Normal file
1
static/bundled/js/CqsBem.js
Normal file
File diff suppressed because one or more lines are too long
1
static/bundled/js/DX2W9I.js
Normal file
1
static/bundled/js/DX2W9I.js
Normal file
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
|
||||
import{l as e,c as t,W as n,s as a,i as s,a as i,d as o,b as c}from"./main.js";let r=[],d={x:0,y:0};const g=new n([]),l=new n(!1),m=new n(!1),h=new n(!1);let u,p,f=!1;function v(){return g.get().map((e=>r[e.i]))}function y(){const e=v().slice(-a.get().trailLength);return e.slice(0,e.length-1)}function w(){const e=v();return e[e.length-1]}function I(){const e=g.get(),t=a.get(),n=e.length>0?e[e.length-1].i:t.index,i=[];for(let e=0;e<7;e++)i.push(r[s(n+e,t.length)]);return i}function E(){const e=g.get(),t=a.get();return r[o(e[e.length-1].i,t.length)]}function x(){const e=g.get(),t=a.get();return r[s(e[e.length-1].i,t.length)]}function L(e){if(l.get()||m.get()||!f)return;const t={x:e.clientX,y:e.clientY};if(Math.hypot(t.x-d.x,t.y-d.y)>a.get().threshold){d=t,i();const e={i:a.get().index,...t};g.set([...g.get(),e].slice(-a.get().length))}}function W(){if(m.get()||!f)return;l.set(!0),m.set(!0),k([w(),E(),x()]);const e=u.timeline();e.to(y(),{y:"+=20",ease:p.easeIn,stagger:.075,duration:.3,delay:.1,opacity:0}),e.to(w(),{x:0,y:0,ease:p.easeInOut,duration:.7,delay:.3}),e.to(w(),{delay:.1,scale:1,ease:p.easeInOut}),e.then((()=>{m.set(!1)})).catch((e=>{console.log(e)}))}function H(){if(m.get()||!f)return;l.set(!1),m.set(!0),N([w()]),N(y());const e=u.timeline();e.to(w(),{scale:.6,duration:.6,ease:p.easeInOut}),e.to(w(),{delay:.3,duration:.7,ease:p.easeInOut,x:g.get()[g.get().length-1].x-window.innerWidth/2,y:g.get()[g.get().length-1].y-window.innerHeight/2}),e.to(y(),{y:"-=20",ease:p.easeOut,stagger:-.1,duration:.3,opacity:1}),e.then((()=>{m.set(!1)})).catch((e=>{console.log(e)}))}function k(e){e.forEach((e=>{e.src=e.dataset.hiUrl,e.height=parseInt(e.dataset.hiImgH),e.width=parseInt(e.dataset.hiImgW)}))}function N(e){e.forEach((e=>{e.src=e.dataset.loUrl,e.height=parseInt(e.dataset.loImgH),e.width=parseInt(e.dataset.loImgW)}))}const b=document.createElement("div"),A=document.createElement("div");function O(e){const t=e.clientX,n=e.clientY;b.style.transform=`translate3d(${t}px, ${n}px, 0)`}function U(e){A.innerText=e}const T=document.getElementById("main"),S=[T.getAttribute("prevText"),T.getAttribute("closeText"),T.getAttribute("nextText")];function B(e){e===S[0]?$():e===S[1]?H():Y()}function X(e){if(!l.get()&&!m.get())switch(e.key){case"ArrowLeft":$();break;case"Escape":H();break;case"ArrowRight":Y()}}function Y(){m.get()||(g.set(g.get().map((e=>({...e,i:s(e.i,a.get().length)})))),i())}function $(){m.get()||(g.set(g.get().map((e=>({...e,i:o(e.i,a.get().length)})))),c())}function j(n){b.className="cursor",A.className="cursorInner",b.append(A),t.append(b),window.addEventListener("mousemove",O,{passive:!0}),h.addWatcher((e=>{e?b.classList.add("active"):b.classList.remove("active")})),function(n){!function(e){const n=document.createElement("div");n.className="stage";for(const t of e){const e=document.createElement("img");e.height=t.loImgH,e.width=t.loImgW,e.dataset.hiUrl=t.hiUrl,e.dataset.hiImgH=t.hiImgH.toString(),e.dataset.hiImgW=t.hiImgW.toString(),e.dataset.loUrl=t.loUrl,e.dataset.loImgH=t.loImgH.toString(),e.dataset.loImgW=t.loImgW.toString(),e.alt=t.alt,n.append(e)}t.append(n)}(n);const s=document.getElementsByClassName("stage").item(0);r=Array.from(s.getElementsByTagName("img")),s.addEventListener("click",(()=>{W()})),s.addEventListener("keydown",(()=>{W()})),window.addEventListener("mousemove",L,{passive:!0}),l.addWatcher((e=>{h.set(e&&!m.get())})),m.addWatcher((e=>{h.set(l.get()&&!e)})),g.addWatcher((e=>{!function(){const e=v();0!==e.length&&f&&(N(I()),u.set(e,{x:e=>g.get()[e].x-window.innerWidth/2,y:e=>g.get()[e].y-window.innerHeight/2,opacity:e=>e+1+a.get().trailLength<=g.get().length?0:1,zIndex:e=>e,scale:.6}),l.get()&&(N(v()),k([w(),E(),x()]),u.set(r,{opacity:0}),u.set(w(),{opacity:1,x:0,y:0,scale:1})))}()})),N(I()),window.addEventListener("mousemove",(()=>{e().then((e=>{u=e[0],p=e[1],f=!0})).catch((e=>{console.log(e)}))}),{once:!0,passive:!0})}(n),function(){const e=document.createElement("div");e.className="navOverlay";for(const t of S){const n=document.createElement("div");n.className="overlay",n.addEventListener("click",(()=>{B(t)}),{passive:!0}),n.addEventListener("keydown",(()=>{B(t)}),{passive:!0}),n.addEventListener("mouseover",(()=>{U(t)}),{passive:!0}),n.addEventListener("focus",(()=>{U(t)}),{passive:!0}),e.append(n)}h.addWatcher((()=>{h.get()?e.classList.add("active"):e.classList.remove("active")})),t.append(e),window.addEventListener("keydown",X,{passive:!0})}()}export{j as initDesktop};
|
||||
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
|
||||
import{W as e,s as t,e as n,l as a,f as s,g as c,h as o,j as l,c as m,o as i,k as d}from"./main.js";const r=new e(!1);let p,u,g,h;const E=new e(!1);let y,v,f,N=-1,w=[],x=[],I=[],B=!1;function T(){n.set(!0),I[t.get().index].scrollIntoView({block:"center",behavior:"auto"}),v.to(u,{y:"100%",ease:f.easeInOut,duration:1}),v.to(g,{opacity:0,duration:1.2,delay:.4})}function C(){const e=[];e.push(x[h.activeIndex]),e.push(x[Math.min(h.activeIndex+1,h.slides.length)]),e.push(x[Math.max(h.activeIndex-1,0)]);for(const t of e)t.src=t.dataset.src}let L=[];function k(e){c(e),!E.get()&&B&&(E.set(!0),C(),v.to(g,{opacity:1,duration:1}),v.to(u,{y:0,ease:f.easeInOut,duration:1,delay:.4}),setTimeout((()=>{n.set(!1),E.set(!1)}),1200))}function A(e){(function(e){!function(e){const t=document.createElement("div");t.className="collection";for(const[n,a]of e.entries()){const e=0!==n?d(-25,25):0,s=0!==n?d(-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)}m.append(t)}(e);const t=document.getElementsByClassName("collection").item(0);r.addWatcher((e=>{e?t.classList.remove("hidden"):t.classList.add("hidden")})),L=Array.from(t.getElementsByTagName("img")),L.forEach(((e,t)=>{e.addEventListener("click",(()=>{k(t)}),{passive:!0}),e.addEventListener("keydown",(()=>{k(t)}),{passive:!0}),i(e,(()=>{for(let e=0;e<5;e++){const n=t+e;n<0||n>L.length-1||(L[n].src=L[n].dataset.src)}}))}))})(e),function(e){!function(e){const t=document.createElement("div");t.className="swiper-wrapper";for(const n of e){const e=document.createElement("div");e.className="swiper-slide";const a=document.createElement("img");a.dataset.src=n.hiUrl,a.height=n.hiImgH,a.width=n.hiImgW,a.alt=n.alt,e.append(a),t.append(e)}const n=document.createElement("div");n.className="galleryInner",n.append(t);const a=document.createElement("div");a.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 s=document.createElement("div"),c=document.getElementById("main")?.getAttribute("closeText");s.innerText=l(c),s.addEventListener("click",(()=>{T()}),{passive:!0}),s.addEventListener("keydown",(()=>{T()}),{passive:!0});const o=document.createElement("div");o.className="nav",o.append(a,s);const i=document.createElement("div");i.className="gallery",i.append(n),i.append(o);const d=document.createElement("div");d.className="curtain",m.append(i,d)}(e),w=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),x=Array.from(u.getElementsByTagName("img")),I=Array.from(document.getElementsByClassName("collection").item(0)?.getElementsByTagName("img")??[]),t.addWatcher((()=>{const e=t.get();var n;e.index!==N&&(n=e.index,C(),h.slideTo(n,0),function(){const e=o(t.get().index+1),n=o(t.get().length);w.forEach(((t,a)=>{t.innerText=a<4?e[a]:n[a-4]}))}(),N=e.index)})),r.addWatcher((e=>{e&&n.set(!0)})),window.addEventListener("touchstart",(()=>{a().then((e=>{v=e[0],f=e[1]})).catch((e=>{console.log(e)})),s().then((e=>{y=e,h=new y(p,{spaceBetween:20}),h.on("slideChange",(({realIndex:e})=>{c(e)}))})).catch((e=>{console.log(e)})),B=!0}),{once:!0,passive:!0}),r.set(!0)}(e)}export{A as initMobile};
|
||||
File diff suppressed because one or more lines are too long
BIN
static/lib/fonts/NotoSansTamil-Regular.woff2
Normal file
BIN
static/lib/fonts/NotoSansTamil-Regular.woff2
Normal file
Binary file not shown.
@@ -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
30
vite.config.ts
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user