Compare commits
No commits in common. "main" and "old-cms-migration" have entirely different histories.
main
...
old-cms-mi
1
.dockerignore
Normal file
1
.dockerignore
Normal file
@ -0,0 +1 @@
|
||||
/Dockerfile
|
35
.drone.yml
35
.drone.yml
@ -2,46 +2,15 @@
|
||||
kind: pipeline
|
||||
name: publish pipeline
|
||||
steps:
|
||||
- name: publish astro container
|
||||
- name: publish container
|
||||
image: plugins/docker
|
||||
settings:
|
||||
username: 3wordchant
|
||||
password:
|
||||
from_secret: git_autonomic_zone_token_3wc
|
||||
repo: git.autonomic.zone/ruangrupa/lumbung-kios-astro
|
||||
repo: git.autonomic.zone/autonomic-cooperative/justice-equity-technology
|
||||
auto_tag: true
|
||||
registry: git.autonomic.zone
|
||||
context: astro
|
||||
dockerfile: astro/Dockerfile
|
||||
target: prod
|
||||
- name: publish payload container
|
||||
image: plugins/docker
|
||||
settings:
|
||||
username: 3wordchant
|
||||
password:
|
||||
from_secret: git_autonomic_zone_token_3wc
|
||||
repo: git.autonomic.zone/ruangrupa/lumbung-kios-payload
|
||||
auto_tag: true
|
||||
registry: git.autonomic.zone
|
||||
context: payload
|
||||
dockerfile: payload/Dockerfile
|
||||
target: prod
|
||||
- name: deploy stack
|
||||
image: git.coopcloud.tech/coop-cloud/stack-ssh-deploy:latest
|
||||
settings:
|
||||
stack: kios_lumbung_space
|
||||
host: lumbung.space
|
||||
deploy_key:
|
||||
from_secret: drone_ssh_lumbung.space
|
||||
environment:
|
||||
DOMAIN: kios.lumbung.space
|
||||
STACK_NAME: kios_lumbung_space
|
||||
SECRET_PAYLOAD_SECRET_VERSION: v1
|
||||
SECRET_TOKEN_VERSION: v1
|
||||
SECRET_MONGO_PASSWORD_VERSION: v1
|
||||
depends_on:
|
||||
- publish astro container
|
||||
- publish payload container
|
||||
trigger:
|
||||
branch:
|
||||
- main
|
||||
|
11
.env
11
.env
@ -1,11 +0,0 @@
|
||||
NAME=astroad
|
||||
ASTRO_HOST=localhost:3000
|
||||
PAYLOAD_HOST=payload:3001
|
||||
PAYLOAD_URL=http://localhost:3001
|
||||
PAYLOAD_PORT=3001
|
||||
PAYLOAD_SECRET=supersecretkey
|
||||
MONGODB_URI=mongodb://payload:test@mongo:27017
|
||||
MONGODB_USER=payload
|
||||
MONGODB_PW=test
|
||||
TOKEN=supersecrettoken
|
||||
REPOSITORY=mooxl/astroad
|
16
.env.example
Normal file
16
.env.example
Normal file
@ -0,0 +1,16 @@
|
||||
MONGODB_URI=mongodb://db:27017
|
||||
DB_NAME=turbopress
|
||||
# # FIXME: this is ignored? 🤔🤔🤔
|
||||
ASTRO_PORT=3000
|
||||
ASTRO_HOST=0.0.0.0
|
||||
HOST=0.0.0.0
|
||||
PAYLOAD_PORT=3001
|
||||
PAYLOAD_HOST=0.0.0.0
|
||||
PAYLOAD_PUBLIC_SERVER_URL=http://jett.localhost:3001
|
||||
PAYLOAD_SECRET=37aecd563edc5550a44bbf1e
|
||||
# PAYLOAD_CONFIG_PATH=src/payload.config.ts
|
||||
S3_ACCESS_KEY_ID=
|
||||
S3_SECRET_ACCESS_KEY=
|
||||
S3_REGION=
|
||||
S3_BUCKET=
|
||||
S3_ENDPOINT=
|
5
.eslintrc.cjs
Normal file
5
.eslintrc.cjs
Normal file
@ -0,0 +1,5 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: ["custom"],
|
||||
rules: {},
|
||||
};
|
10
.github/dependabot.yml
vendored
10
.github/dependabot.yml
vendored
@ -1,10 +0,0 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/astro"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/payload"
|
||||
schedule:
|
||||
interval: "daily"
|
29
.github/workflows/payload.yml
vendored
29
.github/workflows/payload.yml
vendored
@ -1,29 +0,0 @@
|
||||
name: Payload update
|
||||
on:
|
||||
repository_dispatch:
|
||||
types: [payload_update]
|
||||
jobs:
|
||||
cancel:
|
||||
name: Cancel Previous Runs
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Cancel Previous Runs
|
||||
uses: styfle/cancel-workflow-action@0.11.0
|
||||
with:
|
||||
ignore_sha: true
|
||||
access_token: ${{ github.token }}
|
||||
workflow_id: "payload.yml"
|
||||
|
||||
build:
|
||||
needs: cancel
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Trigger build
|
||||
uses: appleboy/ssh-action@master
|
||||
with:
|
||||
host: ${{ secrets.HOST }}
|
||||
username: ${{ secrets.USER }}
|
||||
key: ${{ secrets.KEY }}
|
||||
script: |
|
||||
cd ${{ secrets.PATH }}
|
||||
yarn prod astro
|
74
.github/workflows/push.yml
vendored
74
.github/workflows/push.yml
vendored
@ -1,74 +0,0 @@
|
||||
name: Code Deployment and Environment Setup
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
jobs:
|
||||
build:
|
||||
name: Run remote SSH commands
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Clone or pull repository
|
||||
uses: appleboy/ssh-action@v0.1.10
|
||||
with:
|
||||
host: ${{ secrets.HOST }}
|
||||
username: ${{ secrets.USER }}
|
||||
key: ${{ secrets.KEY }}
|
||||
script_stop: false
|
||||
script: |
|
||||
if [ -d ${{ secrets.PATH }} ]; then
|
||||
cd ${{ secrets.PATH }}
|
||||
git pull
|
||||
else
|
||||
mkdir -p ${{ secrets.PATH }}
|
||||
cd ${{ secrets.PATH }}
|
||||
git clone git@github.com:${{ github.repository }}.git .
|
||||
fi
|
||||
|
||||
- name: Update environment variables
|
||||
uses: appleboy/ssh-action@v0.1.10
|
||||
with:
|
||||
host: ${{ secrets.HOST }}
|
||||
username: ${{ secrets.USER }}
|
||||
key: ${{ secrets.KEY }}
|
||||
script_stop: false
|
||||
script: |
|
||||
# Bash function to replace env variables
|
||||
replace_env() {
|
||||
local env_var=$1
|
||||
local new_value=$2
|
||||
awk -v var="${env_var}" -v val="${new_value}" -F '=' '{OFS=FS} $1 == var {$2 = val} 1' .env > .env.tmp && mv .env.tmp .env
|
||||
}
|
||||
|
||||
cd ${{ secrets.PATH }}
|
||||
|
||||
# Replace .env with vars
|
||||
replace_env 'NAME' '${{ vars.NAME }}'
|
||||
replace_env 'ASTRO_HOST' '${{ vars.ASTRO_HOST }}'
|
||||
replace_env 'PAYLOAD_HOST' '${{ vars.PAYLOAD_HOST }}'
|
||||
replace_env 'PAYLOAD_URL' '${{ vars.PAYLOAD_URL }}'
|
||||
replace_env 'PAYLOAD_PORT' '${{ vars.PAYLOAD_PORT }}'
|
||||
|
||||
# Replace .env with secrets
|
||||
replace_env 'PAYLOAD_SECRET' '${{ secrets.PAYLOAD_SECRET }}'
|
||||
replace_env 'MONGODB_URI' 'mongodb://${{ secrets.MONGODB_USER }}:${{ secrets.MONGODB_PW }}@mongo:27017'
|
||||
replace_env 'MONGODB_USER' '${{ secrets.MONGODB_USER }}'
|
||||
replace_env 'MONGODB_PW' '${{ secrets.MONGODB_PW }}'
|
||||
replace_env 'TOKEN' '${{ secrets.TOKEN }}'
|
||||
|
||||
# Replace .env with GitHub repository
|
||||
replace_env 'REPOSITORY' '${{ github.repository }}'
|
||||
|
||||
mkdir -p ./astro
|
||||
cp .env ./astro/.env
|
||||
|
||||
- name: Start Production Services
|
||||
uses: appleboy/ssh-action@v0.1.10
|
||||
with:
|
||||
host: ${{ secrets.HOST }}
|
||||
username: ${{ secrets.USER }}
|
||||
key: ${{ secrets.KEY }}
|
||||
script: |
|
||||
cd ${{ secrets.PATH }}
|
||||
yarn prod payload
|
||||
yarn prod astro
|
37
.gitignore
vendored
37
.gitignore
vendored
@ -1,3 +1,36 @@
|
||||
data
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
node_modules
|
||||
.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
coverage
|
||||
|
||||
# next.js
|
||||
.next/
|
||||
out/
|
||||
build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
yarn-error.log*
|
||||
|
||||
# local env files
|
||||
.env
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
# turbo
|
||||
.turbo
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
4
.husky/pre-commit
Executable file
4
.husky/pre-commit
Executable file
@ -0,0 +1,4 @@
|
||||
#!/usr/bin/env sh
|
||||
. "$(dirname -- "$0")/_/husky.sh"
|
||||
|
||||
pnpm test && pnpm exec lint-staged
|
12
.prettierignore
Normal file
12
.prettierignore
Normal file
@ -0,0 +1,12 @@
|
||||
.DS_Store
|
||||
node_modules
|
||||
/build
|
||||
/package
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
# Ignore files for PNPM, NPM and YARN
|
||||
pnpm-lock.yaml
|
||||
package-lock.json
|
||||
yarn.lock
|
13
.prettierrc
Normal file
13
.prettierrc
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"trailingComma": "all",
|
||||
"plugins": ["prettier-plugin-astro", "prettier-plugin-svelte"],
|
||||
"overrides": [
|
||||
{
|
||||
"files": "*.astro",
|
||||
"options": {
|
||||
"parser": "astro"
|
||||
}
|
||||
},
|
||||
{ "files": "*.svelte", "options": { "parser": "svelte" } }
|
||||
]
|
||||
}
|
6
.vscode/settings.json
vendored
Normal file
6
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"prettier.documentSelectors": ["**/*.astro"],
|
||||
"[astro]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
}
|
||||
}
|
128
CODE_OF_CONDUCT.md
Normal file
128
CODE_OF_CONDUCT.md
Normal file
@ -0,0 +1,128 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
We as members, contributors, and leaders pledge to make participation in our
|
||||
community a harassment-free experience for everyone, regardless of age, body
|
||||
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||
identity and expression, level of experience, education, socio-economic status,
|
||||
nationality, personal appearance, race, religion, or sexual identity
|
||||
and orientation.
|
||||
|
||||
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||
diverse, inclusive, and healthy community.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to a positive environment for our
|
||||
community include:
|
||||
|
||||
* Demonstrating empathy and kindness toward other people
|
||||
* Being respectful of differing opinions, viewpoints, and experiences
|
||||
* Giving and gracefully accepting constructive feedback
|
||||
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||
and learning from the experience
|
||||
* Focusing on what is best not just for us as individuals, but for the
|
||||
overall community
|
||||
|
||||
Examples of unacceptable behavior include:
|
||||
|
||||
* The use of sexualized language or imagery, and sexual attention or
|
||||
advances of any kind
|
||||
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or email
|
||||
address, without their explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Enforcement Responsibilities
|
||||
|
||||
Community leaders are responsible for clarifying and enforcing our standards of
|
||||
acceptable behavior and will take appropriate and fair corrective action in
|
||||
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||
or harmful.
|
||||
|
||||
Community leaders have the right and responsibility to remove, edit, or reject
|
||||
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||
decisions when appropriate.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies within all community spaces, and also applies when
|
||||
an individual is officially representing the community in public spaces.
|
||||
Examples of representing our community include using an official e-mail address,
|
||||
posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported to the community leaders responsible for enforcement at
|
||||
.
|
||||
All complaints will be reviewed and investigated promptly and fairly.
|
||||
|
||||
All community leaders are obligated to respect the privacy and security of the
|
||||
reporter of any incident.
|
||||
|
||||
## Enforcement Guidelines
|
||||
|
||||
Community leaders will follow these Community Impact Guidelines in determining
|
||||
the consequences for any action they deem in violation of this Code of Conduct:
|
||||
|
||||
### 1. Correction
|
||||
|
||||
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||
unprofessional or unwelcome in the community.
|
||||
|
||||
**Consequence**: A private, written warning from community leaders, providing
|
||||
clarity around the nature of the violation and an explanation of why the
|
||||
behavior was inappropriate. A public apology may be requested.
|
||||
|
||||
### 2. Warning
|
||||
|
||||
**Community Impact**: A violation through a single incident or series
|
||||
of actions.
|
||||
|
||||
**Consequence**: A warning with consequences for continued behavior. No
|
||||
interaction with the people involved, including unsolicited interaction with
|
||||
those enforcing the Code of Conduct, for a specified period of time. This
|
||||
includes avoiding interactions in community spaces as well as external channels
|
||||
like social media. Violating these terms may lead to a temporary or
|
||||
permanent ban.
|
||||
|
||||
### 3. Temporary Ban
|
||||
|
||||
**Community Impact**: A serious violation of community standards, including
|
||||
sustained inappropriate behavior.
|
||||
|
||||
**Consequence**: A temporary ban from any sort of interaction or public
|
||||
communication with the community for a specified period of time. No public or
|
||||
private interaction with the people involved, including unsolicited interaction
|
||||
with those enforcing the Code of Conduct, is allowed during this period.
|
||||
Violating these terms may lead to a permanent ban.
|
||||
|
||||
### 4. Permanent Ban
|
||||
|
||||
**Community Impact**: Demonstrating a pattern of violation of community
|
||||
standards, including sustained inappropriate behavior, harassment of an
|
||||
individual, or aggression toward or disparagement of classes of individuals.
|
||||
|
||||
**Consequence**: A permanent ban from any sort of public interaction within
|
||||
the community.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||
version 2.0, available at
|
||||
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
||||
|
||||
Community Impact Guidelines were inspired by [Mozilla's code of conduct
|
||||
enforcement ladder](https://github.com/mozilla/diversity).
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
|
||||
For answers to common questions about this code of conduct, see the FAQ at
|
||||
https://www.contributor-covenant.org/faq. Translations are available at
|
||||
https://www.contributor-covenant.org/translations.
|
12
Dockerfile
Normal file
12
Dockerfile
Normal file
@ -0,0 +1,12 @@
|
||||
FROM node:21-alpine3.18
|
||||
RUN npm i -g pnpm turbo
|
||||
|
||||
WORKDIR /app
|
||||
COPY . .
|
||||
|
||||
RUN pnpm install
|
||||
ENV PAYLOAD_CONFIG_PATH=src/payload.config.ts
|
||||
ENV PAYLOAD_PUBLIC_SERVER_URL=http://localhost:3000
|
||||
RUN pnpm build
|
||||
|
||||
CMD ["pnpm", "serve"]
|
20
LICENSE
20
LICENSE
@ -1,20 +0,0 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2023 Max Schmidt
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
382
LICENSE.md
Normal file
382
LICENSE.md
Normal file
@ -0,0 +1,382 @@
|
||||
# Mozilla Public License Version 2.0
|
||||
|
||||
1. Definitions
|
||||
|
||||
---
|
||||
|
||||
1.1. "Contributor"
|
||||
means each individual or legal entity that creates, contributes to
|
||||
the creation of, or owns Covered Software.
|
||||
|
||||
1.2. "Contributor Version"
|
||||
means the combination of the Contributions of others (if any) used
|
||||
by a Contributor and that particular Contributor's Contribution.
|
||||
|
||||
1.3. "Contribution"
|
||||
means Covered Software of a particular Contributor.
|
||||
|
||||
1.4. "Covered Software"
|
||||
means Source Code Form to which the initial Contributor has attached
|
||||
the notice in Exhibit A, the Executable Form of such Source Code
|
||||
Form, and Modifications of such Source Code Form, in each case
|
||||
including portions thereof.
|
||||
|
||||
1.5. "Incompatible With Secondary Licenses"
|
||||
means
|
||||
|
||||
(a) that the initial Contributor has attached the notice described
|
||||
in Exhibit B to the Covered Software; or
|
||||
|
||||
(b) that the Covered Software was made available under the terms of
|
||||
version 1.1 or earlier of the License, but not also under the
|
||||
terms of a Secondary License.
|
||||
|
||||
1.6. "Executable Form"
|
||||
means any form of the work other than Source Code Form.
|
||||
|
||||
1.7. "Larger Work"
|
||||
means a work that combines Covered Software with other material, in
|
||||
a separate file or files, that is not Covered Software.
|
||||
|
||||
1.8. "License"
|
||||
means this document.
|
||||
|
||||
1.9. "Licensable"
|
||||
means having the right to grant, to the maximum extent possible,
|
||||
whether at the time of the initial grant or subsequently, any and
|
||||
all of the rights conveyed by this License.
|
||||
|
||||
1.10. "Modifications"
|
||||
means any of the following:
|
||||
|
||||
(a) any file in Source Code Form that results from an addition to,
|
||||
deletion from, or modification of the contents of Covered
|
||||
Software; or
|
||||
|
||||
(b) any new file in Source Code Form that contains any Covered
|
||||
Software.
|
||||
|
||||
1.11. "Patent Claims" of a Contributor
|
||||
means any patent claim(s), including without limitation, method,
|
||||
process, and apparatus claims, in any patent Licensable by such
|
||||
Contributor that would be infringed, but for the grant of the
|
||||
License, by the making, using, selling, offering for sale, having
|
||||
made, import, or transfer of either its Contributions or its
|
||||
Contributor Version.
|
||||
|
||||
1.12. "Secondary License"
|
||||
means either the GNU General Public License, Version 2.0, the GNU
|
||||
Lesser General Public License, Version 2.1, the GNU Affero General
|
||||
Public License, Version 3.0, or any later versions of those
|
||||
licenses.
|
||||
|
||||
1.13. "Source Code Form"
|
||||
means the form of the work preferred for making modifications.
|
||||
|
||||
1.14. "You" (or "Your")
|
||||
means an individual or a legal entity exercising rights under this
|
||||
License. For legal entities, "You" includes any entity that
|
||||
controls, is controlled by, or is under common control with You. For
|
||||
purposes of this definition, "control" means (a) the power, direct
|
||||
or indirect, to cause the direction or management of such entity,
|
||||
whether by contract or otherwise, or (b) ownership of more than
|
||||
fifty percent (50%) of the outstanding shares or beneficial
|
||||
ownership of such entity.
|
||||
|
||||
2. License Grants and Conditions
|
||||
|
||||
---
|
||||
|
||||
2.1. Grants
|
||||
|
||||
Each Contributor hereby grants You a world-wide, royalty-free,
|
||||
non-exclusive license:
|
||||
|
||||
(a) under intellectual property rights (other than patent or trademark)
|
||||
Licensable by such Contributor to use, reproduce, make available,
|
||||
modify, display, perform, distribute, and otherwise exploit its
|
||||
Contributions, either on an unmodified basis, with Modifications, or
|
||||
as part of a Larger Work; and
|
||||
|
||||
(b) under Patent Claims of such Contributor to make, use, sell, offer
|
||||
for sale, have made, import, and otherwise transfer either its
|
||||
Contributions or its Contributor Version.
|
||||
|
||||
2.2. Effective Date
|
||||
|
||||
The licenses granted in Section 2.1 with respect to any Contribution
|
||||
become effective for each Contribution on the date the Contributor first
|
||||
distributes such Contribution.
|
||||
|
||||
2.3. Limitations on Grant Scope
|
||||
|
||||
The licenses granted in this Section 2 are the only rights granted under
|
||||
this License. No additional rights or licenses will be implied from the
|
||||
distribution or licensing of Covered Software under this License.
|
||||
Notwithstanding Section 2.1(b) above, no patent license is granted by a
|
||||
Contributor:
|
||||
|
||||
(a) for any code that a Contributor has removed from Covered Software;
|
||||
or
|
||||
|
||||
(b) for infringements caused by: (i) Your and any other third party's
|
||||
modifications of Covered Software, or (ii) the combination of its
|
||||
Contributions with other software (except as part of its Contributor
|
||||
Version); or
|
||||
|
||||
(c) under Patent Claims infringed by Covered Software in the absence of
|
||||
its Contributions.
|
||||
|
||||
This License does not grant any rights in the trademarks, service marks,
|
||||
or logos of any Contributor (except as may be necessary to comply with
|
||||
the notice requirements in Section 3.4).
|
||||
|
||||
2.4. Subsequent Licenses
|
||||
|
||||
No Contributor makes additional grants as a result of Your choice to
|
||||
distribute the Covered Software under a subsequent version of this
|
||||
License (see Section 10.2) or under the terms of a Secondary License (if
|
||||
permitted under the terms of Section 3.3).
|
||||
|
||||
2.5. Representation
|
||||
|
||||
Each Contributor represents that the Contributor believes its
|
||||
Contributions are its original creation(s) or it has sufficient rights
|
||||
to grant the rights to its Contributions conveyed by this License.
|
||||
|
||||
2.6. Fair Use
|
||||
|
||||
This License is not intended to limit any rights You have under
|
||||
applicable copyright doctrines of fair use, fair dealing, or other
|
||||
equivalents.
|
||||
|
||||
2.7. Conditions
|
||||
|
||||
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
|
||||
in Section 2.1.
|
||||
|
||||
3. Responsibilities
|
||||
|
||||
---
|
||||
|
||||
3.1. Distribution of Source Form
|
||||
|
||||
All distribution of Covered Software in Source Code Form, including any
|
||||
Modifications that You create or to which You contribute, must be under
|
||||
the terms of this License. You must inform recipients that the Source
|
||||
Code Form of the Covered Software is governed by the terms of this
|
||||
License, and how they can obtain a copy of this License. You may not
|
||||
attempt to alter or restrict the recipients' rights in the Source Code
|
||||
Form.
|
||||
|
||||
3.2. Distribution of Executable Form
|
||||
|
||||
If You distribute Covered Software in Executable Form then:
|
||||
|
||||
(a) such Covered Software must also be made available in Source Code
|
||||
Form, as described in Section 3.1, and You must inform recipients of
|
||||
the Executable Form how they can obtain a copy of such Source Code
|
||||
Form by reasonable means in a timely manner, at a charge no more
|
||||
than the cost of distribution to the recipient; and
|
||||
|
||||
(b) You may distribute such Executable Form under the terms of this
|
||||
License, or sublicense it under different terms, provided that the
|
||||
license for the Executable Form does not attempt to limit or alter
|
||||
the recipients' rights in the Source Code Form under this License.
|
||||
|
||||
3.3. Distribution of a Larger Work
|
||||
|
||||
You may create and distribute a Larger Work under terms of Your choice,
|
||||
provided that You also comply with the requirements of this License for
|
||||
the Covered Software. If the Larger Work is a combination of Covered
|
||||
Software with a work governed by one or more Secondary Licenses, and the
|
||||
Covered Software is not Incompatible With Secondary Licenses, this
|
||||
License permits You to additionally distribute such Covered Software
|
||||
under the terms of such Secondary License(s), so that the recipient of
|
||||
the Larger Work may, at their option, further distribute the Covered
|
||||
Software under the terms of either this License or such Secondary
|
||||
License(s).
|
||||
|
||||
3.4. Notices
|
||||
|
||||
You may not remove or alter the substance of any license notices
|
||||
(including copyright notices, patent notices, disclaimers of warranty,
|
||||
or limitations of liability) contained within the Source Code Form of
|
||||
the Covered Software, except that You may alter any license notices to
|
||||
the extent required to remedy known factual inaccuracies.
|
||||
|
||||
3.5. Application of Additional Terms
|
||||
|
||||
You may choose to offer, and to charge a fee for, warranty, support,
|
||||
indemnity or liability obligations to one or more recipients of Covered
|
||||
Software. However, You may do so only on Your own behalf, and not on
|
||||
behalf of any Contributor. You must make it absolutely clear that any
|
||||
such warranty, support, indemnity, or liability obligation is offered by
|
||||
You alone, and You hereby agree to indemnify every Contributor for any
|
||||
liability incurred by such Contributor as a result of warranty, support,
|
||||
indemnity or liability terms You offer. You may include additional
|
||||
disclaimers of warranty and limitations of liability specific to any
|
||||
jurisdiction.
|
||||
|
||||
4. Inability to Comply Due to Statute or Regulation
|
||||
|
||||
---
|
||||
|
||||
If it is impossible for You to comply with any of the terms of this
|
||||
License with respect to some or all of the Covered Software due to
|
||||
statute, judicial order, or regulation then You must: (a) comply with
|
||||
the terms of this License to the maximum extent possible; and (b)
|
||||
describe the limitations and the code they affect. Such description must
|
||||
be placed in a text file included with all distributions of the Covered
|
||||
Software under this License. Except to the extent prohibited by statute
|
||||
or regulation, such description must be sufficiently detailed for a
|
||||
recipient of ordinary skill to be able to understand it.
|
||||
|
||||
5. Termination
|
||||
|
||||
---
|
||||
|
||||
5.1. The rights granted under this License will terminate automatically
|
||||
if You fail to comply with any of its terms. However, if You become
|
||||
compliant, then the rights granted under this License from a particular
|
||||
Contributor are reinstated (a) provisionally, unless and until such
|
||||
Contributor explicitly and finally terminates Your grants, and (b) on an
|
||||
ongoing basis, if such Contributor fails to notify You of the
|
||||
non-compliance by some reasonable means prior to 60 days after You have
|
||||
come back into compliance. Moreover, Your grants from a particular
|
||||
Contributor are reinstated on an ongoing basis if such Contributor
|
||||
notifies You of the non-compliance by some reasonable means, this is the
|
||||
first time You have received notice of non-compliance with this License
|
||||
from such Contributor, and You become compliant prior to 30 days after
|
||||
Your receipt of the notice.
|
||||
|
||||
5.2. If You initiate litigation against any entity by asserting a patent
|
||||
infringement claim (excluding declaratory judgment actions,
|
||||
counter-claims, and cross-claims) alleging that a Contributor Version
|
||||
directly or indirectly infringes any patent, then the rights granted to
|
||||
You by any and all Contributors for the Covered Software under Section
|
||||
2.1 of this License shall terminate.
|
||||
|
||||
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
|
||||
end user license agreements (excluding distributors and resellers) which
|
||||
have been validly granted by You or Your distributors under this License
|
||||
prior to termination shall survive termination.
|
||||
|
||||
---
|
||||
|
||||
- *
|
||||
- 6. Disclaimer of Warranty \*
|
||||
- ------------------------- \*
|
||||
- *
|
||||
- Covered Software is provided under this License on an "as is" \*
|
||||
- basis, without warranty of any kind, either expressed, implied, or \*
|
||||
- statutory, including, without limitation, warranties that the \*
|
||||
- Covered Software is free of defects, merchantable, fit for a \*
|
||||
- particular purpose or non-infringing. The entire risk as to the \*
|
||||
- quality and performance of the Covered Software is with You. \*
|
||||
- Should any Covered Software prove defective in any respect, You \*
|
||||
- (not any Contributor) assume the cost of any necessary servicing, \*
|
||||
- repair, or correction. This disclaimer of warranty constitutes an \*
|
||||
- essential part of this License. No use of any Covered Software is \*
|
||||
- authorized under this License except under this disclaimer. \*
|
||||
- *
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
- *
|
||||
- 7. Limitation of Liability \*
|
||||
- -------------------------- \*
|
||||
- *
|
||||
- Under no circumstances and under no legal theory, whether tort \*
|
||||
- (including negligence), contract, or otherwise, shall any \*
|
||||
- Contributor, or anyone who distributes Covered Software as \*
|
||||
- permitted above, be liable to You for any direct, indirect, \*
|
||||
- special, incidental, or consequential damages of any character \*
|
||||
- including, without limitation, damages for lost profits, loss of \*
|
||||
- goodwill, work stoppage, computer failure or malfunction, or any \*
|
||||
- and all other commercial damages or losses, even if such party \*
|
||||
- shall have been informed of the possibility of such damages. This \*
|
||||
- limitation of liability shall not apply to liability for death or \*
|
||||
- personal injury resulting from such party's negligence to the \*
|
||||
- extent applicable law prohibits such limitation. Some \*
|
||||
- jurisdictions do not allow the exclusion or limitation of \*
|
||||
- incidental or consequential damages, so this exclusion and \*
|
||||
- limitation may not apply to You. \*
|
||||
- *
|
||||
|
||||
---
|
||||
|
||||
8. Litigation
|
||||
|
||||
---
|
||||
|
||||
Any litigation relating to this License may be brought only in the
|
||||
courts of a jurisdiction where the defendant maintains its principal
|
||||
place of business and such litigation shall be governed by laws of that
|
||||
jurisdiction, without reference to its conflict-of-law provisions.
|
||||
Nothing in this Section shall prevent a party's ability to bring
|
||||
cross-claims or counter-claims.
|
||||
|
||||
9. Miscellaneous
|
||||
|
||||
---
|
||||
|
||||
This License represents the complete agreement concerning the subject
|
||||
matter hereof. If any provision of this License is held to be
|
||||
unenforceable, such provision shall be reformed only to the extent
|
||||
necessary to make it enforceable. Any law or regulation which provides
|
||||
that the language of a contract shall be construed against the drafter
|
||||
shall not be used to construe this License against a Contributor.
|
||||
|
||||
10. Versions of the License
|
||||
|
||||
---
|
||||
|
||||
10.1. New Versions
|
||||
|
||||
Mozilla Foundation is the license steward. Except as provided in Section
|
||||
10.3, no one other than the license steward has the right to modify or
|
||||
publish new versions of this License. Each version will be given a
|
||||
distinguishing version number.
|
||||
|
||||
10.2. Effect of New Versions
|
||||
|
||||
You may distribute the Covered Software under the terms of the version
|
||||
of the License under which You originally received the Covered Software,
|
||||
or under the terms of any subsequent version published by the license
|
||||
steward.
|
||||
|
||||
10.3. Modified Versions
|
||||
|
||||
If you create software not governed by this License, and you want to
|
||||
create a new license for such software, you may create and use a
|
||||
modified version of this License if you rename the license and remove
|
||||
any references to the name of the license steward (except to note that
|
||||
such modified license differs from this License).
|
||||
|
||||
10.4. Distributing Source Code Form that is Incompatible With Secondary
|
||||
Licenses
|
||||
|
||||
If You choose to distribute Source Code Form that is Incompatible With
|
||||
Secondary Licenses under the terms of this version of the License, the
|
||||
notice described in Exhibit B of this License must be attached.
|
||||
|
||||
## Exhibit A - Source Code Form License Notice
|
||||
|
||||
This Source Code Form is subject to the terms of the Mozilla Public
|
||||
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
If it is not possible or desirable to put the notice in a particular
|
||||
file, then You may include the notice in a location (such as a LICENSE
|
||||
file in a relevant directory) where a recipient would be likely to look
|
||||
for such a notice.
|
||||
|
||||
You may add additional accurate notices of copyright ownership.
|
||||
|
||||
## Exhibit B - "Incompatible With Secondary Licenses" Notice
|
||||
|
||||
This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||
defined by the Mozilla Public License, v. 2.0.
|
127
README.md
127
README.md
@ -1,86 +1,77 @@
|
||||
# Astroad
|
||||
|
||||
Astroad is a pre-configured setup for Astro and Payloadcms, designed to make it easy for you to start building your website. With Astroad, you'll have a complete development environment that you can run locally using Docker. This setup simplifies the testing and development of your website before deploying it to a production environment.
|
||||
|
||||
## Important notes
|
||||
### Adding dependencies
|
||||
1. When adding a dedpendency to astro or payload, you must regenerate the yarn.lock file for the dev server toinstall the dependencies.
|
||||
2. Regenerate the lock file:
|
||||
`cd astro` or `cd payload`
|
||||
`rm yarn.lock`
|
||||
`yarn install`
|
||||
3. Restart dev server `yarn dev`
|
||||
|
||||
## Dev server getting stuck
|
||||
Sometimes the dev script gets stuck, for an unknown reason:
|
||||
## Quickstart
|
||||
```
|
||||
[+] Running 3/0
|
||||
✔ Container astroad-mongo Running 0.0s
|
||||
✔ Container astroad-payload Running 0.0s
|
||||
✔ Container astroad-astro Running
|
||||
docker-compose -f docker-compose.yml up
|
||||
```
|
||||
|
||||
Running `yarn stop` and then `yarn dev` should resolve the issue.
|
||||
## About
|
||||
This project was bootstrapped with a [turbopress](https://github.com/turbopress/turbopress)
|
||||
|
||||
### Useful docs
|
||||
- [Astro docs: Integrating with PayloadCMS](https://docs.astro.build/en/guides/cms/payload/)
|
||||
- [Astro docs: Deployment and SSR adapters](https://docs.astro.build/en/guides/deploy/)
|
||||
- [Astro docs: On demand rendering adapters](https://docs.astro.build/en/guides/server-side-rendering/)
|
||||
|
||||
## Why Astro?
|
||||
|
||||
Astro allow you to use your favorite UI components and libraries. Mix and match React, Preact, Svelte, Vue, SolidJS, AlpineJS, and Lit to build your own website.
|
||||
|
||||
## Why PayloadCMS?
|
||||
|
||||
I need a headless CMS that is easy to use with TypeScript support. PayloadCMS work really well in this use case.
|
||||
|
||||
## What's inside?
|
||||
|
||||
This Turborepo includes the following packages/apps:
|
||||
|
||||
### Apps and Packages
|
||||
|
||||
- `api`: a [Payload](https://payloadcms.com/) app
|
||||
- `web`: an [Astro](https://astro.build/) app
|
||||
- `eslint-config-custom`: `eslint` configurations (includes `eslint-config-prettier` and `eslint-configg-turbo`)
|
||||
|
||||
Each package/app is 100% [TypeScript](https://www.typescriptlang.org/).
|
||||
|
||||
### Utilities
|
||||
|
||||
This Turborepo has some additional tools already setup for you:
|
||||
|
||||
- [TypeScript](https://www.typescriptlang.org/) for static type checking
|
||||
- [TailwindCSS](https://tailwindcss.com/) for CSS utility
|
||||
- [ESLint](https://eslint.org/) for code linting
|
||||
- [Prettier](https://prettier.io) for code formatting
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before getting started with Astroad, make sure you have the necessary software installed:
|
||||
Install `nodejs`, `pnpm` and `turborepo` on your local machine
|
||||
|
||||
- Docker
|
||||
- Node.js
|
||||
- Yarn
|
||||
## Develop
|
||||
|
||||
## Configuration
|
||||
Create a `.env` file in the root folder, you can use the `.env.example` file as an example
|
||||
|
||||
While there's no configuration necessary for local development, deployment via Github Workflows requires specific secrets and variables to be set.
|
||||
I use `pnpm` for this project.
|
||||
|
||||
### Secrets:
|
||||
Run the following command:
|
||||
|
||||
- `USER`: User on the server
|
||||
- `HOST`: IP or URL of the server
|
||||
- `KEY`: SSH KEY for connecting to the server
|
||||
- `MONGODB_PW`: Password for MongoDB
|
||||
- `MONGODB_USER`: User for MongoDB
|
||||
- `PATH`: Path where the repository resides on the server
|
||||
- `PAYLOAD_PORT`: Port at which Payload listens
|
||||
- `PAYLOAD_SECRET`: String to encrypt Payload data
|
||||
- `TOKEN`: Github Access Token for the webhook to trigger the payload.yml workflow and execute a new Astro build
|
||||
```sh
|
||||
pnpm installl
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
### Variables:
|
||||
By default, the payloadCMS will run on port 3000, and Astro will be served on port 3001.
|
||||
|
||||
- `ASTRO_HOST`: Hostdomain of the Frontend
|
||||
- `PAYLOAD_HOST`: Hostdomain of the CMS
|
||||
- `PAYLOAD_URL`: URL of the CMS
|
||||
- `NAME`: Name of the Container and Project
|
||||
## Build & Serve (NodeJs)
|
||||
|
||||
Please remember to set these secrets and variables in your repository settings to ensure a successful deployment through Github Workflows.
|
||||
```sh
|
||||
pnpm build
|
||||
pnpm serve
|
||||
```
|
||||
|
||||
Once the secrets and variables are set on GitHub, they will replace the existing ones in the `.env` file on the server during deployment. This is done by the push.yml workflow, which replaces the placeholders in the `.env` with the actual secrets and variables defined in the repository settings. Please ensure that the names of your secrets and variables match with the placeholders in the `.env` file.
|
||||
## TypeScript support
|
||||
|
||||
## Getting started
|
||||
To fully utilize the type safe features, manually generate the types for PayloadCMS by runng `pnpm generate:types`
|
||||
|
||||
To get started with Astroad, you'll need to have Docker and NPM || Yarn || PNPM installed on your machine.
|
||||
Then, you can import the types easily as simple as
|
||||
|
||||
You have two options for getting the repository:
|
||||
|
||||
1. Use the 'Use this template' button on the Github repository. This will create a new repository in your Github account with the same directory structure and files as Astroad. After the new repository is created, you can clone it to your local machine.
|
||||
1. Alternatively, you can directly clone the Astroad repository: git clone https://github.com/mooxl/astroad.git. If you choose this option, remember to change the origin of your remote repository to a new one to avoid pushing changes directly to the Astroad repository. This can be done with the command: git remote set-url origin https://github.com/USERNAME/REPOSITORY.git where USERNAME is your username and REPOSITORY is the name of your new repository.
|
||||
|
||||
Once you've cloned the repository or created your own from the template, follow these steps:
|
||||
|
||||
1. Change into the repository directory: `cd {newName}`
|
||||
1. Start the containers: `yarn dev`
|
||||
|
||||
This will start up the Astro, Payloadcms and Mongo containers and make them available on your local machine. Astro will be served at http://localhost:3000 and the Payload will be available at http://localhost:3001.
|
||||
|
||||
## Development
|
||||
|
||||
The `docker-compose.yml` and `docker-compose-dev.yml` files includes everything you need to run the containers. The containers use the environment variables declared in the `.env` file and mounted volumes to store data persistently even after the containers are stopped and started.
|
||||
|
||||
## Deployment
|
||||
|
||||
Deployment is handled by a Github Actions Workflow on every push. It logs into the server via SSH, pulls or clones the latest version of the repository, and runs `yarn prod`.
|
||||
|
||||
Because Astro is completely static, a content change in the CMS must trigger a new build of Astro. Therefore, there’s a `payload.yml` workflow that gets triggered by a webhook after every content change from Payload.
|
||||
|
||||
Ensure you have Traefik set up as a reverse proxy before deployment. The prod script will launch your site in a production-ready environment.
|
||||
```ts
|
||||
import type { User } from "@turbopress/api/types";
|
||||
```
|
1
apps/api/.dockerignore
Normal file
1
apps/api/.dockerignore
Normal file
@ -0,0 +1 @@
|
||||
**/node_modules
|
5
apps/api/.eslintrc.cjs
Normal file
5
apps/api/.eslintrc.cjs
Normal file
@ -0,0 +1,5 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: ["custom"],
|
||||
rules: {},
|
||||
};
|
169
apps/api/.gitignore
vendored
Normal file
169
apps/api/.gitignore
vendored
Normal file
@ -0,0 +1,169 @@
|
||||
### Node ###
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# Snowpack dependency directory (https://snowpack.dev/)
|
||||
web_modules/
|
||||
|
||||
# TypeScript cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Optional stylelint cache
|
||||
.stylelintcache
|
||||
|
||||
# Microbundle cache
|
||||
.rpt2_cache/
|
||||
.rts2_cache_cjs/
|
||||
.rts2_cache_es/
|
||||
.rts2_cache_umd/
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variable files
|
||||
.env
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.local
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
.parcel-cache
|
||||
|
||||
# Next.js build output
|
||||
.next
|
||||
out
|
||||
|
||||
# Nuxt.js build / generate output
|
||||
.nuxt
|
||||
dist
|
||||
|
||||
# Gatsby files
|
||||
.cache/
|
||||
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||
# public
|
||||
|
||||
# vuepress build output
|
||||
.vuepress/dist
|
||||
|
||||
# vuepress v2.x temp and cache directory
|
||||
.temp
|
||||
|
||||
# Docusaurus cache and generated files
|
||||
.docusaurus
|
||||
|
||||
# Serverless directories
|
||||
.serverless/
|
||||
|
||||
# FuseBox cache
|
||||
.fusebox/
|
||||
|
||||
# DynamoDB Local files
|
||||
.dynamodb/
|
||||
|
||||
# TernJS port file
|
||||
.tern-port
|
||||
|
||||
# Stores VSCode versions used for testing VSCode extensions
|
||||
.vscode-test
|
||||
|
||||
# yarn v2
|
||||
.yarn/cache
|
||||
.yarn/unplugged
|
||||
.yarn/build-state.yml
|
||||
.yarn/install-state.gz
|
||||
.pnp.*
|
||||
|
||||
### Node Patch ###
|
||||
# Serverless Webpack directories
|
||||
.webpack/
|
||||
|
||||
# Optional stylelint cache
|
||||
|
||||
# SvelteKit build / generate output
|
||||
.svelte-kit
|
||||
|
||||
### VisualStudioCode ###
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
!.vscode/*.code-snippets
|
||||
|
||||
# Local History for Visual Studio Code
|
||||
.history/
|
||||
|
||||
# Built Visual Studio Code Extensions
|
||||
*.vsix
|
||||
|
||||
### VisualStudioCode Patch ###
|
||||
# Ignore all local history of files
|
||||
.history
|
||||
.ionide
|
||||
|
||||
# Support for Project snippet scope
|
||||
.vscode/*.code-snippets
|
||||
|
||||
# Ignore code-workspaces
|
||||
*.code-workspace
|
||||
|
||||
# End of https://www.toptal.com/developers/gitignore/api/node,visualstudiocode
|
||||
|
||||
# Local media files
|
||||
media
|
39
apps/api/Dockerfile
Normal file
39
apps/api/Dockerfile
Normal file
@ -0,0 +1,39 @@
|
||||
FROM node:18-alpine as base
|
||||
RUN npm i -g pnpm turbo
|
||||
|
||||
FROM base AS pruner
|
||||
WORKDIR /app
|
||||
COPY . .
|
||||
RUN turbo prune --scope=@turbopress/api --docker
|
||||
|
||||
# remove all empty node_modules folder structure
|
||||
RUN rm -rf /app/out/full/*/*/node_modules
|
||||
|
||||
FROM base AS builder
|
||||
RUN apk add --no-cache libc6-compat
|
||||
RUN apk update
|
||||
WORKDIR /app
|
||||
|
||||
# Add lockfile and package.json's of isolated subworkspace
|
||||
COPY .gitignore .gitignore
|
||||
COPY --from=pruner /app/out/json/ .
|
||||
RUN pnpm install
|
||||
|
||||
# Build the project and its dependencies
|
||||
COPY --from=pruner /app/out/full/ .
|
||||
ENV PAYLOAD_CONFIG_PATH=src/payload.config.ts
|
||||
RUN pnpm build:api
|
||||
|
||||
# Run App
|
||||
FROM base as runner
|
||||
WORKDIR /app
|
||||
ENV NODE_ENV=production
|
||||
ENV PAYLOAD_CONFIG_PATH=dist/payload.config.js
|
||||
COPY --from=builder /app/apps/api/package.json .
|
||||
RUN pnpm install --prod
|
||||
COPY --from=builder /app/apps/api/dist ./dist
|
||||
COPY --from=builder /app/apps/api/build ./build
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
CMD ["node", "dist/server.js"]
|
19
apps/api/README.md
Normal file
19
apps/api/README.md
Normal file
@ -0,0 +1,19 @@
|
||||
# api
|
||||
|
||||
This project was created using create-payload-app using the blog template.
|
||||
|
||||
## How to Use
|
||||
|
||||
`yarn dev` will start up your application and reload on any changes.
|
||||
|
||||
### Docker
|
||||
|
||||
If you have docker and docker-compose installed, you can run `docker-compose up`
|
||||
|
||||
To build the docker image, run `docker build -t my-tag -f Dockerfile ../..`
|
||||
|
||||
Ensure you are passing all needed environment variables when starting up your container via `--env-file` or setting them with your deployment.
|
||||
|
||||
The 3 typical env vars will be `MONGODB_URI`, `PAYLOAD_SECRET`, and `PAYLOAD_CONFIG_PATH`
|
||||
|
||||
`docker run --env-file .env -p 3000:3000 my-tag`
|
5
apps/api/nodemon.json
Normal file
5
apps/api/nodemon.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"watch": ["src"],
|
||||
"ext": "ts",
|
||||
"exec": "ts-node src/server.ts"
|
||||
}
|
36
apps/api/package.json
Normal file
36
apps/api/package.json
Normal file
@ -0,0 +1,36 @@
|
||||
{
|
||||
"name": "@turbopress/api",
|
||||
"description": "Headless CMS based on Payload",
|
||||
"version": "1.0.0",
|
||||
"main": "dist/server.js",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"dev": "nodemon",
|
||||
"build:payload": "payload build",
|
||||
"build:server": "tsc",
|
||||
"build": "pnpm copyfiles && pnpm build:payload && pnpm build:server",
|
||||
"copyfiles": "copyfiles -u 1 \"src/**/*.{html,css,scss,ttf,woff,woff2,eot,svg,jpg,png}\" dist/",
|
||||
"generate:types": "payload generate:types",
|
||||
"lint": "eslint \"./src/**/*.{js,ts}\" --fix",
|
||||
"serve": "PAYLOAD_CONFIG_PATH=dist/payload.config.js node dist/server.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"dotenv": "^8.2.0",
|
||||
"express": "^4.17.1",
|
||||
"payload": "1.12.0",
|
||||
"@payloadcms/plugin-seo": "latest",
|
||||
"@payloadcms/plugin-cloud-storage": "latest",
|
||||
"@aws-sdk/client-s3": "latest",
|
||||
"@aws-sdk/lib-storage": "latest"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "latest",
|
||||
"@types/express": "^4.17.9",
|
||||
"copyfiles": "^2.4.1",
|
||||
"nodemon": "^2.0.6",
|
||||
"ts-node": "^9.1.1",
|
||||
"typescript": "^4.8.4",
|
||||
"webpack-hot-middleware": "^2.25.4",
|
||||
"eslint-config-custom": "*"
|
||||
}
|
||||
}
|
128
apps/api/src/blocks/Menu.ts
Normal file
128
apps/api/src/blocks/Menu.ts
Normal file
@ -0,0 +1,128 @@
|
||||
import payload from "payload";
|
||||
import { Block } from "payload/types";
|
||||
import Pages from "../collections/Pages";
|
||||
import linkField from "../fields/linkField";
|
||||
|
||||
export const Menu: Block = {
|
||||
slug: "menu",
|
||||
interfaceName: "Menu",
|
||||
fields: [
|
||||
{
|
||||
name: "type",
|
||||
type: "select",
|
||||
options: ["default"],
|
||||
required: true,
|
||||
defaultValue: "default",
|
||||
},
|
||||
{
|
||||
name: "menus",
|
||||
type: "array",
|
||||
fields: [
|
||||
{
|
||||
name: "mainMenu",
|
||||
type: "group",
|
||||
interfaceName: "MainMenu",
|
||||
fields: [
|
||||
{
|
||||
type: "row",
|
||||
fields: [
|
||||
{
|
||||
name: "type",
|
||||
type: "radio",
|
||||
options: [
|
||||
{
|
||||
label: "Internal link",
|
||||
value: "reference",
|
||||
},
|
||||
{
|
||||
label: "Custom URL",
|
||||
value: "custom",
|
||||
},
|
||||
{
|
||||
label: "None",
|
||||
value: "none",
|
||||
},
|
||||
],
|
||||
defaultValue: "reference",
|
||||
admin: {
|
||||
layout: "horizontal",
|
||||
width: "50%",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "newTab",
|
||||
label: "Open in new tab",
|
||||
type: "checkbox",
|
||||
admin: {
|
||||
condition: (_, siblingData) => siblingData?.type != "none",
|
||||
width: "50%",
|
||||
style: {
|
||||
alignSelf: "flex-end",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "reference",
|
||||
label: "Document to link to",
|
||||
type: "relationship",
|
||||
relationTo: [Pages.slug],
|
||||
required: true,
|
||||
maxDepth: 0,
|
||||
admin: {
|
||||
condition: (_, siblingData) =>
|
||||
siblingData?.type === "reference",
|
||||
width: "50%",
|
||||
},
|
||||
hooks: {
|
||||
afterRead: [
|
||||
async ({ value, siblingData }) => {
|
||||
if (value && siblingData.type === "reference") {
|
||||
const id = value.value;
|
||||
const pages = await payload.find({
|
||||
collection: "pages",
|
||||
where: {
|
||||
id: { equals: id },
|
||||
},
|
||||
depth: 0,
|
||||
});
|
||||
|
||||
if (pages.docs[0]?.slug)
|
||||
siblingData.url = pages.docs[0].slug;
|
||||
}
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "url",
|
||||
label: "Custom URL",
|
||||
type: "text",
|
||||
required: true,
|
||||
admin: {
|
||||
condition: (_, siblingData) =>
|
||||
siblingData?.type === "custom",
|
||||
width: "50%",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "label",
|
||||
label: "Label",
|
||||
type: "text",
|
||||
required: true,
|
||||
admin: {
|
||||
width: "50%",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "subMenu",
|
||||
type: "array",
|
||||
fields: [linkField()],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
17
apps/api/src/blocks/PageContent.ts
Normal file
17
apps/api/src/blocks/PageContent.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { Block } from "payload/types";
|
||||
|
||||
export const PageContent: Block = {
|
||||
slug: "pageContent",
|
||||
interfaceName: "PageContent",
|
||||
fields: [
|
||||
{
|
||||
name: "description",
|
||||
type: "textarea",
|
||||
defaultValue:
|
||||
"This block will display the content of the page (if any). Please edit the original page change the value.",
|
||||
admin: {
|
||||
readOnly: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
52
apps/api/src/blocks/PageList.ts
Normal file
52
apps/api/src/blocks/PageList.ts
Normal file
@ -0,0 +1,52 @@
|
||||
import { Block } from "payload/types";
|
||||
import Categories from "../collections/Categories";
|
||||
import { PagesField } from "../collections/Pages";
|
||||
import Tags from "../collections/Tags";
|
||||
|
||||
const PageListField = {
|
||||
numberOfItems: "numberOfItems",
|
||||
filterByCategories: "filterByCategories",
|
||||
filterByTags: "filterByTags",
|
||||
sortBy: "sortBy",
|
||||
pages: "pages",
|
||||
};
|
||||
|
||||
type PageListField = (typeof PageListField)[keyof typeof PageListField];
|
||||
|
||||
export const PageList: Block = {
|
||||
slug: "pageList",
|
||||
interfaceName: "PageList",
|
||||
fields: [
|
||||
{
|
||||
name: PageListField.numberOfItems,
|
||||
type: "number",
|
||||
defaultValue: 5,
|
||||
},
|
||||
{
|
||||
name: PageListField.filterByCategories,
|
||||
type: "relationship",
|
||||
relationTo: [Categories.slug],
|
||||
maxDepth: 0,
|
||||
hasMany: true,
|
||||
},
|
||||
{
|
||||
name: PageListField.filterByTags,
|
||||
type: "relationship",
|
||||
relationTo: [Tags.slug],
|
||||
hasMany: true,
|
||||
maxDepth: 0,
|
||||
},
|
||||
{
|
||||
name: PageListField.sortBy,
|
||||
type: "select",
|
||||
options: [
|
||||
PagesField.title,
|
||||
PagesField.createdAt,
|
||||
PagesField.updatedAt,
|
||||
`-${PagesField.title}`,
|
||||
`-${PagesField.createdAt}`,
|
||||
`-${PagesField.updatedAt}`,
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
15
apps/api/src/blocks/ReusableContent.ts
Normal file
15
apps/api/src/blocks/ReusableContent.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { Block } from "payload/types";
|
||||
import Contents from "../collections/Contents";
|
||||
|
||||
export const ReusableContent: Block = {
|
||||
slug: "reusableContent",
|
||||
interfaceName: "ReusableContent",
|
||||
fields: [
|
||||
{
|
||||
name: "reference",
|
||||
type: "relationship",
|
||||
// maxDepth: 0,
|
||||
relationTo: [Contents.slug],
|
||||
},
|
||||
],
|
||||
};
|
9
apps/api/src/blocks/SiteTitle.ts
Normal file
9
apps/api/src/blocks/SiteTitle.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import { Block } from "payload/types";
|
||||
|
||||
export const SiteTitle: Block = {
|
||||
slug: "siteTitle",
|
||||
interfaceName: "SiteTitle",
|
||||
fields: [
|
||||
{ name: "siteName", type: "text", required: true, admin: { width: "50%" } },
|
||||
],
|
||||
};
|
37
apps/api/src/collections/Categories.ts
Normal file
37
apps/api/src/collections/Categories.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import type { CollectionConfig } from "payload/types";
|
||||
|
||||
const CategoriesField = {
|
||||
name: "name",
|
||||
slug: "slug",
|
||||
};
|
||||
|
||||
type CategoriesField = (typeof CategoriesField)[keyof typeof CategoriesField];
|
||||
|
||||
const Categories: CollectionConfig = {
|
||||
slug: "categories",
|
||||
admin: {
|
||||
useAsTitle: CategoriesField.name,
|
||||
},
|
||||
access: {
|
||||
read: () => true,
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: CategoriesField.name,
|
||||
type: "text",
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: CategoriesField.slug,
|
||||
type: "text",
|
||||
required: true,
|
||||
unique: true,
|
||||
admin: {
|
||||
position: "sidebar",
|
||||
},
|
||||
},
|
||||
],
|
||||
timestamps: false,
|
||||
};
|
||||
|
||||
export default Categories;
|
48
apps/api/src/collections/Contents.ts
Normal file
48
apps/api/src/collections/Contents.ts
Normal file
@ -0,0 +1,48 @@
|
||||
import { CollectionConfig } from "payload/types";
|
||||
import { Menu } from "../blocks/Menu";
|
||||
import { PageContent } from "../blocks/PageContent";
|
||||
import { PageList } from "../blocks/PageList";
|
||||
import { SiteTitle } from "../blocks/SiteTitle";
|
||||
|
||||
const ContentsField = {
|
||||
name: "name",
|
||||
slug: "slug",
|
||||
description: "description",
|
||||
};
|
||||
type ContentsField = (typeof ContentsField)[keyof typeof ContentsField];
|
||||
|
||||
const Contents: CollectionConfig = {
|
||||
slug: "contents",
|
||||
access: {
|
||||
read: () => true,
|
||||
},
|
||||
admin: {
|
||||
useAsTitle: ContentsField.name,
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: ContentsField.name,
|
||||
type: "text",
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: ContentsField.slug,
|
||||
type: "text",
|
||||
unique: true,
|
||||
admin: {
|
||||
position: "sidebar",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: ContentsField.description,
|
||||
type: "text",
|
||||
},
|
||||
{
|
||||
name: "blocks",
|
||||
type: "blocks",
|
||||
blocks: [Menu, PageContent, PageList, SiteTitle],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export default Contents;
|
79
apps/api/src/collections/Layouts.ts
Normal file
79
apps/api/src/collections/Layouts.ts
Normal file
@ -0,0 +1,79 @@
|
||||
import { CollectionConfig } from "payload/types";
|
||||
import { Menu } from "../blocks/Menu";
|
||||
import { PageContent } from "../blocks/PageContent";
|
||||
import { PageList } from "../blocks/PageList";
|
||||
import { ReusableContent } from "../blocks/ReusableContent";
|
||||
import { SiteTitle } from "../blocks/SiteTitle";
|
||||
|
||||
const LayoutsField = {
|
||||
name: "name",
|
||||
slug: "slug",
|
||||
description: "description",
|
||||
};
|
||||
type LayoutsField = (typeof LayoutsField)[keyof typeof LayoutsField];
|
||||
|
||||
const blocks = [ReusableContent];
|
||||
|
||||
const Layouts: CollectionConfig = {
|
||||
slug: "layouts",
|
||||
access: {
|
||||
read: () => true,
|
||||
},
|
||||
admin: {
|
||||
useAsTitle: LayoutsField.name,
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: LayoutsField.name,
|
||||
type: "text",
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: LayoutsField.slug,
|
||||
type: "text",
|
||||
unique: true,
|
||||
admin: {
|
||||
position: "sidebar",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: LayoutsField.description,
|
||||
type: "text",
|
||||
},
|
||||
{
|
||||
name: "header",
|
||||
type: "group",
|
||||
fields: [
|
||||
{
|
||||
name: "blocks",
|
||||
type: "blocks",
|
||||
blocks: [...blocks, Menu, SiteTitle],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "body",
|
||||
type: "group",
|
||||
fields: [
|
||||
{
|
||||
name: "blocks",
|
||||
type: "blocks",
|
||||
blocks: [...blocks, PageContent, PageList],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "footer",
|
||||
type: "group",
|
||||
fields: [
|
||||
{
|
||||
name: "blocks",
|
||||
type: "blocks",
|
||||
blocks: blocks,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export default Layouts;
|
29
apps/api/src/collections/Media.ts
Normal file
29
apps/api/src/collections/Media.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import type { CollectionConfig } from "payload/types";
|
||||
|
||||
const Media: CollectionConfig = {
|
||||
slug: "media",
|
||||
access: {
|
||||
read: () => true,
|
||||
},
|
||||
upload: {
|
||||
disableLocalStorage: true,
|
||||
adminThumbnail: "thumbnail",
|
||||
imageSizes: [
|
||||
{
|
||||
height: 400,
|
||||
width: 400,
|
||||
crop: "center",
|
||||
name: "thumbnail",
|
||||
},
|
||||
{
|
||||
width: 900,
|
||||
height: 450,
|
||||
crop: "center",
|
||||
name: "sixteenByNineMedium",
|
||||
},
|
||||
],
|
||||
},
|
||||
fields: [],
|
||||
};
|
||||
|
||||
export default Media;
|
113
apps/api/src/collections/Pages.ts
Normal file
113
apps/api/src/collections/Pages.ts
Normal file
@ -0,0 +1,113 @@
|
||||
import type { CollectionConfig } from "payload/types";
|
||||
|
||||
export const PagesField = {
|
||||
title: "title",
|
||||
slug: "slug",
|
||||
author: "author",
|
||||
publishedDate: "publishedDate",
|
||||
categories: "categories",
|
||||
tags: "tags",
|
||||
content: "content",
|
||||
status: "status",
|
||||
layout: "layout",
|
||||
createdAt: "createdAt",
|
||||
updatedAt: "updatedAt",
|
||||
};
|
||||
|
||||
type PagesField = (typeof PagesField)[keyof typeof PagesField];
|
||||
|
||||
const PagesFieldStatus = {
|
||||
Draft: "Draft",
|
||||
Published: "Published",
|
||||
};
|
||||
|
||||
type PagesFieldStatus =
|
||||
(typeof PagesFieldStatus)[keyof typeof PagesFieldStatus];
|
||||
|
||||
const Pages: CollectionConfig = {
|
||||
slug: "pages",
|
||||
admin: {
|
||||
defaultColumns: [
|
||||
PagesField.title,
|
||||
PagesField.slug,
|
||||
PagesField.author,
|
||||
PagesField.categories,
|
||||
PagesField.tags,
|
||||
PagesField.status,
|
||||
],
|
||||
useAsTitle: PagesField.title,
|
||||
},
|
||||
access: {
|
||||
read: () => true,
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: PagesField.title,
|
||||
type: "text",
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: PagesField.slug,
|
||||
type: "text",
|
||||
required: true,
|
||||
admin: {
|
||||
position: "sidebar",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: PagesField.author,
|
||||
type: "relationship",
|
||||
relationTo: "users",
|
||||
admin: {
|
||||
position: "sidebar",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: PagesField.publishedDate,
|
||||
type: "date",
|
||||
admin: {
|
||||
position: "sidebar",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: PagesField.categories,
|
||||
type: "relationship",
|
||||
relationTo: "categories",
|
||||
hasMany: true,
|
||||
admin: {
|
||||
position: "sidebar",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: PagesField.tags,
|
||||
type: "relationship",
|
||||
relationTo: "tags",
|
||||
hasMany: true,
|
||||
admin: {
|
||||
position: "sidebar",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: PagesField.status,
|
||||
type: "select",
|
||||
options: Object.entries(PagesFieldStatus).map((e) => {
|
||||
return { label: e[0], value: e[1] };
|
||||
}),
|
||||
defaultValue: PagesFieldStatus.Draft,
|
||||
admin: {
|
||||
position: "sidebar",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: PagesField.layout,
|
||||
type: "relationship",
|
||||
relationTo: "layouts",
|
||||
},
|
||||
{
|
||||
name: PagesField.content,
|
||||
type: "richText",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export default Pages;
|
37
apps/api/src/collections/Tags.ts
Normal file
37
apps/api/src/collections/Tags.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import type { CollectionConfig } from "payload/types";
|
||||
|
||||
const TagsField = {
|
||||
name: "name",
|
||||
slug: "slug",
|
||||
};
|
||||
|
||||
type TagsField = (typeof TagsField)[keyof typeof TagsField];
|
||||
|
||||
const Tags: CollectionConfig = {
|
||||
slug: "tags",
|
||||
admin: {
|
||||
useAsTitle: TagsField.name,
|
||||
},
|
||||
access: {
|
||||
read: () => true,
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: TagsField.name,
|
||||
type: "text",
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: TagsField.slug,
|
||||
type: "text",
|
||||
required: true,
|
||||
unique: true,
|
||||
admin: {
|
||||
position: "sidebar",
|
||||
},
|
||||
},
|
||||
],
|
||||
timestamps: false,
|
||||
};
|
||||
|
||||
export default Tags;
|
18
apps/api/src/collections/Users.ts
Normal file
18
apps/api/src/collections/Users.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import type { CollectionConfig } from "payload/types";
|
||||
|
||||
const Users: CollectionConfig = {
|
||||
slug: "users",
|
||||
auth: true,
|
||||
admin: {
|
||||
useAsTitle: "email",
|
||||
},
|
||||
fields: [
|
||||
// Email added by default
|
||||
{
|
||||
name: "name",
|
||||
type: "text",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export default Users;
|
15
apps/api/src/fields/enableField.ts
Normal file
15
apps/api/src/fields/enableField.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { SelectField } from "payload/types";
|
||||
|
||||
function enableField(fieldOverrides?: Partial<SelectField>): SelectField {
|
||||
return {
|
||||
label: "Enable",
|
||||
name: "enable",
|
||||
type: "select",
|
||||
options: ["Yes", "No"],
|
||||
defaultValue: "Yes",
|
||||
required: true,
|
||||
...fieldOverrides,
|
||||
};
|
||||
}
|
||||
|
||||
export default enableField;
|
100
apps/api/src/fields/linkField.ts
Normal file
100
apps/api/src/fields/linkField.ts
Normal file
@ -0,0 +1,100 @@
|
||||
import payload from "payload";
|
||||
import { GroupField } from "payload/types";
|
||||
|
||||
function linkField(fieldOverrides?: Partial<GroupField>): GroupField {
|
||||
return {
|
||||
name: "link",
|
||||
type: "group",
|
||||
interfaceName: "Link",
|
||||
fields: [
|
||||
{
|
||||
type: "row",
|
||||
fields: [
|
||||
{
|
||||
name: "type",
|
||||
type: "radio",
|
||||
options: [
|
||||
{
|
||||
label: "Internal link",
|
||||
value: "reference",
|
||||
},
|
||||
{
|
||||
label: "Custom URL",
|
||||
value: "custom",
|
||||
},
|
||||
],
|
||||
defaultValue: "reference",
|
||||
admin: {
|
||||
layout: "horizontal",
|
||||
width: "50%",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "newTab",
|
||||
label: "Open in new tab",
|
||||
type: "checkbox",
|
||||
admin: {
|
||||
width: "50%",
|
||||
style: {
|
||||
alignSelf: "flex-end",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "reference",
|
||||
label: "Document to link to",
|
||||
type: "relationship",
|
||||
relationTo: ["pages"],
|
||||
required: true,
|
||||
maxDepth: 0,
|
||||
admin: {
|
||||
condition: (_, siblingData) => siblingData?.type === "reference",
|
||||
width: "50%",
|
||||
},
|
||||
hooks: {
|
||||
afterRead: [
|
||||
async ({ value, siblingData }) => {
|
||||
if (value && siblingData.type === "reference") {
|
||||
const id = value.value;
|
||||
const pages = await payload.find({
|
||||
collection: "pages",
|
||||
where: {
|
||||
id: { equals: id },
|
||||
},
|
||||
depth: 0,
|
||||
});
|
||||
|
||||
if (pages.docs[0]?.slug)
|
||||
if (pages.docs[0]) siblingData.url = pages.docs[0].slug;
|
||||
}
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "url",
|
||||
label: "Custom URL",
|
||||
type: "text",
|
||||
required: true,
|
||||
admin: {
|
||||
condition: (_, siblingData) => siblingData?.type === "custom",
|
||||
width: "50%",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "label",
|
||||
label: "Label",
|
||||
type: "text",
|
||||
required: true,
|
||||
admin: {
|
||||
width: "50%",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
...fieldOverrides,
|
||||
};
|
||||
}
|
||||
|
||||
export default linkField;
|
59
apps/api/src/payload.config.ts
Normal file
59
apps/api/src/payload.config.ts
Normal file
@ -0,0 +1,59 @@
|
||||
import { cloudStorage } from "@payloadcms/plugin-cloud-storage";
|
||||
import { s3Adapter } from "@payloadcms/plugin-cloud-storage/s3";
|
||||
import seo from "@payloadcms/plugin-seo";
|
||||
import { GenerateTitle } from "@payloadcms/plugin-seo/dist/types";
|
||||
import path from "path";
|
||||
import { buildConfig } from "payload/config";
|
||||
import Categories from "./collections/Categories";
|
||||
import Contents from "./collections/Contents";
|
||||
import Layouts from "./collections/Layouts";
|
||||
import Media from "./collections/Media";
|
||||
import Pages from "./collections/Pages";
|
||||
import Tags from "./collections/Tags";
|
||||
import Users from "./collections/Users";
|
||||
const generateTitle: GenerateTitle = ({ slug, doc }) => {
|
||||
let title = "TurboPress";
|
||||
if (slug == "pages") {
|
||||
const page = doc as any;
|
||||
return (title = `TurboPress - ${page?.title?.value}`);
|
||||
}
|
||||
return title;
|
||||
};
|
||||
|
||||
const adapter = s3Adapter({
|
||||
config: {
|
||||
credentials: {
|
||||
accessKeyId: process.env.S3_ACCESS_KEY_ID,
|
||||
secretAccessKey: process.env.S3_SECRET_ACCESS_KEY,
|
||||
},
|
||||
region: process.env.S3_REGION,
|
||||
endpoint: process.env.S3_ENDPOINT,
|
||||
},
|
||||
bucket: process.env.S3_BUCKET,
|
||||
});
|
||||
|
||||
export default buildConfig({
|
||||
serverURL: process.env.PAYLOAD_PUBLIC_SERVER_URL ?? "http://localhost:3000",
|
||||
admin: {
|
||||
user: Users.slug,
|
||||
},
|
||||
collections: [Categories, Contents, Layouts, Media, Pages, Tags, Users],
|
||||
typescript: {
|
||||
outputFile: path.join(__dirname, "../types", "payload.ts"),
|
||||
},
|
||||
plugins: [
|
||||
seo({
|
||||
collections: ["pages"],
|
||||
uploadsCollection: "media",
|
||||
generateTitle: generateTitle,
|
||||
}),
|
||||
cloudStorage({
|
||||
collections: {
|
||||
media: {
|
||||
adapter: adapter,
|
||||
},
|
||||
},
|
||||
}),
|
||||
],
|
||||
cors: "*",
|
||||
});
|
39
apps/api/src/server.ts
Normal file
39
apps/api/src/server.ts
Normal file
@ -0,0 +1,39 @@
|
||||
import express from "express";
|
||||
import payload from "payload";
|
||||
|
||||
const app = express();
|
||||
|
||||
// Redirect root to Admin panel
|
||||
app.get("/", (_, res) => {
|
||||
res.redirect("/admin");
|
||||
});
|
||||
|
||||
const PORT = Number(process.env.PAYLOAD_PORT) ?? 3000;
|
||||
const HOST = process.env.PAYLOAD_HOST ?? 'localhost';
|
||||
|
||||
const start = async () => {
|
||||
// Initialize Payload
|
||||
await payload.init({
|
||||
secret: process.env.PAYLOAD_SECRET,
|
||||
mongoURL: process.env.MONGODB_URI,
|
||||
express: app,
|
||||
onInit: async () => {
|
||||
payload.logger.info(`Payload Admin URL: ${payload.getAdminURL()}`);
|
||||
},
|
||||
mongoOptions: {
|
||||
dbName: process.env.DB_NAME,
|
||||
},
|
||||
});
|
||||
|
||||
// Add your own express routes here
|
||||
|
||||
app.listen(
|
||||
PORT,
|
||||
HOST,
|
||||
() => {
|
||||
console.log(`Server running at http://${HOST}:${PORT}/`);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
start();
|
@ -2,17 +2,21 @@
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"strict": false,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"jsx": "react",
|
||||
"paths": {
|
||||
"@/*": ["./src/*", "./dist/*", "./dist/src/*"]
|
||||
},
|
||||
"jsx": "react"
|
||||
"payload/generated-types": ["./types/payload.ts"]
|
||||
}
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules", "dist", "build"],
|
||||
"ts-node": {
|
||||
"transpileOnly": true,
|
||||
"require": ["tsconfig-paths/register"]
|
||||
"swc": true
|
||||
}
|
||||
}
|
2
apps/api/types/index.ts
Normal file
2
apps/api/types/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from "./payload";
|
||||
export * from "./rich-text-export";
|
200
apps/api/types/payload.ts
Normal file
200
apps/api/types/payload.ts
Normal file
@ -0,0 +1,200 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* This file was automatically generated by Payload.
|
||||
* DO NOT MODIFY IT BY HAND. Instead, modify your source Payload config,
|
||||
* and re-run `payload generate:types` to regenerate this file.
|
||||
*/
|
||||
|
||||
export interface Config {
|
||||
collections: {
|
||||
categories: Category;
|
||||
contents: Content;
|
||||
layouts: Layout;
|
||||
media: Media;
|
||||
pages: Page;
|
||||
tags: Tag;
|
||||
users: User;
|
||||
};
|
||||
globals: {};
|
||||
}
|
||||
export interface Category {
|
||||
id: string;
|
||||
name: string;
|
||||
slug: string;
|
||||
}
|
||||
export interface Content {
|
||||
id: string;
|
||||
name: string;
|
||||
slug?: string;
|
||||
description?: string;
|
||||
blocks?: (Menu | PageContent | PageList | SiteTitle)[];
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
export interface Menu {
|
||||
type: 'default';
|
||||
menus?: {
|
||||
mainMenu: MainMenu;
|
||||
id?: string;
|
||||
}[];
|
||||
id?: string;
|
||||
blockName?: string;
|
||||
blockType: 'menu';
|
||||
}
|
||||
export interface MainMenu {
|
||||
type?: 'reference' | 'custom' | 'none';
|
||||
newTab?: boolean;
|
||||
reference: {
|
||||
value: string | Page;
|
||||
relationTo: 'pages';
|
||||
};
|
||||
url: string;
|
||||
label: string;
|
||||
subMenu?: {
|
||||
link: Link;
|
||||
id?: string;
|
||||
}[];
|
||||
}
|
||||
export interface Page {
|
||||
id: string;
|
||||
title: string;
|
||||
slug: string;
|
||||
author?: string | User;
|
||||
publishedDate?: string;
|
||||
categories?: string[] | Category[];
|
||||
tags?: string[] | Tag[];
|
||||
status?: 'Draft' | 'Published';
|
||||
layout?: string | Layout;
|
||||
content?: {
|
||||
[k: string]: unknown;
|
||||
}[];
|
||||
meta?: {
|
||||
title?: string;
|
||||
description?: string;
|
||||
image?: string | Media;
|
||||
};
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
export interface User {
|
||||
id: string;
|
||||
name?: string;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
email: string;
|
||||
resetPasswordToken?: string;
|
||||
resetPasswordExpiration?: string;
|
||||
salt?: string;
|
||||
hash?: string;
|
||||
loginAttempts?: number;
|
||||
lockUntil?: string;
|
||||
password?: string;
|
||||
}
|
||||
export interface Tag {
|
||||
id: string;
|
||||
name: string;
|
||||
slug: string;
|
||||
}
|
||||
export interface Layout {
|
||||
id: string;
|
||||
name: string;
|
||||
slug?: string;
|
||||
description?: string;
|
||||
header?: {
|
||||
blocks?: (ReusableContent | Menu | SiteTitle)[];
|
||||
};
|
||||
body?: {
|
||||
blocks?: (ReusableContent | PageContent | PageList)[];
|
||||
};
|
||||
footer?: {
|
||||
blocks?: ReusableContent[];
|
||||
};
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
export interface ReusableContent {
|
||||
reference?: {
|
||||
value: string | Content;
|
||||
relationTo: 'contents';
|
||||
};
|
||||
id?: string;
|
||||
blockName?: string;
|
||||
blockType: 'reusableContent';
|
||||
}
|
||||
export interface SiteTitle {
|
||||
siteName: string;
|
||||
id?: string;
|
||||
blockName?: string;
|
||||
blockType: 'siteTitle';
|
||||
}
|
||||
export interface PageContent {
|
||||
description?: string;
|
||||
id?: string;
|
||||
blockName?: string;
|
||||
blockType: 'pageContent';
|
||||
}
|
||||
export interface PageList {
|
||||
numberOfItems?: number;
|
||||
filterByCategories?:
|
||||
| {
|
||||
value: string;
|
||||
relationTo: 'categories';
|
||||
}[]
|
||||
| {
|
||||
value: Category;
|
||||
relationTo: 'categories';
|
||||
}[];
|
||||
filterByTags?:
|
||||
| {
|
||||
value: string;
|
||||
relationTo: 'tags';
|
||||
}[]
|
||||
| {
|
||||
value: Tag;
|
||||
relationTo: 'tags';
|
||||
}[];
|
||||
sortBy?: 'title' | 'createdAt' | 'updatedAt' | '-title' | '-createdAt' | '-updatedAt';
|
||||
id?: string;
|
||||
blockName?: string;
|
||||
blockType: 'pageList';
|
||||
}
|
||||
export interface Media {
|
||||
id: string;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
url?: string;
|
||||
filename?: string;
|
||||
mimeType?: string;
|
||||
filesize?: number;
|
||||
width?: number;
|
||||
height?: number;
|
||||
sizes?: {
|
||||
thumbnail?: {
|
||||
url?: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
mimeType?: string;
|
||||
filesize?: number;
|
||||
filename?: string;
|
||||
};
|
||||
sixteenByNineMedium?: {
|
||||
url?: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
mimeType?: string;
|
||||
filesize?: number;
|
||||
filename?: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
export interface Link {
|
||||
type?: 'reference' | 'custom';
|
||||
newTab?: boolean;
|
||||
reference: {
|
||||
value: string | Page;
|
||||
relationTo: 'pages';
|
||||
};
|
||||
url: string;
|
||||
label: string;
|
||||
}
|
22
apps/api/types/rich-text-export.ts
Normal file
22
apps/api/types/rich-text-export.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import type {
|
||||
RichTextElement,
|
||||
RichTextLeaf,
|
||||
} from "payload/dist/fields/config/types";
|
||||
import type { RichTextCustomElement, RichTextCustomLeaf } from "payload/types";
|
||||
|
||||
type DefaultRichTextLeaf = Exclude<RichTextLeaf, RichTextCustomLeaf>;
|
||||
export type FormattedText = {
|
||||
[key in DefaultRichTextLeaf]?: boolean;
|
||||
} & {
|
||||
text: string;
|
||||
};
|
||||
|
||||
type DefaultRichTextElement =
|
||||
| Exclude<RichTextElement, RichTextCustomElement>
|
||||
| "li"
|
||||
| "quote";
|
||||
export type FormattedElement = {
|
||||
type: DefaultRichTextElement;
|
||||
url?: string;
|
||||
children: FormattedText[];
|
||||
};
|
17
apps/web/.eslintrc.cjs
Normal file
17
apps/web/.eslintrc.cjs
Normal file
@ -0,0 +1,17 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: ["custom", "plugin:astro/recommended"],
|
||||
overrides: [
|
||||
{
|
||||
files: ["*.astro"],
|
||||
parser: "astro-eslint-parser",
|
||||
parserOptions: {
|
||||
parser: "@typescript-eslint/parser",
|
||||
extraFileExtensions: [".astro"],
|
||||
},
|
||||
rules: {
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
8
astro/.gitignore → apps/web/.gitignore
vendored
8
astro/.gitignore → apps/web/.gitignore
vendored
@ -1,6 +1,5 @@
|
||||
# build output
|
||||
dist/
|
||||
|
||||
# generated types
|
||||
.astro/
|
||||
|
||||
@ -14,8 +13,9 @@ yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
|
||||
|
||||
# environment variables
|
||||
.env
|
||||
.env.production
|
||||
|
||||
# macOS-specific files
|
||||
.DS_Store
|
||||
|
||||
# types
|
||||
src/types.ts
|
13
apps/web/README.md
Normal file
13
apps/web/README.md
Normal file
@ -0,0 +1,13 @@
|
||||
# Astro with Tailwind
|
||||
|
||||
```
|
||||
npm create astro@latest -- --template with-tailwindcss
|
||||
```
|
||||
|
||||
[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/withastro/astro/tree/latest/examples/with-tailwindcss)
|
||||
[![Open with CodeSandbox](https://assets.codesandbox.io/github/button-edit-lime.svg)](https://codesandbox.io/p/sandbox/github/withastro/astro/tree/latest/examples/with-tailwindcss)
|
||||
[![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/withastro/astro?devcontainer_path=.devcontainer/with-tailwindcss/devcontainer.json)
|
||||
|
||||
Astro comes with [Tailwind](https://tailwindcss.com) support out of the box. This example showcases how to style your Astro project with Tailwind.
|
||||
|
||||
For complete setup instructions, please see our [Tailwind Integration Guide](https://docs.astro.build/en/guides/integrations-guide/tailwind).
|
27
apps/web/astro.config.mjs
Normal file
27
apps/web/astro.config.mjs
Normal file
@ -0,0 +1,27 @@
|
||||
import mdx from "@astrojs/mdx";
|
||||
import node from "@astrojs/node";
|
||||
import svelte from "@astrojs/svelte";
|
||||
import tailwind from "@astrojs/tailwind";
|
||||
|
||||
import { defineConfig } from "astro/config";
|
||||
|
||||
export default defineConfig({
|
||||
integrations: [mdx(), tailwind(), svelte()],
|
||||
server: {
|
||||
port: parseInt(process.env.ASTRO_PORT ?? "3000"),
|
||||
// FIXME 3wc: seems to be ignored?
|
||||
hostname: process.env.ASTRO_HOST ?? "0.0.0.0",
|
||||
},
|
||||
output: "server",
|
||||
adapter: node({
|
||||
mode: "standalone",
|
||||
}),
|
||||
vite: {
|
||||
// FIXME 3wc: shouldn't need to hardcode this
|
||||
define: {
|
||||
"import.meta.env.PAYLOAD_PUBLIC_SERVER_URL": JSON.stringify(
|
||||
"http://api:3001"
|
||||
)
|
||||
},
|
||||
},
|
||||
});
|
43
apps/web/package.json
Normal file
43
apps/web/package.json
Normal file
@ -0,0 +1,43 @@
|
||||
{
|
||||
"name": "@turbopress/web",
|
||||
"description": "Front end website based on Astro",
|
||||
"type": "module",
|
||||
"version": "0.0.1",
|
||||
"scripts": {
|
||||
"dev": "astro dev",
|
||||
"start": "astro dev",
|
||||
"build": "astro build",
|
||||
"preview": "astro preview",
|
||||
"astro": "astro",
|
||||
"lint": "eslint \"./src/**/*.{js,ts,tsx,astro}\" --fix",
|
||||
"serve": "node dist/server/entry.mjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/mdx": "latest",
|
||||
"@astrojs/node": "latest",
|
||||
"@astrojs/svelte": "latest",
|
||||
"@astrojs/tailwind": "latest",
|
||||
"@turbopress/api": "*",
|
||||
"astro": "latest",
|
||||
"autoprefixer": "latest",
|
||||
"@iconify/svelte": "latest",
|
||||
"postcss": "latest",
|
||||
"qs": "^6.11.2",
|
||||
"slate": "latest",
|
||||
"tailwindcss": "latest",
|
||||
"astro-icon": "latest",
|
||||
"nanostores": "latest",
|
||||
"svelte": "latest",
|
||||
"svelte-inview": "4.0.1 "
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/typography": "^0.5.9",
|
||||
"@types/escape-html": "^1.0.2",
|
||||
"@types/qs": "^6.9.7",
|
||||
"escape-html": "^1.0.3",
|
||||
"eslint": "latest",
|
||||
"eslint-config-custom": "*",
|
||||
"eslint-plugin-astro": "latest",
|
||||
"svelte-breakpoints": "latest"
|
||||
}
|
||||
}
|
9
apps/web/public/favicon.svg
Normal file
9
apps/web/public/favicon.svg
Normal file
@ -0,0 +1,9 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 128 128">
|
||||
<path d="M50.4 78.5a75.1 75.1 0 0 0-28.5 6.9l24.2-65.7c.7-2 1.9-3.2 3.4-3.2h29c1.5 0 2.7 1.2 3.4 3.2l24.2 65.7s-11.6-7-28.5-7L67 45.5c-.4-1.7-1.6-2.8-2.9-2.8-1.3 0-2.5 1.1-2.9 2.7L50.4 78.5Zm-1.1 28.2Zm-4.2-20.2c-2 6.6-.6 15.8 4.2 20.2a17.5 17.5 0 0 1 .2-.7 5.5 5.5 0 0 1 5.7-4.5c2.8.1 4.3 1.5 4.7 4.7.2 1.1.2 2.3.2 3.5v.4c0 2.7.7 5.2 2.2 7.4a13 13 0 0 0 5.7 4.9v-.3l-.2-.3c-1.8-5.6-.5-9.5 4.4-12.8l1.5-1a73 73 0 0 0 3.2-2.2 16 16 0 0 0 6.8-11.4c.3-2 .1-4-.6-6l-.8.6-1.6 1a37 37 0 0 1-22.4 2.7c-5-.7-9.7-2-13.2-6.2Z" />
|
||||
<style>
|
||||
path { fill: #000; }
|
||||
@media (prefers-color-scheme: dark) {
|
||||
path { fill: #FFF; }
|
||||
}
|
||||
</style>
|
||||
</svg>
|
After Width: | Height: | Size: 749 B |
6
apps/web/src/components/NoHomePage.astro
Normal file
6
apps/web/src/components/NoHomePage.astro
Normal file
@ -0,0 +1,6 @@
|
||||
<div class="grid place-items-center h-full w-full content-center">
|
||||
<div class="text-xl font-bold">No homepage has been setup.</div>
|
||||
<div class="text-lg">
|
||||
Create a new page with slug 'home' in the admin panel.
|
||||
</div>
|
||||
</div>
|
18
apps/web/src/components/RenderLayout.astro
Normal file
18
apps/web/src/components/RenderLayout.astro
Normal file
@ -0,0 +1,18 @@
|
||||
---
|
||||
import type {
|
||||
FormattedElement,
|
||||
FormattedText,
|
||||
Layout,
|
||||
} from "@turbopress/api/types";
|
||||
import RenderBody from "./body/RenderBody.astro";
|
||||
import RenderHeader from "./header/RenderHeader.astro";
|
||||
interface Props {
|
||||
layout: Layout;
|
||||
content?: (FormattedElement | FormattedText)[];
|
||||
}
|
||||
|
||||
const { layout, content } = Astro.props;
|
||||
---
|
||||
|
||||
<RenderHeader blocks={layout.header?.blocks} />
|
||||
<RenderBody blocks={layout.body?.blocks} {content} />
|
30
apps/web/src/components/RenderReusableContent.astro
Normal file
30
apps/web/src/components/RenderReusableContent.astro
Normal file
@ -0,0 +1,30 @@
|
||||
---
|
||||
import type { Content } from "@turbopress/api/types";
|
||||
import { getContentSingle } from "../services/api/content.service";
|
||||
import RenderSiteTitle from "./header/RenderSiteTitle.astro";
|
||||
import RenderMenu from "./menu/RenderMenu.astro";
|
||||
interface Props {
|
||||
content: string | Content;
|
||||
}
|
||||
|
||||
const content: Content | undefined =
|
||||
typeof Astro.props.content == "string"
|
||||
? await getContentSingle(Astro.props.content)
|
||||
: Astro.props.content;
|
||||
|
||||
if (!content?.blocks) return;
|
||||
|
||||
const blocks = content.blocks;
|
||||
---
|
||||
|
||||
{
|
||||
blocks.map((block) => {
|
||||
if (!block.id) return;
|
||||
if (block.blockType == "siteTitle")
|
||||
return <RenderSiteTitle siteTitle={block} />;
|
||||
if (block.blockType == "menu" && block.menus)
|
||||
return <RenderMenu menu={block} />;
|
||||
|
||||
return <div>block = {block.id}</div>;
|
||||
})
|
||||
}
|
36
apps/web/src/components/body/RenderBody.astro
Normal file
36
apps/web/src/components/body/RenderBody.astro
Normal file
@ -0,0 +1,36 @@
|
||||
---
|
||||
import type {
|
||||
FormattedElement,
|
||||
FormattedText,
|
||||
PageContent,
|
||||
PageList,
|
||||
ReusableContent,
|
||||
} from "@turbopress/api/types";
|
||||
import RenderContent from "../RenderReusableContent.astro";
|
||||
import RenderPageContent from "./RenderPageContent.astro";
|
||||
// import RenderPageList from "./page-list/RenderPageList.astro";
|
||||
import RenderPageList from "./page-list/RenderPageList.svelte";
|
||||
interface Props {
|
||||
blocks?: (PageList | ReusableContent | PageContent)[];
|
||||
content?: (FormattedElement | FormattedText)[];
|
||||
}
|
||||
|
||||
const { blocks = [], content } = Astro.props;
|
||||
|
||||
if (blocks.length === 0) return;
|
||||
---
|
||||
|
||||
<main class="p-6 flex flex-wrap">
|
||||
{
|
||||
blocks.map((block) => {
|
||||
if (!block.id) return;
|
||||
if (block.blockType == "reusableContent" && block.reference?.value)
|
||||
return <RenderContent content={block.reference.value} />;
|
||||
if (block.blockType == "pageContent")
|
||||
return <RenderPageContent {content} />;
|
||||
if (block.blockType == "pageList")
|
||||
return <RenderPageList {block} client:only />;
|
||||
return <div>{block.id}</div>;
|
||||
})
|
||||
}
|
||||
</main>
|
17
apps/web/src/components/body/RenderPageContent.astro
Normal file
17
apps/web/src/components/body/RenderPageContent.astro
Normal file
@ -0,0 +1,17 @@
|
||||
---
|
||||
import type { FormattedElement, FormattedText } from "@turbopress/api/types";
|
||||
import RichText from "../rich-text/RichText.astro";
|
||||
interface Props {
|
||||
content?: (FormattedElement | FormattedText)[];
|
||||
title?: string;
|
||||
}
|
||||
const { content } = Astro.props;
|
||||
---
|
||||
|
||||
{
|
||||
content && (
|
||||
<article class="w-full justify-center prose max-w-none prose-headings:m-0 prose-headings:my-3 prose-p:m-0 prose-p:my-2">
|
||||
<RichText richText={content} />
|
||||
</article>
|
||||
)
|
||||
}
|
18
apps/web/src/components/body/page-list/PageListItem.svelte
Normal file
18
apps/web/src/components/body/page-list/PageListItem.svelte
Normal file
@ -0,0 +1,18 @@
|
||||
<script lang="ts">
|
||||
import type { Page } from "@turbopress/api/types";
|
||||
import RichText from "../../rich-text/RichText.svelte";
|
||||
|
||||
export let page: Page;
|
||||
</script>
|
||||
|
||||
{#if page.content}
|
||||
<section class="">
|
||||
<header>
|
||||
<h2>{page.title}</h2>
|
||||
</header>
|
||||
<article>
|
||||
<RichText richText={page.content}></RichText>
|
||||
</article>
|
||||
<hr />
|
||||
</section>
|
||||
{/if}
|
65
apps/web/src/components/body/page-list/RenderPageList.svelte
Normal file
65
apps/web/src/components/body/page-list/RenderPageList.svelte
Normal file
@ -0,0 +1,65 @@
|
||||
<script lang="ts">
|
||||
import type { Page, PageList } from "@turbopress/api/types";
|
||||
import { inview } from "svelte-inview";
|
||||
import { writable, type Writable } from "svelte/store";
|
||||
import { getPageCollection } from "../../../services/api";
|
||||
import type { PayloadCollection } from "../../../types";
|
||||
import PageListItem from "./PageListItem.svelte";
|
||||
|
||||
export let block: PageList;
|
||||
|
||||
const query = {
|
||||
where: {
|
||||
or: [
|
||||
{
|
||||
categories: {
|
||||
in: block.filterByCategories?.map((f) => f.value),
|
||||
},
|
||||
},
|
||||
{
|
||||
tags: {
|
||||
in: block.filterByTags?.map((f) => f.value),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
limit: block.numberOfItems ?? 5,
|
||||
page: 1,
|
||||
sort: block.sortBy,
|
||||
};
|
||||
|
||||
const queryState = writable(query);
|
||||
const collection: Writable<PayloadCollection<Page> | undefined> = writable();
|
||||
|
||||
async function getPages() {
|
||||
const pages = await getPageCollection($queryState);
|
||||
collection.set(pages);
|
||||
}
|
||||
|
||||
queryState.subscribe((s) => {
|
||||
getPages();
|
||||
});
|
||||
|
||||
$: pages = $collection?.docs;
|
||||
|
||||
function handleChange({ detail }: CustomEvent<ObserverEventDetails>) {
|
||||
isInView = detail.inView;
|
||||
if (detail.inView && $collection?.hasNextPage) {
|
||||
queryState.set({ ...$queryState, limit: $queryState.limit + 1 });
|
||||
}
|
||||
}
|
||||
|
||||
let isInView: boolean;
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="w-full prose max-w-none prose-headings:m-0 prose-headings:my-3 prose-p:m-0 prose-p:my-2"
|
||||
>
|
||||
{#if pages}
|
||||
{#each pages as page}
|
||||
<PageListItem {page}></PageListItem>
|
||||
{/each}
|
||||
{/if}
|
||||
|
||||
<div use:inview on:inview_change={handleChange}></div>
|
||||
</div>
|
29
apps/web/src/components/header/RenderHeader.astro
Normal file
29
apps/web/src/components/header/RenderHeader.astro
Normal file
@ -0,0 +1,29 @@
|
||||
---
|
||||
import type { Menu, ReusableContent, SiteTitle } from "@turbopress/api/types";
|
||||
import RenderContent from "../RenderReusableContent.astro";
|
||||
import RenderMenu from "../menu/RenderMenu.astro";
|
||||
import RenderSiteTitle from "./RenderSiteTitle.astro";
|
||||
// import SiteTitle from "./SiteTitle.astro";
|
||||
interface Props {
|
||||
blocks?: (Menu | ReusableContent | SiteTitle)[];
|
||||
}
|
||||
|
||||
const { blocks = [] } = Astro.props;
|
||||
|
||||
if (blocks.length === 0) return;
|
||||
---
|
||||
|
||||
<header class="shadow p-6 flex flex-wrap">
|
||||
{
|
||||
blocks.map((block) => {
|
||||
if (!block.id) return;
|
||||
if (block.blockType == "siteTitle")
|
||||
return <RenderSiteTitle siteTitle={block} />;
|
||||
if (block.blockType == "reusableContent" && block.reference?.value)
|
||||
return <RenderContent content={block.reference.value} />;
|
||||
if (block.blockType == "menu" && block.menus)
|
||||
return <RenderMenu menu={block} />;
|
||||
return <div>{block.id}</div>;
|
||||
})
|
||||
}
|
||||
</header>
|
16
apps/web/src/components/header/RenderSiteTitle.astro
Normal file
16
apps/web/src/components/header/RenderSiteTitle.astro
Normal file
@ -0,0 +1,16 @@
|
||||
---
|
||||
import type { SiteTitle } from "@turbopress/api/types";
|
||||
import Link from "../link/Link.astro";
|
||||
interface Props {
|
||||
siteTitle: SiteTitle;
|
||||
}
|
||||
|
||||
const { siteTitle } = Astro.props;
|
||||
---
|
||||
|
||||
<div class="flex-grow">
|
||||
<Link link="/">
|
||||
<div class="font-bold text-lg">{siteTitle.siteName}</div>
|
||||
</Link>
|
||||
<div class="w-full"></div>
|
||||
</div>
|
22
apps/web/src/components/link/Link.astro
Normal file
22
apps/web/src/components/link/Link.astro
Normal file
@ -0,0 +1,22 @@
|
||||
---
|
||||
interface Props {
|
||||
link?: string | undefined;
|
||||
target?: "_self" | "_blank" | "_top" | "_parent";
|
||||
class?: string;
|
||||
}
|
||||
|
||||
const { link, target, class: className } = Astro.props;
|
||||
---
|
||||
|
||||
{
|
||||
link && (
|
||||
<a
|
||||
href={link}
|
||||
{target}
|
||||
class={"cursor-pointer hover:text-indigo-600 " + className}
|
||||
>
|
||||
<slot />
|
||||
</a>
|
||||
)
|
||||
}
|
||||
{!link && <slot />}
|
22
apps/web/src/components/link/Link.svelte
Normal file
22
apps/web/src/components/link/Link.svelte
Normal file
@ -0,0 +1,22 @@
|
||||
<script lang="ts">
|
||||
export let link: string | undefined = undefined;
|
||||
export let target:
|
||||
| "_self"
|
||||
| "_blank"
|
||||
| "_top"
|
||||
| "_parent"
|
||||
| undefined
|
||||
| null = undefined;
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
{#if link}
|
||||
<a
|
||||
href={link}
|
||||
{target}
|
||||
class="cursor-pointer hover:text-indigo-600 {$$props.class ?? ''}"
|
||||
><slot /></a
|
||||
>
|
||||
{:else}
|
||||
<slot />
|
||||
{/if}
|
18
apps/web/src/components/menu/RenderMenu.astro
Normal file
18
apps/web/src/components/menu/RenderMenu.astro
Normal file
@ -0,0 +1,18 @@
|
||||
---
|
||||
import type { MainMenu, Menu } from "@turbopress/api/types";
|
||||
import DefaultMenu from "./default/DefaultMenu.svelte";
|
||||
interface Props {
|
||||
menu: Menu;
|
||||
}
|
||||
|
||||
const { menu } = Astro.props;
|
||||
const menus = menu.menus ?? [];
|
||||
|
||||
const mainMenus: MainMenu[] = menus.map((menu) => menu.mainMenu);
|
||||
---
|
||||
|
||||
{
|
||||
() => {
|
||||
return <DefaultMenu menus={mainMenus} client:only="svelte" />;
|
||||
}
|
||||
}
|
18
apps/web/src/components/menu/default/DefaultMenu.svelte
Normal file
18
apps/web/src/components/menu/default/DefaultMenu.svelte
Normal file
@ -0,0 +1,18 @@
|
||||
<script lang="ts">
|
||||
import type { MainMenu } from "@turbopress/api/types";
|
||||
import { useMediaQuery } from "svelte-breakpoints";
|
||||
import DefaultDesktopMenu from "./desktop/DefaultDesktopMenu.svelte";
|
||||
import DefaultMobileMenu from "./mobile/DefaultMobileMenu.svelte";
|
||||
|
||||
export let menus: MainMenu[];
|
||||
|
||||
const isDesktop = useMediaQuery("(min-width: 1024px)");
|
||||
</script>
|
||||
|
||||
{#if !$isDesktop}
|
||||
<DefaultMobileMenu {menus}></DefaultMobileMenu>
|
||||
{/if}
|
||||
|
||||
{#if $isDesktop}
|
||||
<DefaultDesktopMenu {menus}></DefaultDesktopMenu>
|
||||
{/if}
|
11
apps/web/src/components/menu/default/defaultMenu.ts
Normal file
11
apps/web/src/components/menu/default/defaultMenu.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { map } from "nanostores";
|
||||
|
||||
interface MobileMenuState {
|
||||
isOpen: boolean;
|
||||
activeIndex?: number;
|
||||
}
|
||||
|
||||
export const mobileMenuState = map<MobileMenuState>({
|
||||
isOpen: false,
|
||||
activeIndex: undefined,
|
||||
});
|
@ -0,0 +1,32 @@
|
||||
<script lang="ts">
|
||||
import type { MainMenu } from "@turbopress/api/types";
|
||||
import { mobileMenuState } from "../defaultMenu";
|
||||
import MainMenuSvelte from "./_MainMenu.svelte";
|
||||
|
||||
export let menus: MainMenu[];
|
||||
|
||||
function handleClick() {
|
||||
mobileMenuState.setKey("isOpen", !isOpen);
|
||||
}
|
||||
|
||||
$: isOpen = $mobileMenuState.isOpen;
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div
|
||||
class="flex items-center cursor-pointer text-sm"
|
||||
on:click={handleClick}
|
||||
on:keypress={handleClick}
|
||||
>
|
||||
{#each menus as menu, i}
|
||||
<MainMenuSvelte {menu} />
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<!-- {#if isOpen}
|
||||
<div class="w-full cursor-pointer lg:hidden">
|
||||
{#each menus as menu, i}
|
||||
<MainMenuSvelte {menu} index={i} />
|
||||
{/each}
|
||||
</div>
|
||||
{/if} -->
|
@ -0,0 +1,39 @@
|
||||
<script lang="ts">
|
||||
import type { MainMenu } from "@turbopress/api/types";
|
||||
import Link from "../../../link/Link.svelte";
|
||||
|
||||
export let menu: MainMenu;
|
||||
|
||||
const subMenus = menu.subMenu ?? [];
|
||||
</script>
|
||||
|
||||
<div class="group relative inline-block text-left group">
|
||||
<div id="menu-button" aria-expanded="false" aria-haspopup="true">
|
||||
<Link
|
||||
link={menu.url}
|
||||
class="hover:text-black w-full group-hover:bg-slate-100 p-2 "
|
||||
>
|
||||
{menu.label}
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="mt-2 absolute right-0 z-10 w-56 origin-top-right rounded-sm bg-white ring-1 ring-slate-200 focus:outline-none hidden group-hover:block"
|
||||
role="menu"
|
||||
aria-orientation="vertical"
|
||||
aria-labelledby="menu-button"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div class="" role="none">
|
||||
{#each subMenus as subMenu}
|
||||
<Link link={subMenu.link.url}>
|
||||
<div
|
||||
class="px-3 py-1.5 block hover:bg-slate-100 ring-1 ring-inset ring-gray-200 ring-opacity-30"
|
||||
>
|
||||
{subMenu.link.label}
|
||||
</div>
|
||||
</Link>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
17
apps/web/src/components/menu/default/desktop/_SubMenu.svelte
Normal file
17
apps/web/src/components/menu/default/desktop/_SubMenu.svelte
Normal file
@ -0,0 +1,17 @@
|
||||
<script lang="ts">
|
||||
import type { Link } from "@turbopress/api/types";
|
||||
import LinkSvelte from "../../../link/Link.svelte";
|
||||
export let subMenu: {
|
||||
link: Link;
|
||||
id?: string;
|
||||
};
|
||||
</script>
|
||||
|
||||
<LinkSvelte
|
||||
link={subMenu.link.url}
|
||||
class="h-7 hover:text-black hover:bg-slate-100 flex items-center"
|
||||
>
|
||||
<div class="w-full px-3">
|
||||
{subMenu.link.label}
|
||||
</div>
|
||||
</LinkSvelte>
|
@ -0,0 +1,36 @@
|
||||
<script lang="ts">
|
||||
import Icon from "@iconify/svelte";
|
||||
import type { MainMenu } from "@turbopress/api/types";
|
||||
import { mobileMenuState } from "../defaultMenu";
|
||||
import MainMenuSvelte from "./_MainMenu.svelte";
|
||||
|
||||
export let menus: MainMenu[];
|
||||
|
||||
function handleClick() {
|
||||
mobileMenuState.setKey("isOpen", !isOpen);
|
||||
}
|
||||
|
||||
$: isOpen = $mobileMenuState.isOpen;
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div
|
||||
class="flex items-center cursor-pointer font-semibold hover:text-indigo-600"
|
||||
on:click={handleClick}
|
||||
on:keypress={handleClick}
|
||||
>
|
||||
{#if isOpen}
|
||||
<Icon icon="ic:round-close" class="h-5 w-5 mr-2" />
|
||||
{:else}
|
||||
<Icon icon="ic:baseline-menu" class="h-5 w-5 mr-2" />
|
||||
{/if}
|
||||
<div>Menu</div>
|
||||
</div>
|
||||
|
||||
{#if isOpen}
|
||||
<div class="w-full cursor-pointer lg:hidden">
|
||||
{#each menus as menu, i}
|
||||
<MainMenuSvelte {menu} index={i} />
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
44
apps/web/src/components/menu/default/mobile/_MainMenu.svelte
Normal file
44
apps/web/src/components/menu/default/mobile/_MainMenu.svelte
Normal file
@ -0,0 +1,44 @@
|
||||
<script lang="ts">
|
||||
import Icon from "@iconify/svelte";
|
||||
import type { MainMenu } from "@turbopress/api/types";
|
||||
import Link from "../../../link/Link.svelte";
|
||||
import { mobileMenuState } from "../defaultMenu";
|
||||
import SubMenu from "./_SubMenu.svelte";
|
||||
|
||||
export let menu: MainMenu;
|
||||
export let index: number;
|
||||
|
||||
$: isOpen = $mobileMenuState.activeIndex == index;
|
||||
|
||||
function handleClick() {
|
||||
if (isOpen) mobileMenuState.setKey("activeIndex", undefined);
|
||||
else mobileMenuState.setKey("activeIndex", index);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex items-center h-7">
|
||||
<Link link={menu.url} class="hover:text-black w-full hover:bg-slate-100 p-1 ">
|
||||
<div class="w-full">{menu.label}</div>
|
||||
</Link>
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div
|
||||
class="hover:bg-slate-100 h-8 w-10 {isOpen ? 'bg-indigo-50' : ''}"
|
||||
on:click={handleClick}
|
||||
on:keydown={handleClick}
|
||||
>
|
||||
<Icon
|
||||
icon="ic:sharp-keyboard-arrow-down"
|
||||
class="text-2xl mx-auto mt-1 transition-transform duration-200 {isOpen
|
||||
? 'rotate-180 text-indigo-800 '
|
||||
: ''}"
|
||||
></Icon>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if isOpen}
|
||||
{#if menu.subMenu}
|
||||
{#each menu.subMenu as subMenu}
|
||||
<SubMenu {subMenu} />
|
||||
{/each}
|
||||
{/if}
|
||||
{/if}
|
17
apps/web/src/components/menu/default/mobile/_SubMenu.svelte
Normal file
17
apps/web/src/components/menu/default/mobile/_SubMenu.svelte
Normal file
@ -0,0 +1,17 @@
|
||||
<script lang="ts">
|
||||
import type { Link } from "@turbopress/api/types";
|
||||
import LinkSvelte from "../../../link/Link.svelte";
|
||||
export let subMenu: {
|
||||
link: Link;
|
||||
id?: string;
|
||||
};
|
||||
</script>
|
||||
|
||||
<LinkSvelte
|
||||
link={subMenu.link.url}
|
||||
class="h-7 hover:text-black hover:bg-slate-100 flex items-center"
|
||||
>
|
||||
<div class="w-full px-3">
|
||||
{subMenu.link.label}
|
||||
</div>
|
||||
</LinkSvelte>
|
65
apps/web/src/components/rich-text/RichText.astro
Normal file
65
apps/web/src/components/rich-text/RichText.astro
Normal file
@ -0,0 +1,65 @@
|
||||
---
|
||||
import type { FormattedElement, FormattedText } from "@turbopress/api/types";
|
||||
import escapeHTML from "escape-html";
|
||||
import { Text } from "slate";
|
||||
|
||||
interface Props {
|
||||
richText: (FormattedElement | FormattedText)[];
|
||||
}
|
||||
|
||||
const { richText } = Astro.props;
|
||||
---
|
||||
|
||||
{
|
||||
richText.map((node) => {
|
||||
return Text.isText(node) ? (
|
||||
<Fragment>
|
||||
{node.bold && <strong>{node.text}</strong>}
|
||||
{node.code && <code>{node.text}</code>}
|
||||
{node.italic && <em>{node.text}</em>}
|
||||
{!node.bold && !node.code && !node.italic && (
|
||||
<Fragment>{node.text}</Fragment>
|
||||
)}
|
||||
</Fragment>
|
||||
) : (
|
||||
<Fragment>
|
||||
{node.type === "h1" && (
|
||||
<h1>{<Astro.self richText={node.children} />}</h1>
|
||||
)}
|
||||
{node.type === "h2" && (
|
||||
<h2>{<Astro.self richText={node.children} />}</h2>
|
||||
)}
|
||||
{node.type === "h3" && (
|
||||
<h3>{<Astro.self richText={node.children} />}</h3>
|
||||
)}
|
||||
{node.type === "h4" && (
|
||||
<h4>{<Astro.self richText={node.children} />}</h4>
|
||||
)}
|
||||
{node.type === "h5" && (
|
||||
<h5>{<Astro.self richText={node.children} />}</h5>
|
||||
)}
|
||||
{node.type === "h6" && (
|
||||
<h6>{<Astro.self richText={node.children} />}</h6>
|
||||
)}
|
||||
{node.type === "quote" && (
|
||||
<p>{<Astro.self richText={node.children} />}</p>
|
||||
)}
|
||||
{node.type === "ul" && (
|
||||
<ul>{<Astro.self richText={node.children} />}</ul>
|
||||
)}
|
||||
{node.type === "ol" && (
|
||||
<ol>{<Astro.self richText={node.children} />}</ol>
|
||||
)}
|
||||
{node.type === "li" && (
|
||||
<li>{<Astro.self richText={node.children} />}</li>
|
||||
)}
|
||||
{node.type === "link" && (
|
||||
<a href={escapeHTML(node.url)}>
|
||||
{<Astro.self richText={node.children} />}
|
||||
</a>
|
||||
)}
|
||||
{!node.type && <p>{<Astro.self richText={node.children} />}</p>}
|
||||
</Fragment>
|
||||
);
|
||||
})
|
||||
}
|
45
apps/web/src/components/rich-text/RichText.svelte
Normal file
45
apps/web/src/components/rich-text/RichText.svelte
Normal file
@ -0,0 +1,45 @@
|
||||
<script lang="ts">
|
||||
import type { FormattedElement, FormattedText } from "@turbopress/api/types";
|
||||
import { Text } from "slate";
|
||||
|
||||
export let richText: (FormattedElement | FormattedText | any)[];
|
||||
</script>
|
||||
|
||||
{#each richText as node}
|
||||
{#if Text.isText(node)}
|
||||
{#if node.bold}
|
||||
<strong>{node.text}</strong>
|
||||
{/if}
|
||||
{#if node.code}
|
||||
<strong>{node.text}</strong>
|
||||
{/if}
|
||||
{#if node.italic}
|
||||
<strong>{node.text}</strong>
|
||||
{/if}
|
||||
{#if !node.bold && !node.code && !node.italic}
|
||||
{node.text}
|
||||
{/if}
|
||||
{:else}
|
||||
{#if node.type === "h1"}
|
||||
<h1><svelte:self richText={node.children}></svelte:self></h1>
|
||||
{/if}
|
||||
{#if node.type === "h2"}
|
||||
<h2><svelte:self richText={node.children}></svelte:self></h2>
|
||||
{/if}
|
||||
{#if node.type === "h3"}
|
||||
<h3><svelte:self richText={node.children}></svelte:self></h3>
|
||||
{/if}
|
||||
{#if node.type === "h4"}
|
||||
<h4><svelte:self richText={node.children}></svelte:self></h4>
|
||||
{/if}
|
||||
{#if node.type === "h5"}
|
||||
<h5><svelte:self richText={node.children}></svelte:self></h5>
|
||||
{/if}
|
||||
{#if node.type === "h6"}
|
||||
<h6><svelte:self richText={node.children}></svelte:self></h6>
|
||||
{/if}
|
||||
{#if !node.type}
|
||||
<p><svelte:self richText={node.children}></svelte:self></p>
|
||||
{/if}
|
||||
{/if}
|
||||
{/each}
|
5
apps/web/src/env.d.ts
vendored
Normal file
5
apps/web/src/env.d.ts
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
/// <reference types="astro/client" />
|
||||
|
||||
interface ImportMetaEnv {
|
||||
readonly ASTRO_PORT: string;
|
||||
}
|
48
apps/web/src/layouts/MainLayout.astro
Normal file
48
apps/web/src/layouts/MainLayout.astro
Normal file
@ -0,0 +1,48 @@
|
||||
---
|
||||
import type {
|
||||
FormattedElement,
|
||||
FormattedText,
|
||||
Layout,
|
||||
Media,
|
||||
} from "@turbopress/api/types";
|
||||
import RenderLayout from "../components/RenderLayout.astro";
|
||||
interface Props {
|
||||
title?: string;
|
||||
description?: string;
|
||||
layout?: string | Layout;
|
||||
content?: (FormattedElement | FormattedText | any)[];
|
||||
image?: string | Media;
|
||||
}
|
||||
const {
|
||||
title = "AstroCMS",
|
||||
description = "Astro, TailwindCSS, and PayloadCMS",
|
||||
layout,
|
||||
content,
|
||||
image,
|
||||
} = Astro.props;
|
||||
|
||||
const metaImage = image
|
||||
? typeof image === "string"
|
||||
? image
|
||||
: image.url
|
||||
: undefined;
|
||||
---
|
||||
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<meta name="description" content={description} />
|
||||
{metaImage && <meta property="og:image" content={metaImage} />}
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<title>{title}</title>
|
||||
</head>
|
||||
<body class="h-screen">
|
||||
{
|
||||
layout && typeof layout != "string" && (
|
||||
<RenderLayout layout={layout} {content} />
|
||||
)
|
||||
}
|
||||
{!layout && <slot />}
|
||||
</body>
|
||||
</html>
|
12
apps/web/src/pages/404.astro
Normal file
12
apps/web/src/pages/404.astro
Normal file
@ -0,0 +1,12 @@
|
||||
---
|
||||
import MainLayout from "../layouts/MainLayout.astro";
|
||||
import { getLayoutSingle } from "../services/api/layout.service";
|
||||
|
||||
const layout = await getLayoutSingle("404");
|
||||
---
|
||||
|
||||
<MainLayout title="Error 404" layout={layout} description="Page not found">
|
||||
Not found, error 404 The page you are looking for no longer exists. Perhaps
|
||||
you can return back to the homepage and see if you can find what you are
|
||||
looking for. Or, you can try finding it by using the search form below.
|
||||
</MainLayout>
|
16
apps/web/src/pages/[...slug].astro
Normal file
16
apps/web/src/pages/[...slug].astro
Normal file
@ -0,0 +1,16 @@
|
||||
---
|
||||
import MainLayout from "../layouts/MainLayout.astro";
|
||||
import { getPageSingle } from "../services/api";
|
||||
const { slug } = Astro.params;
|
||||
|
||||
const page = await getPageSingle(slug!);
|
||||
if (!page) return Astro.redirect("/404");
|
||||
if (page.slug == "home") return Astro.redirect("/");
|
||||
---
|
||||
|
||||
<MainLayout title={page.title} layout={page.layout} description="">
|
||||
{page.title}
|
||||
{page.layout}
|
||||
<!-- {homePage && <RenderPage page={homePage} />}
|
||||
{!homePage && <NoHomePage />} -->
|
||||
</MainLayout>
|
18
apps/web/src/pages/index.astro
Normal file
18
apps/web/src/pages/index.astro
Normal file
@ -0,0 +1,18 @@
|
||||
---
|
||||
import MainLayout from "../layouts/MainLayout.astro";
|
||||
import { getPageSingle } from "../services/api";
|
||||
|
||||
const homePage = await getPageSingle("home");
|
||||
|
||||
const pageTitle = homePage?.meta?.title ?? homePage?.title ?? "TurboPress";
|
||||
|
||||
const layout = homePage?.layout;
|
||||
---
|
||||
|
||||
<MainLayout
|
||||
title={pageTitle}
|
||||
{layout}
|
||||
description={homePage?.meta?.description}
|
||||
content={homePage?.content}
|
||||
image={homePage?.meta?.image}
|
||||
/>
|
31
apps/web/src/services/api/api.service.ts
Normal file
31
apps/web/src/services/api/api.service.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import qs from "qs";
|
||||
import type { PayloadCollection } from "../../types";
|
||||
|
||||
export async function apiFetch<T = any>(
|
||||
url: string | URL,
|
||||
options: RequestInit = {},
|
||||
) {
|
||||
const defaultOptions = {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
};
|
||||
|
||||
const res = await fetch(url, { ...defaultOptions, ...options });
|
||||
if (res.ok) {
|
||||
return res.json() as T;
|
||||
}
|
||||
throw new Error(`Error fetching data: ${res.statusText} (${res.status})}`);
|
||||
}
|
||||
|
||||
export async function getPayloadCollection<CollectionType>(
|
||||
url: string | URL,
|
||||
query: any = null,
|
||||
) {
|
||||
const stringifiedQuery = qs.stringify(query, { addQueryPrefix: true });
|
||||
return apiFetch<PayloadCollection<CollectionType>>(url + stringifiedQuery);
|
||||
}
|
||||
|
||||
export async function getPayloadDocument<CollectionType>(url: string | URL) {
|
||||
return apiFetch<CollectionType>(url);
|
||||
}
|
16
apps/web/src/services/api/content.service.ts
Normal file
16
apps/web/src/services/api/content.service.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import type { Layout } from "@turbopress/api/types";
|
||||
import { getPayloadCollection } from "./api.service";
|
||||
|
||||
export async function getContentCollection(query: any = null) {
|
||||
const url = ` ${process.env.PAYLOAD_PUBLIC_SERVER_URL}/api/layouts`;
|
||||
return getPayloadCollection<Layout>(url, query);
|
||||
}
|
||||
|
||||
export async function getContentSingle(
|
||||
name: string,
|
||||
): Promise<Layout | undefined> {
|
||||
const pages = await getContentCollection({
|
||||
where: { name: { equals: name } },
|
||||
});
|
||||
if (pages.docs[0]) return pages.docs[0];
|
||||
}
|
2
apps/web/src/services/api/index.ts
Normal file
2
apps/web/src/services/api/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from "./api.service";
|
||||
export * from "./page.service";
|
16
apps/web/src/services/api/layout.service.ts
Normal file
16
apps/web/src/services/api/layout.service.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import type { Layout } from "@turbopress/api/types";
|
||||
import { getPayloadCollection } from "./api.service";
|
||||
|
||||
export async function getLayoutCollection(query: any = null) {
|
||||
const url = ` ${process.env.PAYLOAD_PUBLIC_SERVER_URL}/api/layouts`;
|
||||
return getPayloadCollection<Layout>(url, query);
|
||||
}
|
||||
|
||||
export async function getLayoutSingle(
|
||||
name: string,
|
||||
): Promise<Layout | undefined> {
|
||||
const pages = await getLayoutCollection({
|
||||
where: { name: { equals: name } },
|
||||
});
|
||||
if (pages.docs[0]) return pages.docs[0];
|
||||
}
|
19
apps/web/src/services/api/media.service.ts
Normal file
19
apps/web/src/services/api/media.service.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import type { Media } from "@turbopress/api/types";
|
||||
import { getPayloadCollection, getPayloadDocument } from "./api.service";
|
||||
|
||||
export async function getMediaCollection(query: any = null) {
|
||||
const url = `${process.env.PAYLOAD_PUBLIC_SERVER_URL}/api/medias`;
|
||||
return getPayloadCollection<Media>(url, query);
|
||||
}
|
||||
|
||||
export async function getMediaSingle(slug: string): Promise<Media | undefined> {
|
||||
const medias = await getMediaCollection({
|
||||
where: { slug: { equals: slug } },
|
||||
});
|
||||
if (medias.docs[0]) return medias.docs[0];
|
||||
}
|
||||
|
||||
export async function getMediaById(id: string): Promise<Media | undefined> {
|
||||
const url = `${process.env.PAYLOAD_PUBLIC_SERVER_URL}/api/media/` + id;
|
||||
return getPayloadDocument<Media>(url);
|
||||
}
|
16
apps/web/src/services/api/page.service.ts
Normal file
16
apps/web/src/services/api/page.service.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import type { Page } from "@turbopress/api/types";
|
||||
import { getPayloadCollection } from "./api.service";
|
||||
|
||||
export async function getPageCollection(query: any = null) {
|
||||
// FIXME 3wc: shouldn't need to hardcode this?!
|
||||
//const url = `${import.meta.env.PAYLOAD_PUBLIC_SERVER_URL}/api/pages`;
|
||||
const url = `http://api:3001/api/pages`;
|
||||
return getPayloadCollection<Page>(url, query);
|
||||
}
|
||||
|
||||
export async function getPageSingle(slug: string): Promise<Page | undefined> {
|
||||
const pages = await getPageCollection({
|
||||
where: { slug: { equals: slug } },
|
||||
});
|
||||
if (pages.docs[0]) return pages.docs[0];
|
||||
}
|
22
apps/web/src/types.ts
Normal file
22
apps/web/src/types.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import type { FormattedElement, FormattedText } from "@turbopress/api/types";
|
||||
|
||||
declare module "slate" {
|
||||
interface CustomTypes {
|
||||
Element: FormattedElement;
|
||||
Text: FormattedText;
|
||||
}
|
||||
}
|
||||
|
||||
export type PayloadCollection<CollectionType = any> = {
|
||||
totalDocs?: number;
|
||||
limit?: number;
|
||||
totalPages?: number;
|
||||
page?: number;
|
||||
pagingCounter?: number;
|
||||
hasPrevPage?: boolean;
|
||||
hasNextPage?: boolean;
|
||||
prevPage?: number;
|
||||
nextPage?: number;
|
||||
hasMore?: boolean;
|
||||
docs: CollectionType[];
|
||||
};
|
7
apps/web/tailwind.config.cjs
Normal file
7
apps/web/tailwind.config.cjs
Normal file
@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
content: ["./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}"],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [require("@tailwindcss/typography")],
|
||||
};
|
4
apps/web/tsconfig.json
Normal file
4
apps/web/tsconfig.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"extends": "astro/tsconfigs/strict",
|
||||
"exclude": ["node_modules"]
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
node_modules
|
||||
Dockerfile
|
||||
README.md
|
@ -1 +0,0 @@
|
||||
PAYLOAD_URL=http://localhost:3001
|
@ -1,11 +0,0 @@
|
||||
{
|
||||
"plugins": ["prettier-plugin-astro", "prettier-plugin-tailwindcss"],
|
||||
"overrides": [
|
||||
{
|
||||
"files": "*.astro",
|
||||
"options": {
|
||||
"parser": "astro"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
@ -1,22 +0,0 @@
|
||||
FROM node:lts-alpine as base
|
||||
WORKDIR /base
|
||||
COPY package.json yarn.lock ./
|
||||
RUN yarn install --frozen-lockfile
|
||||
COPY . .
|
||||
|
||||
FROM base AS dev
|
||||
ENV NODE_ENV=development
|
||||
EXPOSE 3000
|
||||
CMD ["yarn","dev"]
|
||||
|
||||
FROM base AS build
|
||||
ENV NODE_ENV=production
|
||||
WORKDIR /build
|
||||
COPY --from=base /base .
|
||||
ADD "https://random-uuid.deno.dev" skipcache
|
||||
RUN yarn build
|
||||
|
||||
FROM nginx:stable AS prod
|
||||
COPY nginx.conf /etc/nginx/nginx.conf
|
||||
COPY --from=build /build/dist /usr/share/nginx/html
|
||||
EXPOSE 3000
|
@ -1,25 +0,0 @@
|
||||
import { defineConfig } from "astro/config";
|
||||
import tailwind from "@astrojs/tailwind";
|
||||
import image from "@astrojs/image";
|
||||
import sitemap from "@astrojs/sitemap";
|
||||
import prefetch from "@astrojs/prefetch";
|
||||
import react from "@astrojs/react";
|
||||
|
||||
// https://astro.build/config
|
||||
export default defineConfig({
|
||||
compressHTML: true,
|
||||
server: { port: 3000 },
|
||||
build: {
|
||||
inlineStylesheets: "auto"
|
||||
},
|
||||
viewTransitions: true,
|
||||
integrations: [tailwind({
|
||||
config: {
|
||||
applyBaseStyles: false
|
||||
}
|
||||
}), image({
|
||||
serviceEntryPoint: "@astrojs/image/sharp"
|
||||
}), prefetch({
|
||||
selector: "a"
|
||||
}), sitemap(), react()]
|
||||
});
|
@ -1,18 +0,0 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "default",
|
||||
"rsc": false,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "tailwind.config.cjs",
|
||||
"css": "@/styles/globals.css",
|
||||
"baseColor": "neutral",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/utils",
|
||||
"styles": "@/styles"
|
||||
}
|
||||
}
|
@ -1,36 +0,0 @@
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
http {
|
||||
include mime.types;
|
||||
gzip on;
|
||||
gzip_vary on;
|
||||
gzip_proxied any;
|
||||
gzip_comp_level 6;
|
||||
gzip_buffers 16 8k;
|
||||
gzip_http_version 1.1;
|
||||
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
|
||||
location / {
|
||||
root /usr/share/nginx/html;
|
||||
try_files $uri $uri/ =404;
|
||||
}
|
||||
|
||||
location /error_page.html {
|
||||
internal;
|
||||
}
|
||||
|
||||
location ~* \.(jpg|jpeg|png|gif|ico|css|js|htm|html)$ {
|
||||
root /usr/share/nginx/html;
|
||||
expires 30d;
|
||||
add_header Pragma public;
|
||||
add_header Cache-Control "public";
|
||||
}
|
||||
|
||||
error_page 404 /error_page.html;
|
||||
}
|
||||
}
|
@ -1,51 +0,0 @@
|
||||
{
|
||||
"name": "astroad",
|
||||
"description": "Astroad - Astro",
|
||||
"type": "module",
|
||||
"version": "1.1",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"dev": "astro dev --host",
|
||||
"build": "astro build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/image": "0.18.0",
|
||||
"@astrojs/prefetch": "0.4.1",
|
||||
"@astrojs/react": "^3.1.1",
|
||||
"@astrojs/sitemap": "3.1.2",
|
||||
"@astrojs/tailwind": "5.1.0",
|
||||
"@hookform/resolvers": "^3.3.4",
|
||||
"@radix-ui/react-dialog": "^1.0.5",
|
||||
"@radix-ui/react-label": "^2.0.2",
|
||||
"@radix-ui/react-slot": "^1.0.2",
|
||||
"@tanstack/react-query": "^5.28.14",
|
||||
"@tanstack/react-query-devtools": "^5.29.0",
|
||||
"@types/leaflet": "^1.9.8",
|
||||
"@types/react": "^18.2.74",
|
||||
"@types/react-dom": "^18.2.23",
|
||||
"astro": "^4.5.13",
|
||||
"axios": "^1.6.8",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.1.0",
|
||||
"css-select": "5.1.0",
|
||||
"human-id": "^4.1.1",
|
||||
"leaflet": "^1.9.4",
|
||||
"lucide-react": "^0.364.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-hook-form": "^7.51.2",
|
||||
"react-leaflet": "^4.2.1",
|
||||
"sharp": "^0.32.6",
|
||||
"slate-serializers": "0.4.1",
|
||||
"tailwind-merge": "^2.2.2",
|
||||
"tailwindcss": "^3.3.3",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"typescript": "^5.4.3",
|
||||
"zod": "^3.22.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"prettier": "^3.0.2",
|
||||
"prettier-plugin-astro": "^0.11.1",
|
||||
"prettier-plugin-tailwindcss": "^0.5.4"
|
||||
}
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
<svg width="100" height="100" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M50.0529 99.6475C22.7705 99.7533 -0.0352444 77.3352 0.422986 49.2422C0.669726 35.6715 5.63976 24.0395 15.3331 14.5224C24.9559 5.04054 36.6937 0.281993 50.1586 0.317242C63.835 0.317242 75.6433 5.28728 85.3014 15.0159C94.9947 24.7797 99.7533 36.7289 99.718 50.4759C99.6828 59.2527 97.3916 67.5009 92.915 75.1146C88.4385 82.7282 82.3757 88.7557 74.7268 93.1618C67.1132 97.4974 58.865 99.6475 50.0529 99.6475Z" fill="#CFADC5"/>
|
||||
<path d="M42.4392 67.2541C41.9105 67.2541 41.417 67.2541 40.8883 67.2541C40.6768 67.2541 40.5358 67.1837 40.3948 67.0074C37.7864 63.6236 35.2133 60.2749 32.6049 56.8911C31.7942 55.8336 31.4769 55.8336 30.8424 56.9968C30.7367 57.1731 30.7014 57.3846 30.7367 57.5608C30.8424 60.2397 30.7719 62.8833 30.7719 65.5622C30.7719 65.9852 30.7367 66.4082 30.7367 66.8312C30.7367 67.1484 30.631 67.2541 30.3137 67.2541C28.4808 67.2541 26.6479 67.2541 24.815 67.2541C24.4977 67.2541 24.392 67.1837 24.3567 66.8312C24.2862 66.1967 24.2862 65.5975 24.2862 64.963C24.2862 54.5647 24.2862 44.1664 24.2862 33.7681C24.2862 29.8202 24.2862 25.9077 24.2862 21.9598C24.2862 21.8541 24.2862 21.7483 24.2862 21.6073C24.2862 20.6556 24.674 20.3031 25.6257 20.3736C26.6126 20.4794 27.6348 20.4441 28.6218 20.4441C28.9743 20.4441 29.362 20.3736 29.7145 20.3736C30.4547 20.3736 30.8072 20.6556 30.8072 21.3958C30.8072 23.4755 30.8072 25.5552 30.8072 27.5996C30.8072 33.2041 30.8072 38.8086 30.8072 44.3779C30.8072 45.4353 30.8424 46.528 30.7719 47.5855C30.7719 47.8675 30.7015 48.2552 31.0187 48.3962C31.2654 48.5019 31.4769 48.1847 31.6884 47.9732C33.2393 46.387 34.755 44.8009 36.306 43.2147C37.1519 42.3335 38.0331 41.4522 38.8791 40.571C39.2668 40.1481 39.7251 39.9366 40.289 39.9718C41.9105 40.0423 43.5672 40.0071 45.1886 40.0071C45.6821 40.0071 46.2108 39.9718 46.7043 39.9718C46.81 39.9718 46.9158 39.9718 46.9863 39.9718C47.2682 40.0071 47.6207 39.9718 47.7265 40.289C47.797 40.5005 47.4797 40.6415 47.3035 40.8178C45.4001 42.6507 43.4967 44.5189 41.5932 46.3518C40.148 47.7617 38.6676 49.1364 37.2224 50.5111C36.5879 51.1103 36.5175 51.5686 37.0814 52.2735C40.289 56.3271 43.4967 60.3807 46.7043 64.4343C47.3035 65.1745 47.3035 65.1745 48.1142 64.6105C48.1847 64.5753 48.22 64.54 48.2905 64.4695C48.6077 64.2228 48.8544 64.0465 49.3127 64.1875C50.0529 64.399 50.8283 63.9408 51.0751 63.1301C51.3218 62.3899 51.8505 61.9316 52.4145 61.5086C52.8375 61.1914 53.2605 60.9094 53.613 60.5217C53.895 60.2749 54.1065 59.993 54.2474 59.6405C54.4237 59.2527 54.6704 58.9355 55.0229 58.724C55.1992 58.6535 55.3049 58.5125 55.4106 58.3363C55.6574 57.9485 55.9394 57.6313 56.2566 57.3493C56.7148 56.9616 57.1026 56.5386 57.2788 55.9394C57.3493 55.7279 57.4903 55.5869 57.6313 55.4106C58.0543 54.9172 58.5125 54.4589 58.865 53.9302C59.0412 53.7187 59.2175 53.472 59.4642 53.331C60.1339 52.9433 60.4864 52.344 60.7332 51.6391C61.1562 50.4759 61.3676 49.2774 61.6496 48.079C61.9669 46.8805 62.7071 46.0698 63.976 45.7526C64.3638 45.6468 64.7868 45.6821 65.1392 45.8583C66.5139 46.528 67.9239 47.1625 69.1576 48.079C70.4265 49.0307 71.0962 50.4054 71.2725 51.9915C71.343 52.6613 71.1667 53.331 70.6028 53.8245C70.2855 54.0712 70.1093 54.4237 70.0035 54.8114C69.933 55.1992 69.792 55.5516 69.51 55.8336C68.7698 56.6443 68.5936 57.6666 68.5231 58.6888C68.4526 59.8872 68.2764 61.0504 67.9591 62.2136C67.7124 63.0596 67.6066 63.9408 67.9944 64.7868C68.1001 65.0335 68.2411 65.245 68.4173 65.4565C69.1928 66.3024 70.1445 66.2672 70.8495 65.386C71.202 64.9277 71.484 64.399 72.0832 64.2228C72.2242 64.1875 72.3299 63.9408 72.4004 63.7998C72.8234 63.1301 73.2464 62.4251 73.6694 61.7554C73.9866 61.2619 74.2686 60.7684 74.7268 60.3807C75.0793 60.0635 75.2556 59.6052 75.4671 59.1822C75.6433 58.8298 75.7843 58.5125 75.9605 58.16C76.2425 57.5256 76.8065 57.2788 77.441 57.1731C77.7229 57.1378 77.8639 57.3141 78.0049 57.5256C78.7452 58.9355 79.2034 60.3807 79.4854 61.9316C79.6969 63.1301 79.6969 64.3638 79.6616 65.5975C79.6616 65.95 79.5559 66.2672 79.3444 66.5492C78.7452 67.3246 78.1459 68.1354 77.5115 68.9461C77.4762 69.0166 77.441 69.0518 77.3705 69.0871C76.6655 69.3691 76.1368 69.933 75.5728 70.3913C75.1498 70.7085 74.7268 70.9905 74.1629 71.0257C73.7399 71.061 73.3521 71.202 72.9644 71.4135C72.4004 71.6955 71.766 71.7307 71.1667 71.7307C69.4396 71.6602 67.7476 71.343 66.1262 70.6733C64.822 70.1445 63.7998 69.2633 62.9538 68.1354C61.7906 66.6197 61.7906 66.6197 61.6849 64.5753C61.6496 64.117 61.5791 63.694 61.3676 63.2711C61.0504 62.7071 60.4864 62.5661 59.9577 62.9538C59.6405 63.1653 59.429 63.4473 59.2527 63.7998C59.006 64.3285 58.6535 64.7515 58.2305 65.1392C56.7853 66.4434 55.6574 67.9591 54.3884 69.4043C53.5072 70.356 52.5203 71.202 51.4981 72.0127C51.0398 72.4004 50.4054 72.4709 49.8414 72.5414C48.9954 72.6472 48.1847 72.9292 47.515 73.4579C45.8583 74.7268 44.1311 74.5858 42.404 73.6341C41.417 73.1054 40.571 72.4709 39.8661 71.5545C39.2668 70.779 39.1258 69.8978 39.3726 68.9461C39.5136 68.3821 39.8661 68.0296 40.43 67.8886C41.135 67.7124 41.8047 67.5009 42.5097 67.2894C42.4392 67.2894 42.4392 67.2894 42.4392 67.2541Z" fill="#84285E"/>
|
||||
<path d="M73.7046 36.5175C73.7046 37.5397 73.6694 38.4561 73.4579 39.3726C73.3874 39.7603 73.2111 40.1128 72.9644 40.43C72.2947 41.3818 71.3782 42.122 70.497 42.8974C70.4618 42.9327 70.3913 42.9679 70.356 43.0032C68.8051 43.4262 67.2541 43.8139 65.7737 42.7564C64.4695 42.545 64.0113 41.417 63.4826 40.43C62.9186 39.4431 62.7776 38.3504 62.7776 37.2577C62.7776 36.0945 63.0948 35.0018 63.4121 33.9091C63.694 32.9574 64.4343 32.3229 64.963 31.5122C64.963 31.5122 64.963 31.4769 64.9982 31.4769C65.527 31.3359 65.9147 30.8777 66.5139 30.8425C67.0427 30.8072 67.5714 30.7367 68.1001 30.6662C70.1093 30.3842 71.5545 31.2654 72.6119 32.9221C73.0349 33.5566 73.3169 34.2263 73.5989 34.9313C73.8104 35.4953 73.6694 36.0592 73.7046 36.5175Z" fill="#84285E"/>
|
||||
</svg>
|
Before Width: | Height: | Size: 5.8 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Before Width: | Height: | Size: 3.4 KiB |
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user