├── src ├── index.css ├── api │ ├── _db │ │ ├── mock-sqlite3.ts │ │ ├── index.ts │ │ ├── store.d.ts │ │ ├── datastore.ts │ │ └── sqlite3.ts │ ├── subscriber │ │ ├── index.patch.ts │ │ ├── index.patch.spec.ts │ │ ├── index.delete.ts │ │ ├── index.post.spec.ts │ │ ├── index.post.ts │ │ └── upload.post.ts │ ├── _utils │ │ ├── index.ts │ │ ├── string.ts │ │ ├── test_utils.ts │ │ └── http.ts │ ├── subscribers.get.ts │ ├── session.post.ts │ ├── template.delete.ts │ ├── _mailer │ │ └── smtp.ts │ ├── login.post.ts │ ├── templates.get.ts │ ├── template.post.ts │ ├── subscribers.get.spec.ts │ ├── template.patch.ts │ ├── mail.post.ts │ └── _assets │ │ └── default_template.ts ├── vite-env.d.ts ├── components │ ├── Async │ │ ├── index.ts │ │ └── Async.tsx │ ├── Layout │ │ ├── index.ts │ │ └── Layout.tsx │ ├── Prompt │ │ ├── index.ts │ │ └── Prompt.tsx │ ├── CrudMenu │ │ ├── index.ts │ │ └── CrudMenu.tsx │ ├── UserDialog │ │ ├── index.ts │ │ └── UserDialog.tsx │ ├── UploadDialog │ │ ├── index.ts │ │ └── UploadDialog.tsx │ ├── TemplateDialog │ │ ├── index.ts │ │ └── TemplateDialog.tsx │ ├── TemplatePreview │ │ ├── index.ts │ │ └── TemplatePreview.tsx │ └── TemplatePreviewDrawer │ │ ├── index.ts │ │ └── TemplatePreviewDrawer.tsx ├── utils │ ├── index.ts │ └── fetcher.ts ├── types │ ├── fetch-data.d.ts │ ├── user.d.ts │ ├── mui.d.ts │ ├── template.d.ts │ └── seo.d.ts ├── context.ts ├── prerender.ts ├── entry-client.tsx ├── pages │ ├── templates.actions.ts │ ├── subscribers.actions.ts │ ├── index.actions.ts │ ├── login.tsx │ ├── templates.tsx │ ├── subscribers.tsx │ └── index.tsx ├── routes.tsx ├── App.tsx ├── mui-theme.tsx ├── entry-server.tsx ├── assets │ └── react.svg └── vite-server.ts ├── babel.config.cjs ├── redirects.json ├── jest.config.cjs ├── tsconfig.node.json ├── Dockerfile ├── index.html ├── .gitignore ├── .github └── dependabot.yml ├── tsconfig.json ├── vite.config.ts ├── LICENSE ├── public ├── logo.svg └── stormkit-logo.svg ├── vite.config.ssr.ts ├── package.json ├── vite.config.api.ts └── README.md /src/index.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/api/_db/mock-sqlite3.ts: -------------------------------------------------------------------------------- 1 | export default () => {}; 2 | -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/components/Async/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./Async"; 2 | -------------------------------------------------------------------------------- /babel.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { presets: ["@babel/preset-env"] }; 2 | -------------------------------------------------------------------------------- /src/components/Layout/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./Layout"; 2 | -------------------------------------------------------------------------------- /src/components/Prompt/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./Prompt"; 2 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export { default as fetcher } from "./fetcher"; 2 | -------------------------------------------------------------------------------- /src/api/subscriber/index.patch.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./index.post"; 2 | -------------------------------------------------------------------------------- /src/components/CrudMenu/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./CrudMenu"; 2 | -------------------------------------------------------------------------------- /src/components/UserDialog/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./UserDialog"; 2 | -------------------------------------------------------------------------------- /src/components/UploadDialog/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./UploadDialog"; 2 | -------------------------------------------------------------------------------- /src/components/TemplateDialog/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./TemplateDialog"; 2 | -------------------------------------------------------------------------------- /src/components/TemplatePreview/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./TemplatePreview"; 2 | -------------------------------------------------------------------------------- /src/components/TemplatePreviewDrawer/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./TemplatePreviewDrawer"; 2 | -------------------------------------------------------------------------------- /redirects.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "from": "/*", 4 | "to": "/index.html", 5 | "assets": false 6 | } 7 | ] 8 | -------------------------------------------------------------------------------- /src/api/_utils/index.ts: -------------------------------------------------------------------------------- 1 | export { default as httpUtils } from "./http"; 2 | export * as stringUtils from "./string"; 3 | -------------------------------------------------------------------------------- /src/types/fetch-data.d.ts: -------------------------------------------------------------------------------- 1 | declare type FetchDataFunc = ( 2 | match: Record 3 | ) => Promise<{ head: SEO; context: any }>; 4 | -------------------------------------------------------------------------------- /jest.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: "ts-jest", 3 | transform: { 4 | "^.+\\.(ts|tsx)?$": "ts-jest", 5 | "^.+\\.(js|jsx)$": "babel-jest", 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /src/api/_utils/string.ts: -------------------------------------------------------------------------------- 1 | export function isValidEmail(email?: string): boolean { 2 | if (!email) { 3 | return false; 4 | } 5 | 6 | return email.includes("@") && email.includes("."); 7 | } 8 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "allowSyntheticDefaultImports": true 7 | }, 8 | "include": ["vite.config.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /src/types/user.d.ts: -------------------------------------------------------------------------------- 1 | declare interface User { 2 | recordId?: string; 3 | email: string; 4 | firstName?: string; 5 | lastName?: string; 6 | isUnsubscribed: boolean; 7 | attributes?: Record; 8 | createdAt: number; 9 | } 10 | -------------------------------------------------------------------------------- /src/types/mui.d.ts: -------------------------------------------------------------------------------- 1 | import "@mui/material/styles"; 2 | 3 | declare module "@mui/material/styles" { 4 | interface Palette { 5 | container: Palette["primary"]; 6 | } 7 | 8 | interface PaletteOptions { 9 | container: PaletteOptions["primary"]; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/types/template.d.ts: -------------------------------------------------------------------------------- 1 | declare interface Template { 2 | recordId?: string; 3 | name: string; 4 | html: string; 5 | description?: string; 6 | defaultSubject?: string; 7 | isDefault?: boolean; 8 | variables?: string[]; // list of variables available for this template 9 | } 10 | -------------------------------------------------------------------------------- /src/context.ts: -------------------------------------------------------------------------------- 1 | import { createContext } from "react"; 2 | 3 | interface Context { 4 | setIsLoggedIn?: (v: boolean) => void; 5 | accessToken: string | null; 6 | isLoggedIn: boolean; 7 | } 8 | 9 | export default createContext({ 10 | accessToken: null, 11 | isLoggedIn: false, 12 | }); 13 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:18-alpine AS builder 2 | WORKDIR /app 3 | COPY . . 4 | RUN npm install 5 | 6 | FROM gcr.io/distroless/nodejs18-debian11 AS mailer 7 | COPY --from=builder /app /app 8 | WORKDIR /app 9 | 10 | ENV NODE_NO_WARNINGS=1 11 | 12 | EXPOSE 5173 13 | 14 | CMD [ "--loader", "ts-node/esm", "/app/src/vite-server.ts" ] -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/prerender.ts: -------------------------------------------------------------------------------- 1 | // This file contains the routes to be prerendered. 2 | // The exported array is a static one, but could be easily 3 | // transformed into a dynamic list. 4 | // Check import.meta.glob provided by Vite to scan folders dynamically 5 | // and build the routes to be prerendered. 6 | 7 | export default ["/", "/my-page", "/my-other-page"]; 8 | -------------------------------------------------------------------------------- /src/api/subscribers.get.ts: -------------------------------------------------------------------------------- 1 | import http from "node:http"; 2 | import { httpUtils as hu } from "./_utils"; 3 | import db from "./_db"; 4 | 5 | export default hu.app( 6 | async (_: http.IncomingMessage, res: http.ServerResponse) => { 7 | const store = await db(); 8 | 9 | hu.send(res, { 10 | users: await store.users.subscribers(), 11 | }); 12 | } 13 | ); 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | .stormkit 15 | 16 | # Editor directories and files 17 | .vscode/* 18 | !.vscode/extensions.json 19 | .idea 20 | .DS_Store 21 | *.suo 22 | *.ntvs* 23 | *.njsproj 24 | *.sln 25 | *.sw? 26 | 27 | .env 28 | -------------------------------------------------------------------------------- /src/api/_db/index.ts: -------------------------------------------------------------------------------- 1 | import type { Store } from "./store.d"; 2 | 3 | export default async function (): Promise { 4 | // If it's Stormkit, return the datastore 5 | if (process.env.STORMKIT === "true") { 6 | const { default: datastore } = await import("./datastore"); 7 | return datastore; 8 | } 9 | 10 | // Otherwise fallback to sqlite3 11 | const { default: sqlite } = await import("./sqlite3"); 12 | return sqlite.setup(); 13 | } 14 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "npm" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /src/api/_db/store.d.ts: -------------------------------------------------------------------------------- 1 | export interface Store { 2 | templates: { 3 | list(): Promise; 4 | byId(id: string): Promise