├── app
├── _custom
│ ├── styles.css
│ ├── admin-panel-styles.css
│ ├── readme.md
│ ├── snippets
│ │ └── import
│ │ │ └── example.json
│ ├── routes
│ │ ├── README.md
│ │ ├── _site.c.tier-lists+
│ │ │ └── components
│ │ │ │ ├── TierListData.tsx
│ │ │ │ ├── TierFive.tsx
│ │ │ │ ├── TierFour.tsx
│ │ │ │ ├── TierOne.tsx
│ │ │ │ ├── TierSix.tsx
│ │ │ │ ├── TierTwo.tsx
│ │ │ │ └── TierThree.tsx
│ │ └── _site.c.pokemon+
│ │ │ └── components
│ │ │ ├── Label.tsx
│ │ │ └── RatingsLabel.tsx
│ ├── blocks
│ │ └── Example.tsx
│ ├── classes.ts
│ ├── collections
│ │ ├── readme.md
│ │ ├── index.ts
│ │ ├── evolution-requirements.ts
│ │ ├── raid-guides.ts
│ │ ├── _types.ts
│ │ └── weather.ts
│ └── collection-config-example.ts
├── utils
│ ├── delay.ts
│ ├── stripe.server.ts
│ ├── remember.server.ts
│ ├── url-slug.ts
│ ├── nanoid.ts
│ ├── isBotProvider.tsx
│ ├── typsense.server.ts
│ ├── form.ts
│ ├── time-ago.ts
│ ├── use-debounce.ts
│ ├── pinnedLinkUrlGenerator.ts
│ ├── useWindowDimensions.ts
│ ├── useSiteLoaderData.tsx
│ └── theme.server.ts
├── db
│ ├── access
│ │ ├── isSiteStaff.ts
│ │ ├── isSiteOwner.ts
│ │ ├── isSiteAdmin.ts
│ │ ├── isSiteContributor.ts
│ │ └── isSiteOwnerOrAdmin.ts
│ ├── hooks
│ │ └── replaceVersionAuthor.ts
│ ├── collections
│ │ ├── collections
│ │ │ └── collections-search-schema.json
│ │ ├── entries
│ │ │ └── entries-search-schema.json
│ │ ├── posts
│ │ │ └── posts-search-schema.json
│ │ ├── custom-pages
│ │ │ └── custom-pages-schema.json
│ │ ├── images
│ │ │ └── images.access.ts
│ │ ├── user-data
│ │ │ └── user-data.access.ts
│ │ └── index.ts
│ └── custom
│ │ └── CustomImages.ts
├── routes
│ ├── _site+
│ │ ├── settings+
│ │ │ ├── utils
│ │ │ │ ├── RoleActionSchema.ts
│ │ │ │ ├── ApplicationReviewSchema.ts
│ │ │ │ ├── copyToClipBoard.tsx
│ │ │ │ ├── imgPreview.ts
│ │ │ │ └── fetchApplicationData.tsx
│ │ │ ├── components
│ │ │ │ ├── RoleBadge.tsx
│ │ │ │ ├── ApplicationStatus.tsx
│ │ │ │ └── SettingsMenuLink.tsx
│ │ │ └── payouts.tsx
│ │ ├── _components
│ │ │ ├── _datepicker
│ │ │ │ ├── date-picker
│ │ │ │ │ ├── types.ts
│ │ │ │ │ ├── date-button.tsx
│ │ │ │ │ ├── month-picker.tsx
│ │ │ │ │ └── year-picker.tsx
│ │ │ │ ├── time-picker
│ │ │ │ │ └── types.ts
│ │ │ │ └── util.ts
│ │ │ └── Column-1.tsx
│ │ ├── c_+
│ │ │ ├── $collectionId_.$entryId
│ │ │ │ ├── utils
│ │ │ │ │ ├── EntrySchema.ts
│ │ │ │ │ ├── SectionSchema.tsx
│ │ │ │ │ ├── CollectionSchema.ts
│ │ │ │ │ └── _entryTypes.ts
│ │ │ │ ├── components
│ │ │ │ │ ├── SectionType.tsx
│ │ │ │ │ └── ScrollToHashElement.tsx
│ │ │ │ └── _entry.tsx
│ │ │ ├── _components
│ │ │ │ └── fuzzyFilter.tsx
│ │ │ └── $collectionId
│ │ │ │ └── utils
│ │ │ │ └── listMeta.ts
│ │ ├── search
│ │ │ └── SiteSearchOn.tsx
│ │ ├── p+
│ │ │ └── components
│ │ │ │ ├── PostHeaderView.tsx
│ │ │ │ └── PostBannerView.tsx
│ │ └── posts+
│ │ │ └── components
│ │ │ └── PostListHeader.tsx
│ ├── inngest+
│ │ ├── utils
│ │ │ └── inngest-client.ts
│ │ └── api.ts
│ ├── 404.tsx
│ ├── _auth+
│ │ ├── components
│ │ │ ├── LoggedIn.tsx
│ │ │ ├── LoggedOut.tsx
│ │ │ ├── Staff.tsx
│ │ │ ├── AdminOrStaffOrOwner.tsx
│ │ │ ├── NotAdminOrStaffOrOwner.tsx
│ │ │ ├── FollowingSite.tsx
│ │ │ ├── CustomSite.tsx
│ │ │ ├── NotFollowingSite.tsx
│ │ │ └── AdminOrStaffOrOwnerOrContributor.tsx
│ │ ├── utils
│ │ │ ├── handleLogout.client.ts
│ │ │ └── useIsStaffSiteAdminOwner.ts
│ │ └── check-email.tsx
│ ├── _seo+
│ │ └── robots[.]txt.tsx
│ ├── _editor+
│ │ ├── blocks+
│ │ │ ├── table
│ │ │ │ └── src
│ │ │ │ │ ├── utils
│ │ │ │ │ ├── is-element.ts
│ │ │ │ │ ├── point.ts
│ │ │ │ │ ├── types.ts
│ │ │ │ │ ├── is-of-type.ts
│ │ │ │ │ ├── has-common.ts
│ │ │ │ │ └── matrix.ts
│ │ │ │ │ ├── weak-maps.ts
│ │ │ │ │ ├── normalization
│ │ │ │ │ ├── with-normalization.ts
│ │ │ │ │ └── normalize-content.ts
│ │ │ │ │ └── with-fragments.ts
│ │ │ ├── embed.tsx
│ │ │ ├── inline-ad.tsx
│ │ │ └── two-column.tsx
│ │ └── core
│ │ │ └── constants.ts
│ ├── _user+
│ │ ├── components
│ │ │ ├── UserMenuItems.tsx
│ │ │ └── UserMenuLink.tsx
│ │ └── user+
│ │ │ └── appearance.tsx
│ ├── _home+
│ │ ├── privacy.tsx
│ │ └── components
│ │ │ └── mouse-position.tsx
│ └── _proxy+
│ │ └── proxy.gtag.js.tsx
├── components
│ ├── Loading.tsx
│ ├── DotLoader.tsx
│ └── Link.tsx
├── config.ts
└── entry.client.tsx
├── .eslintignore
├── fly
├── typesense.Dockerfile
├── fly.core.toml
├── fly.custom.toml
├── fly.home.toml
└── fly.typesense.toml
├── icons
├── icon-128.webp
├── icon-192.webp
├── icon-256.webp
├── icon-48.webp
├── icon-512.webp
├── icon-72.webp
└── icon-96.webp
├── public
├── favicon.ico
├── fonts
│ ├── Nunito_Sans
│ │ ├── NunitoSans-Bold.ttf
│ │ ├── NunitoSans-Bold.woff
│ │ ├── NunitoSans-Bold.woff2
│ │ ├── NunitoSans-Italic.ttf
│ │ ├── NunitoSans-Italic.woff
│ │ ├── NunitoSans-Italic.woff2
│ │ ├── NunitoSans-Regular.ttf
│ │ ├── NunitoSans-Regular.woff
│ │ ├── NunitoSans-Regular.woff2
│ │ ├── NunitoSans-SemiBold.ttf
│ │ ├── NunitoSans-SemiBold.woff
│ │ └── NunitoSans-SemiBold.woff2
│ └── Source_Sans_Pro
│ │ ├── SourceSansPro-It.otf
│ │ ├── sourcesanspro-bold.otf
│ │ ├── SourceSansPro-Black.otf
│ │ ├── SourceSansPro-BlackIt.otf
│ │ ├── SourceSansPro-BoldIt.otf
│ │ ├── SourceSansPro-Light.otf
│ │ ├── SourceSansPro-LightIt.otf
│ │ ├── SourceSansPro-Regular.otf
│ │ ├── SourceSansPro-Semibold.otf
│ │ ├── SourceSansPro-ExtraLight.otf
│ │ ├── SourceSansPro-ExtraLightIt.otf
│ │ ├── SourceSansPro-SemiboldIt.otf
│ │ ├── sourcesanspro-bold-webfont.woff
│ │ └── sourcesanspro-bold-webfont.woff2
├── icons
│ ├── minus.svg
│ ├── slash.svg
│ ├── check.svg
│ ├── chevron-down.svg
│ ├── chevron-up.svg
│ ├── chevron-left.svg
│ ├── chevron-right.svg
│ ├── loader-2.svg
│ ├── moon.svg
│ ├── plus.svg
│ ├── x.svg
│ ├── arrow-up.svg
│ ├── brackets.svg
│ ├── send.svg
│ ├── arrow-down.svg
│ ├── arrow-left.svg
│ ├── arrow-right.svg
│ ├── message-circle.svg
│ ├── move-right.svg
│ ├── search.svg
│ ├── bookmark.svg
│ ├── chevrons-up.svg
│ ├── line-chart.svg
│ ├── chevrons-down.svg
│ ├── chevrons-left.svg
│ ├── chevrons-right.svg
│ ├── code.svg
│ ├── bold.svg
│ ├── chevrons-up-down.svg
│ ├── clock-9.svg
│ ├── rectangle-horizontal.svg
│ ├── reply.svg
│ ├── text.svg
│ ├── columns-2.svg
│ ├── triangle.svg
│ ├── info.svg
│ ├── list-filter.svg
│ ├── pen-line.svg
│ ├── pencil.svg
│ ├── underline.svg
│ ├── user.svg
│ ├── corner-down-left.svg
│ ├── lock.svg
│ ├── pie-chart.svg
│ ├── plus-circle.svg
│ ├── rows.svg
│ ├── columns.svg
│ ├── corner-down-right.svg
│ ├── credit-card.svg
│ ├── home.svg
│ ├── captions.svg
│ ├── key.svg
│ ├── mail.svg
│ ├── star.svg
│ ├── ellipsis.svg
│ ├── square-plus.svg
│ ├── dollar-sign.svg
│ ├── wallet-2.svg
│ ├── menu.svg
│ ├── more-vertical.svg
│ ├── arrow-up-down.svg
│ ├── copy.svg
│ ├── database.svg
│ ├── folder.svg
│ ├── globe.svg
│ ├── italic.svg
│ ├── link-2.svg
│ ├── more-horizontal.svg
│ ├── trash.svg
│ ├── type.svg
│ ├── users-2.svg
│ ├── settings-2.svg
│ ├── sort.svg
│ ├── table.svg
│ ├── archive.svg
│ ├── heading-2.svg
│ ├── list-plus.svg
│ ├── strikethrough.svg
│ ├── bar-chart-2.svg
│ ├── layout.svg
│ ├── log-out.svg
│ ├── pen-square.svg
│ ├── upload.svg
│ ├── download.svg
│ ├── eye.svg
│ ├── link.svg
│ ├── square-pen.svg
│ ├── coins.svg
│ ├── image.svg
│ ├── message-square-plus.svg
│ ├── table-cells-split.svg
│ ├── zap.svg
│ ├── wrench.svg
│ ├── arrow-up-right-from-square.svg
│ ├── bolt.svg
│ ├── copy-check.svg
│ ├── external-link.svg
│ ├── hash.svg
│ ├── link-2-off.svg
│ ├── list-tree.svg
│ ├── shield.svg
│ ├── flame.svg
│ ├── expand.svg
│ ├── file-code-2.svg
│ ├── layout-panel-top.svg
│ ├── table-cells-merge.svg
│ ├── users.svg
│ ├── sword.svg
│ ├── calendar.svg
│ ├── pin.svg
│ ├── refresh-ccw.svg
│ ├── heading-3.svg
│ ├── piggy-bank.svg
│ ├── badge-check.svg
│ ├── server.svg
│ ├── accessibility.svg
│ ├── archive-restore.svg
│ ├── binary.svg
│ ├── component.svg
│ ├── image-minus.svg
│ ├── captions-off.svg
│ ├── copy-x.svg
│ ├── layout-grid.svg
│ ├── layout-list.svg
│ ├── trash-2.svg
│ ├── grip-vertical.svg
│ ├── hourglass.svg
│ ├── list-ordered.svg
│ ├── move.svg
│ ├── file-text.svg
│ ├── image-plus.svg
│ ├── list.svg
│ ├── calendar-clock.svg
│ ├── hard-drive.svg
│ ├── eye-off.svg
│ ├── globe-2.svg
│ ├── github.svg
│ ├── sun.svg
│ ├── calendar-plus.svg
│ ├── swords.svg
│ ├── telescope.svg
│ ├── palette.svg
│ ├── gamepad-2.svg
│ ├── dog.svg
│ └── settings.svg
└── manifest.webmanifest
├── postcss.config.js
├── .dockerignore
├── tsconfig.custom.json
├── sly.json
├── .vscode
├── extensions.json
└── settings.json
├── .gitignore
├── .editorconfig
├── tsconfig.server.json
├── supervisord.conf
├── .github
└── workflows
│ ├── fly-home.yml
│ ├── fly-app.yml
│ ├── fly-core.yml
│ └── fly-custom.yml
├── README.md
├── .env.local
├── scripts
└── clean-payload-types.js
├── doppler-template.yaml
├── .env.example
├── patches
└── remix-development-tools+3.7.4.patch
├── fly.toml
├── LICENSE
└── custom.server.ts
/app/_custom/styles.css:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/_custom/admin-panel-styles.css:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/_custom/readme.md:
--------------------------------------------------------------------------------
1 | # Custom site documentation
2 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | !.*
3 | dist
4 | build
5 |
--------------------------------------------------------------------------------
/fly/typesense.Dockerfile:
--------------------------------------------------------------------------------
1 | FROM typesense/typesense:26.0
2 |
3 | EXPOSE 8108
--------------------------------------------------------------------------------
/icons/icon-128.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gamepress/pokemongo/HEAD/icons/icon-128.webp
--------------------------------------------------------------------------------
/icons/icon-192.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gamepress/pokemongo/HEAD/icons/icon-192.webp
--------------------------------------------------------------------------------
/icons/icon-256.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gamepress/pokemongo/HEAD/icons/icon-256.webp
--------------------------------------------------------------------------------
/icons/icon-48.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gamepress/pokemongo/HEAD/icons/icon-48.webp
--------------------------------------------------------------------------------
/icons/icon-512.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gamepress/pokemongo/HEAD/icons/icon-512.webp
--------------------------------------------------------------------------------
/icons/icon-72.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gamepress/pokemongo/HEAD/icons/icon-72.webp
--------------------------------------------------------------------------------
/icons/icon-96.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gamepress/pokemongo/HEAD/icons/icon-96.webp
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gamepress/pokemongo/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/app/_custom/snippets/import/example.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "id": "sample_id"
4 | }
5 | ]
6 |
--------------------------------------------------------------------------------
/app/utils/delay.ts:
--------------------------------------------------------------------------------
1 | export function delay(ms: number) {
2 | return new Promise((res) => setTimeout(res, ms));
3 | }
4 |
--------------------------------------------------------------------------------
/app/_custom/routes/README.md:
--------------------------------------------------------------------------------
1 | Insert routes for your custom wiki in this folder.
2 |
3 | They'll be automatically loaded by the wiki.
4 |
--------------------------------------------------------------------------------
/public/fonts/Nunito_Sans/NunitoSans-Bold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gamepress/pokemongo/HEAD/public/fonts/Nunito_Sans/NunitoSans-Bold.ttf
--------------------------------------------------------------------------------
/app/utils/stripe.server.ts:
--------------------------------------------------------------------------------
1 | import Stripe from "stripe";
2 |
3 | export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY as string, {});
4 |
--------------------------------------------------------------------------------
/public/fonts/Nunito_Sans/NunitoSans-Bold.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gamepress/pokemongo/HEAD/public/fonts/Nunito_Sans/NunitoSans-Bold.woff
--------------------------------------------------------------------------------
/public/fonts/Nunito_Sans/NunitoSans-Bold.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gamepress/pokemongo/HEAD/public/fonts/Nunito_Sans/NunitoSans-Bold.woff2
--------------------------------------------------------------------------------
/public/fonts/Nunito_Sans/NunitoSans-Italic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gamepress/pokemongo/HEAD/public/fonts/Nunito_Sans/NunitoSans-Italic.ttf
--------------------------------------------------------------------------------
/public/fonts/Nunito_Sans/NunitoSans-Italic.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gamepress/pokemongo/HEAD/public/fonts/Nunito_Sans/NunitoSans-Italic.woff
--------------------------------------------------------------------------------
/public/fonts/Nunito_Sans/NunitoSans-Italic.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gamepress/pokemongo/HEAD/public/fonts/Nunito_Sans/NunitoSans-Italic.woff2
--------------------------------------------------------------------------------
/public/fonts/Nunito_Sans/NunitoSans-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gamepress/pokemongo/HEAD/public/fonts/Nunito_Sans/NunitoSans-Regular.ttf
--------------------------------------------------------------------------------
/public/fonts/Nunito_Sans/NunitoSans-Regular.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gamepress/pokemongo/HEAD/public/fonts/Nunito_Sans/NunitoSans-Regular.woff
--------------------------------------------------------------------------------
/public/fonts/Nunito_Sans/NunitoSans-Regular.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gamepress/pokemongo/HEAD/public/fonts/Nunito_Sans/NunitoSans-Regular.woff2
--------------------------------------------------------------------------------
/public/fonts/Nunito_Sans/NunitoSans-SemiBold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gamepress/pokemongo/HEAD/public/fonts/Nunito_Sans/NunitoSans-SemiBold.ttf
--------------------------------------------------------------------------------
/public/fonts/Nunito_Sans/NunitoSans-SemiBold.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gamepress/pokemongo/HEAD/public/fonts/Nunito_Sans/NunitoSans-SemiBold.woff
--------------------------------------------------------------------------------
/public/fonts/Source_Sans_Pro/SourceSansPro-It.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gamepress/pokemongo/HEAD/public/fonts/Source_Sans_Pro/SourceSansPro-It.otf
--------------------------------------------------------------------------------
/public/fonts/Nunito_Sans/NunitoSans-SemiBold.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gamepress/pokemongo/HEAD/public/fonts/Nunito_Sans/NunitoSans-SemiBold.woff2
--------------------------------------------------------------------------------
/public/fonts/Source_Sans_Pro/sourcesanspro-bold.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gamepress/pokemongo/HEAD/public/fonts/Source_Sans_Pro/sourcesanspro-bold.otf
--------------------------------------------------------------------------------
/app/db/access/isSiteStaff.ts:
--------------------------------------------------------------------------------
1 | export function isSiteStaff(roles: ["staff" | "user"] | undefined) {
2 | return roles ? roles.includes("staff") : false;
3 | }
4 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('postcss-load-config').Config} */
2 | module.exports = {
3 | plugins: {
4 | autoprefixer: {},
5 | },
6 | };
7 |
--------------------------------------------------------------------------------
/public/fonts/Source_Sans_Pro/SourceSansPro-Black.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gamepress/pokemongo/HEAD/public/fonts/Source_Sans_Pro/SourceSansPro-Black.otf
--------------------------------------------------------------------------------
/public/fonts/Source_Sans_Pro/SourceSansPro-BlackIt.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gamepress/pokemongo/HEAD/public/fonts/Source_Sans_Pro/SourceSansPro-BlackIt.otf
--------------------------------------------------------------------------------
/public/fonts/Source_Sans_Pro/SourceSansPro-BoldIt.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gamepress/pokemongo/HEAD/public/fonts/Source_Sans_Pro/SourceSansPro-BoldIt.otf
--------------------------------------------------------------------------------
/public/fonts/Source_Sans_Pro/SourceSansPro-Light.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gamepress/pokemongo/HEAD/public/fonts/Source_Sans_Pro/SourceSansPro-Light.otf
--------------------------------------------------------------------------------
/public/fonts/Source_Sans_Pro/SourceSansPro-LightIt.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gamepress/pokemongo/HEAD/public/fonts/Source_Sans_Pro/SourceSansPro-LightIt.otf
--------------------------------------------------------------------------------
/public/fonts/Source_Sans_Pro/SourceSansPro-Regular.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gamepress/pokemongo/HEAD/public/fonts/Source_Sans_Pro/SourceSansPro-Regular.otf
--------------------------------------------------------------------------------
/public/fonts/Source_Sans_Pro/SourceSansPro-Semibold.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gamepress/pokemongo/HEAD/public/fonts/Source_Sans_Pro/SourceSansPro-Semibold.otf
--------------------------------------------------------------------------------
/public/fonts/Source_Sans_Pro/SourceSansPro-ExtraLight.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gamepress/pokemongo/HEAD/public/fonts/Source_Sans_Pro/SourceSansPro-ExtraLight.otf
--------------------------------------------------------------------------------
/public/fonts/Source_Sans_Pro/SourceSansPro-ExtraLightIt.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gamepress/pokemongo/HEAD/public/fonts/Source_Sans_Pro/SourceSansPro-ExtraLightIt.otf
--------------------------------------------------------------------------------
/public/fonts/Source_Sans_Pro/SourceSansPro-SemiboldIt.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gamepress/pokemongo/HEAD/public/fonts/Source_Sans_Pro/SourceSansPro-SemiboldIt.otf
--------------------------------------------------------------------------------
/public/fonts/Source_Sans_Pro/sourcesanspro-bold-webfont.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gamepress/pokemongo/HEAD/public/fonts/Source_Sans_Pro/sourcesanspro-bold-webfont.woff
--------------------------------------------------------------------------------
/public/fonts/Source_Sans_Pro/sourcesanspro-bold-webfont.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gamepress/pokemongo/HEAD/public/fonts/Source_Sans_Pro/sourcesanspro-bold-webfont.woff2
--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------
1 | /.git
2 | /node_modules
3 | \*.log
4 | .DS_Store
5 | .dockerignore
6 | .env
7 | Dockerfile
8 | fly.toml
9 | /fly
10 |
11 | /.cache
12 | /public/build
13 | /build
--------------------------------------------------------------------------------
/app/db/access/isSiteOwner.ts:
--------------------------------------------------------------------------------
1 | export const isSiteOwner = (
2 | userId: string | undefined,
3 | siteOwner: string | undefined,
4 | ) => {
5 | return userId === siteOwner;
6 | };
7 |
--------------------------------------------------------------------------------
/app/routes/_site+/settings+/utils/RoleActionSchema.ts:
--------------------------------------------------------------------------------
1 | import { z } from "zod";
2 |
3 | export const RoleActionSchema = z.object({
4 | siteId: z.string(),
5 | userId: z.string(),
6 | });
7 |
--------------------------------------------------------------------------------
/app/routes/inngest+/utils/inngest-client.ts:
--------------------------------------------------------------------------------
1 | import { Inngest } from "inngest";
2 |
3 | // Create a client to send and receive events
4 | export const inngest = new Inngest({
5 | id: "core",
6 | });
7 |
--------------------------------------------------------------------------------
/app/db/access/isSiteAdmin.ts:
--------------------------------------------------------------------------------
1 | export function isSiteAdmin(
2 | userId: string | undefined,
3 | admins: string[] | undefined,
4 | ) {
5 | return userId && admins ? admins.includes(userId) : false;
6 | }
7 |
--------------------------------------------------------------------------------
/app/routes/_site+/_components/_datepicker/date-picker/types.ts:
--------------------------------------------------------------------------------
1 | export type WeekStartDay = 'Sunday' | 'Monday';
2 |
3 | export type DisplayDate = {
4 | date: Date;
5 | active: boolean;
6 | ms: number;
7 | };
8 |
--------------------------------------------------------------------------------
/app/routes/404.tsx:
--------------------------------------------------------------------------------
1 | export default function NotFound() {
2 | return (
3 |
4 |
404 - Page Not Found
5 |
The page you are looking for does not exist.
6 |
7 | );
8 | }
9 |
--------------------------------------------------------------------------------
/app/utils/remember.server.ts:
--------------------------------------------------------------------------------
1 | export function remember(key: string, value: T) {
2 | const g = global as any;
3 | g.__singletons ??= {};
4 | g.__singletons[key] ??= value;
5 | return g.__singletons[key];
6 | }
7 |
--------------------------------------------------------------------------------
/app/db/access/isSiteContributor.ts:
--------------------------------------------------------------------------------
1 | export function isSiteContributor(
2 | userId: string | undefined,
3 | contributors: string[] | undefined,
4 | ) {
5 | return contributors && userId ? contributors.includes(userId) : false;
6 | }
7 |
--------------------------------------------------------------------------------
/app/routes/_site+/settings+/utils/ApplicationReviewSchema.ts:
--------------------------------------------------------------------------------
1 | import { z } from "zod";
2 |
3 | export const ApplicationReviewSchema = z.object({
4 | siteId: z.string(),
5 | reviewMessage: z.string().optional(),
6 | applicantUserId: z.string(),
7 | });
8 |
--------------------------------------------------------------------------------
/app/utils/url-slug.ts:
--------------------------------------------------------------------------------
1 | import urlSlug from "url-slug";
2 |
3 | export function manaSlug(value: string) {
4 | return urlSlug(value, {
5 | dictionary: {
6 | "+": "plus",
7 | "♂": "male",
8 | "♀": "female",
9 | },
10 | });
11 | }
12 |
--------------------------------------------------------------------------------
/tsconfig.custom.json:
--------------------------------------------------------------------------------
1 | {
2 | // Extend the root-level config to reuse common options.
3 | "extends": "./tsconfig.server.json",
4 | "files": ["custom.server.ts", "app/db/payload.custom.config.ts"],
5 | "compilerOptions": {
6 | "outDir": "build/custom"
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/app/routes/_auth+/components/LoggedIn.tsx:
--------------------------------------------------------------------------------
1 | import { useRootLoaderData } from "~/utils/useSiteLoaderData";
2 |
3 | export const LoggedIn = ({ children }: { children: React.ReactNode }) => {
4 | const { user } = useRootLoaderData();
5 | return user ? <>{children}> : null;
6 | };
7 |
--------------------------------------------------------------------------------
/app/routes/_auth+/components/LoggedOut.tsx:
--------------------------------------------------------------------------------
1 | import { useRootLoaderData } from "~/utils/useSiteLoaderData";
2 |
3 | export const LoggedOut = ({ children }: { children: React.ReactNode }) => {
4 | const { user } = useRootLoaderData();
5 | return user ? null : <>{children}>;
6 | };
7 |
--------------------------------------------------------------------------------
/sly.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://sly-cli.fly.dev/registry/config.json",
3 | "libraries": [
4 | {
5 | "name": "lucide-icons",
6 | "directory": "./public/icons",
7 | "transformers": ["scripts/generate-icons.ts"]
8 | }
9 | ]
10 | }
11 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": [
3 | "bradlc.vscode-tailwindcss",
4 | "vscode-icons-team.vscode-icons",
5 | "esbenp.prettier-vscode",
6 | "wix.vscode-import-cost",
7 | "tamasfe.even-better-toml",
8 | "naumovs.color-highlight"
9 | ]
10 | }
11 |
--------------------------------------------------------------------------------
/app/routes/_auth+/components/Staff.tsx:
--------------------------------------------------------------------------------
1 | import { useRootLoaderData } from "~/utils/useSiteLoaderData";
2 |
3 | export const Staff = ({ children }: { children: React.ReactNode }) => {
4 | const { user } = useRootLoaderData();
5 | return user?.roles?.includes("staff") ? <>{children}> : null;
6 | };
7 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "cSpell.words": ["clsx"],
3 | "typescript.preferences.autoImportFileExcludePatterns": [
4 | "**/node_modules/react-router-dom/**"
5 | ],
6 | "javascript.preferences.autoImportFileExcludePatterns": [
7 | "**/node_modules/react-router-dom/**"
8 | ]
9 | }
10 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 |
3 | /.cache
4 | /build
5 | /dist
6 | /public/build
7 | .env
8 | app/styles/tailwind.css
9 | .DS_Store
10 |
11 | # generated by payload
12 | yarn-error.log
13 | app/_custom/seed/images
14 | payload-types.ts
15 | schema.graphql
16 | payload-custom-types.ts
17 | app/_custom/site-config.ts
18 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # Editor configuration, see http://editorconfig.org
2 | root = true
3 |
4 | [*]
5 | charset = utf-8
6 | indent_style = space
7 | indent_size = 3
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 | end_of_line = LF
11 |
12 | [*.md]
13 | max_line_length = off
14 | trim_trailing_whitespace = false
--------------------------------------------------------------------------------
/app/routes/_seo+/robots[.]txt.tsx:
--------------------------------------------------------------------------------
1 | export const loader = () => {
2 | const robotText = `
3 | User-agent: *
4 | Disallow: /admin/
5 | `;
6 | return new Response(robotText, {
7 | status: 200,
8 | headers: {
9 | "Content-Type": "text/plain",
10 | },
11 | });
12 | };
13 |
--------------------------------------------------------------------------------
/app/utils/nanoid.ts:
--------------------------------------------------------------------------------
1 | import { customAlphabet } from "nanoid";
2 | const alphabet = "123456789ABCDEFGHIJKLMNPQRSTUVWXYZabcdefghijklmnpqrstuvwxyz";
3 | const lowercase = "123456789abcdefghijklmnpqrstuvwxyz";
4 |
5 | export const safeNanoID = customAlphabet(alphabet, 10);
6 |
7 | export const siteNanoID = customAlphabet(lowercase, 10);
8 |
--------------------------------------------------------------------------------
/app/components/Loading.tsx:
--------------------------------------------------------------------------------
1 | import { Icon } from "./Icon";
2 |
3 | export const Loading = () => (
4 |
5 |
10 |
11 | );
12 |
--------------------------------------------------------------------------------
/app/routes/_editor+/blocks+/table/src/utils/is-element.ts:
--------------------------------------------------------------------------------
1 | import type { Node } from "slate";
2 | import { Editor, Element } from "slate";
3 |
4 | import type { WithType } from "./types";
5 |
6 | export function isElement(node: Node): node is WithType {
7 | return !Editor.isEditor(node) && Element.isElement(node) && "type" in node;
8 | }
9 |
--------------------------------------------------------------------------------
/app/_custom/blocks/Example.tsx:
--------------------------------------------------------------------------------
1 | import type { ReactNode } from "react";
2 |
3 | import type { ListElement } from "~/routes/_editor+/core/types";
4 |
5 | type Props = {
6 | element: ListElement;
7 | children: ReactNode;
8 | };
9 |
10 | export const ExampleBlock = ({ element, children }: Props) => {
11 | return {children}
;
12 | };
13 |
--------------------------------------------------------------------------------
/app/routes/_auth+/components/AdminOrStaffOrOwner.tsx:
--------------------------------------------------------------------------------
1 | import { useIsStaffOrSiteAdminOrStaffOrOwner } from "../utils/useIsStaffSiteAdminOwner";
2 |
3 | export const AdminOrStaffOrOwner = ({
4 | children,
5 | }: {
6 | children: React.ReactNode;
7 | }) => {
8 | const hasAccess = useIsStaffOrSiteAdminOrStaffOrOwner();
9 | return hasAccess ? <>{children}> : null;
10 | };
11 |
--------------------------------------------------------------------------------
/app/routes/_auth+/components/NotAdminOrStaffOrOwner.tsx:
--------------------------------------------------------------------------------
1 | import { useIsStaffOrSiteAdminOrStaffOrOwner } from "../utils/useIsStaffSiteAdminOwner";
2 |
3 | export const NotAdminOrStaffOrOwner = ({
4 | children,
5 | }: {
6 | children: React.ReactNode;
7 | }) => {
8 | const hasAccess = useIsStaffOrSiteAdminOrStaffOrOwner();
9 |
10 | return !hasAccess ? <>{children}> : null;
11 | };
12 |
--------------------------------------------------------------------------------
/app/_custom/routes/_site.c.tier-lists+/components/TierListData.tsx:
--------------------------------------------------------------------------------
1 | export type TierListData = {
2 | id: string;
3 | slug: string;
4 | name: string;
5 | number: string;
6 | icon: {
7 | url: string;
8 | };
9 | type: [
10 | {
11 | name: string;
12 | slug: string;
13 | icon: {
14 | url: string;
15 | };
16 | },
17 | ];
18 | };
19 |
--------------------------------------------------------------------------------
/app/_custom/classes.ts:
--------------------------------------------------------------------------------
1 | export const InfoBlock_Container =
2 | "border border-color-sub divide-y divide-color-sub shadow-sm shadow-1 rounded-lg [&>*:nth-of-type(odd)]:bg-zinc-50 dark:[&>*:nth-of-type(odd)]:bg-dark350 overflow-hidden";
3 | export const InfoBlock_Row = "flex items-center justify-between p-3 text-sm";
4 | export const InfoBlock_Label = "font-bold";
5 | export const InfoBlock_Content = "text-1 font-semibold";
6 |
--------------------------------------------------------------------------------
/public/icons/minus.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/public/icons/slash.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/public/icons/check.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/public/icons/chevron-down.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/public/icons/chevron-up.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/public/icons/chevron-left.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/public/icons/chevron-right.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/public/icons/loader-2.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/public/icons/moon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/public/icons/plus.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/public/icons/x.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/public/icons/arrow-up.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/public/icons/brackets.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/public/icons/send.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/app/routes/_site+/_components/_datepicker/time-picker/types.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Time type
3 | */
4 | export type Time = {
5 | hours: number;
6 | minutes: number;
7 | };
8 |
9 | /**
10 | * Time display type
11 | */
12 | export type TimeDisplay = {
13 | hours: number;
14 | minutes: number;
15 | meridiem?: Meridiem;
16 | };
17 |
18 | export type DisplayFormat = '12hr' | '24hr';
19 |
20 | export enum Meridiem {
21 | AM = 'AM',
22 | PM = 'PM',
23 | }
24 |
--------------------------------------------------------------------------------
/app/routes/_site+/c_+/$collectionId_.$entryId/utils/EntrySchema.ts:
--------------------------------------------------------------------------------
1 | import { z } from "zod";
2 |
3 | export const EntrySchemaUpdateSchema = z.object({
4 | name: z.string().min(1).max(200),
5 | entryId: z.string(),
6 | slug: z.string(),
7 | existingSlug: z.string().optional(),
8 | siteId: z.string().optional(),
9 | collectionId: z.string().optional(),
10 | entryIcon: z.any().optional(),
11 | entryIconId: z.string().optional(),
12 | });
13 |
--------------------------------------------------------------------------------
/public/icons/arrow-down.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/public/icons/arrow-left.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/public/icons/arrow-right.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/public/icons/message-circle.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/public/icons/move-right.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/public/icons/search.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/tsconfig.server.json:
--------------------------------------------------------------------------------
1 | {
2 | // Extend the root-level config to reuse common options.
3 | "extends": "./tsconfig.json",
4 | "files": ["remix.env.d.ts", "core.server.ts", "app/db/payload.config.ts"],
5 | "include": [],
6 | "compilerOptions": {
7 | "lib": ["ES2022"],
8 | "module": "NodeNext",
9 | "moduleResolution": "NodeNext",
10 | "outDir": "build/core",
11 | "baseUrl": ".",
12 | "noEmit": false
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/public/icons/bookmark.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/public/icons/chevrons-up.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/public/icons/line-chart.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/app/routes/_auth+/components/FollowingSite.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | useRootLoaderData,
3 | useSiteLoaderData,
4 | } from "~/utils/useSiteLoaderData";
5 |
6 | export const FollowingSite = ({ children }: { children: React.ReactNode }) => {
7 | const { following } = useRootLoaderData();
8 |
9 | const { site } = useSiteLoaderData();
10 |
11 | if (site && following?.some((e: any) => e.id === site?.id))
12 | return <>{children}>;
13 | return null;
14 | };
15 |
--------------------------------------------------------------------------------
/public/icons/chevrons-down.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/public/icons/chevrons-left.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/public/icons/chevrons-right.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/public/icons/code.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/app/routes/_editor+/blocks+/table/src/utils/point.ts:
--------------------------------------------------------------------------------
1 | export class Point {
2 | public x: number;
3 | public y: number;
4 |
5 | constructor(x: number, y: number) {
6 | this.x = x;
7 | this.y = y;
8 | }
9 |
10 | public static valueOf(x: number, y: number): Point {
11 | return new this(x, y);
12 | }
13 |
14 | public static equals(point: Point, another: Point): boolean {
15 | return point.x === another.x && point.y === another.y;
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/app/utils/isBotProvider.tsx:
--------------------------------------------------------------------------------
1 | import type { ReactNode } from "react";
2 | import { createContext, useContext } from "react";
3 |
4 | type Props = { isBot: boolean; children: ReactNode };
5 |
6 | const context = createContext(false);
7 |
8 | export function useIsBot() {
9 | return useContext(context) ?? false;
10 | }
11 |
12 | export function IsBotProvider({ isBot, children }: Props) {
13 | return {children};
14 | }
15 |
--------------------------------------------------------------------------------
/public/icons/bold.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/public/icons/chevrons-up-down.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/public/icons/clock-9.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/public/icons/rectangle-horizontal.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/public/icons/reply.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/public/icons/text.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/public/icons/columns-2.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/public/icons/triangle.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/app/routes/_user+/components/UserMenuItems.tsx:
--------------------------------------------------------------------------------
1 | import { UserMenuLink } from "./UserMenuLink";
2 |
3 | export function UserMenuItems() {
4 | return (
5 |
6 |
7 |
8 | {/* */}
9 |
10 | );
11 | }
12 |
--------------------------------------------------------------------------------
/public/icons/info.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/public/icons/list-filter.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/public/icons/pen-line.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/public/icons/pencil.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/public/icons/underline.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/public/icons/user.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/app/routes/_home+/privacy.tsx:
--------------------------------------------------------------------------------
1 | import type { MetaFunction } from "@remix-run/node";
2 |
3 | import { PolicyTemplate } from "../_site+/privacy";
4 |
5 | export const meta: MetaFunction = () => {
6 | return [
7 | {
8 | title: "Privacy Policy - Mana",
9 | },
10 | ];
11 | };
12 |
13 | export default function PrivacyPolicy() {
14 | return (
15 |
18 | );
19 | }
20 |
--------------------------------------------------------------------------------
/app/routes/_site+/search/SiteSearchOn.tsx:
--------------------------------------------------------------------------------
1 | import { InstantSearch } from "react-instantsearch";
2 |
3 | import type { Site } from "payload/generated-types";
4 |
5 | import { searchClient } from "./_search";
6 | import { Autocomplete } from "./components/Autocomplete";
7 |
8 | export function SiteSearchOn({ site }: { site: Site }) {
9 | return (
10 |
11 |
12 |
13 | );
14 | }
15 |
--------------------------------------------------------------------------------
/public/icons/corner-down-left.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/public/icons/lock.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/public/icons/pie-chart.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/public/icons/plus-circle.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/public/icons/rows.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/app/_custom/collections/readme.md:
--------------------------------------------------------------------------------
1 | ## Collections
2 |
3 | `collectionSlug.tsx` in this (`collections`} directory
4 |
5 | ```
6 | import type { CollectionConfig } from "payload/types";
7 |
8 | const Collection: CollectionConfig = {
9 | slug: "",
10 | fields: [
11 | {
12 | name: "entry",
13 | type: "relationship",
14 | relationTo: "entries",
15 | hasMany: false,
16 | },
17 | ],
18 | };
19 |
20 | export const CustomCollections = [];
21 | ```
22 |
--------------------------------------------------------------------------------
/public/icons/columns.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/public/icons/corner-down-right.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/public/icons/credit-card.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/public/icons/home.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/app/routes/_site+/c_+/_components/fuzzyFilter.tsx:
--------------------------------------------------------------------------------
1 | import { rankItem } from "@tanstack/match-sorter-utils";
2 | import type { FilterFn } from "@tanstack/react-table";
3 |
4 | export const fuzzyFilter: FilterFn = (row, columnId, value, addMeta) => {
5 | // Rank the item
6 | const itemRank = rankItem(row.getValue(columnId), value);
7 |
8 | // Store the ranking info
9 | addMeta(itemRank);
10 |
11 | // Return if the item should be filtered in/out
12 | return itemRank.passed;
13 | };
14 |
--------------------------------------------------------------------------------
/public/icons/captions.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/public/icons/key.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/public/icons/mail.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/public/icons/star.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/app/config.ts:
--------------------------------------------------------------------------------
1 | export type ManaConfig = {
2 | title?: string; //Title of the site, used in meta tags
3 | fromEmail?: string; //Email address to send emails from
4 | fromName?: string; //Name to send emails from
5 | typesenseHost?: string; //Host of the typesense server
6 | typesenseSearchOnlyKey?: string; //API key that only allows search operations
7 | };
8 |
9 | export const settings: ManaConfig = {
10 | fromName: "GamePress - Pokemon GO",
11 | fromEmail: "noreply@gamepress.gg",
12 | };
13 |
--------------------------------------------------------------------------------
/public/icons/ellipsis.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/public/icons/square-plus.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/public/icons/dollar-sign.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/public/icons/wallet-2.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/app/utils/typsense.server.ts:
--------------------------------------------------------------------------------
1 | import Typesense from "typesense";
2 |
3 | import { settings } from "../config";
4 |
5 | export const typesensePrivateClient = new Typesense.Client({
6 | apiKey: process.env.TYPESENSE_PRIVATE_KEY ?? "", // Be sure to use an API key that only allows search operations
7 | nodes: [
8 | {
9 | host: settings?.typesenseHost ?? "search.mana.wiki",
10 | port: 443,
11 | protocol: "https",
12 | },
13 | ],
14 | connectionTimeoutSeconds: 2,
15 | });
16 |
--------------------------------------------------------------------------------
/public/icons/menu.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/public/icons/more-vertical.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/public/icons/arrow-up-down.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/public/icons/copy.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/public/icons/database.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/public/icons/folder.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/public/icons/globe.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/public/icons/italic.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/public/icons/link-2.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/public/icons/more-horizontal.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/public/icons/trash.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/public/icons/type.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/public/icons/users-2.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/public/icons/settings-2.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/public/icons/sort.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/public/icons/table.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/public/icons/archive.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/public/icons/heading-2.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/public/icons/list-plus.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/public/icons/strikethrough.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/app/_custom/routes/_site.c.pokemon+/components/Label.tsx:
--------------------------------------------------------------------------------
1 | import { Pokemon } from "../../../collections/pokemon";
2 |
3 | export function Label({
4 | fieldName,
5 | value,
6 | }: {
7 | fieldName: string;
8 | value: string | undefined | null;
9 | }) {
10 | //@ts-ignore
11 | const getLabel = Pokemon?.fields
12 | .find((element: any) => element?.name == fieldName)
13 | //@ts-ignore
14 | .options.find((element: any) => element.value == value);
15 | return <>{getLabel?.label && getLabel.label}>;
16 | }
17 |
--------------------------------------------------------------------------------
/public/icons/bar-chart-2.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/public/icons/layout.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/public/icons/log-out.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/public/icons/pen-square.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/public/icons/upload.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/supervisord.conf:
--------------------------------------------------------------------------------
1 | [supervisord]
2 | logfile=/dev/stdout
3 | logfile_maxbytes=0
4 | loglevel=info
5 | pidfile=/tmp/supervisord.pid
6 | nodaemon=true
7 | user=root
8 |
9 | [unix_http_server]
10 | file=/tmp/supervisor.sock
11 | username=user
12 | password=pass
13 |
14 | [program:custom]
15 | command=yarn start:custom
16 | stdout_logfile=/dev/fd/1
17 | stdout_logfile_maxbytes=0
18 | redirect_stderr=true
19 |
20 | [program:core]
21 | command=yarn start:core
22 | stdout_logfile=/dev/fd/1
23 | stdout_logfile_maxbytes=0
24 | redirect_stderr=true
--------------------------------------------------------------------------------
/app/entry.client.tsx:
--------------------------------------------------------------------------------
1 | // https://github.com/remix-run/remix/issues/2813
2 | import { Buffer } from "buffer";
3 |
4 | import { startTransition, StrictMode } from "react";
5 |
6 | import { RemixBrowser } from "@remix-run/react";
7 | import { hydrateRoot } from "react-dom/client";
8 |
9 | // https://github.com/remix-run/remix/issues/2813
10 | globalThis.Buffer = Buffer;
11 |
12 | startTransition(() => {
13 | hydrateRoot(
14 | document,
15 |
16 |
17 | ,
18 | );
19 | });
20 |
--------------------------------------------------------------------------------
/public/icons/download.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/public/icons/eye.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/public/icons/link.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/public/icons/square-pen.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/app/_custom/collections/index.ts:
--------------------------------------------------------------------------------
1 | import { Types } from "./_types";
2 | import { EvolutionRequirements } from "./evolution-requirements";
3 | import { Moves } from "./moves";
4 | import { Pokemon } from "./pokemon";
5 | import { PokemonFamilies } from "./pokemon-family";
6 | import { RaidGuides } from "./raid-guides";
7 | import { Weather } from "./weather";
8 |
9 | export const CustomCollections = [
10 | Pokemon,
11 | Moves,
12 | Types,
13 | Weather,
14 | EvolutionRequirements,
15 | RaidGuides,
16 | PokemonFamilies,
17 | ];
18 |
--------------------------------------------------------------------------------
/app/routes/_site+/c_+/$collectionId_.$entryId/utils/SectionSchema.tsx:
--------------------------------------------------------------------------------
1 | import { z } from "zod";
2 |
3 | export const SectionSchema = z.object({
4 | collectionId: z.string(),
5 | name: z.string(),
6 | sectionSlug: z
7 | .string()
8 | .regex(
9 | new RegExp(/^[a-z0-9_]+((\.-?|-\.?)[a-z0-9_]+)*$/),
10 | "Section slug contains invalid characters",
11 | ),
12 | showTitle: z.coerce.boolean(),
13 | showAd: z.coerce.boolean(),
14 | type: z.enum(["editor", "customTemplate", "qna", "comments"]),
15 | });
16 |
--------------------------------------------------------------------------------
/public/icons/coins.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/public/icons/image.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/public/icons/message-square-plus.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/app/routes/_site+/_components/_datepicker/util.ts:
--------------------------------------------------------------------------------
1 | import type { Time } from "./time-picker/types";
2 |
3 | export const getCurrentTime = (): Time => convertDateToTime(new Date());
4 |
5 | export const convertDateToTime = (d: Date): Time => ({
6 | hours: d.getHours(),
7 | minutes: d.getMinutes(),
8 | });
9 |
10 | export const convertTimeToDate = (t: Time, d?: Date): Date => {
11 | const newDate = d ? new Date(d.valueOf()) : new Date();
12 | newDate.setHours(t.hours);
13 | newDate.setMinutes(t.minutes);
14 | return newDate;
15 | };
16 |
--------------------------------------------------------------------------------
/public/icons/table-cells-split.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/public/icons/zap.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/app/routes/inngest+/api.ts:
--------------------------------------------------------------------------------
1 | import { serve } from "inngest/remix";
2 |
3 | import { inngest } from "~/routes/inngest+/utils/inngest-client";
4 |
5 | import { loadAnalyticsCron } from "./utils/loadAnalyticsCron.server";
6 | import { updateSiteAnalytics } from "./utils/updateSiteAnalytics.server";
7 |
8 | const handler = serve({
9 | client: inngest,
10 | serveHost: "http://localhost:3000",
11 | servePath: "/inngest/api",
12 | functions: [loadAnalyticsCron, updateSiteAnalytics],
13 | });
14 |
15 | export { handler as action, handler as loader };
16 |
--------------------------------------------------------------------------------
/public/icons/wrench.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/app/routes/_auth+/components/CustomSite.tsx:
--------------------------------------------------------------------------------
1 | import { useMatches } from "@remix-run/react";
2 |
3 | import type { Site } from "payload/generated-types";
4 |
5 | export const CustomSite = ({ children }: { children: React.ReactNode }) => {
6 | //site data should live in layout, this may be potentially brittle if we shift site architecture around
7 | const { site } = (useMatches()?.[1]?.data as { site: Site | null }) ?? {
8 | site: null,
9 | };
10 | const isCustom = site?.type === "custom";
11 | return isCustom ? <>{children}> : null;
12 | };
13 |
--------------------------------------------------------------------------------
/public/icons/arrow-up-right-from-square.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/public/icons/bolt.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/public/icons/copy-check.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/public/icons/external-link.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/public/icons/hash.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/public/icons/link-2-off.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/public/icons/list-tree.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/public/icons/shield.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/public/icons/flame.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/public/icons/expand.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/public/icons/file-code-2.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/public/icons/layout-panel-top.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/public/icons/table-cells-merge.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/public/icons/users.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/public/icons/sword.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/public/icons/calendar.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/app/components/DotLoader.tsx:
--------------------------------------------------------------------------------
1 | export const DotLoader = () => {
2 | return (
3 |
9 | );
10 | };
11 |
--------------------------------------------------------------------------------
/app/routes/_site+/settings+/utils/copyToClipBoard.tsx:
--------------------------------------------------------------------------------
1 | import { Icon } from "~/components/Icon";
2 | import { delay } from "~/utils/delay";
3 |
4 | export async function copyToClipBoard(copyMe: string, setCopySuccess: any) {
5 | try {
6 | await navigator.clipboard.writeText(copyMe);
7 | setCopySuccess(
8 | ,
9 | );
10 | await delay(2000);
11 | setCopySuccess();
12 | } catch (err) {
13 | setCopySuccess();
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/public/icons/pin.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/public/icons/refresh-ccw.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/app/routes/_user+/user+/appearance.tsx:
--------------------------------------------------------------------------------
1 | import type { MetaFunction } from "@remix-run/react";
2 |
3 | import { ThemeToggleDesktop } from "../components/ThemeToggleDesktop";
4 | import { UserContainer } from "../components/UserContainer";
5 |
6 | export default function UserAppearance() {
7 | return (
8 |
9 |
10 |
11 | );
12 | }
13 |
14 | export const meta: MetaFunction = () => {
15 | return [
16 | {
17 | title: `Appearance | User Settings - Mana`,
18 | },
19 | ];
20 | };
21 |
--------------------------------------------------------------------------------
/fly/fly.core.toml:
--------------------------------------------------------------------------------
1 | app = "mana"
2 | primary_region = "sjc"
3 | kill_signal = "SIGINT"
4 | kill_timeout = "5s"
5 |
6 | [build]
7 | dockerfile = "core.Dockerfile"
8 |
9 | [env]
10 | PORT = "3000"
11 |
12 | [http_service]
13 | internal_port = 3000
14 | force_https = true
15 | auto_stop_machines = "suspend"
16 | auto_start_machines = true
17 | min_machines_running = 1
18 | processes = ["app"]
19 | [http_service.concurrency]
20 | type = "requests"
21 | soft_limit = 800
22 | hard_limit = 1000
23 |
24 | [[vm]]
25 | cpu_kind = "shared"
26 | cpus = 1
27 | memory_mb = 1024
28 |
--------------------------------------------------------------------------------
/fly/fly.custom.toml:
--------------------------------------------------------------------------------
1 | app = ""
2 | primary_region = "sjc"
3 | kill_signal = "SIGINT"
4 | kill_timeout = "5s"
5 |
6 | [build]
7 | dockerfile = "custom.Dockerfile"
8 |
9 | [env]
10 | PORT = "4000"
11 |
12 | [http_service]
13 | internal_port = 4000
14 | force_https = true
15 | auto_stop_machines = "suspend"
16 | auto_start_machines = true
17 | min_machines_running = 0
18 | processes = ["app"]
19 | [http_service.concurrency]
20 | type = "requests"
21 | soft_limit = 800
22 | hard_limit = 1000
23 |
24 | [[vm]]
25 | cpu_kind = "shared"
26 | cpus = 1
27 | memory_mb = 1024
28 |
--------------------------------------------------------------------------------
/public/icons/heading-3.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/public/icons/piggy-bank.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/.github/workflows/fly-home.yml:
--------------------------------------------------------------------------------
1 | name: Fly Deploy - Home
2 | on:
3 | push:
4 | branches:
5 | - main
6 |
7 | concurrency:
8 | group: ${{ github.workflow }}-${{ github.ref }}
9 | cancel-in-progress: true
10 |
11 | jobs:
12 | deploy:
13 | name: Deploy home app
14 | runs-on: ubuntu-latest
15 | steps:
16 | - uses: actions/checkout@v4
17 | - uses: superfly/flyctl-actions/setup-flyctl@master
18 | - run: flyctl deploy --config ./fly/fly.home.toml --depot=false
19 | env:
20 | FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
21 |
--------------------------------------------------------------------------------
/app/_custom/routes/_site.c.pokemon+/components/RatingsLabel.tsx:
--------------------------------------------------------------------------------
1 | import { Pokemon } from "../../../collections/pokemon";
2 |
3 | export function RatingsLabel({
4 | fieldName,
5 | value,
6 | }: {
7 | fieldName: string;
8 | value: string | undefined;
9 | }) {
10 | const getLabel = Pokemon?.fields
11 | .find((element: any) => element?.label == "Ratings")
12 | //@ts-ignore
13 | ?.fields[0]?.fields?.find((element: any) => element.name == fieldName)
14 | .options.find((element: any) => element.value == value);
15 | return <>{getLabel?.label && getLabel.label}>;
16 | }
17 |
--------------------------------------------------------------------------------
/public/icons/badge-check.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/public/icons/server.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/app/db/access/isSiteOwnerOrAdmin.ts:
--------------------------------------------------------------------------------
1 | import type { Site } from "payload/generated-types";
2 | import type { PostData } from "~/routes/_site+/p+/utils/fetchPostWithSlug.server";
3 |
4 | export function isSiteOwnerOrAdmin(
5 | userId: string,
6 | site: Site | undefined | null | PostData["site"],
7 | ) {
8 | const siteAdmins = site?.admins;
9 | const siteOwner = site?.owner;
10 | const isSiteOwner = userId == (siteOwner as any);
11 | //@ts-ignore
12 | const isSiteAdmin = siteAdmins && siteAdmins.includes(userId);
13 | if (isSiteOwner || isSiteAdmin) return true;
14 | return false;
15 | }
16 |
--------------------------------------------------------------------------------
/public/icons/accessibility.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/public/icons/archive-restore.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/public/icons/binary.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/public/icons/component.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/public/icons/image-minus.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/app/routes/_auth+/utils/handleLogout.client.ts:
--------------------------------------------------------------------------------
1 | export async function handleLogout() {
2 | try {
3 | await fetch("/api/users/logout", {
4 | method: "POST",
5 | credentials: "include",
6 | headers: {
7 | "Content-Type": "application/json",
8 | },
9 | });
10 |
11 | // We need to do this because the payload rest logout has the wrong cookie domain
12 | await fetch("/logout", {
13 | method: "POST",
14 | });
15 |
16 | location.replace("/");
17 | } catch (error) {
18 | console.error("Logout failed:", error);
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/public/icons/captions-off.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/public/icons/copy-x.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/public/icons/layout-grid.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/public/icons/layout-list.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/public/icons/trash-2.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Mana provides the tools and infrastructure needed to create, maintain, and grow comprehensive wikis for niche communities. It's used by gaming communities like [Pokémon GO](https://pokemongo.gamepress.gg/), [Fate/Grand Order](https://grandorder.gamepress.gg/), [Honkai: Star Rail](https://starrail.mana.wiki/), [Zenless Zone Zero](https://zzz.mana.wiki/), and [Pokémon TCG Pocket](https://pokemonpocket.tcg.wiki/).
2 |
3 | ### Getting Started
4 |
5 | 1. [Fork](https://github.com/manawiki/mana/fork) or clone this repo
6 | 2. Copy `.env.local` as `.env`to set up env variables.
7 | 3. Start local development with `yarn; yarn dev`.
8 |
--------------------------------------------------------------------------------
/app/routes/_editor+/blocks+/embed.tsx:
--------------------------------------------------------------------------------
1 | import { Transforms } from "slate";
2 | import { ReactEditor, type RenderElementProps, useSlate } from "slate-react";
3 |
4 | import type { EmbedElement, CustomElement } from "../core/types";
5 |
6 | export function BlockEmbed({
7 | children,
8 | element,
9 | readOnly,
10 | attributes,
11 | }: RenderElementProps & {
12 | element: EmbedElement;
13 | readOnly: Boolean;
14 | }) {
15 | const editor = useSlate();
16 |
17 | const path = ReactEditor.findPath(editor, element);
18 |
19 | return ;
20 | }
21 |
--------------------------------------------------------------------------------
/app/routes/_auth+/components/NotFollowingSite.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | useRootLoaderData,
3 | useSiteLoaderData,
4 | } from "~/utils/useSiteLoaderData";
5 |
6 | export const NotFollowingSite = ({
7 | children,
8 | }: {
9 | children: React.ReactNode;
10 | }) => {
11 | const { user, following } = useRootLoaderData();
12 |
13 | //site data should live in layout, this may be potentially brittle if we shift site architecture around
14 | const { site } = useSiteLoaderData();
15 |
16 | if (!user) return null;
17 | if (following?.some((e: any) => e.id === site?.id)) return null;
18 | return <>{children}>;
19 | };
20 |
--------------------------------------------------------------------------------
/public/icons/grip-vertical.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/public/icons/hourglass.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/app/utils/form.ts:
--------------------------------------------------------------------------------
1 | import type { ZodIssue } from "zod";
2 |
3 | export function isProcessing(state: "idle" | "submitting" | "loading") {
4 | return state === "submitting" || state === "loading";
5 | }
6 |
7 | export function isAdding(item: any, type: string) {
8 | return item.state === "submitting" && item.formData.get("intent") === type;
9 | }
10 |
11 | export function isLoading(item: any) {
12 | return item.state === "loading";
13 | }
14 |
15 | export interface FormResponse {
16 | success?: string;
17 | error?: string;
18 | /**
19 | * Any server-side only issues
20 | */
21 | serverIssues?: ZodIssue[];
22 | }
23 |
--------------------------------------------------------------------------------
/.github/workflows/fly-app.yml:
--------------------------------------------------------------------------------
1 | name: Fly Deploy - App
2 | on:
3 | push:
4 | branches:
5 | - main
6 |
7 | concurrency:
8 | group: ${{ github.workflow }}-${{ github.ref }}
9 | cancel-in-progress: true
10 |
11 | jobs:
12 | deploy:
13 | name: Deploy mana app
14 | runs-on: ubuntu-latest
15 | steps:
16 | - uses: actions/checkout@v4
17 | - uses: superfly/flyctl-actions/setup-flyctl@master
18 | - run: flyctl deploy --app "$APP_NAME" --depot=false
19 | env:
20 | FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
21 | APP_NAME: ${{ secrets.APP_NAME }}
22 |
--------------------------------------------------------------------------------
/app/components/Link.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import { DataInteractive as HeadlessDataInteractive } from "@headlessui/react";
4 | import { Link as RemixLink, type LinkProps } from "@remix-run/react";
5 |
6 | interface LinkPropsWithHref extends Omit {
7 | href: LinkProps["to"];
8 | }
9 |
10 | export const Link = React.forwardRef(function Link(
11 | props: LinkPropsWithHref,
12 | ref: React.ForwardedRef,
13 | ) {
14 | return (
15 |
16 |
17 |
18 | );
19 | });
20 |
--------------------------------------------------------------------------------
/public/icons/list-ordered.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/app/routes/_site+/settings+/components/RoleBadge.tsx:
--------------------------------------------------------------------------------
1 | import { Badge } from "~/components/Badge";
2 |
3 | import type { TeamMember } from "../team";
4 |
5 | export function RoleBadge({ role }: { role: TeamMember["role"] }) {
6 | let color = "" as any;
7 | switch (role) {
8 | case "Owner":
9 | color = "purple";
10 | break;
11 | case "Admin":
12 | color = "amber";
13 | break;
14 | case "Contributor":
15 | color = "blue";
16 | break;
17 | default:
18 | color = "gray";
19 | break;
20 | }
21 |
22 | return {role};
23 | }
24 |
--------------------------------------------------------------------------------
/public/icons/move.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/fly/fly.home.toml:
--------------------------------------------------------------------------------
1 | app = "mana-home"
2 | primary_region = "sjc"
3 | kill_signal = "SIGINT"
4 | kill_timeout = "5s"
5 |
6 | [build]
7 | dockerfile = "core.Dockerfile"
8 |
9 | [build.args]
10 | IS_HOME = "true"
11 |
12 | [env]
13 | PORT = "3000"
14 |
15 | [http_service]
16 | internal_port = 3000
17 | force_https = true
18 | auto_stop_machines = "suspend"
19 | auto_start_machines = true
20 | min_machines_running = 0
21 | processes = ["app"]
22 | [http_service.concurrency]
23 | type = "requests"
24 | soft_limit = 800
25 | hard_limit = 1000
26 |
27 | [[vm]]
28 | cpu_kind = "shared"
29 | cpus = 1
30 | memory_mb = 1024
31 |
--------------------------------------------------------------------------------
/public/icons/file-text.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/public/icons/image-plus.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/public/icons/list.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/public/icons/calendar-clock.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/public/icons/hard-drive.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/public/icons/eye-off.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/.github/workflows/fly-core.yml:
--------------------------------------------------------------------------------
1 | name: Fly Deploy - Core
2 | on:
3 | push:
4 | branches:
5 | - main
6 |
7 | concurrency:
8 | group: ${{ github.workflow }}-${{ github.ref }}
9 | cancel-in-progress: true
10 |
11 | jobs:
12 | deploy:
13 | name: Deploy core app
14 | runs-on: ubuntu-latest
15 | steps:
16 | - uses: actions/checkout@v4
17 | - uses: superfly/flyctl-actions/setup-flyctl@master
18 | - run: flyctl deploy --app "$APP_NAME" --config ./fly/fly.core.toml --depot=false
19 | env:
20 | FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
21 | APP_NAME: ${{ secrets.APP_NAME }}
22 |
--------------------------------------------------------------------------------
/app/db/hooks/replaceVersionAuthor.ts:
--------------------------------------------------------------------------------
1 | import payload from "payload";
2 | import type { CollectionBeforeChangeHook } from "payload/types";
3 |
4 | // Automatically replaces the author field before a change is submitted
5 | // in order to allow us to determine which user made a specific document version
6 | export const replaceVersionAuthor: CollectionBeforeChangeHook = async ({
7 | data,
8 | req,
9 | operation,
10 | originalDoc,
11 | }) => {
12 | try {
13 | if (operation == "update") {
14 | data.author = req.user.id;
15 | }
16 | return data;
17 | } catch (err: unknown) {
18 | payload.logger.error(`${err}`);
19 | }
20 | };
21 |
--------------------------------------------------------------------------------
/public/icons/globe-2.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/.env.local:
--------------------------------------------------------------------------------
1 | # CORE - MIN REQUIRED SECRETS TO RUN LOCAL SERVER, COPY to .ENV
2 | SITE_SLUG=
3 | DB_URI= #MongoDB URI should start with either mongodb:// or mongodb+srv://
4 | PAYLOADCMS_SECRET= #Secret key for payloadcms, use `openssl rand -hex 32` or visit https://1password.com/password-generator/ to generate
5 | BACKBLAZE_APPLICATION_KEY=
6 | BACKBLAZE_KEYID=
7 |
8 | #CUSTOM SITE VARIABLES - Required if using custom site
9 | CUSTOM_DB_NAME= #Custom site db name, ex: site-prod-db, override by CUSTOM_DB_URI
10 | CUSTOM_DB_URI= #for multi-cluster setup, full URI to a separate db
11 | FILE_PREFIX= #Folder to save custom site file uploads ex:https://static.mana.wiki/prefix/filename.png
--------------------------------------------------------------------------------
/.github/workflows/fly-custom.yml:
--------------------------------------------------------------------------------
1 | name: Fly Deploy - Custom
2 | on:
3 | push:
4 | branches:
5 | - main
6 |
7 | concurrency:
8 | group: ${{ github.workflow }}-${{ github.ref }}
9 | cancel-in-progress: true
10 |
11 | jobs:
12 | deploy:
13 | name: Deploy custom app
14 | runs-on: ubuntu-latest
15 | steps:
16 | - uses: actions/checkout@v4
17 | - uses: superfly/flyctl-actions/setup-flyctl@master
18 | - run: flyctl deploy --app "$APP_NAME"-db --config ./fly/fly.custom.toml --depot=false
19 | env:
20 | FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
21 | APP_NAME: ${{ secrets.APP_NAME }}
22 |
--------------------------------------------------------------------------------
/app/_custom/routes/_site.c.tier-lists+/components/TierFive.tsx:
--------------------------------------------------------------------------------
1 | import { ListTable } from "~/routes/_site+/c_+/_components/ListTable";
2 |
3 | import { columns, filters, gridView } from "./ListTierList";
4 |
5 | export function TierFive({ data }: { data: any }) {
6 | return (
7 |
17 | );
18 | }
19 |
--------------------------------------------------------------------------------
/app/_custom/routes/_site.c.tier-lists+/components/TierFour.tsx:
--------------------------------------------------------------------------------
1 | import { ListTable } from "~/routes/_site+/c_+/_components/ListTable";
2 |
3 | import { columns, filters, gridView } from "./ListTierList";
4 |
5 | export function TierFour({ data }: { data: any }) {
6 | return (
7 |
17 | );
18 | }
19 |
--------------------------------------------------------------------------------
/app/_custom/routes/_site.c.tier-lists+/components/TierOne.tsx:
--------------------------------------------------------------------------------
1 | import { ListTable } from "~/routes/_site+/c_+/_components/ListTable";
2 |
3 | import { columns, filters, gridView } from "./ListTierList";
4 |
5 | export function TierOne({ data }: { data: any }) {
6 | return (
7 |
17 | );
18 | }
19 |
--------------------------------------------------------------------------------
/app/_custom/routes/_site.c.tier-lists+/components/TierSix.tsx:
--------------------------------------------------------------------------------
1 | import { ListTable } from "~/routes/_site+/c_+/_components/ListTable";
2 |
3 | import { columns, filters, gridView } from "./ListTierList";
4 |
5 | export function TierSix({ data }: { data: any }) {
6 | return (
7 |
17 | );
18 | }
19 |
--------------------------------------------------------------------------------
/app/_custom/routes/_site.c.tier-lists+/components/TierTwo.tsx:
--------------------------------------------------------------------------------
1 | import { ListTable } from "~/routes/_site+/c_+/_components/ListTable";
2 |
3 | import { columns, filters, gridView } from "./ListTierList";
4 |
5 | export function TierTwo({ data }: { data: any }) {
6 | return (
7 |
17 | );
18 | }
19 |
--------------------------------------------------------------------------------
/app/_custom/routes/_site.c.tier-lists+/components/TierThree.tsx:
--------------------------------------------------------------------------------
1 | import { ListTable } from "~/routes/_site+/c_+/_components/ListTable";
2 |
3 | import { columns, filters, gridView } from "./ListTierList";
4 |
5 | export function TierThree({ data }: { data: any }) {
6 | return (
7 |
17 | );
18 | }
19 |
--------------------------------------------------------------------------------
/public/icons/github.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/public/icons/sun.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/public/icons/calendar-plus.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/app/routes/_editor+/blocks+/inline-ad.tsx:
--------------------------------------------------------------------------------
1 | import { AdPlaceholder, AdUnit } from "~/routes/_site+/_components/RampUnit";
2 |
3 | import type { InlineAdElement } from "../core/types";
4 |
5 | export function BlockInlineAd({ element }: { element: InlineAdElement }) {
6 | return (
7 |
8 |
18 |
19 | );
20 | }
21 |
--------------------------------------------------------------------------------
/app/db/collections/collections/collections-search-schema.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "collections",
3 | "fields": [
4 | {
5 | "name": "name",
6 | "type": "string",
7 | "sort": true
8 | },
9 | {
10 | "name": "relativeURL",
11 | "type": "string"
12 | },
13 | {
14 | "name": "absoluteURL",
15 | "type": "string"
16 | },
17 | {
18 | "name": "category",
19 | "type": "string"
20 | },
21 | {
22 | "name": "icon",
23 | "type": "string",
24 | "optional": true
25 | },
26 | {
27 | "name": "site",
28 | "type": "string",
29 | "facet": true
30 | }
31 | ]
32 | }
33 |
--------------------------------------------------------------------------------
/app/routes/_site+/p+/components/PostHeaderView.tsx:
--------------------------------------------------------------------------------
1 | import { PostAuthorHeader } from "./PostAuthorHeader";
2 | import type { PostData } from "../utils/fetchPostWithSlug.server";
3 |
4 | export function PostHeaderView({ post }: { post: PostData }) {
5 | return (
6 |
7 |
8 | {post.name}
9 |
10 |
11 | {post.subtitle && (
12 |
13 | {post.subtitle}
14 |
15 | )}
16 |
17 | );
18 | }
19 |
--------------------------------------------------------------------------------
/app/routes/_editor+/blocks+/table/src/weak-maps.ts:
--------------------------------------------------------------------------------
1 | import type { Editor, Element } from "slate";
2 |
3 | import type { WithTableOptions } from "./options";
4 | import type { NodeEntryWithContext } from "./utils/types";
5 |
6 | /** Weak reference between the `Editor` and the `WithTableOptions` */
7 | export const EDITOR_TO_WITH_TABLE_OPTIONS = new WeakMap<
8 | Editor,
9 | WithTableOptions
10 | >();
11 |
12 | /** Weak reference between the `Editor` and the selected elements */
13 | export const EDITOR_TO_SELECTION = new WeakMap<
14 | Editor,
15 | NodeEntryWithContext[][]
16 | >();
17 |
18 | /** Weak reference between the `Editor` and a set of the selected elements */
19 | export const EDITOR_TO_SELECTION_SET = new WeakMap>();
20 |
--------------------------------------------------------------------------------
/app/routes/_editor+/core/constants.ts:
--------------------------------------------------------------------------------
1 | import type { Format } from "./types";
2 | import { BlockType } from "./types";
3 |
4 | export const HOTKEYS: Record = {
5 | "mod+b": "bold",
6 | "mod+i": "italic",
7 | "mod+u": "underline",
8 | "mod+s": "strikeThrough",
9 | };
10 |
11 | export const LIST_WRAPPER: Record = {
12 | "*": BlockType.BulletedList,
13 | "-": BlockType.BulletedList,
14 | "+": BlockType.BulletedList,
15 | "1.": BlockType.NumberedList,
16 | };
17 |
18 | export const SHORTCUTS: Record = {
19 | "*": BlockType.ListItem,
20 | "-": BlockType.ListItem,
21 | "+": BlockType.ListItem,
22 | "1.": BlockType.ListItem,
23 | "##": BlockType.H2,
24 | "###": BlockType.H3,
25 | };
26 |
--------------------------------------------------------------------------------
/app/routes/_home+/components/mouse-position.tsx:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from "react";
2 |
3 | interface MousePosition {
4 | x: number;
5 | y: number;
6 | }
7 |
8 | export const useMousePosition = (): MousePosition => {
9 | const [mousePosition, setMousePosition] = useState({
10 | x: 0,
11 | y: 0,
12 | });
13 |
14 | useEffect(() => {
15 | const handleMouseMove = (event: MouseEvent) => {
16 | setMousePosition({ x: event.clientX, y: event.clientY });
17 | };
18 |
19 | window.addEventListener("mousemove", handleMouseMove);
20 |
21 | return () => {
22 | window.removeEventListener("mousemove", handleMouseMove);
23 | };
24 | }, []);
25 |
26 | return mousePosition;
27 | };
28 |
--------------------------------------------------------------------------------
/app/routes/_editor+/blocks+/table/src/utils/types.ts:
--------------------------------------------------------------------------------
1 | import type { Element, NodeEntry } from "slate";
2 |
3 | export type CellElement = WithType<
4 | { rowSpan?: number; colSpan?: number } & Element
5 | >;
6 |
7 | /** Extends an element with the "type" property */
8 | export type WithType = T & Record<"type", unknown>;
9 |
10 | export type NodeEntryWithContext = [
11 | NodeEntry,
12 | {
13 | rtl: number; // right-to-left (colspan)
14 | ltr: number; // left-to-right (colspan)
15 | ttb: number; // top-to-bottom (rowspan)
16 | btt: number; // bottom-to-top (rowspan)
17 | },
18 | ];
19 |
20 | export type SelectionMode = "start" | "end" | "all";
21 |
22 | export type Edge = "start" | "end" | "top" | "bottom";
23 |
--------------------------------------------------------------------------------
/scripts/clean-payload-types.js:
--------------------------------------------------------------------------------
1 | const fs = require("fs");
2 |
3 | // We should probably avoid hard-coding the path like this
4 | process?.env?.PAYLOAD_CONFIG_PATH === "./app/db/payload.custom.config.ts"
5 | ? cleanFile("./app/db/payload-custom-types.ts")
6 | : cleanFile("./app/db/payload-types.ts");
7 |
8 | function cleanFile(file) {
9 | console.log(`Cleaning ${file}`);
10 |
11 | // Read the file
12 | let content = fs.readFileSync(file, "utf-8");
13 |
14 | // Fix Payload depth issues
15 | content = content
16 | .replace(/string \| (?=[A-Z_])/g, "")
17 | .replace(/string\[\] \| (?=[A-Z_])/g, "")
18 | .replace(/\(string \| null\) \| (?=[A-Z_])/g, "");
19 |
20 | // Write the file
21 | fs.writeFileSync(file, content, "utf-8");
22 | }
23 |
--------------------------------------------------------------------------------
/app/routes/_proxy+/proxy.gtag.js.tsx:
--------------------------------------------------------------------------------
1 | // proxy the js response from https://www.googletagmanager.com/gtag/js?id=G-${gtag}
2 | import type { LoaderFunctionArgs } from "@remix-run/server-runtime";
3 |
4 | import { cacheThis } from "~/utils/cache.server";
5 |
6 | export async function loader({ request }: LoaderFunctionArgs) {
7 | const url =
8 | "https://www.googletagmanager.com/gtag/js" + new URL(request.url).search;
9 |
10 | const body = await cacheThis(
11 | () => fetch(url).then((res) => res.text()),
12 | url,
13 | 604800000,
14 | );
15 | return new Response(body, {
16 | headers: {
17 | "content-type": "application/javascript",
18 | "Cache-Control": "public, max-age=604800, immutable",
19 | },
20 | });
21 | }
22 |
--------------------------------------------------------------------------------
/public/icons/swords.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/app/_custom/collections/evolution-requirements.ts:
--------------------------------------------------------------------------------
1 | import type { CollectionConfig } from "payload/types";
2 |
3 | import { isStaff } from "../../db/collections/users/users.access";
4 |
5 | export const EvolutionRequirements: CollectionConfig = {
6 | slug: "evolution-requirements",
7 | admin: {
8 | group: "Custom",
9 | useAsTitle: "name",
10 | },
11 | access: {
12 | create: isStaff,
13 | read: () => true,
14 | update: isStaff,
15 | delete: isStaff,
16 | },
17 | fields: [
18 | {
19 | name: "id",
20 | type: "text",
21 | },
22 | {
23 | name: "name",
24 | type: "text",
25 | },
26 | {
27 | name: "slug",
28 | type: "text",
29 | },
30 | ],
31 | };
32 |
--------------------------------------------------------------------------------
/app/utils/time-ago.ts:
--------------------------------------------------------------------------------
1 | export function timeAgo(input: Date) {
2 | const date = input instanceof Date ? input : new Date(input);
3 | const formatter = new Intl.RelativeTimeFormat("en");
4 | const ranges = [
5 | ["years", 3600 * 24 * 365],
6 | ["months", 3600 * 24 * 30],
7 | ["weeks", 3600 * 24 * 7],
8 | ["days", 3600 * 24],
9 | ["hours", 3600],
10 | ["minutes", 60],
11 | ["seconds", 1],
12 | ] as const;
13 | const secondsElapsed = (date.getTime() - Date.now()) / 1000;
14 |
15 | for (const [rangeType, rangeVal] of ranges) {
16 | if (rangeVal < Math.abs(secondsElapsed)) {
17 | const delta = secondsElapsed / rangeVal;
18 | return formatter.format(Math.round(delta), rangeType);
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/app/_custom/collections/raid-guides.ts:
--------------------------------------------------------------------------------
1 | import type { CollectionConfig } from "payload/types";
2 |
3 | import { isStaff } from "../../db/collections/users/users.access";
4 |
5 | export const RaidGuides: CollectionConfig = {
6 | slug: "raid-guides",
7 | admin: {
8 | group: "Custom",
9 | useAsTitle: "name",
10 | },
11 | access: {
12 | create: isStaff,
13 | read: () => true,
14 | update: isStaff,
15 | delete: isStaff,
16 | },
17 | fields: [
18 | {
19 | name: "name",
20 | type: "text",
21 | },
22 | {
23 | name: "id",
24 | type: "text",
25 | },
26 | {
27 | name: "icon",
28 | type: "upload",
29 | relationTo: "images",
30 | },
31 | ],
32 | };
33 |
--------------------------------------------------------------------------------
/app/routes/_site+/p+/components/PostBannerView.tsx:
--------------------------------------------------------------------------------
1 | import { Image } from "~/components/Image";
2 | import type { Post } from "~/db/payload-types";
3 |
4 | export function PostBannerView({ post }: { post: Post }) {
5 | return (
6 | post?.banner && (
7 |
11 |
18 |
19 | )
20 | );
21 | }
22 |
--------------------------------------------------------------------------------
/fly/fly.typesense.toml:
--------------------------------------------------------------------------------
1 | app = "mana-typesense"
2 | primary_region = "sjc"
3 | kill_signal = "SIGINT"
4 | kill_timeout = "5s"
5 |
6 | [build]
7 | dockerfile = "typesense.Dockerfile"
8 |
9 | [env]
10 | TYPESENSE_DATA_DIR= "/typesense-data"
11 | TYPESENSE_ENABLE_CORS= "true"
12 | TYPESENSE_API_KEY=""
13 |
14 | [http_service]
15 | internal_port = 8108
16 | force_https = true
17 | auto_stop_machines = "suspend"
18 | auto_start_machines = true
19 | min_machines_running = 0
20 | processes = ["app"]
21 | [http_service.concurrency]
22 | type = "requests"
23 | soft_limit = 800
24 | hard_limit = 1000
25 |
26 | [[vm]]
27 | cpu_kind = "shared"
28 | cpus = 1
29 | memory_mb = 2048
30 |
31 | [mounts]
32 | source = "typesense_data"
33 | destination = "/typesense-data"
--------------------------------------------------------------------------------
/doppler-template.yaml:
--------------------------------------------------------------------------------
1 | projects:
2 | - name: "UPDATE"
3 | environments:
4 | - name: Production
5 | slug: prd
6 |
7 | secrets:
8 | prd:
9 | METRONOME_API_KEY: ${mana.prd.METRONOME_API_KEY}
10 | DB_URI: ${mana.prd.DB_URI}
11 | PAYLOADCMS_SECRET: ${mana.prd.PAYLOADCMS_SECRET}
12 | NODEMAILER_PASSWORD: ${mana.prd.NODEMAILER_PASSWORD}
13 | BACKBLAZE_APPLICATION_KEY: ${mana.prd.BACKBLAZE_APPLICATION_KEY}
14 | BACKBLAZE_KEYID: ${mana.prd.BACKBLAZE_KEYID}
15 | STRIPE_PUBLIC_KEY: ${mana.prd.STRIPE_PUBLIC_KEY}
16 | STRIPE_SECRET_KEY: ${mana.prd.STRIPE_SECRET_KEY}
17 | FLY_API_TOKEN: ${mana.prd.FLY_API_TOKEN}
18 | CUSTOM_DB_URI: "UPDATE"
19 | FILE_PREFIX: "UPDATE"
20 |
--------------------------------------------------------------------------------
/app/routes/_site+/_components/Column-1.tsx:
--------------------------------------------------------------------------------
1 | import { useLoaderData } from "@remix-run/react";
2 |
3 | import type { loader as siteLoaderType } from "~/routes/_site+/_layout";
4 |
5 | import { ColumnOneMenu } from "./Column-1-Menu";
6 |
7 | export function ColumnOne() {
8 | const { site } = useLoaderData();
9 |
10 | return (
11 |
22 | );
23 | }
24 |
--------------------------------------------------------------------------------
/app/routes/_editor+/blocks+/table/src/utils/is-of-type.ts:
--------------------------------------------------------------------------------
1 | import type { Editor, Element, Node, NodeMatch } from "slate";
2 |
3 | import { isElement } from "./is-element";
4 | import type { WithType } from "./types";
5 | import type { WithTableOptions } from "../options";
6 | import { EDITOR_TO_WITH_TABLE_OPTIONS } from "../weak-maps";
7 |
8 | /** @returns a `NodeMatch` function which is used to match the elements of a specific `type`. */
9 | export function isOfType>(
10 | editor: Editor,
11 | ...types: Array
12 | ): NodeMatch {
13 | const options = EDITOR_TO_WITH_TABLE_OPTIONS.get(editor),
14 | elementTypes = types.map((type) => options?.blocks?.[type]);
15 |
16 | return (node: Node): boolean =>
17 | isElement(node) && elementTypes.includes(node.type);
18 | }
19 |
--------------------------------------------------------------------------------
/app/db/collections/entries/entries-search-schema.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "entries",
3 | "fields": [
4 | {
5 | "name": "name",
6 | "type": "string",
7 | "sort": true
8 | },
9 | {
10 | "name": "relativeURL",
11 | "type": "string"
12 | },
13 | {
14 | "name": "absoluteURL",
15 | "type": "string"
16 | },
17 | {
18 | "name": "category",
19 | "type": "string"
20 | },
21 | {
22 | "name": "collection",
23 | "type": "string",
24 | "facet": true
25 | },
26 | {
27 | "name": "icon",
28 | "type": "string",
29 | "optional": true
30 | },
31 | {
32 | "name": "site",
33 | "type": "string",
34 | "facet": true
35 | }
36 | ]
37 | }
38 |
--------------------------------------------------------------------------------
/app/db/collections/posts/posts-search-schema.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "posts",
3 | "fields": [
4 | {
5 | "name": "name",
6 | "type": "string",
7 | "sort": true
8 | },
9 | {
10 | "name": "relativeURL",
11 | "type": "string"
12 | },
13 | {
14 | "name": "absoluteURL",
15 | "type": "string"
16 | },
17 | {
18 | "name": "category",
19 | "type": "string"
20 | },
21 | {
22 | "name": "icon",
23 | "type": "string",
24 | "optional": true
25 | },
26 | {
27 | "name": "description",
28 | "type": "string",
29 | "optional": true
30 | },
31 | {
32 | "name": "site",
33 | "type": "string",
34 | "facet": true
35 | }
36 | ]
37 | }
38 |
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
1 | # OPTIONAL OVERRIDE VARIABLES
2 | BACKBLAZE_BUCKET= #Override default "mana-prod" static asset bucket name
3 | HOST_DOMAIN= #Override default "mana.wiki" domain name
4 | IS_HOME= #Build home page if set to true
5 | API_ENDPOINT= #Override default "mana.wiki" api endpoint
6 | NODEMAILER_HOST=
7 | NODEMAILER_PORT=
8 | NODEMAILER_USER=
9 |
10 | # PRODUCTION VARIABLES - CORE ONLY
11 | STRIPE_PUBLIC_KEY=
12 | STRIPE_SECRET_KEY=
13 | NODEMAILER_PASSWORD=
14 | FLY_API_TOKEN=
15 | MANA_APP_KEY= #For inngest
16 | INNGEST_SIGNING_KEY=
17 | GA_CLIENT_EMAIL= #analytics service account email
18 | GA_PRIVATE_KEY= #analytics private key
19 | TYPESENSE_PRIVATE_KEY=
20 |
21 | #CUSTOM SITE VARIABLES - Required if using custom site
22 | CUSTOM_DB_NAME=For single cluster setup
23 | CUSTOM_DB_URI=For external db setup
24 | CUSTOM_DB_APP_KEY=
25 | SITE_ID=
26 | FILE_PREFIX=
--------------------------------------------------------------------------------
/app/routes/_site+/c_+/$collectionId_.$entryId/components/SectionType.tsx:
--------------------------------------------------------------------------------
1 | import { Icon } from "~/components/Icon";
2 |
3 | export function SectionType({ type }: { type: string }) {
4 | let icon = "" as any;
5 | switch (type) {
6 | case "editor":
7 | icon = "pen-square";
8 | break;
9 | case "customTemplate":
10 | icon = "layout-grid";
11 | break;
12 | case "qna":
13 | icon = "pencil";
14 | break;
15 | case "comments":
16 | icon = "pencil";
17 | break;
18 | default:
19 | icon = "pencil";
20 | break;
21 | }
22 | return (
23 |
24 |
25 |
26 | );
27 | }
28 |
--------------------------------------------------------------------------------
/app/routes/_site+/settings+/payouts.tsx:
--------------------------------------------------------------------------------
1 | import type { MetaFunction, LoaderFunctionArgs } from "@remix-run/node";
2 |
3 | import { getSiteSlug } from "../_utils/getSiteSlug.server";
4 |
5 | export async function loader({
6 | context: { payload, user },
7 | request,
8 | }: LoaderFunctionArgs) {
9 | const { siteSlug } = await getSiteSlug(request, payload, user);
10 |
11 | return null;
12 | }
13 |
14 | export default function AdPayouts() {
15 | return ;
16 | }
17 |
18 | export const meta: MetaFunction = ({ matches }) => {
19 | const siteName = matches.find(
20 | ({ id }: { id: string }) => id === "routes/_site+/_layout",
21 | //@ts-ignore
22 | )?.data?.site.name;
23 |
24 | return [
25 | {
26 | title: `Payouts | Settings - ${siteName}`,
27 | },
28 | ];
29 | };
30 |
--------------------------------------------------------------------------------
/patches/remix-development-tools+3.7.4.patch:
--------------------------------------------------------------------------------
1 | diff --git a/node_modules/remix-development-tools/dist/server.cjs b/node_modules/remix-development-tools/dist/server.cjs
2 | index b86835a..7be7778 100644
3 | --- a/node_modules/remix-development-tools/dist/server.cjs
4 | +++ b/node_modules/remix-development-tools/dist/server.cjs
5 | @@ -694,6 +694,9 @@ var analyzeDeferred = (id, start, response) => {
6 | response.data[key].then(() => {
7 | const end = diffInMs(start);
8 | infoLog(`Deferred value ${source_default.white(key)} resolved in ${source_default.blueBright(id)} - ${source_default.white(`${end}ms`)}`);
9 | + }).catch((e) => {
10 | + errorLog(`Deferred value ${source_default.white(key)} rejected in ${source_default.blueBright(id)}`);
11 | + errorLog(e?.message ? e.message : e)
12 | });
13 | });
14 | }
15 |
--------------------------------------------------------------------------------
/public/icons/telescope.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/app/db/collections/custom-pages/custom-pages-schema.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "customPages",
3 | "fields": [
4 | {
5 | "name": "name",
6 | "type": "string",
7 | "sort": true
8 | },
9 | {
10 | "name": "relativeURL",
11 | "type": "string"
12 | },
13 | {
14 | "name": "absoluteURL",
15 | "type": "string"
16 | },
17 | {
18 | "name": "category",
19 | "type": "string"
20 | },
21 | {
22 | "name": "description",
23 | "type": "string",
24 | "optional": true
25 | },
26 | {
27 | "name": "icon",
28 | "type": "string",
29 | "optional": true
30 | },
31 | {
32 | "name": "site",
33 | "type": "string",
34 | "facet": true
35 | }
36 | ]
37 | }
38 |
--------------------------------------------------------------------------------
/public/icons/palette.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/app/routes/_site+/settings+/components/ApplicationStatus.tsx:
--------------------------------------------------------------------------------
1 | import { Badge } from "~/components/Badge";
2 | import type { SiteApplication } from "~/db/payload-types";
3 |
4 | export function ApplicationStatus({
5 | status,
6 | }: {
7 | status: SiteApplication["status"];
8 | }) {
9 | let color = "" as any;
10 | let statusLabel = "" as any;
11 | switch (status) {
12 | case "under-review":
13 | color = "cyan";
14 | statusLabel = "Needs Review";
15 | break;
16 | case "approved":
17 | color = "green";
18 | statusLabel = "Approved";
19 | break;
20 | case "denied":
21 | color = "red";
22 | statusLabel = "Denied";
23 | break;
24 | default:
25 | color = "gray";
26 | break;
27 | }
28 |
29 | return {statusLabel};
30 | }
31 |
--------------------------------------------------------------------------------
/public/icons/gamepad-2.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/app/utils/use-debounce.ts:
--------------------------------------------------------------------------------
1 | import { useState, useEffect, useRef } from "react";
2 |
3 | export function useDebouncedValue(input: T, time = 500) {
4 | const [debouncedValue, setDebouncedValue] = useState(input);
5 |
6 | // every time input value has changed - set interval before it's actually commited
7 | useEffect(() => {
8 | const timeout = setTimeout(() => {
9 | setDebouncedValue(input);
10 | }, time);
11 |
12 | return () => {
13 | clearTimeout(timeout);
14 | };
15 | }, [input, time]);
16 |
17 | return debouncedValue;
18 | }
19 |
20 | //Checks if initial render is mounted, used to prevent useEffect from running on initial render
21 | export function useIsMount() {
22 | const isMountRef = useRef(true);
23 | useEffect(() => {
24 | isMountRef.current = false;
25 | }, []);
26 | return isMountRef.current;
27 | }
28 |
--------------------------------------------------------------------------------
/app/utils/pinnedLinkUrlGenerator.ts:
--------------------------------------------------------------------------------
1 | export const pinnedLinkUrlGenerator = (item: any) => {
2 | const type = item.relation?.relationTo;
3 |
4 | switch (type) {
5 | case "customPages": {
6 | const slug = item.relation?.value.customPageSlug;
7 | return `/${slug}`;
8 | }
9 | case "collections": {
10 | const slug = item.relation?.value.collectionSlug;
11 | return `/c/${slug}`;
12 | }
13 | case "entries": {
14 | const slug = item.relation?.value.slug;
15 | const id = item.relation?.value.id;
16 | const collection = item.relation?.value.collectionEntity.slug;
17 | return `/c/${collection}/${id}/${slug}`;
18 | }
19 | case "posts": {
20 | const id = item.relation?.value.id;
21 | return `/p/${id}`;
22 | }
23 | default:
24 | return "/";
25 | }
26 | };
27 |
--------------------------------------------------------------------------------
/fly.toml:
--------------------------------------------------------------------------------
1 | app = ""
2 | primary_region = "sjc"
3 | kill_signal = "SIGINT"
4 | kill_timeout = "5s"
5 |
6 | [build]
7 | dockerfile = "Dockerfile"
8 |
9 | [env]
10 | PORT = "3000"
11 |
12 | [http_service]
13 | internal_port = 3000
14 | force_https = true
15 | auto_stop_machines = "suspend"
16 | auto_start_machines = true
17 | min_machines_running = 0
18 | processes = ["app"]
19 | [http_service.concurrency]
20 | soft_limit = 400
21 | hard_limit = 500
22 | type = "requests"
23 |
24 | [[services]]
25 | protocol = "tcp"
26 | internal_port = 4000
27 | processes = ["app"]
28 | auto_stop_machines = "suspend"
29 | auto_start_machines = true
30 | min_machines_running = 0
31 |
32 | [services.concurrency]
33 | soft_limit = 200
34 | hard_limit = 250
35 | type = "requests"
36 |
37 | [[services.ports]]
38 | port = 4000
39 | handlers = ["tls", "http"]
40 |
41 | [[vm]]
42 | cpu_kind = "shared"
43 | cpus = 8
44 | memory_mb = 2048
45 |
--------------------------------------------------------------------------------
/app/utils/useWindowDimensions.ts:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from "react";
2 |
3 | export function useWindowDimensions() {
4 | const hasWindow = typeof window !== "undefined";
5 |
6 | function getWindowDimensions() {
7 | const width = hasWindow ? window.innerWidth : null;
8 | const height = hasWindow ? window.innerHeight : null;
9 | return {
10 | width,
11 | height,
12 | };
13 | }
14 |
15 | const [windowDimensions, setWindowDimensions] = useState(
16 | getWindowDimensions(),
17 | );
18 |
19 | useEffect(() => {
20 | if (hasWindow) {
21 | function handleResize() {
22 | setWindowDimensions(getWindowDimensions());
23 | }
24 |
25 | window.addEventListener("resize", handleResize);
26 | return () => window.removeEventListener("resize", handleResize);
27 | }
28 | }, [hasWindow]);
29 |
30 | return windowDimensions;
31 | }
32 |
--------------------------------------------------------------------------------
/public/icons/dog.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/app/routes/_editor+/blocks+/table/src/utils/has-common.ts:
--------------------------------------------------------------------------------
1 | import type { Span } from "slate";
2 | import { Editor, Node } from "slate";
3 |
4 | import { isOfType } from "./is-of-type";
5 | import type { WithTableOptions } from "../options";
6 |
7 | /**
8 | * Determines whether two paths belong to the same types by checking
9 | * if they share a common ancestor node of type table
10 | */
11 | export function hasCommon(
12 | editor: Editor,
13 | [path, another]: Span,
14 | ...types: Array
15 | ) {
16 | const [node, commonPath] = Node.common(editor, path, another);
17 |
18 | if (isOfType(editor, ...types)(node, commonPath)) {
19 | return true;
20 | }
21 |
22 | // Warning: returns the common ancestor but will return `undefined` if the
23 | // `commonPath` is equal to the specified types path
24 | return !!Editor.above(editor, {
25 | match: isOfType(editor, ...types),
26 | at: commonPath,
27 | });
28 | }
29 |
--------------------------------------------------------------------------------
/public/icons/settings.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/app/_custom/collections/_types.ts:
--------------------------------------------------------------------------------
1 | import type { CollectionConfig } from "payload/types";
2 | import { isStaff } from "../../db/collections/users/users.access";
3 |
4 | export const Types: CollectionConfig = {
5 | slug: "types",
6 | admin: {
7 | group: "Custom",
8 | useAsTitle: "name",
9 | },
10 | access: {
11 | create: isStaff,
12 | read: () => true,
13 | update: isStaff,
14 | delete: isStaff,
15 | },
16 | fields: [
17 | {
18 | name: "id",
19 | type: "text",
20 | },
21 | {
22 | name: "icon",
23 | type: "upload",
24 | relationTo: "images",
25 | },
26 | {
27 | name: "slug",
28 | type: "text",
29 | },
30 | {
31 | name: "name",
32 | type: "text",
33 | },
34 | {
35 | name: "boostedWeather",
36 | type: "relationship",
37 | relationTo: "weather",
38 | hasMany: false,
39 | },
40 | ],
41 | };
42 |
--------------------------------------------------------------------------------
/app/routes/_site+/c_+/$collectionId_.$entryId/components/ScrollToHashElement.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useMemo } from "react";
2 |
3 | import { useLocation } from "@remix-run/react";
4 |
5 | // OTHER
6 | export function ScrollToHashElement() {
7 | let location = useLocation();
8 |
9 | let hashElement = useMemo(() => {
10 | let hash = location.hash;
11 | const removeHashCharacter = (str: string) => {
12 | const result = str.slice(1);
13 | return result;
14 | };
15 |
16 | if (hash) {
17 | let element = document.getElementById(removeHashCharacter(hash));
18 | return element;
19 | } else {
20 | return null;
21 | }
22 | }, [location]);
23 |
24 | useEffect(() => {
25 | if (hashElement) {
26 | hashElement.scrollIntoView({
27 | behavior: "smooth",
28 | // block: "end",
29 | inline: "nearest",
30 | });
31 | }
32 | }, [hashElement]);
33 |
34 | return null;
35 | }
36 |
--------------------------------------------------------------------------------
/app/_custom/collections/weather.ts:
--------------------------------------------------------------------------------
1 | import type { CollectionConfig } from "payload/types";
2 |
3 | import { isStaff } from "../../db/collections/users/users.access";
4 |
5 | export const Weather: CollectionConfig = {
6 | slug: "weather",
7 | admin: {
8 | group: "Custom",
9 | useAsTitle: "name",
10 | },
11 | access: {
12 | create: isStaff,
13 | read: () => true,
14 | update: isStaff,
15 | delete: isStaff,
16 | },
17 | fields: [
18 | {
19 | name: "name",
20 | type: "text",
21 | },
22 | {
23 | name: "id",
24 | type: "text",
25 | },
26 | {
27 | name: "slug",
28 | type: "text",
29 | },
30 | {
31 | name: "icon",
32 | type: "upload",
33 | relationTo: "images",
34 | },
35 | {
36 | name: "boostedTypes",
37 | type: "relationship",
38 | relationTo: "types",
39 | hasMany: true,
40 | },
41 | ],
42 | };
43 |
--------------------------------------------------------------------------------
/app/routes/_editor+/blocks+/table/src/normalization/with-normalization.ts:
--------------------------------------------------------------------------------
1 | import type { Editor } from "slate";
2 |
3 | import { normalizeAttributes } from "./normalize-attributes";
4 | import { normalizeContent } from "./normalize-content";
5 | import { normalizeSections } from "./normalize-sections";
6 | import { normalizeTable } from "./normalize-table";
7 | import { normalizeTd } from "./normalize-td";
8 | import { normalizeTr } from "./normalize-tr";
9 | import type { WithTableOptions } from "../options";
10 |
11 | export function withNormalization(
12 | editor: T,
13 | options: WithTableOptions,
14 | ): T {
15 | if (!options.withNormalization) {
16 | return editor;
17 | }
18 |
19 | editor = normalizeAttributes(editor, options);
20 | editor = normalizeContent(editor, options);
21 | editor = normalizeSections(editor, options);
22 | editor = normalizeTable(editor, options);
23 | editor = normalizeTd(editor, options);
24 | editor = normalizeTr(editor, options);
25 |
26 | return editor;
27 | }
28 |
--------------------------------------------------------------------------------
/app/routes/_site+/settings+/utils/imgPreview.ts:
--------------------------------------------------------------------------------
1 | import type { PixelCrop } from "react-image-crop";
2 |
3 | import { canvasPreview } from "./canvasPreview";
4 |
5 | let previewUrl = "";
6 |
7 | function toBlob(canvas: HTMLCanvasElement): Promise {
8 | return new Promise((resolve) => {
9 | canvas.toBlob(resolve);
10 | });
11 | }
12 |
13 | // Returns an image source you should set to state and pass
14 | // `{previewSrc &&
}`
15 | export async function imgPreview(
16 | image: HTMLImageElement,
17 | crop: PixelCrop,
18 | scale = 1,
19 | rotate = 0,
20 | ) {
21 | const canvas = document.createElement("canvas");
22 | canvasPreview(image, canvas, crop, scale, rotate);
23 |
24 | const blob = await toBlob(canvas);
25 |
26 | if (!blob) {
27 | console.error("Failed to create blob");
28 | return "";
29 | }
30 |
31 | if (previewUrl) {
32 | URL.revokeObjectURL(previewUrl);
33 | }
34 |
35 | previewUrl = URL.createObjectURL(blob);
36 | return previewUrl;
37 | }
38 |
--------------------------------------------------------------------------------
/app/db/collections/images/images.access.ts:
--------------------------------------------------------------------------------
1 | import type { Access } from "payload/types";
2 |
3 | import { isSiteStaff } from "../../access/isSiteStaff";
4 |
5 | //@ts-ignore
6 | export const canDeleteImages: Access = async ({ req: { user } }) => {
7 | if (user) {
8 | const isStaff = isSiteStaff(user?.roles);
9 | if (isStaff) return true;
10 | return {
11 | or: [
12 | {
13 | createdBy: { equals: user.id },
14 | },
15 | {
16 | "site.owner": {
17 | equals: user.id,
18 | },
19 | },
20 | {
21 | "site.admins": {
22 | contains: user.id,
23 | },
24 | },
25 | ],
26 | };
27 | }
28 | // Reject everyone else
29 | return false;
30 | };
31 |
32 | export const canCreateImage: Access = async ({ req: { user }, data }) => {
33 | const isSelf = user?.id === data?.createdBy;
34 | const isStaff = user?.roles?.includes("staff");
35 | return isStaff || isSelf;
36 | };
37 |
--------------------------------------------------------------------------------
/app/utils/useSiteLoaderData.tsx:
--------------------------------------------------------------------------------
1 | import type { SerializeFrom } from "@remix-run/node";
2 | import { useMatches, useRouteLoaderData } from "@remix-run/react";
3 |
4 | import type { loader as rootLoaderType } from "~/root";
5 | import type { loader as layoutLoaderType } from "~/routes/_site+/_layout";
6 |
7 | // export typesafe helpers to get shared loaders from hierachy
8 |
9 | // export a typesafe function to get the root data
10 | export function useRootLoaderData() {
11 | //the ! will tell TS that the type is not nullable (not null or undefined)
12 | return useRouteLoaderData("root")!;
13 | }
14 |
15 | // get site data from layout loader
16 | export function useSiteLoaderData() {
17 | //the ! will tell TS that the type is not nullable (not null or undefined)
18 | // return useRouteLoaderData("routes/_site+/_layout")!
19 |
20 | // more brittle since we're using the layout hierarchy
21 | return (
22 | (useMatches()?.[1]?.data as SerializeFrom) ?? {
23 | site: undefined,
24 | }
25 | );
26 | }
27 |
--------------------------------------------------------------------------------
/app/routes/_site+/c_+/$collectionId_.$entryId/_entry.tsx:
--------------------------------------------------------------------------------
1 | import { json } from "@remix-run/node";
2 | import type { LoaderFunctionArgs, MetaFunction } from "@remix-run/node";
3 |
4 | import { Entry } from "./components/Entry";
5 | import { fetchEntry } from "./utils/fetchEntry.server";
6 |
7 | export async function loader({
8 | context: { payload, user },
9 | params,
10 | request,
11 | }: LoaderFunctionArgs) {
12 | const { entry } = await fetchEntry({
13 | payload,
14 | params,
15 | request,
16 | user,
17 | });
18 | return json({ entry });
19 | }
20 |
21 | export const meta: MetaFunction = ({
22 | matches,
23 | data,
24 | }: {
25 | matches: any;
26 | data: any;
27 | }) => {
28 | const siteName = matches.find(
29 | ({ id }: { id: string }) => id === "routes/_site+/_layout",
30 | )?.data?.site?.name;
31 |
32 | return [
33 | {
34 | title: `${data?.entry.name} | ${data?.entry.collectionName} - ${siteName}`,
35 | },
36 | ];
37 | };
38 |
39 | export default function CollectionEntry() {
40 | return ;
41 | }
42 |
--------------------------------------------------------------------------------
/app/routes/_user+/components/UserMenuLink.tsx:
--------------------------------------------------------------------------------
1 | import { NavLink } from "@remix-run/react";
2 | import clsx from "clsx";
3 |
4 | import { Icon } from "~/components/Icon";
5 | import type { IconName } from "~/components/icons";
6 |
7 | export function UserMenuLink({
8 | text,
9 | icon,
10 | to,
11 | }: {
12 | text: string;
13 | icon: IconName;
14 | to: string;
15 | }) {
16 | return (
17 |
19 | clsx(
20 | isActive
21 | ? "bg-zinc-200/80 dark:bg-dark450 laptop:dark:bg-dark350 font-bold"
22 | : "hover:bg-zinc-100 hover:dark:bg-dark500 laptop:dark:hover:bg-dark400 font-semibold",
23 | "desktop:py-2 desktop:px-3 max-desktop:p-2 laptop:justify-center desktop:justify-start flex items-center gap-3 rounded-lg text-sm",
24 | )
25 | }
26 | to={to}
27 | >
28 |
29 | {text}
30 |
31 | );
32 | }
33 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | (The MIT License)
2 |
3 | Copyright (c) Mana Wiki (https://mana.wiki) info@mana.wiki
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6 |
7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8 |
9 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
10 |
--------------------------------------------------------------------------------
/app/db/custom/CustomImages.ts:
--------------------------------------------------------------------------------
1 | import type { CollectionConfig } from "payload/types";
2 |
3 | import type { User } from "payload/generated-types";
4 |
5 | import { isStaff, isStaffFieldLevel } from "../collections/users/users.access";
6 |
7 | export const CustomImages: CollectionConfig = {
8 | slug: "images",
9 | access: {
10 | read: (): boolean => true, // Everyone can read Images
11 | update: isStaff,
12 | delete: isStaff,
13 | create: isStaff,
14 | },
15 | fields: [
16 | {
17 | name: "id",
18 | type: "text",
19 | },
20 | {
21 | name: "checksum",
22 | type: "text",
23 | },
24 | {
25 | name: "createdBy",
26 | type: "relationship",
27 | relationTo: "users",
28 | maxDepth: 2,
29 | required: true,
30 | defaultValue: ({ user }: { user: User }) => user?.id,
31 | access: {
32 | read: isStaffFieldLevel,
33 | update: isStaffFieldLevel,
34 | },
35 | },
36 | {
37 | name: "site",
38 | type: "text",
39 | },
40 | ],
41 | };
42 |
--------------------------------------------------------------------------------
/app/routes/_auth+/utils/useIsStaffSiteAdminOwner.ts:
--------------------------------------------------------------------------------
1 | import {
2 | useRootLoaderData,
3 | useSiteLoaderData,
4 | } from "~/utils/useSiteLoaderData";
5 |
6 | /**
7 | * Determines if the current user is a staff member, site admin, or owner of the site.
8 | * @returns {boolean} True if the user is a staff member, site admin, or owner; otherwise, false.
9 | */
10 |
11 | export function useIsStaffOrSiteAdminOrStaffOrOwner() {
12 | const { user } = useRootLoaderData();
13 |
14 | const { site } = useSiteLoaderData();
15 |
16 | //always false if not logged in
17 | if (!user || !user?.id) return false;
18 |
19 | // return true if user is Mana Admin
20 | if (user?.roles?.includes("staff")) return true;
21 |
22 | // return true if user is site owner
23 | if (
24 | typeof site?.owner === "string"
25 | ? site?.owner === user.id
26 | : site?.owner?.id === user.id
27 | )
28 | return true;
29 |
30 | // return true if user is site admin
31 | if (site?.admins?.some((e: any) => e.id === user.id)) return true;
32 |
33 | // return false if none of the above
34 | return false;
35 | }
36 |
--------------------------------------------------------------------------------
/app/routes/_site+/settings+/components/SettingsMenuLink.tsx:
--------------------------------------------------------------------------------
1 | import { NavLink } from "@remix-run/react";
2 | import clsx from "clsx";
3 |
4 | import { Icon } from "~/components/Icon";
5 | import type { IconName } from "~/components/icons";
6 |
7 | export function SettingsMenuLink({
8 | text,
9 | to,
10 | icon,
11 | }: {
12 | text: string;
13 | to: string;
14 | icon: IconName;
15 | }) {
16 | return (
17 |
21 | clsx(isActive ? "" : "text-1", "flex items-center relative h-full")
22 | }
23 | >
24 | {({ isActive }) => (
25 |
26 | {isActive && (
27 |
28 | )}
29 |
30 | {text}
31 |
32 | )}
33 |
34 | );
35 | }
36 |
--------------------------------------------------------------------------------
/app/utils/theme.server.ts:
--------------------------------------------------------------------------------
1 | import * as cookie from "cookie";
2 |
3 | const cookieName = "en-theme";
4 | export type Theme = "light" | "dark" | "system";
5 |
6 | export function getTheme(request: Request): Theme | null {
7 | const cookieHeader = request.headers.get("cookie");
8 | const parsed = cookieHeader
9 | ? cookie.parse(cookieHeader)[cookieName]
10 | : "light";
11 | if (parsed === "light" || parsed === "dark") return parsed;
12 | return null;
13 | }
14 |
15 | export function setTheme(theme: Theme | "system", request: Request) {
16 | let { hostname } = new URL(request.url);
17 |
18 | // Remove subdomain
19 | let domain = hostname.split(".").slice(-2).join(".");
20 |
21 | // don't set cookie domain on fly.dev due to Public Suffix List supercookie issue
22 | if (domain === "fly.dev") domain = "";
23 |
24 | return theme === "system"
25 | ? cookie.serialize(cookieName, "", {
26 | path: "/",
27 | maxAge: -1,
28 | domain,
29 | })
30 | : cookie.serialize(cookieName, theme, {
31 | path: "/",
32 | maxAge: 31536000,
33 | domain,
34 | });
35 | }
36 |
--------------------------------------------------------------------------------
/app/db/collections/user-data/user-data.access.ts:
--------------------------------------------------------------------------------
1 | import type { Access } from "payload/types";
2 |
3 | import { isSiteStaff } from "../../access/isSiteStaff";
4 |
5 | //@ts-ignore
6 | export const canReadUserData: Access = async ({ req: { user } }) => {
7 | return {
8 | author: { equals: user.id },
9 | };
10 | };
11 |
12 | export const canCreateUserData: Access = async ({ req: { user } }) => {
13 | return {
14 | author: { equals: user.id },
15 | };
16 | };
17 |
18 | //@ts-ignore
19 | export const canDeleteUserData: Access = async ({ req: { user } }) => {
20 | if (user) {
21 | const isStaff = isSiteStaff(user?.roles);
22 | if (isStaff) return true;
23 | return {
24 | author: { equals: user.id },
25 | };
26 | }
27 | // Reject everyone else
28 | return false;
29 | };
30 |
31 | //@ts-ignore
32 | export const canUpdateUserData: Access = async ({ req: { user } }) => {
33 | if (user) {
34 | const isStaff = isSiteStaff(user?.roles);
35 | if (isStaff) return true;
36 | return {
37 | author: { equals: user.id },
38 | };
39 | }
40 | // Reject everyone else
41 | return false;
42 | };
43 |
--------------------------------------------------------------------------------
/app/routes/_auth+/components/AdminOrStaffOrOwnerOrContributor.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | useRootLoaderData,
3 | useSiteLoaderData,
4 | } from "~/utils/useSiteLoaderData";
5 |
6 | export const AdminOrStaffOrOwnerOrContributor = ({
7 | children,
8 | }: {
9 | children: React.ReactNode;
10 | }) => {
11 | const hasAccess = useIsStaffSiteAdminOwnerContributor();
12 | return hasAccess ? <>{children}> : null;
13 | };
14 |
15 | export function useIsStaffSiteAdminOwnerContributor() {
16 | const { site } = useSiteLoaderData();
17 |
18 | const { user } = useRootLoaderData();
19 |
20 | //always false if not logged in
21 | if (!user || !user?.id) return false;
22 |
23 | if (user?.roles?.includes("staff")) return true;
24 |
25 | // return true if user is site owner
26 | if (
27 | typeof site?.owner === "string"
28 | ? site?.owner === user.id
29 | : site?.owner?.id === user.id
30 | )
31 | return true;
32 |
33 | if (site?.admins?.some((e) => e.id === user.id)) return true;
34 |
35 | if (site?.contributors?.some((e) => e.id === user.id)) return true;
36 |
37 | // return false if none of the above
38 | return false;
39 | }
40 |
--------------------------------------------------------------------------------
/public/manifest.webmanifest:
--------------------------------------------------------------------------------
1 | {
2 | "icons": [
3 | {
4 | "src": "../icons/icon-48.webp",
5 | "type": "image/png",
6 | "sizes": "48x48",
7 | "purpose": "any maskable"
8 | },
9 | {
10 | "src": "../icons/icon-72.webp",
11 | "type": "image/png",
12 | "sizes": "72x72",
13 | "purpose": "any maskable"
14 | },
15 | {
16 | "src": "../icons/icon-96.webp",
17 | "type": "image/png",
18 | "sizes": "96x96",
19 | "purpose": "any maskable"
20 | },
21 | {
22 | "src": "../icons/icon-128.webp",
23 | "type": "image/png",
24 | "sizes": "128x128",
25 | "purpose": "any maskable"
26 | },
27 | {
28 | "src": "../icons/icon-192.webp",
29 | "type": "image/png",
30 | "sizes": "192x192",
31 | "purpose": "any maskable"
32 | },
33 | {
34 | "src": "../icons/icon-256.webp",
35 | "type": "image/png",
36 | "sizes": "256x256",
37 | "purpose": "any maskable"
38 | },
39 | {
40 | "src": "../icons/icon-512.webp",
41 | "type": "image/png",
42 | "sizes": "512x512",
43 | "purpose": "any maskable"
44 | }
45 | ]
46 | }
47 |
--------------------------------------------------------------------------------
/app/routes/_editor+/blocks+/two-column.tsx:
--------------------------------------------------------------------------------
1 | import type { ReactNode } from "react";
2 |
3 | import { useSlate } from "slate-react";
4 |
5 | // eslint-disable-next-line import/no-cycle
6 | import { NestedEditor } from "../core/dnd";
7 | import type { TwoColumnElement } from "../core/types";
8 |
9 | type Props = {
10 | element: TwoColumnElement;
11 | children: ReactNode;
12 | readOnly: boolean;
13 | };
14 |
15 | export function BlockTwoColumn({ element, children, readOnly }: Props) {
16 | const editor = useSlate();
17 |
18 | return (
19 |
23 |
24 |
30 |
31 |
32 |
38 |
39 |
40 | );
41 | }
42 |
--------------------------------------------------------------------------------
/app/routes/_editor+/blocks+/table/src/with-fragments.ts:
--------------------------------------------------------------------------------
1 | import type { Descendant, Editor } from "slate";
2 | import { Node } from "slate";
3 |
4 | import type { WithTableOptions } from "./options";
5 | import { isOfType } from "./utils/is-of-type";
6 |
7 | export function withFragments(
8 | editor: T,
9 | { withFragments }: WithTableOptions,
10 | ): T {
11 | if (!withFragments) {
12 | return editor;
13 | }
14 |
15 | const { getFragment } = editor;
16 |
17 | editor.getFragment = () => {
18 | const newFragment: Descendant[] = [];
19 |
20 | for (const fragment of getFragment()) {
21 | if (!isOfType(editor, "table")(fragment, [])) {
22 | newFragment.push(fragment);
23 | continue;
24 | }
25 |
26 | for (const [node] of Node.nodes(fragment, {
27 | pass: ([node]) => isOfType(editor, "content")(node, []),
28 | })) {
29 | if (isOfType(editor, "content")(node, [])) {
30 | //@ts-ignore
31 | newFragment.push(node);
32 | }
33 | }
34 | }
35 |
36 | return newFragment;
37 | };
38 |
39 | return editor;
40 | }
41 |
--------------------------------------------------------------------------------
/app/routes/_site+/c_+/$collectionId_.$entryId/utils/CollectionSchema.ts:
--------------------------------------------------------------------------------
1 | import { z } from "zod";
2 |
3 | export const CollectionSchema = z.object({
4 | name: z
5 | .string()
6 | .min(1, "Collection name cannot less than 1 characters")
7 | .max(40, "Collection name cannot be more than 40 characters"),
8 | slug: z
9 | .string()
10 | .min(1)
11 | .max(40)
12 | .regex(
13 | new RegExp(/^[a-z0-9_]+((\.-?|-\.?)[a-z0-9_]+)*$/),
14 | "Collection slug contains invalid characters",
15 | ),
16 | siteId: z.string(),
17 | hiddenCollection: z.coerce.boolean(),
18 | customListTemplate: z.coerce.boolean(),
19 | customEntryTemplate: z.coerce.boolean(),
20 | customDatabase: z.coerce.boolean(),
21 | });
22 |
23 | export const CollectionUpdateSchema = z.object({
24 | name: z.string().min(1).max(40),
25 | collectionId: z.string(),
26 | siteId: z.string(),
27 | hiddenCollection: z.coerce.boolean(),
28 | customListTemplate: z.coerce.boolean(),
29 | customEntryTemplate: z.coerce.boolean(),
30 | customDatabase: z.coerce.boolean(),
31 | collectionIcon: z.any().optional(),
32 | collectionIconId: z.string().optional(),
33 | });
34 |
--------------------------------------------------------------------------------
/app/routes/_site+/_components/_datepicker/date-picker/date-button.tsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback, useMemo } from "react";
2 |
3 | type DateButtonProps = {
4 | date: Date;
5 | active: boolean;
6 | selected: boolean;
7 | onClick: (date: Date) => void;
8 | };
9 |
10 | const dateOptions: Intl.DateTimeFormatOptions = {
11 | weekday: "long",
12 | month: "long",
13 | day: "numeric",
14 | year: "numeric",
15 | };
16 |
17 | export function DateButton({
18 | date,
19 | active,
20 | onClick,
21 | selected,
22 | }: DateButtonProps) {
23 | const handleClick = useCallback(() => {
24 | onClick(date);
25 | }, [onClick, date]);
26 |
27 | return (
28 |
41 | );
42 | }
43 |
--------------------------------------------------------------------------------
/app/routes/_editor+/blocks+/table/src/utils/matrix.ts:
--------------------------------------------------------------------------------
1 | import type { Location, NodeEntry } from "slate";
2 | import { Editor, Node, Span } from "slate";
3 |
4 | import { isElement } from "./is-element";
5 | import { isOfType } from "./is-of-type";
6 | import type { CellElement } from "./types";
7 |
8 | export function* matrix(
9 | editor: Editor,
10 | options: { at?: Location | Span; reverse?: boolean } = {},
11 | ): Generator[], undefined> {
12 | const { at, reverse } = options;
13 |
14 | const [table] = Editor.nodes(editor, {
15 | match: isOfType(editor, "table"),
16 | at,
17 | });
18 |
19 | if (!table) {
20 | return;
21 | }
22 |
23 | const [, tablePath] = table;
24 |
25 | for (const [, rowPath] of Editor.nodes(editor, {
26 | at: Span.isSpan(at) ? at : tablePath,
27 | match: isOfType(editor, "tr"),
28 | reverse,
29 | })) {
30 | const cells: NodeEntry[] = [];
31 |
32 | for (const [cell, path] of Node.children(editor, rowPath, { reverse })) {
33 | if (isElement(cell)) {
34 | cells.push([cell, path]);
35 | }
36 | }
37 |
38 | yield cells;
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/app/db/collections/index.ts:
--------------------------------------------------------------------------------
1 | import { Collections } from "./collections/collections-config";
2 | import { Comments } from "./comments/comments-config";
3 | import { ContentEmbeds } from "./content-embeds/config";
4 | import { CustomPages } from "./custom-pages/custom-pages-config";
5 | import { Entries } from "./entries/entries-config";
6 | import { HomeContents } from "./home-contents/config";
7 | import { Images } from "./images/images.config";
8 | import { PostContents } from "./post-contents/post-contents-config";
9 | import { PostTags } from "./post-tags/post-tags-config";
10 | import { Posts } from "./posts/posts-config";
11 | import { SiteApplications } from "./site-applications/site-applications-config";
12 | import { Sites } from "./sites/site-config";
13 | import { Updates } from "./updates/config";
14 | import { UserData } from "./user-data/user-data.config";
15 | import { Users } from "./users/users.config";
16 |
17 | export const collections = [
18 | Sites,
19 | Images,
20 | Users,
21 | Posts,
22 | Collections,
23 | Entries,
24 | CustomPages,
25 | Updates,
26 | ContentEmbeds,
27 | HomeContents,
28 | PostTags,
29 | Comments,
30 | PostContents,
31 | SiteApplications,
32 | UserData,
33 | ];
34 |
--------------------------------------------------------------------------------
/app/routes/_editor+/blocks+/table/src/normalization/normalize-content.ts:
--------------------------------------------------------------------------------
1 | import type { Editor } from "slate";
2 | import { Node, Transforms } from "slate";
3 |
4 | import type { WithTableOptions } from "../options";
5 | import { isElement } from "../utils/is-element";
6 |
7 | /**
8 | * Will normalize the `content` node. It will remove
9 | * table-related elements and unwrap their children.
10 | */
11 | export function normalizeContent(
12 | editor: T,
13 | { blocks }: WithTableOptions,
14 | ): T {
15 | const { table, thead, tbody, tfoot, tr, th, td, content } = blocks;
16 | const FORBIDDEN = [table, thead, tbody, tfoot, tr, th, td, content];
17 |
18 | const { normalizeNode } = editor;
19 |
20 | editor.normalizeNode = (entry, options) => {
21 | const [node, path] = entry;
22 | if (isElement(node) && node.type === content) {
23 | for (const [child, childPath] of Node.children(editor, path)) {
24 | if (isElement(child) && FORBIDDEN.includes(child.type)) {
25 | return Transforms.unwrapNodes(editor, { at: childPath });
26 | }
27 | }
28 | }
29 |
30 | normalizeNode(entry, options);
31 | };
32 |
33 | return editor;
34 | }
35 |
--------------------------------------------------------------------------------
/app/routes/_site+/c_+/$collectionId_.$entryId/utils/_entryTypes.ts:
--------------------------------------------------------------------------------
1 | import type { Params } from "@remix-run/react";
2 | import type { Payload } from "payload";
3 |
4 | import type { RemixRequestContext } from "remix.env";
5 |
6 | export type EntryType = {
7 | siteId: string;
8 | id: string;
9 | name?: string;
10 | slug?: string;
11 | icon?: {
12 | url?: string;
13 | };
14 | };
15 |
16 | export interface EntryAllData extends EntryType {
17 | embeddedContent: JSON;
18 | }
19 |
20 | // https://stackoverflow.com/questions/40510611/typescript-interface-require-one-of-two-properties-to-exist
21 | type RequireOnlyOneOptional = Pick<
22 | T,
23 | Exclude
24 | > &
25 | {
26 | [K in Keys]-?: Pick & Partial, undefined>>;
27 | }[Keys];
28 |
29 | export type RestOrGraphql = RequireOnlyOneOptional<
30 | EntryFetchType,
31 | "rest" | "gql"
32 | >;
33 |
34 | interface EntryFetchType {
35 | payload: Payload;
36 | params: Params;
37 | request: Request;
38 | user: RemixRequestContext["user"];
39 | rest?: {
40 | depth?: number;
41 | };
42 | gql?: {
43 | query: string;
44 | variables?: {};
45 | };
46 | }
47 |
--------------------------------------------------------------------------------
/app/routes/_site+/_components/_datepicker/date-picker/month-picker.tsx:
--------------------------------------------------------------------------------
1 | import React, { useMemo } from "react";
2 |
3 | import type { OptionType } from "../components/select";
4 | import CustomSelect from "../components/select";
5 |
6 | export type MonthPickerProps = {
7 | value: string;
8 | onChange: (year: string) => void;
9 | disabled: boolean;
10 | };
11 |
12 | const months = [
13 | "January",
14 | "February",
15 | "March",
16 | "April",
17 | "May",
18 | "June",
19 | "July",
20 | "August",
21 | "September",
22 | "October",
23 | "November",
24 | "December",
25 | ];
26 |
27 | export function MonthPicker({ value, onChange, disabled }: MonthPickerProps) {
28 | const options = useMemo(
29 | () =>
30 | months.map(
31 | (m) =>
32 | ({
33 | value: m,
34 | label: m,
35 | disabled: false,
36 | } as OptionType)
37 | ),
38 | []
39 | );
40 |
41 | return (
42 |
49 | );
50 | }
51 |
--------------------------------------------------------------------------------
/app/_custom/collection-config-example.ts:
--------------------------------------------------------------------------------
1 | import type { CollectionConfig } from "payload/types";
2 |
3 | import {
4 | afterChangeSearchSyncHook,
5 | afterDeleteSearchSyncHook,
6 | } from "./hooks/search-hooks";
7 | import { isStaff } from "../db/collections/users/users.access";
8 |
9 | export const ExampleConfig: CollectionConfig = {
10 | slug: "example",
11 | labels: { singular: "Example", plural: "Examples" },
12 | access: {
13 | read: () => true,
14 | create: isStaff,
15 | delete: isStaff,
16 | update: isStaff,
17 | },
18 | admin: {
19 | useAsTitle: "name",
20 | defaultColumns: ["name"],
21 | },
22 | hooks: {
23 | afterDelete: [afterDeleteSearchSyncHook],
24 | afterChange: [afterChangeSearchSyncHook],
25 | },
26 | fields: [
27 | {
28 | name: "id",
29 | type: "text",
30 | },
31 | {
32 | name: "name",
33 | type: "text",
34 | required: true,
35 | },
36 | {
37 | name: "slug",
38 | type: "text",
39 | required: true,
40 | },
41 | {
42 | name: "icon",
43 | type: "upload",
44 | relationTo: "images",
45 | required: true,
46 | },
47 | ],
48 | };
49 |
--------------------------------------------------------------------------------
/custom.server.ts:
--------------------------------------------------------------------------------
1 | import compression from "compression";
2 | import express from "express";
3 | import morgan from "morgan";
4 | import payload from "payload";
5 | import invariant from "tiny-invariant";
6 |
7 | require("dotenv").config();
8 |
9 | //Start custom database (payload instance only)
10 | async function startCustom() {
11 | const app = express();
12 |
13 | // Redirect all traffic at root to admin UI
14 | app.get("/", function (_, res) {
15 | res.redirect("/admin");
16 | });
17 |
18 | app.get("/robots.txt", function (_, res) {
19 | res.type("text/plain");
20 | res.send("User-agent: *\nDisallow: /");
21 | });
22 |
23 | invariant(process.env.PAYLOADCMS_SECRET, "PAYLOADCMS_SECRET is required");
24 | await payload.init({
25 | secret: process.env.PAYLOADCMS_SECRET,
26 | express: app,
27 | onInit: () => {
28 | payload.logger.info(`Payload Admin URL: ${payload.getAdminURL()}`);
29 | },
30 | });
31 |
32 | app.use(compression());
33 |
34 | app.disable("x-powered-by");
35 |
36 | app.use(morgan("tiny"));
37 |
38 | const port = 4000;
39 |
40 | app.listen(port, () => {
41 | console.log(`Custom DB listening on port http://localhost:${port}`);
42 | });
43 | }
44 | startCustom();
45 |
--------------------------------------------------------------------------------
/app/routes/_site+/c_+/$collectionId/utils/listMeta.ts:
--------------------------------------------------------------------------------
1 | import type { MetaFunction } from "@remix-run/react";
2 |
3 | import { getMeta } from "~/components/getMeta";
4 |
5 | export const listMeta: MetaFunction = ({ matches }: { matches: any }) => {
6 | const site = matches?.[1]?.data?.site;
7 |
8 | const collectionId = matches?.[2]?.pathname?.split("/")[2];
9 |
10 | const collection = site?.collections?.find(
11 | (collection: any) => collection.slug === collectionId,
12 | );
13 |
14 | const title = `${collection?.name} | ${site?.name}`;
15 |
16 | const icon = collection?.icon?.url;
17 |
18 | const sections = collection?.sections
19 | ?.map((section: any) => section?.name)
20 | ?.slice(1);
21 |
22 | const description =
23 | `Browse ${collection?.name} ` +
24 | sections?.join(", ") +
25 | ` on ${site?.name}. `;
26 |
27 | const siteDomain = matches?.[1]?.data?.site?.domain;
28 | const siteSlug = matches?.[1]?.data?.site?.slug;
29 |
30 | const canonicalURL = `https://${
31 | siteDomain ?? `${siteSlug}.mana.wiki`
32 | }/c/${collection?.slug}`;
33 |
34 | return getMeta({
35 | title,
36 | description,
37 | icon,
38 | siteName: site?.name,
39 | canonicalURL,
40 | });
41 | };
42 |
--------------------------------------------------------------------------------
/app/routes/_site+/settings+/utils/fetchApplicationData.tsx:
--------------------------------------------------------------------------------
1 | import type { Payload } from "payload";
2 |
3 | import type { RemixRequestContext } from "remix.env";
4 |
5 | export async function fetchApplicationData({
6 | payload,
7 | siteSlug,
8 | user,
9 | }: {
10 | payload: Payload;
11 | siteSlug: string | undefined;
12 | user: RemixRequestContext["user"];
13 | }) {
14 | const { docs } = await payload.find({
15 | collection: "siteApplications",
16 | where: {
17 | "site.slug": {
18 | equals: siteSlug,
19 | },
20 | },
21 | overrideAccess: false,
22 | user,
23 | depth: 2,
24 | sort: "-createdAt",
25 | });
26 | const applications = docs.map((doc) => ({
27 | id: doc.id,
28 | createdBy: {
29 | id: doc.createdBy.id,
30 | username: doc.createdBy.username,
31 | avatar: {
32 | url: doc.createdBy.avatar?.url,
33 | },
34 | },
35 | discordUsername: doc?.discordUsername,
36 | createdAt: doc.createdBy.createdAt,
37 | reviewMessage: doc.reviewMessage,
38 | status: doc.status,
39 | primaryDetails: doc.primaryDetails,
40 | additionalNotes: doc.additionalNotes,
41 | }));
42 | return applications;
43 | }
44 |
--------------------------------------------------------------------------------
/app/routes/_auth+/check-email.tsx:
--------------------------------------------------------------------------------
1 | import type { LoaderFunctionArgs, MetaFunction } from "@remix-run/node";
2 | import { redirect } from "@remix-run/node";
3 |
4 | import { Icon } from "~/components/Icon";
5 |
6 | export async function loader({
7 | context: { user },
8 | request,
9 | }: LoaderFunctionArgs) {
10 | if (user) {
11 | return redirect("/");
12 | }
13 | return null;
14 | }
15 |
16 | //TODO Fix server side translation
17 | export const meta: MetaFunction = () => {
18 | return [
19 | {
20 | title: "Check Email - Mana",
21 | },
22 | { name: "viewport", content: "width=device-width, initial-scale=1" },
23 | ];
24 | };
25 |
26 | export default function CheckEmail() {
27 | return (
28 |
32 |
36 |
37 |
38 | Check your email to verify your account
39 |
40 | );
41 | }
42 |
--------------------------------------------------------------------------------
/app/routes/_site+/_components/_datepicker/date-picker/year-picker.tsx:
--------------------------------------------------------------------------------
1 | import React, { useMemo } from "react";
2 |
3 | import type { OptionType } from "../components/select";
4 | import CustomSelect from "../components/select";
5 |
6 | export type YearPickerProps = {
7 | fromYear: number;
8 | toYear: number;
9 | value: number;
10 | onChange: (year: number) => void;
11 | disabled: boolean;
12 | };
13 |
14 | function* generateRange(from: number, to: number, step: number) {
15 | for (let i = from; i <= to; i += step) {
16 | yield i;
17 | }
18 | }
19 |
20 | export function YearPicker({
21 | fromYear,
22 | toYear,
23 | value,
24 | onChange,
25 | disabled,
26 | }: YearPickerProps) {
27 | const options = useMemo(
28 | () =>
29 | Array.from(generateRange(fromYear, toYear, 1)).map(
30 | (v) =>
31 | ({
32 | value: v,
33 | label: v.toString(),
34 | disabled: false,
35 | } as OptionType)
36 | ),
37 | [fromYear, toYear]
38 | );
39 |
40 | return (
41 |
47 | );
48 | }
49 |
--------------------------------------------------------------------------------
/app/routes/_site+/posts+/components/PostListHeader.tsx:
--------------------------------------------------------------------------------
1 | import { Form } from "@remix-run/react";
2 |
3 | import { Icon } from "~/components/Icon";
4 | import { AdminOrStaffOrOwnerOrContributor } from "~/routes/_auth+/components/AdminOrStaffOrOwnerOrContributor";
5 |
6 | export function PostListHeader() {
7 | return (
8 |
9 |
Posts
10 |
11 |
12 |
24 |
25 |
26 | );
27 | }
28 |
--------------------------------------------------------------------------------