├── .babelrc ├── .github └── workflows │ └── deploy.yml ├── .gitignore ├── .meteor ├── .finished-upgraders ├── .gitignore ├── .id ├── packages ├── platforms ├── release └── versions ├── .prettierignore ├── .run └── run.run.xml ├── .zcloudignore ├── CHANGELOG.md ├── Dockerfile ├── README.md ├── app ├── access │ └── Access.js ├── clicks │ ├── ClicksCollection.js │ ├── clicksMethods.js │ └── clicksPublishes.js ├── components │ ├── Button.js │ ├── ErrorFallback.js │ ├── Loading.js │ └── MyAlert.js ├── general │ ├── App.js │ ├── NotFound.js │ ├── RoutePaths.js │ └── Router.js ├── home │ └── Home.js ├── infra │ ├── cron.js │ └── migrations.js ├── layouts │ ├── AnonymousLayout.js │ ├── ConditionalLayout.js │ ├── LoggedLayout.js │ ├── PageWithHeader.js │ ├── PageWithoutHeader.js │ └── PublicLayout.js ├── lib │ └── tailwind │ │ └── safelist.txt ├── private │ └── Private.js └── users │ └── UsersCollection.js ├── client ├── main.css ├── main.html └── main.js ├── lefthook.yml ├── package-lock.json ├── package.json ├── postcss.config.mjs ├── prettier.config.js ├── private └── env │ └── dev │ └── settings.json └── server ├── main.js ├── metrics.js └── rest.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "babel-plugin-react-compiler" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Docs Deploy 2 | on: 3 | push: 4 | branches: ['main'] 5 | jobs: 6 | deploy: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v4 10 | - name: Deploy 11 | uses: zcloud-ws/zcloud-deploy-action@main 12 | with: 13 | env-token: ${{ secrets.ZCLOUD_ENV_TOKEN }} 14 | env: "quave-meteor-template" 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .idea/ 3 | .eslint-meteor-files 4 | 5 | # Cursor / VSCode 6 | jsconfig.json 7 | launch.json 8 | -------------------------------------------------------------------------------- /.meteor/.finished-upgraders: -------------------------------------------------------------------------------- 1 | # This file contains information which helps Meteor properly upgrade your 2 | # app when you run 'meteor update'. You should check it into version control 3 | # with your project. 4 | 5 | notices-for-0.9.0 6 | notices-for-0.9.1 7 | 0.9.4-platform-file 8 | notices-for-facebook-graph-api-2 9 | 1.2.0-standard-minifiers-package 10 | 1.2.0-meteor-platform-split 11 | 1.2.0-cordova-changes 12 | 1.2.0-breaking-changes 13 | 1.3.0-split-minifiers-package 14 | 1.4.0-remove-old-dev-bundle-link 15 | 1.4.1-add-shell-server-package 16 | 1.4.3-split-account-service-packages 17 | 1.5-add-dynamic-import-package 18 | 1.7-split-underscore-from-meteor-base 19 | 1.8.3-split-jquery-from-blaze 20 | -------------------------------------------------------------------------------- /.meteor/.gitignore: -------------------------------------------------------------------------------- 1 | local 2 | -------------------------------------------------------------------------------- /.meteor/.id: -------------------------------------------------------------------------------- 1 | # This file contains a token that is unique to your project. 2 | # Check it into your repository along with the rest of this directory. 3 | # It can be used for purposes such as: 4 | # - ensuring you don't accidentally deploy one app on top of another 5 | # - providing package authors with aggregated statistics 6 | 7 | wdfxnup4zydl.nr9xdj0wls6a 8 | -------------------------------------------------------------------------------- /.meteor/packages: -------------------------------------------------------------------------------- 1 | # Meteor packages used by this project, one per line. 2 | # Check this file (and the other files in this directory) into your repository. 3 | # 4 | # 'meteor add' and 'meteor remove' will edit this file for you, 5 | # but you can also edit it by hand. 6 | 7 | meteor-base@1.5.2 # Packages every Meteor app needs to have 8 | mobile-experience@1.1.2 # Packages for a great mobile UX 9 | mongo@2.1.0 # The database Meteor supports right now 10 | reactive-var@1.0.13 # Reactive variable for tracker 11 | 12 | standard-minifier-css@1.9.3 13 | standard-minifier-js@3.0.0 # JS minifier run for production mode 14 | es5-shim@4.8.1 # ECMAScript 5 compatibility for older browsers 15 | ecmascript@0.16.10 # Enable ECMAScript2015+ syntax in app code 16 | typescript@5.6.3 # Enable TypeScript syntax in .ts and .tsx modules 17 | shell-server@0.6.1 # Server-side component of the `meteor shell` command 18 | hot-module-replacement@0.5.4 # Update client in development without reloading the page 19 | 20 | static-html@1.4.0 # Define static page content in .html files 21 | react-meteor-data # React higher-order component for reactively tracking Meteor data 22 | 23 | quave:accounts-passwordless-react 24 | quave:logged-user-react 25 | quave:alert-react-tailwind 26 | quave:synced-cron 27 | quave:migrations 28 | quave:email-postmark 29 | quave:collections 30 | zodern:types 31 | -------------------------------------------------------------------------------- /.meteor/platforms: -------------------------------------------------------------------------------- 1 | server 2 | browser 3 | -------------------------------------------------------------------------------- /.meteor/release: -------------------------------------------------------------------------------- 1 | METEOR@3.1.2 2 | -------------------------------------------------------------------------------- /.meteor/versions: -------------------------------------------------------------------------------- 1 | accounts-base@3.0.4 2 | accounts-passwordless@3.0.1 3 | allow-deny@2.1.0 4 | autoupdate@2.0.0 5 | babel-compiler@7.11.3 6 | babel-runtime@1.5.2 7 | base64@1.0.13 8 | binary-heap@1.0.12 9 | boilerplate-generator@2.0.0 10 | caching-compiler@2.0.1 11 | callback-hook@1.6.0 12 | check@1.4.4 13 | core-runtime@1.0.0 14 | ddp@1.4.2 15 | ddp-client@3.1.0 16 | ddp-common@1.4.4 17 | ddp-rate-limiter@1.2.2 18 | ddp-server@3.1.0 19 | diff-sequence@1.1.3 20 | dynamic-import@0.7.4 21 | ecmascript@0.16.10 22 | ecmascript-runtime@0.8.3 23 | ecmascript-runtime-client@0.12.3 24 | ecmascript-runtime-server@0.11.1 25 | ejson@1.1.4 26 | email@3.1.2 27 | es5-shim@4.8.1 28 | facts-base@1.0.2 29 | fetch@0.1.6 30 | geojson-utils@1.0.12 31 | hot-code-push@1.0.5 32 | hot-module-replacement@0.5.4 33 | id-map@1.2.0 34 | inter-process-messaging@0.1.2 35 | launch-screen@2.0.1 36 | localstorage@1.2.1 37 | logging@1.3.6 38 | meteor@2.1.0 39 | meteor-base@1.5.2 40 | minifier-css@2.0.1 41 | minifier-js@3.0.1 42 | minimongo@2.0.2 43 | mobile-experience@1.1.2 44 | mobile-status-bar@1.1.1 45 | modern-browsers@0.2.1 46 | modules@0.20.3 47 | modules-runtime@0.13.2 48 | modules-runtime-hot@0.14.3 49 | mongo@2.1.1 50 | mongo-decimal@0.2.0 51 | mongo-dev-server@1.1.1 52 | mongo-id@1.0.9 53 | npm-mongo@6.10.2 54 | ordered-dict@1.2.0 55 | promise@1.0.0 56 | quave:accounts-passwordless-react@2.2.1 57 | quave:alert-react-tailwind@4.0.0 58 | quave:collections@3.1.2 59 | quave:email-postmark@1.3.0 60 | quave:logged-user-react@1.1.0 61 | quave:migrations@2.0.2 62 | quave:settings@1.1.0 63 | quave:synced-cron@2.2.1 64 | random@1.2.2 65 | rate-limit@1.1.2 66 | react-fast-refresh@0.2.9 67 | react-meteor-data@3.0.3 68 | reactive-var@1.0.13 69 | reload@1.3.2 70 | retry@1.1.1 71 | routepolicy@1.1.2 72 | sha@1.0.10 73 | shell-server@0.6.1 74 | socket-stream-client@0.6.0 75 | standard-minifier-css@1.9.3 76 | standard-minifier-js@3.0.0 77 | static-html@1.4.0 78 | static-html-tools@1.0.0 79 | tracker@1.3.4 80 | typescript@5.6.3 81 | url@1.3.5 82 | webapp@2.0.5 83 | webapp-hashing@1.1.2 84 | zodern:types@1.0.13 85 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .meteor 2 | -------------------------------------------------------------------------------- /.run/run.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /.zcloudignore: -------------------------------------------------------------------------------- 1 | .meteor/local 2 | node_modules 3 | .git 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 0.0.7 (2025-01-30) 4 | 5 | - Upgrades to Tailwind CSS 4.0.0 6 | 7 | ## 0.0.6 (2024-09-25) 8 | 9 | - Upgrades to React 19 RC 10 | - Upgrades to Meteor 3.0.3 11 | - Updates Dockerfile to run in zCloud with Meteor 3.0.3 12 | - Removes dependencies from `@headlessui/react` and `@heroicons/react` 13 | 14 | ## 0.0.5 (2023-04-12) 15 | 16 | - Upgrades Meteor version to 2.11.0 17 | - Upgrades all dependencies to latest. 18 | - Adding Dockerfile to create container image to run app 19 | - Adding [zcloud.ws](https://zcloud.ws) instructions to deploy 20 | - Adding [Meteor up](https://meteor-up.com/) config example to running using [zcloud.ws](https://zcloud.ws) images 21 | 22 | ## 0.0.4 (2023-02-27) 23 | 24 | - Upgrades Meteor version to 2.10. 25 | - Upgrades all dependencies to latest. 26 | - Includes conditional router rendering supporting: 27 | - Pages for authenticated people only (LoggedLayout) 28 | - Pages for anonymous people only (AnonymousLayout) 29 | - Pages for all people (PublicLayout) 30 | 31 | ## 0.0.3 (2022-01-28) 32 | 33 | - Upgrades Meteor version to 2.6-rc.1 from 2.5.5. 34 | 35 | ## 0.0.2 (2022-01-19) 36 | 37 | - Fixes tailwind.config.js: it was purging wrong paths. 38 | - Upgrades Meteor version to 2.5.5 from 2.5.3. 39 | - Fixes app name in passwordless emails. 40 | 41 | ## 0.0.1 (2022-01-17) 42 | 43 | - Initial version. 44 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM zcloudws/meteor-build:3.1.2 as builder 2 | 3 | WORKDIR /build/source 4 | USER root 5 | 6 | RUN chown zcloud:zcloud -R /build 7 | 8 | USER zcloud 9 | COPY --chown=zcloud:zcloud . /build/source 10 | 11 | ENV METEOR_DISABLE_OPTIMISTIC_CACHING=1 12 | 13 | # --legacy-peer-deps because of react-error-boundary 14 | RUN meteor npm i --no-audit --legacy-peer-deps && meteor build --platforms web.browser,web.cordova --directory ../app-build 15 | 16 | FROM zcloudws/meteor-node-mongodb-runtime:3.1.2-with-tools 17 | 18 | COPY --from=builder /build/app-build/bundle /home/zcloud/app 19 | 20 | # --legacy-peer-deps because of react-error-boundary 21 | RUN cd /home/zcloud/app/programs/server && npm i --no-audit --legacy-peer-deps 22 | 23 | WORKDIR /home/zcloud/app 24 | 25 | ENTRYPOINT ["/scripts/startup.sh"] 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Meteor template by quave 2 | 3 | [quave.dev](https://www.quave.dev) 4 | 5 | Start your Meteor project with this template if you want to use React and TailwindCSS. 6 | 7 | ## What is it? 8 | 9 | It's a template project ready for you to implement your business idea. It includes: 10 | - sign-up and sign-in using email (passwordless authentication) 11 | - router setup 12 | - basic styles 13 | - in-app alert system 14 | - email system 15 | 16 | ## Dependencies 17 | 18 | ### Npm packages for React: 19 | - react 20 | - react-dom 21 | - react-router-dom 22 | 23 | ### Npm packages for TailwindCSS: 24 | - tailwindcss 25 | 26 | ### Meteor packages for React: 27 | - [react-meteor-data](https://github.com/meteor/react-packages/tree/master/packages/react-meteor-data) 28 | 29 | ### Meteor packages for MongoDB: 30 | - [quave:collections](https://github.com/quavedev/collections) 31 | 32 | ### Meteor packages for Authentication: 33 | - [quave:accounts-passwordless-react](https://github.com/quavedev/accounts-passwordless-react) 34 | - quave:alert-react-tailwind 35 | 36 | ### Meteor pacakges for Migrations: 37 | - [quave:migrations](https://github.com/quavedev/meteor-packages/tree/main/migrations) 38 | 39 | ### Meteor pacakges for Synced Cron: 40 | - [quave:synced-cron](https://github.com/quavedev/meteor-packages/tree/main/synced-cron) 41 | ### Meteor packages for Email: 42 | - [quave:email-postmark](https://github.com/quavedev/email-postmark) 43 | 44 | ### Meteor packages for Alerts: 45 | - quave:logged-user-react 46 | 47 | ### Other packages from Quave that are not included in the template yet: 48 | 49 | #### Meteor pacakges for File Uploads to AWS, CloudFlare and OCI: 50 | 51 | - [quave:slingshot](https://github.com/quavedev/meteor-packages/tree/main/meteor-slingshot) 52 | 53 | 54 | ## Set up your project 55 | 56 | ### Replace our placeholders 57 | 58 | #### Inform your app info 59 | - Fill the fields inside the settings in `public.appInfo` to make sure your app works properly. 60 | 61 | #### Sending emails with Postmark 62 | - Sign up for a [Postmark account](https://postmarkapp.com/signup) 63 | - replace the following property with your postmark API KEY in the settings: `YOUR_API_TOKEN` 64 | - Follow the steps to verify your domain in their website 65 | - replace the following property with your desired `from` for the emails in the settings: `YOUR_FROM_EMAIL@yourdomain.com` 66 | 67 | ## Updating your project 68 | 69 | If you want to keep your project up-to-date with the changes made here, read our [CHANGELOG](CHANGELOG.md). 70 | -------------------------------------------------------------------------------- /app/access/Access.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useNavigate } from 'react-router-dom'; 3 | import { Passwordless } from 'meteor/quave:accounts-passwordless-react'; 4 | 5 | import { RoutePaths } from '../general/RoutePaths'; 6 | 7 | export function Access() { 8 | const navigate = useNavigate(); 9 | 10 | const onEnterToken = () => { 11 | navigate(RoutePaths.PRIVATE); 12 | }; 13 | 14 | return ( 15 |
16 |

17 | Access 18 |

19 | 20 | navigate(RoutePaths.HOME)} 22 | className="mt-5 cursor-pointer text-base font-medium text-indigo-700 hover:text-indigo-600" 23 | > 24 | Back to Home 25 | 26 |
27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /app/clicks/ClicksCollection.js: -------------------------------------------------------------------------------- 1 | import { createCollection } from 'meteor/quave:collections'; 2 | import SimpleSchema from 'simpl-schema'; 3 | 4 | const ClickSchema = new SimpleSchema({ 5 | count: { 6 | type: SimpleSchema.Integer, 7 | defaultValue: 0, 8 | }, 9 | }); 10 | 11 | export const ClicksCollection = createCollection({ 12 | name: 'clicks', 13 | schema: ClickSchema, 14 | helpers: { 15 | getCountText() { 16 | return `${this.count}x`; 17 | }, 18 | }, 19 | }); 20 | -------------------------------------------------------------------------------- /app/clicks/clicksMethods.js: -------------------------------------------------------------------------------- 1 | import { Meteor } from 'meteor/meteor'; 2 | import { ClicksCollection } from './ClicksCollection'; 3 | import { UsersCollection } from '../users/UsersCollection'; 4 | 5 | Meteor.methods({ 6 | 'clicks.increment': async function incrementCount() { 7 | await ClicksCollection.upsertAsync({}, { $inc: { count: 1 } }); 8 | }, 9 | 'clicks.doubleIncrementGmail': async function incrementCount() { 10 | const { userId } = this; 11 | const user = await UsersCollection.findOneAsync(userId); 12 | if (!user?.emails[0].address.includes('@gmail')) { 13 | throw new Meteor.Error('Not Allowed', 'Only for gmail users'); 14 | } 15 | await ClicksCollection.upsertAsync({}, { $inc: { count: 2 } }); 16 | }, 17 | 'clicks.invalidUpdate': async function incrementCount() { 18 | await ClicksCollection.upsertAsync({}, { $set: { count: 'not a number' } }); 19 | }, 20 | }); 21 | -------------------------------------------------------------------------------- /app/clicks/clicksPublishes.js: -------------------------------------------------------------------------------- 1 | import { Meteor } from 'meteor/meteor'; 2 | import { ClicksCollection } from './ClicksCollection'; 3 | 4 | Meteor.publish('countData', () => ClicksCollection.find()); 5 | -------------------------------------------------------------------------------- /app/components/Button.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const buttonStyles = { 4 | primary: { 5 | bg: 'bg-indigo-600', 6 | hover: 'hover:bg-indigo-700', 7 | text: 'text-white', 8 | border: 'border-transparent', 9 | }, 10 | secondary: { 11 | bg: 'bg-white', 12 | hover: 'hover:bg-gray-50', 13 | text: 'text-indigo-600', 14 | border: 'border-indigo-600', 15 | }, 16 | tertiary: { 17 | bg: 'bg-gray-200', 18 | hover: 'hover:bg-gray-300', 19 | text: 'text-gray-800', 20 | border: 'border-gray-800', 21 | }, 22 | }; 23 | 24 | export const Button = ({ 25 | primary = false, 26 | secondary = false, 27 | tertiary = false, 28 | children, 29 | ...rest 30 | }) => { 31 | const getButtonStyle = () => { 32 | if (primary) return buttonStyles.primary; 33 | if (secondary) return buttonStyles.secondary; 34 | if (tertiary) return buttonStyles.tertiary; 35 | return buttonStyles.primary; 36 | }; 37 | 38 | const style = getButtonStyle(); 39 | 40 | return ( 41 | 47 | ); 48 | }; 49 | -------------------------------------------------------------------------------- /app/components/ErrorFallback.js: -------------------------------------------------------------------------------- 1 | import { useNavigate } from 'react-router-dom'; 2 | import { Meteor } from 'meteor/meteor'; 3 | import { RoutePaths } from '../general/RoutePaths'; 4 | import React from 'react'; 5 | 6 | export function ErrorFallback({ error, resetErrorBoundary }) { 7 | const navigate = useNavigate(); 8 | 9 | return ( 10 |
11 | {Meteor.isDevelopment && ( 12 |
13 |

DEV ONLY!

14 |

Something went wrong:

15 |
{error.message}
16 | 17 |
18 | )} 19 |
20 |
21 | { 23 | resetErrorBoundary(); 24 | navigate(RoutePaths.HOME); 25 | }} 26 | > 27 | Home 28 | 29 |
30 |
An error happened. Contact support please!
31 |
32 |
33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /app/components/Loading.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export function Loading({ name }) { 4 | if (name) { 5 | // eslint-disable-next-line no-console 6 | console.log('Loading', name); 7 | } 8 | 9 | return ( 10 |
11 |
12 | loading 13 |
14 |
15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /app/components/MyAlert.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | 3 | export const MyAlert = ({ message, isOpen, clear, autoCloseIn = 3_000 }) => { 4 | useEffect(() => { 5 | if (autoCloseIn && isOpen) { 6 | const timer = setTimeout(() => { 7 | clear(); 8 | }, autoCloseIn); 9 | 10 | return () => clearTimeout(timer); 11 | } 12 | return () => {}; 13 | }, [isOpen, clear]); 14 | 15 | if (!message || !isOpen) return null; 16 | 17 | return ( 18 |
19 |
20 |
21 |

{message}

22 | 35 |
36 |
37 |
38 | ); 39 | }; 40 | -------------------------------------------------------------------------------- /app/general/App.js: -------------------------------------------------------------------------------- 1 | import React, { Suspense } from 'react'; 2 | import { BrowserRouter } from 'react-router-dom'; 3 | import { Router } from './Router'; 4 | import { AlertProvider, Alert } from 'meteor/quave:alert-react-tailwind'; 5 | import { Loading } from '../components/Loading'; 6 | import { PageWithHeader } from '../layouts/PageWithHeader'; 7 | import { MyAlert } from '../components/MyAlert'; 8 | 9 | export function App() { 10 | return ( 11 | 12 | 15 | 16 | 17 | } 18 | > 19 | 20 |
21 | 22 | 23 |
24 |
25 |
26 |
27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /app/general/NotFound.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { RoutePaths } from './RoutePaths'; 3 | import { useNavigate } from 'react-router-dom'; 4 | 5 | export function NotFound() { 6 | const navigate = useNavigate(); 7 | return ( 8 |
9 |

10 | Page not found. 11 |

12 | 15 |
16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /app/general/RoutePaths.js: -------------------------------------------------------------------------------- 1 | export const RoutePaths = { 2 | HOME: '/', 3 | ACCESS: '/access', 4 | PRIVATE: '/private', 5 | }; 6 | -------------------------------------------------------------------------------- /app/general/Router.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Route, Routes } from 'react-router-dom'; 3 | 4 | import { RoutePaths } from './RoutePaths'; 5 | import { Home } from '../home/Home'; 6 | import { Access } from '../access/Access'; 7 | import { NotFound } from './NotFound'; 8 | import { Private } from '../private/Private'; 9 | import { PublicLayout } from '../layouts/PublicLayout'; 10 | import { AnonymousLayout } from '../layouts/AnonymousLayout'; 11 | import { LoggedLayout } from '../layouts/LoggedLayout'; 12 | 13 | export function Router() { 14 | return ( 15 | 16 | 20 | 21 | 22 | } 23 | /> 24 | 28 | 29 | 30 | } 31 | /> 32 | 36 | 37 | 38 | } 39 | /> 40 | 44 | 45 | 46 | } 47 | /> 48 | 49 | ); 50 | } 51 | -------------------------------------------------------------------------------- /app/home/Home.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Meteor } from 'meteor/meteor'; 3 | import { useNavigate } from 'react-router-dom'; 4 | import { useLoggedUser } from 'meteor/quave:logged-user-react'; 5 | import { RoutePaths } from '../general/RoutePaths'; 6 | import { useSubscribe, useFind } from 'meteor/react-meteor-data'; 7 | import { ClicksCollection } from '../clicks/ClicksCollection'; 8 | import { useAlert } from 'meteor/quave:alert-react-tailwind'; 9 | import { Button } from '../components/Button'; 10 | 11 | export function Home() { 12 | const { openAlert } = useAlert(); 13 | const navigate = useNavigate(); 14 | const { loggedUser, isLoadingLoggedUser } = useLoggedUser(); 15 | 16 | useSubscribe('countData'); 17 | 18 | const documents = useFind(() => ClicksCollection.find(), []); 19 | 20 | const onCount = async () => { 21 | try { 22 | await Meteor.callAsync('clicks.increment'); 23 | openAlert('Incremented!'); 24 | } catch (e) { 25 | openAlert(e.reason); 26 | } 27 | }; 28 | const onDouble = async () => { 29 | try { 30 | await Meteor.callAsync('clicks.doubleIncrementGmail'); 31 | openAlert('Incremented by 2!'); 32 | } catch (e) { 33 | openAlert(e.reason); 34 | } 35 | }; 36 | 37 | const onInvalidUpdate = async () => { 38 | try { 39 | await Meteor.callAsync('clicks.invalidUpdate'); 40 | } catch (e) { 41 | openAlert(e.reason); 42 | } 43 | }; 44 | 45 | const clickDocument = documents[0]; 46 | return ( 47 |
48 |
49 |

50 | 51 | Ready to use Meteor{' '} 52 | {window.__meteor_runtime_config__.meteorRelease.replace( 53 | 'METEOR@', 54 | '' 55 | )} 56 | ? 57 | 58 | Template by quave 59 |

60 |
61 |
62 |
63 |
Clicks
64 |
65 | {clickDocument?.getCountText() || 'No clicks yet'} 66 |
67 | {clickDocument && ( 68 |
69 | (I'm using a collection helper) 70 |
71 | )} 72 |
73 | 74 | {loggedUser && ( 75 | 78 | )} 79 | 82 |
83 |
84 | coming from MongoDB clicks{' '} 85 | collection 86 |
87 | {loggedUser && ( 88 | <> 89 |
90 | Logged as {loggedUser.emails[0].address} 91 |
92 | 95 | 96 | )} 97 | 99 | loggedUser ? Meteor.logout() : navigate(RoutePaths.ACCESS) 100 | } 101 | className={`cursor-pointer text-base font-medium text-indigo-700 hover:text-indigo-500 ${ 102 | isLoadingLoggedUser ? 'invisible' : '' 103 | }`} 104 | > 105 | {loggedUser ? 'Log out' : 'Access'} 106 | 107 | 108 |
109 |
110 | 111 | 123 |
124 | ); 125 | } 126 | -------------------------------------------------------------------------------- /app/infra/cron.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable newline-per-chained-call */ 2 | import { SyncedCron } from 'meteor/quave:synced-cron'; 3 | import { Meteor } from 'meteor/meteor'; 4 | 5 | SyncedCron.config({ 6 | log: true, 7 | }); 8 | 9 | function sleepSeconds(number) { 10 | return new Promise((resolve) => { 11 | setTimeout(resolve, number * 1000); 12 | }); 13 | } 14 | 15 | Meteor.startup(() => { 16 | SyncedCron.add({ 17 | name: 'I run every 1 minute for 10 seconds', 18 | schedule: (parser) => parser.text('every 1 minute'), 19 | job: async () => { 20 | // eslint-disable-next-line no-console 21 | console.log(`I'll start now ${new Date()}`); 22 | await sleepSeconds(10); 23 | // eslint-disable-next-line no-console 24 | console.log(`I've finished now ${new Date()}`); 25 | }, 26 | onSuccess: async ({ intendedAt }) => { 27 | // eslint-disable-next-line no-console 28 | console.log( 29 | `cron job finished after persist in the database now ${new Date()}, I took ${new Date() - intendedAt}ms` 30 | ); 31 | }, 32 | onError: async ({ error, intendedAt }) => { 33 | console.error( 34 | `cron job errored after persist in the database now ${new Date()}, I took ${new Date() - intendedAt}ms`, 35 | error 36 | ); 37 | }, 38 | }); 39 | SyncedCron.start(); 40 | }); 41 | -------------------------------------------------------------------------------- /app/infra/migrations.js: -------------------------------------------------------------------------------- 1 | import { Meteor } from 'meteor/meteor'; 2 | import { Migrations } from 'meteor/quave:migrations'; 3 | 4 | Migrations.config({ 5 | log: true, 6 | }); 7 | 8 | Migrations.add({ 9 | version: 1, 10 | name: 'Not really migrating anything', 11 | up() { 12 | // eslint-disable-next-line no-console 13 | console.log("I'm a fake migration"); 14 | }, 15 | }); 16 | 17 | Meteor.startup(() => { 18 | Migrations.migrateTo('latest'); 19 | }); 20 | -------------------------------------------------------------------------------- /app/layouts/AnonymousLayout.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ConditionalLayout } from './ConditionalLayout'; 3 | 4 | export function AnonymousLayout({ children }) { 5 | return {children}; 6 | } 7 | -------------------------------------------------------------------------------- /app/layouts/ConditionalLayout.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useLoggedUser } from 'meteor/quave:logged-user-react'; 3 | import { Navigate } from 'react-router-dom'; 4 | import { ErrorBoundary } from 'react-error-boundary'; 5 | import { RoutePaths } from '../general/RoutePaths'; 6 | import { Loading } from '../components/Loading'; 7 | import { ErrorFallback } from '../components/ErrorFallback'; 8 | import { PageWithHeader } from './PageWithHeader'; 9 | import { PageWithoutHeader } from './PageWithoutHeader'; 10 | 11 | function InnerLayout({ children, onlyLogged, onlyAnonymous }) { 12 | const { loggedUser, isLoadingLoggedUser } = useLoggedUser(); 13 | 14 | if (isLoadingLoggedUser) { 15 | // you should return here your default skeleton layout 16 | return ( 17 | 18 | 19 | 20 | ); 21 | } 22 | 23 | if (onlyLogged) { 24 | if (!loggedUser) { 25 | return ; 26 | } 27 | return {children}; 28 | } 29 | 30 | if (onlyAnonymous) { 31 | if (loggedUser) { 32 | return ; 33 | } 34 | return {children}; 35 | } 36 | 37 | return {children}; 38 | } 39 | 40 | export function ConditionalLayout({ ...props }) { 41 | return ( 42 | 43 | 44 | 45 | ); 46 | } 47 | -------------------------------------------------------------------------------- /app/layouts/LoggedLayout.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ConditionalLayout } from './ConditionalLayout'; 3 | 4 | export function LoggedLayout({ children }) { 5 | return {children}; 6 | } 7 | -------------------------------------------------------------------------------- /app/layouts/PageWithHeader.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export function PageWithHeader({ children }) { 4 | return
{children}
; 5 | } 6 | -------------------------------------------------------------------------------- /app/layouts/PageWithoutHeader.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export function PageWithoutHeader({ children }) { 4 | return ( 5 |
6 | {children} 7 |
8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /app/layouts/PublicLayout.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ConditionalLayout } from './ConditionalLayout'; 3 | 4 | export function PublicLayout({ children }) { 5 | return {children}; 6 | } 7 | -------------------------------------------------------------------------------- /app/lib/tailwind/safelist.txt: -------------------------------------------------------------------------------- 1 | -mx-1.5 2 | -my-1.5 3 | appearance-none 4 | bg-green-100 5 | bg-green-50 6 | bg-indigo-600 7 | bg-red-50 8 | bg-white 9 | block 10 | border 11 | border-gray-300 12 | border-transparent 13 | cursor-pointer 14 | flex 15 | focus:border-indigo-500 16 | focus:outline-none 17 | focus:ring-2 18 | focus:ring-indigo-500 19 | focus:ring-offset-2 20 | focus:ring-offset-red-50 21 | focus:ring-red-600 22 | font-medium 23 | hover:bg-indigo-700 24 | hover:bg-red-100 25 | hover:text-indigo-500 26 | inline-flex 27 | justify-center 28 | justify-end 29 | ml-auto 30 | mt-0 31 | mt-1 32 | mt-8 33 | p-1.5 34 | p-4 35 | pl-3 36 | placeholder-gray-400 37 | px-3 38 | px-4 39 | py-2 40 | py-8 41 | ring-green-600 42 | ring-offset-green-50 43 | rounded-md 44 | shadow 45 | shadow-sm 46 | sm:max-w-md 47 | sm:mx-auto 48 | sm:px-10 49 | sm:rounded-lg 50 | sm:text-sm 51 | sm:w-full 52 | space-y-6 53 | sr-only 54 | text-gray-700 55 | text-green-500 56 | text-green-800 57 | text-indigo-600 58 | text-red-500 59 | text-red-800 60 | text-sm 61 | text-white 62 | w-full -------------------------------------------------------------------------------- /app/private/Private.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useNavigate } from 'react-router-dom'; 3 | 4 | import { RoutePaths } from '../general/RoutePaths'; 5 | 6 | export function Private() { 7 | const navigate = useNavigate(); 8 | 9 | const goHome = () => { 10 | navigate(RoutePaths.HOME); 11 | }; 12 | 13 | return ( 14 |
15 |

16 | You are in the private page 17 |
18 | 22 | Back to Home 23 | 24 |
25 |

26 |
27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /app/users/UsersCollection.js: -------------------------------------------------------------------------------- 1 | import { createCollection } from 'meteor/quave:collections'; 2 | 3 | export const UsersCollection = createCollection({ name: 'users' }); 4 | -------------------------------------------------------------------------------- /client/main.css: -------------------------------------------------------------------------------- 1 | @import 'tailwindcss'; 2 | -------------------------------------------------------------------------------- /client/main.html: -------------------------------------------------------------------------------- 1 | 2 | Meteor Template by quave 3 | 4 | 5 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | -------------------------------------------------------------------------------- /client/main.js: -------------------------------------------------------------------------------- 1 | import React, { StrictMode } from 'react'; 2 | import { Meteor } from 'meteor/meteor'; 3 | import { createRoot } from 'react-dom/client'; 4 | import { App } from '../app/general/App'; 5 | 6 | Meteor.startup(() => { 7 | const root = createRoot(document.getElementById('app')); 8 | root.render( 9 | 10 | 11 | 12 | ); 13 | }); 14 | -------------------------------------------------------------------------------- /lefthook.yml: -------------------------------------------------------------------------------- 1 | pre-commit: 2 | commands: 3 | quave-check: 4 | run: npm run quave-check 5 | update-index: 6 | run: git update-index --again 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "meteor-template", 3 | "private": true, 4 | "scripts": { 5 | "start": "meteor --exclude-archs web.browser.legacy,web.cordova --settings private/env/dev/settings.json", 6 | "quave-eslint": "eslint . --fix", 7 | "quave-prettier": "prettier --write \"**/*.js\"", 8 | "quave-check": "npm run quave-eslint && npm run quave-prettier", 9 | "test": "exit 0" 10 | }, 11 | "dependencies": { 12 | "@babel/runtime": "^7.25.6", 13 | "meteor-node-stubs": "^1.2.10", 14 | "prom-client": "^15.1.3", 15 | "react": "^19.0.0-rc-04bd67a4-20240924", 16 | "react-dom": "^19.0.0-rc-04bd67a4-20240924", 17 | "react-error-boundary": "^4.0.13", 18 | "react-router-dom": "^6.26.2", 19 | "simpl-schema": "^3.4.6" 20 | }, 21 | "meteor": { 22 | "mainModule": { 23 | "client": "client/main.js", 24 | "server": "server/main.js" 25 | } 26 | }, 27 | "devDependencies": { 28 | "@quave/eslint-config-quave": "^3.0.0", 29 | "@tailwindcss/postcss": "^4.0.0", 30 | "@types/meteor": "^2.9.8", 31 | "babel-plugin-react-compiler": "^0.0.0-experimental-6067d4e-20240924", 32 | "eslint-plugin-jest": "^28.8.3", 33 | "eslint-plugin-react-compiler": "^0.0.0-experimental-92aaa43-20240924", 34 | "lefthook": "^1.7.16", 35 | "postcss": "^8.5.1", 36 | "postcss-load-config": "^6.0.1", 37 | "tailwindcss": "^4.0.0" 38 | }, 39 | "eslintConfig": { 40 | "extends": [ 41 | "@quave/quave" 42 | ], 43 | "plugins": [ 44 | "eslint-plugin-react-compiler" 45 | ], 46 | "rules": { 47 | "react-compiler/react-compiler": "error", 48 | "react/jsx-filename-extension": "off" 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-default-export */ 2 | export default { 3 | plugins: { 4 | "@tailwindcss/postcss": {}, 5 | }, 6 | }; -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = require('@quave/eslint-config-quave/prettier.config'); 2 | -------------------------------------------------------------------------------- /private/env/dev/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "public": { 3 | "appInfo": { 4 | "name": "Meteor Template by quave" 5 | } 6 | }, 7 | "packages": { 8 | "quave:email-postmark": { 9 | "from": "media@codeftw.dev", 10 | "apiToken": "abc" 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /server/main.js: -------------------------------------------------------------------------------- 1 | import { Meteor } from 'meteor/meteor'; 2 | import { Accounts } from 'meteor/accounts-base'; 3 | 4 | import '../app/infra/migrations'; 5 | import '../app/infra/cron'; 6 | 7 | import '../app/clicks/clicksMethods'; 8 | import '../app/clicks/clicksPublishes'; 9 | 10 | import './rest'; 11 | 12 | Accounts.emailTemplates.siteName = 13 | Meteor.settings?.public?.appInfo?.name || process.env.ROOT_URL; 14 | 15 | Meteor.startup(() => {}); 16 | -------------------------------------------------------------------------------- /server/metrics.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | import { collectDefaultMetrics, register } from 'prom-client'; 3 | import { WebApp } from 'meteor/webapp'; 4 | 5 | const authMetrics = (req, res, next) => { 6 | const authUser = process.env.AUTH_USER || 'user'; 7 | const authPasswd = process.env.AUTH_PASSWD || 'passwd'; 8 | const b64auth = (req.headers.authorization || '').split(' ')[1] || ''; 9 | const [login, password] = Buffer.from(b64auth, 'base64') 10 | .toString() 11 | .split(':'); 12 | 13 | if (authUser && authPasswd && login === authUser && password === authPasswd) { 14 | next(); 15 | return; 16 | } 17 | res.setHeader('WWW-Authenticate', 'Basic realm="401"'); 18 | res.writeHead(401); 19 | res.end('Authentication required.'); 20 | }; 21 | 22 | export const registerMetrics = ({ path, useAuth }) => { 23 | collectDefaultMetrics({ timeout: 5000 }); 24 | console.log({ useAuth }); 25 | if (useAuth) { 26 | WebApp.handlers.get(path, authMetrics); 27 | } 28 | WebApp.handlers.get(path, async (req, res) => { 29 | const promClientMetrics = await register.metrics(); 30 | res.end(promClientMetrics); 31 | }); 32 | }; 33 | -------------------------------------------------------------------------------- /server/rest.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | import { registerMetrics } from './metrics'; 3 | import { WebApp } from 'meteor/webapp'; 4 | import { ClicksCollection } from '../app/clicks/ClicksCollection'; 5 | 6 | registerMetrics({ 7 | path: '/api/metrics', 8 | useAuth: process.env.USE_METRICS_AUTH, 9 | }); 10 | 11 | let count = 0; 12 | 13 | const fibonacci = (num) => { 14 | if (num <= 1) { 15 | if (count++ % 100 === 0) { 16 | console.log(`fibonacci temp ${num}`); 17 | count = 0; 18 | } 19 | return num; 20 | } 21 | return fibonacci(num - 1) + fibonacci(num - 2); 22 | }; 23 | 24 | WebApp.handlers.get('/api/load-fibonacci', (req, res) => { 25 | res.set('Content-type', 'application/json'); 26 | const { num, timing } = req.query; 27 | if (!num) { 28 | res.status(400).send( 29 | JSON.stringify({ 30 | status: 'error', 31 | message: 'num query param is required', 32 | }) 33 | ); 34 | return; 35 | } 36 | const start = new Date(); 37 | console.log(`fibonacci ${num}`); 38 | const fib = fibonacci(num); 39 | const message = `fibonacci ${num}=${fib}, ${new Date() - start}ms`; 40 | console.log(message); 41 | 42 | if (timing) { 43 | res.status(200).send(JSON.stringify({ status: 'success', message })); 44 | return; 45 | } 46 | 47 | res.set('Content-type', 'application/json'); 48 | res.status(200).send(JSON.stringify({ status: 'success' })); 49 | }); 50 | 51 | WebApp.handlers.get('/api/load-data', async (req, res) => { 52 | res.set('Content-type', 'application/json'); 53 | const { num, timing } = req.query; 54 | if (!num) { 55 | res.status(400).send( 56 | JSON.stringify({ 57 | status: 'error', 58 | message: 'num query param is required', 59 | }) 60 | ); 61 | return; 62 | } 63 | const start = new Date(); 64 | console.log(`data ${num}`); 65 | let clicks = (await ClicksCollection.findOneAsync()) || { counts: 0 }; 66 | 67 | const loopSize = Array.from(Array(+num).keys()); 68 | console.log('loopSize', loopSize); 69 | 70 | // eslint-disable-next-line no-restricted-syntax 71 | for await (const i of loopSize) { 72 | await ClicksCollection.upsertAsync({}, { $inc: { count: 1 } }); 73 | clicks = (await ClicksCollection.findOneAsync()) || { counts: 0 }; 74 | console.log(`iteration ${i}=${clicks.count}`); 75 | } 76 | const message = `data ${num}=${clicks.count}, ${new Date() - start}ms`; 77 | console.log(message); 78 | 79 | if (timing) { 80 | res.status(200).send(JSON.stringify({ status: 'success', message })); 81 | return; 82 | } 83 | 84 | res.set('Content-type', 'application/json'); 85 | res.status(200).send(JSON.stringify({ status: 'success' })); 86 | }); 87 | 88 | WebApp.handlers.get('/api', (req, res) => { 89 | res.set('Content-type', 'application/json'); 90 | res.status(200).send(JSON.stringify({ status: 'success' })); 91 | }); 92 | --------------------------------------------------------------------------------