├── .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 |
4 |
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 |
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 |
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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------