Clone starter
This commit is contained in:
commit
1b7ee20d05
12
.env.example
Normal file
12
.env.example
Normal file
@ -0,0 +1,12 @@
|
||||
MONGODB_URI=mongodb://username:password@localhost:27017
|
||||
DB_NAME=turbopress
|
||||
ASTRO_PORT=3001
|
||||
PAYLOAD_PORT=3000
|
||||
PAYLOAD_PUBLIC_SERVER_URL=http://localhost:3000
|
||||
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: {},
|
||||
};
|
36
.gitignore
vendored
Normal file
36
.gitignore
vendored
Normal file
@ -0,0 +1,36 @@
|
||||
# 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*
|
||||
|
||||
# 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.
|
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.
|
68
README.md
Normal file
68
README.md
Normal file
@ -0,0 +1,68 @@
|
||||
# TurboPress starter
|
||||
|
||||
This is a starter Astro + Payload CMS project using Turborepo.
|
||||
|
||||
## 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
|
||||
|
||||
Install `nodejs`, `pnpm` and `turborepo` on your local machine
|
||||
|
||||
## Develop
|
||||
|
||||
Create a `.env` file in the root folder, you can use the `.env.example` file as an example
|
||||
|
||||
I use `pnpm` for this project.
|
||||
|
||||
Run the following command:
|
||||
|
||||
```sh
|
||||
pnpm installl
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
By default, the payloadCMS will run on port 3000, and Astro will be served on port 3001.
|
||||
|
||||
## Build & Serve (NodeJs)
|
||||
|
||||
```sh
|
||||
pnpm build
|
||||
pnpm serve
|
||||
```
|
||||
|
||||
## TypeScript support
|
||||
|
||||
To fully utilize the type safe features, manually generate the types for PayloadCMS by runng `pnpm generate:types`
|
||||
|
||||
Then, you can import the types easily as simple as
|
||||
|
||||
```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": "latest",
|
||||
"@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: "*",
|
||||
});
|
30
apps/api/src/server.ts
Normal file
30
apps/api/src/server.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import express from "express";
|
||||
import payload from "payload";
|
||||
|
||||
const app = express();
|
||||
|
||||
// Redirect root to Admin panel
|
||||
app.get("/", (_, res) => {
|
||||
res.redirect("/admin");
|
||||
});
|
||||
|
||||
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(process.env.PAYLOAD_PORT ?? 3000);
|
||||
};
|
||||
|
||||
start();
|
22
apps/api/tsconfig.json
Normal file
22
apps/api/tsconfig.json
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"strict": false,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"jsx": "react",
|
||||
"paths": {
|
||||
"payload/generated-types": ["./types/payload.ts"]
|
||||
}
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules", "dist", "build"],
|
||||
"ts-node": {
|
||||
"transpileOnly": true,
|
||||
"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",
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
21
apps/web/.gitignore
vendored
Normal file
21
apps/web/.gitignore
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
# build output
|
||||
dist/
|
||||
# generated types
|
||||
.astro/
|
||||
|
||||
# dependencies
|
||||
node_modules/
|
||||
|
||||
# logs
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
|
||||
|
||||
# environment variables
|
||||
.env
|
||||
.env.production
|
||||
|
||||
# macOS-specific files
|
||||
.DS_Store
|
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).
|
24
apps/web/astro.config.mjs
Normal file
24
apps/web/astro.config.mjs
Normal file
@ -0,0 +1,24 @@
|
||||
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"),
|
||||
},
|
||||
output: "server",
|
||||
adapter: node({
|
||||
mode: "standalone",
|
||||
}),
|
||||
vite: {
|
||||
define: {
|
||||
"import.meta.env.PAYLOAD_PUBLIC_SERVER_URL": JSON.stringify(
|
||||
process.env.PAYLOAD_PUBLIC_SERVER_URL,
|
||||
),
|
||||
},
|
||||
},
|
||||
});
|
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);
|
||||
}
|
14
apps/web/src/services/api/page.service.ts
Normal file
14
apps/web/src/services/api/page.service.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import type { Page } from "@turbopress/api/types";
|
||||
import { getPayloadCollection } from "./api.service";
|
||||
|
||||
export async function getPageCollection(query: any = null) {
|
||||
const url = `${import.meta.env.PAYLOAD_PUBLIC_SERVER_URL}/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"]
|
||||
}
|
46
package.json
Normal file
46
package.json
Normal file
@ -0,0 +1,46 @@
|
||||
{
|
||||
"name": "turbopress",
|
||||
"description": "A web + headless CMS turborepo",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"prepare": "husky install",
|
||||
"build": "turbo build",
|
||||
"build:api": "dotenv -- turbo build --filter=api",
|
||||
"build:web": "dotenv -- turbo build --filter=web",
|
||||
"dev": "dotenv -- turbo dev",
|
||||
"dev:api": "dotenv -- turbo dev --filter=api",
|
||||
"dev:web": "dotenv -- turbo dev --filter=web",
|
||||
"test": "turbo test",
|
||||
"lint": "turbo lint",
|
||||
"format": "prettier --write \"**/*.{ts,tsx,md,astro}\"",
|
||||
"generate:types": "dotenv -- pnpm --filter=api generate:types ",
|
||||
"serve:api": "dotenv -- turbo serve --filter=api",
|
||||
"serve:web": "dotenv -- turbo serve --filter=web",
|
||||
"serve": "dotenv -- turbo serve"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@turbo/gen": "^1.9.7",
|
||||
"dotenv": "^8.2.0",
|
||||
"dotenv-cli": "^7.2.1",
|
||||
"eslint": "latest",
|
||||
"eslint-config-custom": "*",
|
||||
"husky": "^8.0.3",
|
||||
"lint-staged": "^13.2.3",
|
||||
"prettier": "latest",
|
||||
"prettier-plugin-astro": "latest",
|
||||
"prettier-plugin-svelte": "latest",
|
||||
"turbo": "latest"
|
||||
},
|
||||
"preinstall": "npx only-allow pnpm",
|
||||
"lint-staged": {
|
||||
"apps/**/*.{js,ts,astro}": [
|
||||
"eslint --fix"
|
||||
],
|
||||
"packages/**/*.{js,ts,astro}": [
|
||||
"eslint --fix"
|
||||
],
|
||||
"*.json": [
|
||||
"prettier --write"
|
||||
]
|
||||
}
|
||||
}
|
9
packages/eslint-config-custom/index.js
Normal file
9
packages/eslint-config-custom/index.js
Normal file
@ -0,0 +1,9 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: ["plugin:@typescript-eslint/recommended", "turbo", "prettier"],
|
||||
rules: {
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
},
|
||||
parserOptions: {},
|
||||
plugins: ["@typescript-eslint", "turbo"],
|
||||
};
|
15
packages/eslint-config-custom/package.json
Normal file
15
packages/eslint-config-custom/package.json
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "eslint-config-custom",
|
||||
"version": "0.0.0",
|
||||
"main": "index.js",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"eslint-config-prettier": "latest",
|
||||
"eslint-config-turbo": "latest",
|
||||
"@typescript-eslint/parser": "latest",
|
||||
"@typescript-eslint/eslint-plugin": "latest"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
}
|
||||
}
|
13953
pnpm-lock.yaml
Normal file
13953
pnpm-lock.yaml
Normal file
File diff suppressed because it is too large
Load Diff
5
pnpm-workspace.yaml
Normal file
5
pnpm-workspace.yaml
Normal file
@ -0,0 +1,5 @@
|
||||
packages:
|
||||
- "apps/*"
|
||||
- "packages/*"
|
||||
- "!**/test/**"
|
||||
|
20
tsconfig.json
Normal file
20
tsconfig.json
Normal file
@ -0,0 +1,20 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"display": "Default",
|
||||
"compilerOptions": {
|
||||
"composite": false,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"inlineSources": false,
|
||||
"isolatedModules": true,
|
||||
"moduleResolution": "node",
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"preserveWatchOutput": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true
|
||||
},
|
||||
"exclude": ["node_modules"]
|
||||
}
|
16
turbo.json
Normal file
16
turbo.json
Normal file
@ -0,0 +1,16 @@
|
||||
{
|
||||
"$schema": "https://turbo.build/schema.json",
|
||||
"globalDotEnv": [".env"],
|
||||
"pipeline": {
|
||||
"build": {
|
||||
"outputs": ["dist/**", "umd/**", "build/**"]
|
||||
},
|
||||
"test": {},
|
||||
"lint": {},
|
||||
"dev": {
|
||||
"cache": false,
|
||||
"persistent": true
|
||||
},
|
||||
"serve": {}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user