├── 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 | 15 | 16 | -------------------------------------------------------------------------------- /public/icons/slash.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | -------------------------------------------------------------------------------- /public/icons/check.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | -------------------------------------------------------------------------------- /public/icons/chevron-down.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | -------------------------------------------------------------------------------- /public/icons/chevron-up.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | -------------------------------------------------------------------------------- /public/icons/chevron-left.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | -------------------------------------------------------------------------------- /public/icons/chevron-right.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | -------------------------------------------------------------------------------- /public/icons/loader-2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | -------------------------------------------------------------------------------- /public/icons/moon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | -------------------------------------------------------------------------------- /public/icons/plus.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /public/icons/x.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /public/icons/arrow-up.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /public/icons/brackets.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /public/icons/send.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /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 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /public/icons/arrow-left.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /public/icons/arrow-right.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /public/icons/message-circle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | -------------------------------------------------------------------------------- /public/icons/move-right.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /public/icons/search.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /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 | 15 | 16 | -------------------------------------------------------------------------------- /public/icons/chevrons-up.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /public/icons/line-chart.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /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 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /public/icons/chevrons-left.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /public/icons/chevrons-right.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /public/icons/code.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /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 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /public/icons/chevrons-up-down.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /public/icons/clock-9.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /public/icons/rectangle-horizontal.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | -------------------------------------------------------------------------------- /public/icons/reply.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /public/icons/text.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /public/icons/columns-2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /public/icons/triangle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | -------------------------------------------------------------------------------- /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 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /public/icons/list-filter.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /public/icons/pen-line.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /public/icons/pencil.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /public/icons/underline.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /public/icons/user.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /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 |
16 | 17 |
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 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /public/icons/lock.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /public/icons/pie-chart.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /public/icons/plus-circle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /public/icons/rows.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /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 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /public/icons/corner-down-right.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /public/icons/credit-card.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /public/icons/home.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /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 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /public/icons/key.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /public/icons/mail.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /public/icons/star.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | -------------------------------------------------------------------------------- /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 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /public/icons/square-plus.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /public/icons/dollar-sign.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /public/icons/wallet-2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /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 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /public/icons/more-vertical.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /public/icons/arrow-up-down.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /public/icons/copy.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /public/icons/database.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /public/icons/folder.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | -------------------------------------------------------------------------------- /public/icons/globe.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /public/icons/italic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /public/icons/link-2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /public/icons/more-horizontal.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /public/icons/trash.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /public/icons/type.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /public/icons/users-2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /public/icons/settings-2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /public/icons/sort.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /public/icons/table.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /public/icons/archive.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /public/icons/heading-2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /public/icons/list-plus.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /public/icons/strikethrough.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /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 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /public/icons/layout.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /public/icons/log-out.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /public/icons/pen-square.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /public/icons/upload.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /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 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /public/icons/eye.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /public/icons/link.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /public/icons/square-pen.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /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 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /public/icons/image.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /public/icons/message-square-plus.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /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 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /public/icons/zap.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | -------------------------------------------------------------------------------- /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 | 15 | 16 | -------------------------------------------------------------------------------- /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 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /public/icons/bolt.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /public/icons/copy-check.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /public/icons/external-link.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /public/icons/hash.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /public/icons/link-2-off.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /public/icons/list-tree.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /public/icons/shield.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | -------------------------------------------------------------------------------- /public/icons/flame.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | -------------------------------------------------------------------------------- /public/icons/expand.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /public/icons/file-code-2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /public/icons/layout-panel-top.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /public/icons/table-cells-merge.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /public/icons/users.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /public/icons/sword.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /public/icons/calendar.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /app/components/DotLoader.tsx: -------------------------------------------------------------------------------- 1 | export const DotLoader = () => { 2 | return ( 3 |
4 |
5 |
6 |
7 |
8 |
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 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /public/icons/refresh-ccw.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /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 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /public/icons/piggy-bank.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /.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 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /public/icons/server.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /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 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /public/icons/archive-restore.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /public/icons/binary.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /public/icons/component.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /public/icons/image-minus.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /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 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /public/icons/copy-x.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /public/icons/layout-grid.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /public/icons/layout-list.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /public/icons/trash-2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /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 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /public/icons/hourglass.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /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 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /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 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /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 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /public/icons/image-plus.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /public/icons/list.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /public/icons/calendar-clock.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /public/icons/hard-drive.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /public/icons/eye-off.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /.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 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /.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 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /public/icons/sun.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /public/icons/calendar-plus.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /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 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /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 | Post Banner 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 |
15 |
19 | 20 |
21 |
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
{/*
Ad Payouts
*/}
; 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 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /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 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /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 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /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 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /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 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /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 && Crop preview}` 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 |
13 | 23 |
24 |
25 |
26 | ); 27 | } 28 | --------------------------------------------------------------------------------