diff --git a/.gitattributes b/.gitattributes index 2a118614..c41eb537 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1 +1 @@ -*.wasp linguist-language=TypeScript \ No newline at end of file +*.wasp linguist-language=TypeScript diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 8d27fa23..456cc3d9 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,6 +1,6 @@ # These are supported funding model platforms -github: [ wasp-lang ] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] +github: [wasp-lang] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] patreon: # Replace with a single Patreon username open_collective: # Replace with a single Open Collective username ko_fi: # Replace with a single Ko-fi username diff --git a/.github/workflows/blog-deployment.yml b/.github/workflows/blog-deployment.yml index 7746f1ae..cef9a839 100644 --- a/.github/workflows/blog-deployment.yml +++ b/.github/workflows/blog-deployment.yml @@ -5,12 +5,12 @@ on: branches: - main paths: - - 'opensaas-sh/blog/**' + - "opensaas-sh/blog/**" pull_request: branches: - main paths: - - 'opensaas-sh/blog/**' + - "opensaas-sh/blog/**" jobs: build: @@ -23,7 +23,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v3 with: - node-version: 'lts/*' + node-version: "lts/*" - name: Install dependencies working-directory: ./opensaas-sh/blog @@ -47,7 +47,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v3 with: - node-version: 'lts/*' + node-version: "lts/*" - name: Install dependencies working-directory: ./opensaas-sh/blog @@ -64,10 +64,10 @@ jobs: - name: Deploy to Netlify uses: nwtgck/actions-netlify@v2 with: - publish-dir: './opensaas-sh/blog/dist' + publish-dir: "./opensaas-sh/blog/dist" production-branch: main github-token: ${{ secrets.GITHUB_TOKEN }} - deploy-message: 'Deploy from GitHub Actions' + deploy-message: "Deploy from GitHub Actions" env: NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }} diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index cb587575..149a04d0 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -24,7 +24,7 @@ jobs: id: setup-node uses: actions/setup-node@v4 with: - node-version: '22' + node-version: "22" - name: Docker setup uses: docker/setup-buildx-action@v3 @@ -56,7 +56,7 @@ jobs: run: | ./tools/patch.sh - - name: '[e2e-tests] Install Node.js dependencies for Playwright tests' + - name: "[e2e-tests] Install Node.js dependencies for Playwright tests" if: steps.cache-e2e-tests.outputs.cache-hit != 'true' working-directory: ./template run: | @@ -78,14 +78,14 @@ jobs: path: ~/.cache/ms-playwright key: playwright-browsers-${{ env.PLAYWRIGHT_VERSION }}-${{ runner.os }} - - name: '[e2e-tests] Set up Playwright' + - name: "[e2e-tests] Set up Playwright" if: steps.cache-playwright-browsers.outputs.cache-hit != 'true' working-directory: ./template run: | cd e2e-tests npx playwright install --with-deps - - name: '[e2e-tests] Install Stripe CLI' + - name: "[e2e-tests] Install Stripe CLI" run: | curl -s https://packages.stripe.dev/api/security/keypair/stripe-cli-gpg/public | gpg --dearmor | sudo tee /usr/share/keyrings/stripe.gpg echo "deb [signed-by=/usr/share/keyrings/stripe.gpg] https://packages.stripe.dev/stripe-cli-debian-local stable main" | sudo tee -a /etc/apt/sources.list.d/stripe.list @@ -95,13 +95,13 @@ jobs: # For Stripe webhooks to work in development, we need to run the Stripe CLI to listen for webhook events. # The Stripe CLI will receive the webhook events from Stripe test payments and # forward them to our local server so that we can test the payment flow in our e2e tests. - - name: '[e2e-tests] Run Stripe CLI to listen for webhooks' + - name: "[e2e-tests] Run Stripe CLI to listen for webhooks" env: STRIPE_DEVICE_NAME: ${{ secrets.STRIPE_DEVICE_NAME }} run: | stripe listen --api-key ${{ secrets.STRIPE_KEY }} --forward-to localhost:3001/payments-webhook & - - name: '[e2e-tests] Run Playwright tests' + - name: "[e2e-tests] Run Playwright tests" env: # The e2e tests are testing parts of the app that need certain env vars, so we need to access them here. # These secretes can be set in your GitHub repo settings, e.g. https://github.com///settings/secrets/actions diff --git a/.prettierignore b/.prettierignore index e678cbe5..e7a84b38 100644 --- a/.prettierignore +++ b/.prettierignore @@ -7,7 +7,7 @@ app_diff/ # Formatting `.md` and `.mdx` can introduce logical changes. **/blog/**/*.md -**/blog/**/*.mdx +**/blog/**/*.mdx # Ignore minified JS files in the public folder. **/public/**/*.js diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 928608e8..d82128c9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,6 +1,7 @@ Thanks so much for considering contributing to Open SaaS πŸ™ ## Considerations before Contributing + Check if there is a GitHub issue already for the thing you would like to work on. If there is no issue yet, create a new one. Let us know, in the issue, that you would like to work on it and how you plan to approach it. @@ -15,6 +16,7 @@ Repo is divided into two main parts: [template](/template) dir and [opensaas-sh] `opensaas-sh` is the app deployed to https://opensaas.sh , and is actually made with open saas! It contains a demo app and open saas docs. We keep it updated as we work on the template. ## How to Contribute + 1. Make sure you understand the basics of how open-saas works (check out [docs](https://docs.opensaas.sh)). 2. Check out this repo (`main` branch) and make sure you are able to get the app in [template/app/](/template/app) running (to set it up, follow the same steps as for running a new open-saas app, as explained in the open-saas docs). 3. Create a new git branch for your work (aka feature branch) and do your changes on it. diff --git a/README.md b/README.md index 8016c2d5..8f6ca1a7 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,10 @@ ## Welcome to your new SaaS App! πŸŽ‰ + Open SaaS - The open-source SaaS boilerplate with superpowers! | Product Hunt https://github.com/user-attachments/assets/3856276b-23e9-455e-a564-b5f26f4f0e98 -You've decided to build a SaaS app with the Open SaaS template. Great choice! +You've decided to build a SaaS app with the Open SaaS template. Great choice! This template is: @@ -38,6 +39,7 @@ Because we're using Wasp as the full-stack framework, we can leverage a lot of i - πŸš€ [One-command Deploy](https://wasp.sh/docs/advanced/deployment/overview) - Easily deploy your DB, Server, & Client with one commaned to [Railway](https://railway.app) or [Fly.io](https://fly.io) via the CLI. Or deploy manually to any other hosting serivce of your choice. You also get access to Wasp's diverse, helpful community if you get stuck or need help. + - 🀝 [Wasp Discord](https://discord.gg/aCamt5wCpS) ## Getting Started @@ -45,6 +47,7 @@ You also get access to Wasp's diverse, helpful community if you get stuck or nee ### Simple Instructions First, to install the latest version of [Wasp](https://wasp.sh/) on macOS, Linux, or Windows with WSL, run the following command: + ```bash curl -sSL https://get.wasp.sh/installer.sh | sh ``` @@ -66,6 +69,7 @@ We've documented everything in great detail, including installation instructions ## Getting Help & Providing Feedback There are two ways to get help or provide feedback (and we try to always respond quickly!): + 1. [Open an issue](https://github.com/wasp-lang/open-saas/issues) 2. [Wasp Discord](https://discord.gg/aCamt5wCpS) -- please direct questions to the #πŸ™‹questions forum channel @@ -74,4 +78,3 @@ There are two ways to get help or provide feedback (and we try to always respond Note that we've tried to get as many of the core features of a SaaS app into this template as possible, but there still might be some missing features or functionality. We could always use some help tying up loose ends: contributions are welcome! Check out [CONTRIBUTING.md](/CONTRIBUTING.md) for more details. - diff --git a/opensaas-sh/blog/.gitignore b/opensaas-sh/blog/.gitignore index 0b24e723..eed36a42 100644 --- a/opensaas-sh/blog/.gitignore +++ b/opensaas-sh/blog/.gitignore @@ -12,7 +12,6 @@ yarn-debug.log* yarn-error.log* pnpm-debug.log* - # environment variables .env .env.production diff --git a/opensaas-sh/blog/astro.config.mjs b/opensaas-sh/blog/astro.config.mjs index 9729e487..b404b1f9 100644 --- a/opensaas-sh/blog/astro.config.mjs +++ b/opensaas-sh/blog/astro.config.mjs @@ -1,100 +1,102 @@ -import { defineConfig } from 'astro/config'; -import starlight from '@astrojs/starlight'; -import starlightBlog from 'starlight-blog'; +import starlight from "@astrojs/starlight"; +import { defineConfig } from "astro/config"; +import starlightBlog from "starlight-blog"; -import tailwind from '@astrojs/tailwind'; +import tailwind from "@astrojs/tailwind"; // https://astro.build/config export default defineConfig({ - site: 'https://docs.opensaas.sh', - trailingSlash: 'always', + site: "https://docs.opensaas.sh", + trailingSlash: "always", integrations: [ starlight({ - title: 'OpenSaaS.sh', - description: 'Open SaaS is a free, open-source, full-stack SaaS starter kit for React + NodeJS.', - customCss: ['./src/styles/tailwind.css'], + title: "OpenSaaS.sh", + description: + "Open SaaS is a free, open-source, full-stack SaaS starter kit for React + NodeJS.", + customCss: ["./src/styles/tailwind.css"], logo: { - src: '/src/assets/logo.webp', - alt: 'Open SaaS', + src: "/src/assets/logo.webp", + alt: "Open SaaS", }, head: [ { - tag: 'script', + tag: "script", attrs: { defer: true, - 'data-domain': 'docs.opensaas.sh', - 'data-api': 'https://opensaas.sh/wasparadocs/wasp/event', - src: 'https://opensaas.sh/wasparadocs/wasp/script.js', + "data-domain": "docs.opensaas.sh", + "data-api": "https://opensaas.sh/wasparadocs/wasp/event", + src: "https://opensaas.sh/wasparadocs/wasp/script.js", }, }, { - tag: 'script', + tag: "script", attrs: { defer: true, - src: '/piggy.js', + src: "/piggy.js", }, }, ], editLink: { - baseUrl: 'https://github.com/wasp-lang/open-saas/edit/main/opensaas-sh/blog', + baseUrl: + "https://github.com/wasp-lang/open-saas/edit/main/opensaas-sh/blog", }, components: { - SiteTitle: './src/components/MyHeader.astro', + SiteTitle: "./src/components/MyHeader.astro", // We customized ThemeSelect to include a "Copy URL for LLMs" button - ThemeSelect: './src/components/MyRightNavBarItems.astro', - Head: './src/components/HeadWithOGImage.astro', + ThemeSelect: "./src/components/MyRightNavBarItems.astro", + Head: "./src/components/HeadWithOGImage.astro", }, social: { - github: 'https://github.com/wasp-lang/open-saas', - twitter: 'https://twitter.com/wasplang', - discord: 'https://discord.gg/aCamt5wCpS', + github: "https://github.com/wasp-lang/open-saas", + twitter: "https://twitter.com/wasplang", + discord: "https://discord.gg/aCamt5wCpS", }, sidebar: [ { - label: 'Start Here', + label: "Start Here", items: [ - { label: 'Introduction', link: '/' }, - { label: 'Getting Started', link: '/start/getting-started/' }, - { label: 'Guided Tour', link: '/start/guided-tour/' }, + { label: "Introduction", link: "/" }, + { label: "Getting Started", link: "/start/getting-started/" }, + { label: "Guided Tour", link: "/start/guided-tour/" }, ], }, { - label: 'Guides', - autogenerate: { directory: '/guides/' }, + label: "Guides", + autogenerate: { directory: "/guides/" }, }, { - label: 'General', - autogenerate: { directory: '/general/' }, + label: "General", + autogenerate: { directory: "/general/" }, }, ], plugins: [ starlightBlog({ - title: 'Blog', - customCss: ['./src/styles/tailwind.css'], + title: "Blog", + customCss: ["./src/styles/tailwind.css"], authors: { vince: { - name: 'Vince', - title: 'Dev Rel @ Wasp', - picture: '/CRAIG_ROCK.png', // Images in the `public` directory are supported. - url: 'https://wasp.sh', + name: "Vince", + title: "Dev Rel @ Wasp", + picture: "/CRAIG_ROCK.png", // Images in the `public` directory are supported. + url: "https://wasp.sh", }, matija: { - name: 'Matija', - title: 'CEO @ Wasp', - picture: '/matija.jpeg', // Images in the `public` directory are supported. - url: 'https://wasp.sh', + name: "Matija", + title: "CEO @ Wasp", + picture: "/matija.jpeg", // Images in the `public` directory are supported. + url: "https://wasp.sh", }, milica: { - name: 'Milica', - title: 'Growth @ Wasp', - picture: '/milica.jpg', // Images in the `public` directory are supported. - url: 'https://wasp.sh', + name: "Milica", + title: "Growth @ Wasp", + picture: "/milica.jpg", // Images in the `public` directory are supported. + url: "https://wasp.sh", }, martin: { - name: 'Martin', - title: 'CTO @ Wasp', - picture: '/martin.jpg', // Images in the `public` directory are supported. - url: 'https://wasp.sh', + name: "Martin", + title: "CTO @ Wasp", + picture: "/martin.jpg", // Images in the `public` directory are supported. + url: "https://wasp.sh", }, }, }), diff --git a/opensaas-sh/blog/package.json b/opensaas-sh/blog/package.json index 2abe34b6..0b2dc6e1 100644 --- a/opensaas-sh/blog/package.json +++ b/opensaas-sh/blog/package.json @@ -1,14 +1,14 @@ { "name": "docs", - "type": "module", "version": "0.0.1", + "type": "module", "scripts": { - "dev": "astro dev", - "start": "astro dev", - "build": "astro check && astro build", - "preview": "astro preview", "astro": "astro", - "generate-llm-files": "node ./scripts/generate-llm-files.mjs" + "build": "astro check && astro build", + "dev": "astro dev", + "generate-llm-files": "node ./scripts/generate-llm-files.mjs", + "preview": "astro preview", + "start": "astro dev" }, "dependencies": { "@astrojs/check": "^0.9.4", diff --git a/opensaas-sh/blog/scripts/generate-llm-files.mjs b/opensaas-sh/blog/scripts/generate-llm-files.mjs index c1ea657b..67e0bbe3 100644 --- a/opensaas-sh/blog/scripts/generate-llm-files.mjs +++ b/opensaas-sh/blog/scripts/generate-llm-files.mjs @@ -1,57 +1,59 @@ -import fs from 'fs-extra'; -import path from 'path'; -import { globSync } from 'glob'; -import fm from 'front-matter'; +import fm from "front-matter"; +import fs from "fs-extra"; +import { globSync } from "glob"; +import path from "path"; const BLOG_ROOT = process.cwd(); -const PUBLIC_DIR = path.join(BLOG_ROOT, 'public'); -const DOCS_BASE_DIR = path.join(BLOG_ROOT, 'src/content/docs'); -const ORDERED_SOURCES = ['index.mdx', 'start', 'guides', 'general']; // Relative to DOCS_BASE_DIR -const GITHUB_RAW_BASE_URL = 'https://raw.githubusercontent.com/wasp-lang/open-saas/main/opensaas-sh/blog/'; -const OPEN_SAAS_DOCS_BASE_URL = 'https://docs.opensaas.sh/'; -const LLM_FULL_FILENAME = 'llms-full.txt'; -const LLM_OVERVIEW_FILENAME = 'llms.txt'; +const PUBLIC_DIR = path.join(BLOG_ROOT, "public"); +const DOCS_BASE_DIR = path.join(BLOG_ROOT, "src/content/docs"); +const ORDERED_SOURCES = ["index.mdx", "start", "guides", "general"]; // Relative to DOCS_BASE_DIR +const GITHUB_RAW_BASE_URL = + "https://raw.githubusercontent.com/wasp-lang/open-saas/main/opensaas-sh/blog/"; +const OPEN_SAAS_DOCS_BASE_URL = "https://docs.opensaas.sh/"; +const LLM_FULL_FILENAME = "llms-full.txt"; +const LLM_OVERVIEW_FILENAME = "llms.txt"; const OVERVIEW_FILE = path.join(PUBLIC_DIR, LLM_OVERVIEW_FILENAME); const FULL_CONCAT_FILE = path.join(PUBLIC_DIR, LLM_FULL_FILENAME); function cleanContent(content) { - if (!content) return ''; + if (!content) return ""; let cleaned = content; // Remove lines starting with 'import ... from ...;' or just 'import ...' - cleaned = cleaned.replace(/^import\s+.*(?:from\s+['"].*['"])?;?\s*$/gm, ''); + cleaned = cleaned.replace(/^import\s+.*(?:from\s+['"].*['"])?;?\s*$/gm, ""); // Remove lines like {/* TODO: ... */} - cleaned = cleaned.replace(/^\{\/\*.*\*\/\}\s*$/gm, ''); + cleaned = cleaned.replace(/^\{\/\*.*\*\/\}\s*$/gm, ""); // Remove lines like - cleaned = cleaned.replace(/^\s*$/gm, ''); + cleaned = cleaned.replace(/^\s*$/gm, ""); // Remove tags - cleaned = cleaned.replace(/]*?\/>/g, ''); + cleaned = cleaned.replace(/]*?\/>/g, ""); // Remove tag - cleaned = cleaned.replace(//g, ''); + cleaned = cleaned.replace(//g, ""); // Remove Emojis using specific ranges (less likely to remove digits) cleaned = cleaned.replace( /[\u{1F600}-\u{1F64F}]|[\u{1F300}-\u{1F5FF}]|[\u{1F680}-\u{1F6FF}]|[\u{1F1E0}-\u{1F1FF}]|[\u{2600}-\u{26FF}]|[\u{2700}-\u{27BF}]|[\u{FE00}-\u{FE0F}]|[\u{1F900}-\u{1F9FF}]|[\u{1FA70}-\u{1FAFF}]/gu, - '' + "", ); - cleaned = cleaned.replace(/\u00A0/g, ' '); + cleaned = cleaned.replace(/\u00A0/g, " "); // Remove Box Drawing Characters (single and double line) - cleaned = cleaned.replace(/[β”‚β”œβ””β”€β•”β•β•—β•‘β•šβ•]/g, ''); + cleaned = cleaned.replace(/[β”‚β”œβ””β”€β•”β•β•—β•‘β•šβ•]/g, ""); // Remove more than two line breaks - cleaned = cleaned.replace(/\n{3,}/g, '\n\n'); + cleaned = cleaned.replace(/\n{3,}/g, "\n\n"); return cleaned.trim(); } async function generateFiles() { - console.log('Starting LLM file generation...'); + console.log("Starting LLM file generation..."); - const introSummary = '> Open SaaS is a free, open-source, full-stack starter kit for building SaaS applications quickly. It leverages the [Wasp](https://wasp.sh/docs) framework (React, Node.js, Prisma) and integrates with various services like Stripe/Lemon Squeezy, OpenAI API, AWS S3, and different email providers.'; + const introSummary = + "> Open SaaS is a free, open-source, full-stack starter kit for building SaaS applications quickly. It leverages the [Wasp](https://wasp.sh/docs) framework (React, Node.js, Prisma) and integrates with various services like Stripe/Lemon Squeezy, OpenAI API, AWS S3, and different email providers."; let orderedSourceFiles = []; const fullConcatenatedFileUrl = OPEN_SAAS_DOCS_BASE_URL + LLM_FULL_FILENAME; let overviewContent = `# Open SaaS Documentation for LLMs\n\n${introSummary}\n\n## Full Documentation\n - [View Complete LLM-formatted Open SaaS Documentation](${fullConcatenatedFileUrl})\n\n## Individual documentation sections and guides:\n`; - let fullConcatContent = ''; + let fullConcatContent = ""; - console.log('Gathering source files...'); + console.log("Gathering source files..."); for (const sourceItem of ORDERED_SOURCES) { const itemPath = path.join(DOCS_BASE_DIR, sourceItem); try { @@ -60,56 +62,66 @@ async function generateFiles() { orderedSourceFiles.push(itemPath); console.log(` Added file: ${sourceItem}`); } else if (stats.isDirectory()) { - const files = globSync(path.join(itemPath, '**/*.{md,mdx}').replace(/\\/g, '/'), { nodir: true }).sort(); + const files = globSync( + path.join(itemPath, "**/*.{md,mdx}").replace(/\\/g, "/"), + { nodir: true }, + ).sort(); orderedSourceFiles.push(...files); - console.log(` Added ${files.length} files from directory: ${sourceItem}`); + console.log( + ` Added ${files.length} files from directory: ${sourceItem}`, + ); } } catch (error) { - if (error.code === 'ENOENT') { - console.warn(`Warning: Source item not found: ${itemPath}`); + if (error.code === "ENOENT") { + console.warn(`Warning: Source item not found: ${itemPath}`); } else { - console.warn(`Warning: Could not access source item: ${itemPath} - ${error.message}`); + console.warn( + `Warning: Could not access source item: ${itemPath} - ${error.message}`, + ); } } } console.log(`Total source files found: ${orderedSourceFiles.length}`); - console.log('Processing files...'); + console.log("Processing files..."); for (const sourceFilePath of orderedSourceFiles) { try { - const rawContent = await fs.readFile(sourceFilePath, 'utf8'); + const rawContent = await fs.readFile(sourceFilePath, "utf8"); const { attributes, body } = fm(rawContent); const processedContent = cleanContent(body); - const title = attributes.title || path.basename(sourceFilePath, path.extname(sourceFilePath)); + const title = + attributes.title || + path.basename(sourceFilePath, path.extname(sourceFilePath)); - const relativeSourcePathForGithub = path.relative(BLOG_ROOT, sourceFilePath).replace(/\\/g, '/'); + const relativeSourcePathForGithub = path + .relative(BLOG_ROOT, sourceFilePath) + .replace(/\\/g, "/"); const githubRawUrl = GITHUB_RAW_BASE_URL + relativeSourcePathForGithub; overviewContent += `- [${title}](${githubRawUrl})\n`; fullConcatContent += `# ${title}\n\n${processedContent}\n\n---\n\n`; - } catch (error) { - console.error(`Error processing file ${sourceFilePath}:`, error); + console.error(`Error processing file ${sourceFilePath}:`, error); } } - console.log('Writing output files to public/ ...'); + console.log("Writing output files to public/ ..."); try { - await fs.writeFile(OVERVIEW_FILE, overviewContent.trim(), 'utf8'); + await fs.writeFile(OVERVIEW_FILE, overviewContent.trim(), "utf8"); console.log(`Generated overview file: ${OVERVIEW_FILE}`); - await fs.writeFile(FULL_CONCAT_FILE, fullConcatContent.trim(), 'utf8'); + await fs.writeFile(FULL_CONCAT_FILE, fullConcatContent.trim(), "utf8"); console.log(`Generated full concatenated file: ${FULL_CONCAT_FILE}`); } catch (error) { - console.error('Error writing output files:', error); + console.error("Error writing output files:", error); } - console.log('LLM file generation complete.'); + console.log("LLM file generation complete."); } -generateFiles().catch(error => { - console.error('Unhandled error during LLM file generation:', error); +generateFiles().catch((error) => { + console.error("Unhandled error during LLM file generation:", error); process.exit(1); -}); \ No newline at end of file +}); diff --git a/opensaas-sh/blog/src/components/imagePaths.ts b/opensaas-sh/blog/src/components/imagePaths.ts index 73ec0087..937e2889 100644 --- a/opensaas-sh/blog/src/components/imagePaths.ts +++ b/opensaas-sh/blog/src/components/imagePaths.ts @@ -1,16 +1,24 @@ -import path from 'path'; -import { existsSync } from 'fs'; -import { fileURLToPath } from 'url'; +import { existsSync } from "fs"; +import path from "path"; +import { fileURLToPath } from "url"; -export const BANNER_PATH = '/banner-images'; +export const BANNER_PATH = "/banner-images"; -export const DEFAULT_BANNER_IMAGE = 'opensaas.webp'; +export const DEFAULT_BANNER_IMAGE = "opensaas.webp"; export const getBannerImageFilename = ({ path }: { path: string }) => - path.replace(/.*\//, '').replace(/\.\w+$/, '.webp'); + path.replace(/.*\//, "").replace(/\.\w+$/, ".webp"); -export const checkBannerImageExists = ({ bannerImageFileName }: { bannerImageFileName: string }) => { +export const checkBannerImageExists = ({ + bannerImageFileName, +}: { + bannerImageFileName: string; +}) => { const __dirname = path.dirname(fileURLToPath(import.meta.url)); - const imagePath = path.join(__dirname, `../../public/${BANNER_PATH}`, bannerImageFileName); + const imagePath = path.join( + __dirname, + `../../public/${BANNER_PATH}`, + bannerImageFileName, + ); return existsSync(imagePath); }; diff --git a/opensaas-sh/blog/src/content/config.ts b/opensaas-sh/blog/src/content/config.ts index 019002ea..1c3a8c20 100644 --- a/opensaas-sh/blog/src/content/config.ts +++ b/opensaas-sh/blog/src/content/config.ts @@ -1,7 +1,6 @@ -import { defineCollection } from 'astro:content'; -import { i18nSchema, docsSchema } from '@astrojs/starlight/schema'; -import { blogSchema } from 'starlight-blog/schema'; -import { z } from 'astro:content'; +import { docsSchema, i18nSchema } from "@astrojs/starlight/schema"; +import { defineCollection, z } from "astro:content"; +import { blogSchema } from "starlight-blog/schema"; export const collections = { docs: defineCollection({ @@ -16,5 +15,5 @@ export const collections = { }, }), }), - i18n: defineCollection({ type: 'data', schema: i18nSchema() }), -}; \ No newline at end of file + i18n: defineCollection({ type: "data", schema: i18nSchema() }), +}; diff --git a/opensaas-sh/blog/src/virtual.d.ts b/opensaas-sh/blog/src/virtual.d.ts index 1cacca40..45eb6aa7 100644 --- a/opensaas-sh/blog/src/virtual.d.ts +++ b/opensaas-sh/blog/src/virtual.d.ts @@ -1,22 +1,22 @@ -declare module 'virtual:starlight-blog-config' { - const StarlightBlogConfig: import('./libs/config').StarlightBlogConfig; +declare module "virtual:starlight-blog-config" { + const StarlightBlogConfig: import("./libs/config").StarlightBlogConfig; export default StarlightBlogConfig; } -declare module 'virtual:starlight/user-config' { - const Config: import('@astrojs/starlight/types').StarlightConfig; +declare module "virtual:starlight/user-config" { + const Config: import("@astrojs/starlight/types").StarlightConfig; export default Config; } -declare module 'virtual:starlight/user-images' { - type ImageMetadata = import('astro').ImageMetadata; +declare module "virtual:starlight/user-images" { + type ImageMetadata = import("astro").ImageMetadata; export const logos: { dark?: ImageMetadata; light?: ImageMetadata; }; } -declare module 'virtual:astro-config' { - const Config: import('@astrojs/types').Config; +declare module "virtual:astro-config" { + const Config: import("@astrojs/types").Config; export default Config; -} \ No newline at end of file +} diff --git a/opensaas-sh/blog/tailwind.config.mjs b/opensaas-sh/blog/tailwind.config.mjs index 8c5f3931..538e32be 100644 --- a/opensaas-sh/blog/tailwind.config.mjs +++ b/opensaas-sh/blog/tailwind.config.mjs @@ -1,8 +1,8 @@ import starlightPlugin from "@astrojs/starlight-tailwind"; import colors from "tailwindcss/colors"; -const yellow = colors.yellow -const gray = colors.gray +const yellow = colors.yellow; +const gray = colors.gray; /** @type {import('tailwindcss').Config} */ export default { @@ -10,7 +10,8 @@ export default { theme: { extend: { colors: { - accent: yellow, gray + accent: yellow, + gray, }, }, }, diff --git a/opensaas-sh/blog/tsconfig.json b/opensaas-sh/blog/tsconfig.json index 190f211a..d98300ba 100644 --- a/opensaas-sh/blog/tsconfig.json +++ b/opensaas-sh/blog/tsconfig.json @@ -7,4 +7,4 @@ "@assets/*": ["src/assets/*"] } } -} \ No newline at end of file +} diff --git a/opensaas-sh/tools/diff.sh b/opensaas-sh/tools/diff.sh index 05f5dfe6..6314be86 100755 --- a/opensaas-sh/tools/diff.sh +++ b/opensaas-sh/tools/diff.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash -TOOLS_DIR=$(dirname "$(realpath "$0")") # Assumes this script is in `tools/`. +TOOLS_DIR=$(dirname "$(realpath "$0")") # Assumes this script is in `tools/`. cd "${TOOLS_DIR}" && cd ../.. rm -rf opensaas-sh/app_diff diff --git a/opensaas-sh/tools/dope.sh b/opensaas-sh/tools/dope.sh index b52d5af0..0c2f3cf4 100755 --- a/opensaas-sh/tools/dope.sh +++ b/opensaas-sh/tools/dope.sh @@ -8,28 +8,28 @@ # Determine the patch command to use based on OS PATCH_CMD="patch" if [[ "$(uname)" == "Darwin" ]]; then - # On macOS, require gpatch to be installed - if command -v gpatch &> /dev/null; then - PATCH_CMD="gpatch" - else - echo "Error: GNU patch (gpatch) not found. On MacOS, this script requires GNU patch." - echo "Install it with: brew install gpatch" - exit 1 - fi + # On macOS, require gpatch to be installed + if command -v gpatch &> /dev/null; then + PATCH_CMD="gpatch" + else + echo "Error: GNU patch (gpatch) not found. On MacOS, this script requires GNU patch." + echo "Install it with: brew install gpatch" + exit 1 + fi fi # List all the source files in the specified dir. # "Source" files are any files that are not gitignored. list_source_files() { - local dir=$1 - (cd "${dir}" && git ls-files --cached --others --exclude-standard | sort) + local dir=$1 + (cd "${dir}" && git ls-files --cached --others --exclude-standard | sort) } # Check if the required arguments are provided. if [ "$#" -ne 3 ]; then - echo "Usage: $0 " - echo " should be either 'diff' to get the diff between the specified dirs or 'patch' to apply such existing diff onto base dir." - exit 1 + echo "Usage: $0 " + echo " should be either 'diff' to get the diff between the specified dirs or 'patch' to apply such existing diff onto base dir." + exit 1 fi BASE_DIR=$1 @@ -41,41 +41,41 @@ DIFF_DIR_DELETIONS="${DIFF_DIR}/deletions" # Based on base dir and derived dir, creates a diff dir that contains the diff between the two dirs. recreate_diff_dir() { - mkdir -p "${DIFF_DIR}" + mkdir -p "${DIFF_DIR}" - # List all the source files in the base and derived dirs. We skip gitignored files. - local BASE_FILES - BASE_FILES=$(list_source_files "${BASE_DIR}") # File paths relative to the base dir. - local DERIVED_FILES - DERIVED_FILES=$(list_source_files "${DERIVED_DIR}") # File paths relative to the derived dir. + # List all the source files in the base and derived dirs. We skip gitignored files. + local BASE_FILES + BASE_FILES=$(list_source_files "${BASE_DIR}") # File paths relative to the base dir. + local DERIVED_FILES + DERIVED_FILES=$(list_source_files "${DERIVED_DIR}") # File paths relative to the derived dir. - # For each source file in the derived dir, generate a .diff file between it and the - # corresponding source file in the base dir. - while IFS= read -r filepath; do - local derivedFilepath="${DERIVED_DIR}/${filepath}" - local baseFilepath="${BASE_DIR}/${filepath}" + # For each source file in the derived dir, generate a .diff file between it and the + # corresponding source file in the base dir. + while IFS= read -r filepath; do + local derivedFilepath="${DERIVED_DIR}/${filepath}" + local baseFilepath="${BASE_DIR}/${filepath}" - local filepathToBeUsedAsBase="${baseFilepath}" - # If the file is not one of the source files in base dir (e.g. is gitignored or doesn't exist), - # then we set it to /dev/null to indicate it doesn't exist for our purposes. - if ! echo "${BASE_FILES}" | grep -q "^${filepath}$"; then - filepathToBeUsedAsBase="/dev/null" - fi + local filepathToBeUsedAsBase="${baseFilepath}" + # If the file is not one of the source files in base dir (e.g. is gitignored or doesn't exist), + # then we set it to /dev/null to indicate it doesn't exist for our purposes. + if ! echo "${BASE_FILES}" | grep -q "^${filepath}$"; then + filepathToBeUsedAsBase="/dev/null" + fi - local DIFF_OUTPUT - DIFF_OUTPUT=$(diff -Nu --label "${baseFilepath}" --label "${derivedFilepath}" "${filepathToBeUsedAsBase}" "${derivedFilepath}") - if [ $? -eq 1 ]; then - mkdir -p "${DIFF_DIR}/$(dirname "${filepath}")" - echo "${DIFF_OUTPUT}" > "${DIFF_DIR}/${filepath}.diff" - echo "Generated ${DIFF_DIR}/${filepath}.diff" - fi - done <<< "${DERIVED_FILES}" + local DIFF_OUTPUT + DIFF_OUTPUT=$(diff -Nu --label "${baseFilepath}" --label "${derivedFilepath}" "${filepathToBeUsedAsBase}" "${derivedFilepath}") + if [ $? -eq 1 ]; then + mkdir -p "${DIFF_DIR}/$(dirname "${filepath}")" + echo "${DIFF_OUTPUT}" > "${DIFF_DIR}/${filepath}.diff" + echo "Generated ${DIFF_DIR}/${filepath}.diff" + fi + done <<< "${DERIVED_FILES}" - local FILES_ONLY_IN_BASE - FILES_ONLY_IN_BASE=$(comm -23 <(echo "${BASE_FILES}") <(echo "${DERIVED_FILES}")) - echo "${FILES_ONLY_IN_BASE}" > "${DIFF_DIR_DELETIONS}" + local FILES_ONLY_IN_BASE + FILES_ONLY_IN_BASE=$(comm -23 <(echo "${BASE_FILES}") <(echo "${DERIVED_FILES}")) + echo "${FILES_ONLY_IN_BASE}" > "${DIFF_DIR_DELETIONS}" - echo "DONE: generated ${DIFF_DIR}/" + echo "DONE: generated ${DIFF_DIR}/" } RED_COLOR='\033[0;31m' @@ -84,76 +84,76 @@ RESET_COLOR='\033[0m' # Patches the diff dir onto the base dir to get the derived dir. recreate_derived_dir() { - mkdir -p "${DERIVED_DIR}" + mkdir -p "${DERIVED_DIR}" - local BASE_FILES - BASE_FILES=$(list_source_files "${BASE_DIR}") # File paths relative to the base dir. + local BASE_FILES + BASE_FILES=$(list_source_files "${BASE_DIR}") # File paths relative to the base dir. - # Copy all the source files from the base dir over to the derived dir. - while IFS= read -r filepath; do - mkdir -p "${DERIVED_DIR}/$(dirname ${filepath})" - cp "${BASE_DIR}/${filepath}" "${DERIVED_DIR}/${filepath}" - done <<< "${BASE_FILES}" + # Copy all the source files from the base dir over to the derived dir. + while IFS= read -r filepath; do + mkdir -p "${DERIVED_DIR}/$(dirname ${filepath})" + cp "${BASE_DIR}/${filepath}" "${DERIVED_DIR}/${filepath}" + done <<< "${BASE_FILES}" - # For each .diff file in diff dir, apply the patch to the corresponding base file in the derived dir. - #local num_patches_failed - local num_patches_failed=0 - while IFS= read -r diff_filepath; do - local derived_filepath - derived_filepath="${diff_filepath#${DIFF_DIR}/}" - derived_filepath="${derived_filepath%.diff}" + # For each .diff file in diff dir, apply the patch to the corresponding base file in the derived dir. + #local num_patches_failed + local num_patches_failed=0 + while IFS= read -r diff_filepath; do + local derived_filepath + derived_filepath="${diff_filepath#${DIFF_DIR}/}" + derived_filepath="${derived_filepath%.diff}" - local patch_output - local patch_exit_code - patch_output=$("${PATCH_CMD}" --no-backup-if-mismatch --merge "${DERIVED_DIR}/${derived_filepath}" < "${diff_filepath}") - patch_exit_code=$? - if [ ${patch_exit_code} -eq 0 ]; then - echo "${patch_output}" - echo -e "${GREEN_COLOR}[OK]${RESET_COLOR}" - else - echo "${patch_output}" - echo -e "${RED_COLOR}[Failed with exit code ${patch_exit_code}]${RESET_COLOR}" - num_patches_failed=$((num_patches_failed + 1)) - fi - echo "" - done < <(find "${DIFF_DIR}" -name "*.diff") - - # Delete any files that exist in the base dir but shouldn't exist in the derived dir. - # TODO: also allow deletion of dirs. - if [ -f "${DIFF_DIR_DELETIONS}" ]; then - while IFS= read -r filepath; do - local derived_dir_filepath - local rm_exit_code - derived_dir_filepath="${DERIVED_DIR}/${filepath}" - rm "${derived_dir_filepath}" - rm_exit_code=$? - if [ ${rm_exit_code} -eq 0 ]; then - echo "Deleted ${derived_dir_filepath}" - echo -e "${GREEN_COLOR}[OK]${RESET_COLOR}" - else - echo "Failed to delete ${derived_dir_filepath}" - echo -e "${RED_COLOR}[Failed with exit code ${rm_exit_code}]${RESET_COLOR}" - fi - echo "" - done < "${DIFF_DIR_DELETIONS}" - fi - - echo "DONE: generated ${DERIVED_DIR}/" - - if [ ${num_patches_failed} -gt 0 ]; then - echo -e "${RED_COLOR}${num_patches_failed} patches failed, look into generated files for merge conflicts.${RESET_COLOR}" - exit 1; + local patch_output + local patch_exit_code + patch_output=$("${PATCH_CMD}" --no-backup-if-mismatch --merge "${DERIVED_DIR}/${derived_filepath}" < "${diff_filepath}") + patch_exit_code=$? + if [ ${patch_exit_code} -eq 0 ]; then + echo "${patch_output}" + echo -e "${GREEN_COLOR}[OK]${RESET_COLOR}" else - echo -e "${GREEN_COLOR}All patches successfully applied.${RESET_COLOR}" + echo "${patch_output}" + echo -e "${RED_COLOR}[Failed with exit code ${patch_exit_code}]${RESET_COLOR}" + num_patches_failed=$((num_patches_failed + 1)) fi + echo "" + done < <(find "${DIFF_DIR}" -name "*.diff") + + # Delete any files that exist in the base dir but shouldn't exist in the derived dir. + # TODO: also allow deletion of dirs. + if [ -f "${DIFF_DIR_DELETIONS}" ]; then + while IFS= read -r filepath; do + local derived_dir_filepath + local rm_exit_code + derived_dir_filepath="${DERIVED_DIR}/${filepath}" + rm "${derived_dir_filepath}" + rm_exit_code=$? + if [ ${rm_exit_code} -eq 0 ]; then + echo "Deleted ${derived_dir_filepath}" + echo -e "${GREEN_COLOR}[OK]${RESET_COLOR}" + else + echo "Failed to delete ${derived_dir_filepath}" + echo -e "${RED_COLOR}[Failed with exit code ${rm_exit_code}]${RESET_COLOR}" + fi + echo "" + done < "${DIFF_DIR_DELETIONS}" + fi + + echo "DONE: generated ${DERIVED_DIR}/" + + if [ ${num_patches_failed} -gt 0 ]; then + echo -e "${RED_COLOR}${num_patches_failed} patches failed, look into generated files for merge conflicts.${RESET_COLOR}" + exit 1 + else + echo -e "${GREEN_COLOR}All patches successfully applied.${RESET_COLOR}" + fi } if [ "$ACTION" == "diff" ]; then - recreate_diff_dir + recreate_diff_dir elif [ "$ACTION" == "patch" ]; then - recreate_derived_dir + recreate_derived_dir else - echo "Invalid action specified. Use 'diff' to get a diff between specified dirs or 'patch' to patch the existing diff onto base dir." - exit 1 + echo "Invalid action specified. Use 'diff' to get a diff between specified dirs or 'patch' to patch the existing diff onto base dir." + exit 1 fi diff --git a/opensaas-sh/tools/patch.sh b/opensaas-sh/tools/patch.sh index 0b3f654a..42e43154 100755 --- a/opensaas-sh/tools/patch.sh +++ b/opensaas-sh/tools/patch.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash -TOOLS_DIR=$(dirname "$(realpath "$0")") # Assumes this script is in `tools/`. +TOOLS_DIR=$(dirname "$(realpath "$0")") # Assumes this script is in `tools/`. cd "${TOOLS_DIR}" && cd ../.. # Removes all files except for some gitignored files that we don't want to bother regenerating each time, diff --git a/template/README.md b/template/README.md index d6621b4b..55e3a1ec 100644 --- a/template/README.md +++ b/template/README.md @@ -1,6 +1,7 @@ # This project is based on [OpenSaas](https://opensaas.sh) template and consists of three main dirs: + 1. `app` - Your web app, built with [Wasp](https://wasp.sh). 2. `e2e-tests` - [Playwright](https://playwright.dev/) tests for your Wasp web app. 3. `blog` - Your blog / docs, built with [Astro](https://docs.astro.build) based on [Starlight](https://starlight.astro.build/) template. diff --git a/template/app/.cursor/example-prompts.md b/template/app/.cursor/example-prompts.md index 923d9e22..730dad68 100644 --- a/template/app/.cursor/example-prompts.md +++ b/template/app/.cursor/example-prompts.md @@ -2,10 +2,11 @@ ### PRD / initial prompt -I want to create a `` app with the current SaaS boilerplate template project I'm in which uses Wasp and already has payment processing, AWS S3 file upload, a landing page, an admin dashboard, and authentication already setup. Leveraging Wasp's full-stack features (such as Auth), let's build the app based on the following spec: - - `` - - `` - - `` +I want to create a `` app with the current SaaS boilerplate template project I'm in which uses Wasp and already has payment processing, AWS S3 file upload, a landing page, an admin dashboard, and authentication already setup. Leveraging Wasp's full-stack features (such as Auth), let's build the app based on the following spec: + +- `` +- `` +- `` With this in mind, I want you to first evaluate the project template and think about a few possible PRD approaches before landing on the best one. Provide reasoning why this would be the best approach. Remember we're using Wasp, a full-stack framework with batteries included, that can do some of the heavy lifting for us, and we want to use a modified vertical slice implementation approach for LLM-assisted coding so we can start with basic implementations of features first, and add on complexity from there. diff --git a/template/app/README.md b/template/app/README.md index fddadfce..e15c8263 100644 --- a/template/app/README.md +++ b/template/app/README.md @@ -9,8 +9,8 @@ This template includes [ShadCN UI](https://ui.shadcn.com/) v2 for beautiful, acc ## Development ### Running locally - - Make sure you have the `.env.client` and `.env.server` files with correct dev values in the root of the project. - - Run the database with `wasp start db` and leave it running. - - Run `wasp start` and leave it running. - - [OPTIONAL]: If this is the first time starting the app, or you've just made changes to your entities/prisma schema, also run `wasp db migrate-dev`. +- Make sure you have the `.env.client` and `.env.server` files with correct dev values in the root of the project. +- Run the database with `wasp start db` and leave it running. +- Run `wasp start` and leave it running. +- [OPTIONAL]: If this is the first time starting the app, or you've just made changes to your entities/prisma schema, also run `wasp db migrate-dev`. diff --git a/template/app/src/admin/dashboards/analytics/AnalyticsDashboardPage.tsx b/template/app/src/admin/dashboards/analytics/AnalyticsDashboardPage.tsx index 81675efc..0c33529b 100644 --- a/template/app/src/admin/dashboards/analytics/AnalyticsDashboardPage.tsx +++ b/template/app/src/admin/dashboards/analytics/AnalyticsDashboardPage.tsx @@ -1,13 +1,13 @@ -import { type AuthUser } from 'wasp/auth'; -import { getDailyStats, useQuery } from 'wasp/client/operations'; -import { cn } from '../../../lib/utils'; -import DefaultLayout from '../../layout/DefaultLayout'; -import RevenueAndProfitChart from './RevenueAndProfitChart'; -import SourcesTable from './SourcesTable'; -import TotalPageViewsCard from './TotalPageViewsCard'; -import TotalPayingUsersCard from './TotalPayingUsersCard'; -import TotalRevenueCard from './TotalRevenueCard'; -import TotalSignupsCard from './TotalSignupsCard'; +import { type AuthUser } from "wasp/auth"; +import { getDailyStats, useQuery } from "wasp/client/operations"; +import { cn } from "../../../lib/utils"; +import DefaultLayout from "../../layout/DefaultLayout"; +import RevenueAndProfitChart from "./RevenueAndProfitChart"; +import SourcesTable from "./SourcesTable"; +import TotalPageViewsCard from "./TotalPageViewsCard"; +import TotalPayingUsersCard from "./TotalPayingUsersCard"; +import TotalRevenueCard from "./TotalRevenueCard"; +import TotalSignupsCard from "./TotalSignupsCard"; const Dashboard = ({ user }: { user: AuthUser }) => { const { data: stats, isLoading, error } = useQuery(getDailyStats); @@ -15,11 +15,11 @@ const Dashboard = ({ user }: { user: AuthUser }) => { if (error) { return ( -
-
-

Error

-

- {error.message || 'Something went wrong while fetching stats.'} +

+
+

Error

+

+ {error.message || "Something went wrong while fetching stats."}

@@ -29,40 +29,53 @@ const Dashboard = ({ user }: { user: AuthUser }) => { return ( -
+
-
+
- - + +
-
- +
+ -
+
{!stats && ( -
-
-

No daily stats generated yet

-

+

+
+

+ No daily stats generated yet +

+

Stats will appear here once the daily stats job has run

diff --git a/template/app/src/admin/dashboards/analytics/RevenueAndProfitChart.tsx b/template/app/src/admin/dashboards/analytics/RevenueAndProfitChart.tsx index 24f56222..dbcd2ded 100644 --- a/template/app/src/admin/dashboards/analytics/RevenueAndProfitChart.tsx +++ b/template/app/src/admin/dashboards/analytics/RevenueAndProfitChart.tsx @@ -1,22 +1,22 @@ -import { ApexOptions } from 'apexcharts'; -import { useEffect, useMemo, useState } from 'react'; -import ReactApexChart from 'react-apexcharts'; -import { type DailyStatsProps } from '../../../analytics/stats'; +import { ApexOptions } from "apexcharts"; +import { useEffect, useMemo, useState } from "react"; +import ReactApexChart from "react-apexcharts"; +import { type DailyStatsProps } from "../../../analytics/stats"; const options: ApexOptions = { legend: { show: false, - position: 'top', - horizontalAlign: 'left', + position: "top", + horizontalAlign: "left", }, - colors: ['#3C50E0', '#80CAEE'], + colors: ["#3C50E0", "#80CAEE"], chart: { - fontFamily: 'Satoshi, sans-serif', + fontFamily: "Satoshi, sans-serif", height: 335, - type: 'area', + type: "area", dropShadow: { enabled: true, - color: '#623CEA14', + color: "#623CEA14", top: 10, blur: 4, left: 0, @@ -47,7 +47,7 @@ const options: ApexOptions = { ], stroke: { width: [2, 2], - curve: 'straight', + curve: "straight", }, // labels: { // show: false, @@ -70,8 +70,8 @@ const options: ApexOptions = { }, markers: { size: 4, - colors: '#fff', - strokeColors: ['#3056D3', '#80CAEE'], + colors: "#fff", + strokeColors: ["#3056D3", "#80CAEE"], strokeWidth: 3, strokeOpacity: 0.9, strokeDashArray: 0, @@ -83,7 +83,7 @@ const options: ApexOptions = { }, }, xaxis: { - type: 'category', + type: "category", axisBorder: { show: false, }, @@ -94,7 +94,7 @@ const options: ApexOptions = { yaxis: { title: { style: { - fontSize: '0px', + fontSize: "0px", }, }, min: 0, @@ -123,8 +123,8 @@ const RevenueAndProfitChart = ({ weeklyStats, isLoading }: DailyStatsProps) => { if (!!weeklyStats && weeklyStats?.length > 0) { const datesArr = weeklyStats?.map((stat) => { // get day of week, month, and day of month - const dateArr = stat.date.toString().split(' '); - return dateArr.slice(0, 3).join(' '); + const dateArr = stat.date.toString().split(" "); + return dateArr.slice(0, 3).join(" "); }); return datesArr; } @@ -133,7 +133,7 @@ const RevenueAndProfitChart = ({ weeklyStats, isLoading }: DailyStatsProps) => { const [state, setState] = useState({ series: [ { - name: 'Profit', + name: "Profit", data: [4, 7, 10, 11, 13, 14, 17], }, ], @@ -144,7 +144,9 @@ const RevenueAndProfitChart = ({ weeklyStats, isLoading }: DailyStatsProps) => { if (dailyRevenueArray && dailyRevenueArray.length > 0) { setState((prevState) => { // Check if a "Revenue" series already exists - const existingSeriesIndex = prevState.series.findIndex((series) => series.name === 'Revenue'); + const existingSeriesIndex = prevState.series.findIndex( + (series) => series.name === "Revenue", + ); if (existingSeriesIndex >= 0) { // Update existing "Revenue" series data @@ -164,7 +166,7 @@ const RevenueAndProfitChart = ({ weeklyStats, isLoading }: DailyStatsProps) => { series: [ ...prevState.series, { - name: 'Revenue', + name: "Revenue", data: dailyRevenueArray, }, ], @@ -198,37 +200,41 @@ const RevenueAndProfitChart = ({ weeklyStats, isLoading }: DailyStatsProps) => { }, [daysOfWeekArr, dailyRevenueArray]); return ( -
-
-
-
- - +
+
+
+
+ + -
-

Total Profit

-

Last 7 Days

+
+

Total Profit

+

+ Last 7 Days +

-
- - +
+ + -
-

Total Revenue

-

Last 7 Days

+
+

Total Revenue

+

+ Last 7 Days +

-
-
- - -
@@ -236,8 +242,13 @@ const RevenueAndProfitChart = ({ weeklyStats, isLoading }: DailyStatsProps) => {
-
- +
+
diff --git a/template/app/src/admin/dashboards/analytics/SourcesTable.tsx b/template/app/src/admin/dashboards/analytics/SourcesTable.tsx index 306d98cb..16f0084e 100644 --- a/template/app/src/admin/dashboards/analytics/SourcesTable.tsx +++ b/template/app/src/admin/dashboards/analytics/SourcesTable.tsx @@ -1,42 +1,54 @@ -import { type PageViewSource } from 'wasp/entities'; +import { type PageViewSource } from "wasp/entities"; -const SourcesTable = ({ sources }: { sources: PageViewSource[] | undefined }) => { +const SourcesTable = ({ + sources, +}: { + sources: PageViewSource[] | undefined; +}) => { return ( -
-

Top Sources

+
+

+ Top Sources +

-
-
-
-
Source
+
+
+
+
+ Source +
-
-
Visitors
+
+
+ Visitors +
-
-
Sales
+
+
+ Sales +
{sources && sources.length > 0 ? ( sources.map((source) => ( -
-
-

{source.name}

+
+
+

{source.name}

-
-

{source.visitors}

+
+

{source.visitors}

-
-

--

+
+

--

)) ) : ( -
-

No data to display

+
+

No data to display

)}
diff --git a/template/app/src/admin/dashboards/analytics/TotalPageViewsCard.tsx b/template/app/src/admin/dashboards/analytics/TotalPageViewsCard.tsx index bcecf052..eb0c2cb1 100644 --- a/template/app/src/admin/dashboards/analytics/TotalPageViewsCard.tsx +++ b/template/app/src/admin/dashboards/analytics/TotalPageViewsCard.tsx @@ -1,42 +1,57 @@ -import { ArrowDown, ArrowUp, Eye } from 'lucide-react'; -import { Card, CardContent, CardHeader } from '../../../components/ui/card'; -import { cn } from '../../../lib/utils'; +import { ArrowDown, ArrowUp, Eye } from "lucide-react"; +import { Card, CardContent, CardHeader } from "../../../components/ui/card"; +import { cn } from "../../../lib/utils"; type PageViewsStats = { totalPageViews: number | undefined; prevDayViewsChangePercent: string | undefined; }; -const TotalPageViewsCard = ({ totalPageViews, prevDayViewsChangePercent }: PageViewsStats) => { - const prevDayViewsChangePercentValue = parseInt(prevDayViewsChangePercent || ''); +const TotalPageViewsCard = ({ + totalPageViews, + prevDayViewsChangePercent, +}: PageViewsStats) => { + const prevDayViewsChangePercentValue = parseInt( + prevDayViewsChangePercent || "", + ); const isDeltaPositive = prevDayViewsChangePercentValue > 0; return ( -
- +
+
- +
-

{totalPageViews}

- Total page views +

+ {totalPageViews} +

+ + Total page views +
{prevDayViewsChangePercent && prevDayViewsChangePercentValue !== 0 ? `${prevDayViewsChangePercent}%` - : '-'} + : "-"} {prevDayViewsChangePercent && prevDayViewsChangePercentValue !== 0 && (isDeltaPositive ? : )} diff --git a/template/app/src/admin/dashboards/analytics/TotalPayingUsersCard.tsx b/template/app/src/admin/dashboards/analytics/TotalPayingUsersCard.tsx index 31ae3c94..597cd9f2 100644 --- a/template/app/src/admin/dashboards/analytics/TotalPayingUsersCard.tsx +++ b/template/app/src/admin/dashboards/analytics/TotalPayingUsersCard.tsx @@ -1,8 +1,8 @@ -import { ArrowDown, ArrowUp, ShoppingBag } from 'lucide-react'; -import { useMemo } from 'react'; -import { type DailyStatsProps } from '../../../analytics/stats'; -import { Card, CardContent, CardHeader } from '../../../components/ui/card'; -import { cn } from '../../../lib/utils'; +import { ArrowDown, ArrowUp, ShoppingBag } from "lucide-react"; +import { useMemo } from "react"; +import { type DailyStatsProps } from "../../../analytics/stats"; +import { Card, CardContent, CardHeader } from "../../../components/ui/card"; +import { cn } from "../../../lib/utils"; const TotalPayingUsersCard = ({ dailyStats, isLoading }: DailyStatsProps) => { const isDeltaPositive = useMemo(() => { @@ -12,25 +12,30 @@ const TotalPayingUsersCard = ({ dailyStats, isLoading }: DailyStatsProps) => { return ( -
- +
+
- +
-

{dailyStats?.paidUserCount}

- Total Paying Users +

+ {dailyStats?.paidUserCount} +

+ + Total Paying Users +
- {isLoading ? '...' : dailyStats?.paidUserDelta ?? '-'} + {isLoading ? "..." : (dailyStats?.paidUserDelta ?? "-")} {!isLoading && (dailyStats?.paidUserDelta ?? 0) !== 0 && (isDeltaPositive ? : )} diff --git a/template/app/src/admin/dashboards/analytics/TotalRevenueCard.tsx b/template/app/src/admin/dashboards/analytics/TotalRevenueCard.tsx index d931ea41..95ba3d45 100644 --- a/template/app/src/admin/dashboards/analytics/TotalRevenueCard.tsx +++ b/template/app/src/admin/dashboards/analytics/TotalRevenueCard.tsx @@ -1,10 +1,14 @@ -import { ArrowDown, ArrowUp, ShoppingCart } from 'lucide-react'; -import { useMemo } from 'react'; -import { type DailyStatsProps } from '../../../analytics/stats'; -import { Card, CardContent, CardHeader } from '../../../components/ui/card'; -import { cn } from '../../../lib/utils'; +import { ArrowDown, ArrowUp, ShoppingCart } from "lucide-react"; +import { useMemo } from "react"; +import { type DailyStatsProps } from "../../../analytics/stats"; +import { Card, CardContent, CardHeader } from "../../../components/ui/card"; +import { cn } from "../../../lib/utils"; -const TotalRevenueCard = ({ dailyStats, weeklyStats, isLoading }: DailyStatsProps) => { +const TotalRevenueCard = ({ + dailyStats, + weeklyStats, + isLoading, +}: DailyStatsProps) => { const isDeltaPositive = useMemo(() => { if (!weeklyStats) return false; return weeklyStats[0].totalRevenue - weeklyStats[1]?.totalRevenue > 0; @@ -12,37 +16,54 @@ const TotalRevenueCard = ({ dailyStats, weeklyStats, isLoading }: DailyStatsProp const deltaPercentage = useMemo(() => { if (!weeklyStats || weeklyStats.length < 2 || isLoading) return; - if (weeklyStats[1]?.totalRevenue === 0 || weeklyStats[0]?.totalRevenue === 0) return 0; + if ( + weeklyStats[1]?.totalRevenue === 0 || + weeklyStats[0]?.totalRevenue === 0 + ) + return 0; weeklyStats.sort((a, b) => b.id - a.id); const percentage = - ((weeklyStats[0].totalRevenue - weeklyStats[1]?.totalRevenue) / weeklyStats[1]?.totalRevenue) * 100; + ((weeklyStats[0].totalRevenue - weeklyStats[1]?.totalRevenue) / + weeklyStats[1]?.totalRevenue) * + 100; return Math.floor(percentage); }, [weeklyStats]); return ( -
- +
+
- +
-

${dailyStats?.totalRevenue}

- Total Revenue +

+ ${dailyStats?.totalRevenue} +

+ + Total Revenue +
- {isLoading ? '...' : deltaPercentage && deltaPercentage !== 0 ? `${deltaPercentage}%` : '-'} + {isLoading + ? "..." + : deltaPercentage && deltaPercentage !== 0 + ? `${deltaPercentage}%` + : "-"} {!isLoading && deltaPercentage && deltaPercentage !== 0 && diff --git a/template/app/src/admin/dashboards/analytics/TotalSignupsCard.tsx b/template/app/src/admin/dashboards/analytics/TotalSignupsCard.tsx index 1e598b6b..beef4402 100644 --- a/template/app/src/admin/dashboards/analytics/TotalSignupsCard.tsx +++ b/template/app/src/admin/dashboards/analytics/TotalSignupsCard.tsx @@ -1,8 +1,8 @@ -import { ArrowUp, UsersRound } from 'lucide-react'; -import { useMemo } from 'react'; -import { type DailyStatsProps } from '../../../analytics/stats'; -import { Card, CardContent, CardHeader } from '../../../components/ui/card'; -import { cn } from '../../../lib/utils'; +import { ArrowUp, UsersRound } from "lucide-react"; +import { useMemo } from "react"; +import { type DailyStatsProps } from "../../../analytics/stats"; +import { Card, CardContent, CardHeader } from "../../../components/ui/card"; +import { cn } from "../../../lib/utils"; const TotalSignupsCard = ({ dailyStats, isLoading }: DailyStatsProps) => { const isDeltaPositive = useMemo(() => { @@ -12,25 +12,30 @@ const TotalSignupsCard = ({ dailyStats, isLoading }: DailyStatsProps) => { return ( -
- +
+
- +
-

{dailyStats?.userCount}

- Total Signups +

+ {dailyStats?.userCount} +

+ + Total Signups +
- {isLoading ? '...' : dailyStats?.userDelta ?? '-'} + {isLoading ? "..." : (dailyStats?.userDelta ?? "-")} {!isLoading && (dailyStats?.userDelta ?? 0) > 0 && }
diff --git a/template/app/src/admin/dashboards/messages/MessageButton.tsx b/template/app/src/admin/dashboards/messages/MessageButton.tsx index e9fa1d83..671e23b0 100644 --- a/template/app/src/admin/dashboards/messages/MessageButton.tsx +++ b/template/app/src/admin/dashboards/messages/MessageButton.tsx @@ -1,18 +1,18 @@ -import { MessageCircleMore } from 'lucide-react'; -import { Link as WaspRouterLink, routes } from 'wasp/client/router'; +import { MessageCircleMore } from "lucide-react"; +import { Link as WaspRouterLink, routes } from "wasp/client/router"; const MessageButton = () => { return ( -
  • +
  • - + {/* TODO: only animate if there are new messages */} - + - +
  • ); diff --git a/template/app/src/admin/dashboards/messages/MessagesPage.tsx b/template/app/src/admin/dashboards/messages/MessagesPage.tsx index 1d63d578..2b378aaa 100644 --- a/template/app/src/admin/dashboards/messages/MessagesPage.tsx +++ b/template/app/src/admin/dashboards/messages/MessagesPage.tsx @@ -1,13 +1,13 @@ // TODO: Add messages page -import type { AuthUser } from "wasp/auth" -import DefaultLayout from "../../layout/DefaultLayout" +import type { AuthUser } from "wasp/auth"; +import DefaultLayout from "../../layout/DefaultLayout"; -function AdminMessages({user} : {user: AuthUser}) { +function AdminMessages({ user }: { user: AuthUser }) { return (
    This page is under construction 🚧
    - ) + ); } -export default AdminMessages +export default AdminMessages; diff --git a/template/app/src/admin/dashboards/users/DropdownEditDelete.tsx b/template/app/src/admin/dashboards/users/DropdownEditDelete.tsx index 2c048e08..c6dacaef 100644 --- a/template/app/src/admin/dashboards/users/DropdownEditDelete.tsx +++ b/template/app/src/admin/dashboards/users/DropdownEditDelete.tsx @@ -1,26 +1,26 @@ -import { Ellipsis, SquarePen, Trash2 } from 'lucide-react'; +import { Ellipsis, SquarePen, Trash2 } from "lucide-react"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, -} from '../../../components/ui/dropdown-menu'; +} from "../../../components/ui/dropdown-menu"; const DropdownEditDelete = () => { return ( - + - + Edit - + Delete diff --git a/template/app/src/admin/dashboards/users/UsersDashboardPage.tsx b/template/app/src/admin/dashboards/users/UsersDashboardPage.tsx index c99e6ddc..2874030b 100644 --- a/template/app/src/admin/dashboards/users/UsersDashboardPage.tsx +++ b/template/app/src/admin/dashboards/users/UsersDashboardPage.tsx @@ -1,13 +1,13 @@ -import { type AuthUser } from 'wasp/auth'; -import UsersTable from './UsersTable'; -import Breadcrumb from '../../layout/Breadcrumb'; -import DefaultLayout from '../../layout/DefaultLayout'; +import { type AuthUser } from "wasp/auth"; +import Breadcrumb from "../../layout/Breadcrumb"; +import DefaultLayout from "../../layout/DefaultLayout"; +import UsersTable from "./UsersTable"; const Users = ({ user }: { user: AuthUser }) => { return ( - -
    + +
    diff --git a/template/app/src/admin/dashboards/users/UsersTable.tsx b/template/app/src/admin/dashboards/users/UsersTable.tsx index b16195ea..109808ad 100644 --- a/template/app/src/admin/dashboards/users/UsersTable.tsx +++ b/template/app/src/admin/dashboards/users/UsersTable.tsx @@ -1,27 +1,39 @@ -import { X } from 'lucide-react'; -import { useEffect, useState } from 'react'; -import { useAuth } from 'wasp/client/auth'; -import { getPaginatedUsers, updateIsUserAdminById, useQuery } from 'wasp/client/operations'; -import { type User } from 'wasp/entities'; -import useDebounce from '../../../client/hooks/useDebounce'; -import { Button } from '../../../components/ui/button'; -import { Checkbox } from '../../../components/ui/checkbox'; -import { Input } from '../../../components/ui/input'; -import { Label } from '../../../components/ui/label'; -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../../../components/ui/select'; -import { Switch } from '../../../components/ui/switch'; -import { SubscriptionStatus } from '../../../payment/plans'; -import LoadingSpinner from '../../layout/LoadingSpinner'; -import DropdownEditDelete from './DropdownEditDelete'; +import { X } from "lucide-react"; +import { useEffect, useState } from "react"; +import { useAuth } from "wasp/client/auth"; +import { + getPaginatedUsers, + updateIsUserAdminById, + useQuery, +} from "wasp/client/operations"; +import { type User } from "wasp/entities"; +import useDebounce from "../../../client/hooks/useDebounce"; +import { Button } from "../../../components/ui/button"; +import { Checkbox } from "../../../components/ui/checkbox"; +import { Input } from "../../../components/ui/input"; +import { Label } from "../../../components/ui/label"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "../../../components/ui/select"; +import { Switch } from "../../../components/ui/switch"; +import { SubscriptionStatus } from "../../../payment/plans"; +import LoadingSpinner from "../../layout/LoadingSpinner"; +import DropdownEditDelete from "./DropdownEditDelete"; -function AdminSwitch({ id, isAdmin }: Pick) { +function AdminSwitch({ id, isAdmin }: Pick) { const { data: currentUser } = useAuth(); const isCurrentUser = currentUser?.id === id; return ( updateIsUserAdminById({ id: id, isAdmin: value })} + onCheckedChange={(value) => + updateIsUserAdminById({ id: id, isAdmin: value }) + } disabled={isCurrentUser} /> ); @@ -30,10 +42,12 @@ function AdminSwitch({ id, isAdmin }: Pick) { const UsersTable = () => { const [currentPage, setCurrentPage] = useState(1); const [emailFilter, setEmailFilter] = useState(undefined); - const [isAdminFilter, setIsAdminFilter] = useState(undefined); - const [subscriptionStatusFilter, setSubscriptionStatusFilter] = useState>( - [] + const [isAdminFilter, setIsAdminFilter] = useState( + undefined, ); + const [subscriptionStatusFilter, setSubscriptionStatusFilter] = useState< + Array + >([]); const debouncedEmailFilter = useDebounce(emailFilter, 300); @@ -44,7 +58,9 @@ const UsersTable = () => { filter: { ...(debouncedEmailFilter && { emailContains: debouncedEmailFilter }), ...(isAdminFilter !== undefined && { isAdmin: isAdminFilter }), - ...(subscriptionStatusFilter.length > 0 && { subscriptionStatusIn: subscriptionStatusFilter }), + ...(subscriptionStatusFilter.length > 0 && { + subscriptionStatusIn: subscriptionStatusFilter, + }), }, }); @@ -52,7 +68,7 @@ const UsersTable = () => { function backToPageOne() { setCurrentPage(1); }, - [debouncedEmailFilter, subscriptionStatusFilter, isAdminFilter] + [debouncedEmailFilter, subscriptionStatusFilter, isAdminFilter], ); const handleStatusToggle = (status: SubscriptionStatus | null) => { @@ -69,85 +85,99 @@ const UsersTable = () => { setSubscriptionStatusFilter([]); }; - const hasActiveFilters = subscriptionStatusFilter && subscriptionStatusFilter.length > 0; + const hasActiveFilters = + subscriptionStatusFilter && subscriptionStatusFilter.length > 0; return ( -
    -
    -
    - Filters: -
    -
    -