├── .meteor
├── .gitignore
├── release
├── platforms
├── .id
├── .finished-upgraders
├── packages
└── versions
├── tests
├── main.js
└── helpers.js
├── README-Assets
├── sign_in_dark.png
├── task_example.png
├── sign_in_light.png
└── project_structure.png
├── .meteorignore
├── api
├── tasks
│ ├── tasks.js
│ ├── tasks.publications.js
│ ├── tasks.publications.tests.js
│ ├── tasks.methods.js
│ └── tasks.methods.tests.js
├── main.js
├── lib
│ └── auth.js
└── db
│ └── migrations.js
├── ui
├── pages
│ ├── not-found
│ │ └── not-found-page.jsx
│ ├── tasks
│ │ ├── hooks
│ │ │ ├── use-task-item.jsx
│ │ │ ├── use-tasks.jsx
│ │ │ └── use-task-form.jsx
│ │ ├── components
│ │ │ ├── task-form.jsx
│ │ │ └── task-item.jsx
│ │ └── tasks-page.jsx
│ └── auth
│ │ ├── hooks
│ │ └── use-login.jsx
│ │ └── sign-in-page.jsx
├── common
│ └── components
│ │ ├── logout.jsx
│ │ ├── layout.jsx
│ │ ├── loading.jsx
│ │ ├── ui-provider.jsx
│ │ ├── navbar.jsx
│ │ └── footer.jsx
├── main.jsx
├── main.html
└── routes.jsx
├── rspack.config.js
├── private
└── settings.json
├── e2e
├── home.spec.js
└── auth.spec.js
├── LICENSE
├── .gitignore
├── package.json
├── playwright.config.js
├── biome.json
├── README.md
└── tests-examples
└── demo-todo-app.spec.js
/.meteor/.gitignore:
--------------------------------------------------------------------------------
1 | local
2 |
--------------------------------------------------------------------------------
/.meteor/release:
--------------------------------------------------------------------------------
1 | METEOR@3.4-beta.12
2 |
--------------------------------------------------------------------------------
/.meteor/platforms:
--------------------------------------------------------------------------------
1 | server
2 | browser
3 |
--------------------------------------------------------------------------------
/tests/main.js:
--------------------------------------------------------------------------------
1 | import '/api/tasks/tasks.methods.tests.js';
2 | import '/api/tasks/tasks.publications.tests';
3 |
--------------------------------------------------------------------------------
/README-Assets/sign_in_dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fredmaiaarantes/simpletasks/HEAD/README-Assets/sign_in_dark.png
--------------------------------------------------------------------------------
/README-Assets/task_example.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fredmaiaarantes/simpletasks/HEAD/README-Assets/task_example.png
--------------------------------------------------------------------------------
/README-Assets/sign_in_light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fredmaiaarantes/simpletasks/HEAD/README-Assets/sign_in_light.png
--------------------------------------------------------------------------------
/README-Assets/project_structure.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fredmaiaarantes/simpletasks/HEAD/README-Assets/project_structure.png
--------------------------------------------------------------------------------
/.meteorignore:
--------------------------------------------------------------------------------
1 | # Playwright files and directories
2 | playwright-report/
3 | test-results/
4 | e2e/
5 | node_modules/@playwright/
6 | node_modules/playwright/
--------------------------------------------------------------------------------
/tests/helpers.js:
--------------------------------------------------------------------------------
1 | import { Meteor } from 'meteor/meteor';
2 |
3 | export function mockLoggedUserId(userId) {
4 | Meteor.userId = () => userId;
5 | }
6 |
7 | export function getMeteorPublication(name) {
8 | return Meteor.server.publish_handlers[name].apply({});
9 | }
10 |
--------------------------------------------------------------------------------
/api/tasks/tasks.js:
--------------------------------------------------------------------------------
1 | import { Mongo } from 'meteor/mongo';
2 |
3 | export const Tasks = new Mongo.Collection('tasks');
4 |
5 | const schema = {
6 | _id: String,
7 | description: String,
8 | done: Boolean,
9 | createdAt: Date,
10 | userId: String,
11 | };
12 |
13 | Tasks.attachSchema(schema);
14 |
--------------------------------------------------------------------------------
/.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 | j3mxcj0j3jfj.pzmhdxgvur2
8 |
--------------------------------------------------------------------------------
/api/main.js:
--------------------------------------------------------------------------------
1 | import { Migrations } from 'meteor/quave:migrations';
2 | import { Meteor } from 'meteor/meteor';
3 |
4 | import './db/migrations';
5 | import './tasks/tasks.publications';
6 | import './tasks/tasks.methods';
7 |
8 | /**
9 | * This is the server-side entry point
10 | */
11 | Meteor.startup(() => {
12 | Migrations.migrateTo('latest').catch((e) =>
13 | console.error('Error running migrations', e)
14 | );
15 | });
16 |
--------------------------------------------------------------------------------
/api/tasks/tasks.publications.js:
--------------------------------------------------------------------------------
1 | import { Meteor } from 'meteor/meteor';
2 | import { Tasks } from './tasks';
3 |
4 | /**
5 | Finds tasks belonging to the logged-in user.
6 | @function findTasksByLoggedUser
7 | @returns {Mongo.Cursor} - The cursor containing the tasks found.
8 | */
9 | function findTasksByLoggedUser() {
10 | return Tasks.find({ userId: Meteor.userId() });
11 | }
12 |
13 | Meteor.publish('tasksByLoggedUser', findTasksByLoggedUser);
14 |
--------------------------------------------------------------------------------
/ui/pages/not-found/not-found-page.jsx:
--------------------------------------------------------------------------------
1 | import { Flex, Heading, Stack } from '@chakra-ui/react';
2 | import React from 'react';
3 |
4 | export default function NotFoundPage() {
5 | return (
6 |
7 |
8 |
9 | There's nothing here!
10 |
11 |
12 |
13 | );
14 | }
15 |
--------------------------------------------------------------------------------
/rspack.config.js:
--------------------------------------------------------------------------------
1 | import { defineConfig } from '@meteorjs/rspack';
2 |
3 | /**
4 | * Rspack configuration for Meteor projects.
5 | *
6 | * Provides typed flags on the `Meteor` object, such as:
7 | * - `Meteor.isClient` / `Meteor.isServer`
8 | * - `Meteor.isDevelopment` / `Meteor.isProduction`
9 | * - …and other flags available
10 | *
11 | * Use these flags to adjust your build settings based on environment.
12 | */
13 | export default defineConfig(Meteor => {
14 | return {};
15 | });
16 |
--------------------------------------------------------------------------------
/private/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "public": {
3 | "appInfo": {
4 | "name": "Charm - Chakra-UI, React, Meteor"
5 | }
6 | },
7 | "packages": {
8 | "service-configuration": {
9 | "github": {
10 | "loginStyle": "popup",
11 | "clientId": "REPLACE_WITH_YOUR_CLIENT_ID",
12 | "secret": "REPLACE_WITH_YOUR_SECRET"
13 | }
14 | }
15 | },
16 | "galaxy.meteor.com": {
17 | "env": {
18 | "ROOT_URL": "REPLACE_WITH_YOUR_DOMAIN",
19 | "MONGO_URL": "REPLACE_WITH_YOUR_MONGO_URL"
20 | }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/ui/common/components/logout.jsx:
--------------------------------------------------------------------------------
1 | import { Button } from '@chakra-ui/react';
2 | import { Meteor } from 'meteor/meteor';
3 | import { useUserId } from 'meteor/react-meteor-accounts';
4 | import React from 'react';
5 | import { useNavigate } from 'react-router-dom';
6 |
7 | export function Logout() {
8 | const userId = useUserId();
9 | const navigate = useNavigate();
10 |
11 | const logout = () => {
12 | Meteor.logout(() => {
13 | navigate('/');
14 | });
15 | };
16 |
17 | return (
18 | <>
19 | {userId && (
20 |
23 | )}
24 | >
25 | );
26 | }
27 |
--------------------------------------------------------------------------------
/ui/common/components/layout.jsx:
--------------------------------------------------------------------------------
1 | import { Box } from '@chakra-ui/react';
2 | import { useUserId } from 'meteor/react-meteor-accounts';
3 | import React from 'react';
4 | import { Navigate } from 'react-router-dom';
5 | import { routes } from '../../routes';
6 | import { Footer } from './footer';
7 | import { Navbar } from './navbar';
8 |
9 | export function Layout({ loggedOnly = true, children }) {
10 | const userId = useUserId();
11 | if (loggedOnly && !userId) {
12 | return ;
13 | }
14 |
15 | return (
16 | <>
17 |
18 |
19 | {children}
20 |
21 |
22 | >
23 | );
24 | }
25 |
--------------------------------------------------------------------------------
/ui/main.jsx:
--------------------------------------------------------------------------------
1 | import { Meteor } from 'meteor/meteor';
2 | import React, { Suspense } from 'react';
3 | import { createRoot } from 'react-dom/client';
4 | import { Loading } from './common/components/loading';
5 | import { UIProvider } from './common/components/ui-provider';
6 | import { Routes } from './routes';
7 |
8 | /**
9 | * This is the client-side entry point
10 | */
11 | Meteor.startup(() => {
12 | document.documentElement.setAttribute('lang', 'en');
13 | const container = document.getElementById('react-target');
14 | const root = createRoot(container);
15 | root.render(
16 |
17 | }>
18 |
19 |
20 |
21 | );
22 | });
23 |
--------------------------------------------------------------------------------
/ui/common/components/loading.jsx:
--------------------------------------------------------------------------------
1 | import {
2 | Box,
3 | Flex,
4 | Spinner,
5 | Stack,
6 | Text,
7 | useColorModeValue,
8 | } from '@chakra-ui/react';
9 | import React from 'react';
10 |
11 | export function Loading() {
12 | return (
13 |
14 |
15 |
16 |
17 |
21 | Loading...
22 |
23 |
24 |
25 |
26 |
27 |
28 | );
29 | }
30 |
--------------------------------------------------------------------------------
/ui/common/components/ui-provider.jsx:
--------------------------------------------------------------------------------
1 | import { ChakraProvider, ColorModeScript, extendTheme } from '@chakra-ui/react';
2 | import React from 'react';
3 |
4 | const customTheme = extendTheme({
5 | config: {
6 | initialColorMode: 'dark',
7 | useSystemColorMode: false,
8 | },
9 | });
10 |
11 | const toastOptions = {
12 | defaultOptions: {
13 | position: 'top-right',
14 | duration: 5000,
15 | isClosable: true,
16 | },
17 | };
18 |
19 | export function UIProvider({ children }) {
20 | return (
21 | <>
22 |
23 |
24 | {children}
25 |
26 | >
27 | );
28 | }
29 |
--------------------------------------------------------------------------------
/ui/pages/tasks/hooks/use-task-item.jsx:
--------------------------------------------------------------------------------
1 | import { useToast } from '@chakra-ui/react';
2 | import { Meteor } from 'meteor/meteor';
3 |
4 | export function useTaskItem() {
5 | const toast = useToast();
6 |
7 | async function onMarkAsDone(_id) {
8 | await Meteor.callAsync('toggleTaskDone', { taskId: _id });
9 | }
10 |
11 | async function onDelete(_id) {
12 | try {
13 | await Meteor.callAsync('removeTask', { taskId: _id });
14 | toast({
15 | title: 'Task removed.',
16 | status: 'success',
17 | });
18 | } catch (error) {
19 | toast({
20 | title: 'An error occurred.',
21 | description: error.message,
22 | status: 'error',
23 | });
24 | }
25 | }
26 |
27 | return {
28 | onMarkAsDone,
29 | onDelete,
30 | };
31 | }
32 |
--------------------------------------------------------------------------------
/e2e/home.spec.js:
--------------------------------------------------------------------------------
1 | // @ts-check
2 | import { test, expect } from '@playwright/test';
3 |
4 | test('has Simple Tasks title', async ({ page }) => {
5 | try {
6 | await page.goto('/');
7 | // Expect a title "to contain" a substring.
8 | await expect(page).toHaveTitle(/Simple Tasks/);
9 | } catch (error) {
10 | console.error('Test failed:', error);
11 | throw error;
12 | }
13 | });
14 |
15 | test('sign in page has sign in button', async ({ page }) => {
16 | try {
17 | await page.goto('/');
18 |
19 | // Check if the sign in button is visible
20 | const signInButton = page.getByRole('button', { name: 'Sign in' });
21 | await expect(signInButton).toBeVisible();
22 |
23 | } catch (error) {
24 | console.error('Test failed:', error);
25 | throw error;
26 | }
27 | });
--------------------------------------------------------------------------------
/api/lib/auth.js:
--------------------------------------------------------------------------------
1 | import { check } from 'meteor/check';
2 | import { Meteor } from 'meteor/meteor';
3 | import { Tasks } from '../tasks/tasks';
4 |
5 | /**
6 | * Check if user is logged in.
7 | * @throws Will throw an error if user is not logged in.
8 | */
9 | export function checkLoggedIn() {
10 | if (!Meteor.userId()) {
11 | throw new Meteor.Error('Error', 'Not authorized.');
12 | }
13 | }
14 |
15 | /**
16 | * Check if user is logged in and is the task owner.
17 | * @param {{ taskId: String }}
18 | * @throws Will throw an error if user is not logged in or is not the task owner.
19 | */
20 | export async function checkTaskOwner({ taskId }) {
21 | check(taskId, String);
22 | checkLoggedIn();
23 | const task = await Tasks.findOneAsync({
24 | _id: taskId,
25 | userId: Meteor.userId(),
26 | });
27 | if (!task) {
28 | throw new Meteor.Error('Error', 'Access denied.');
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/ui/pages/tasks/hooks/use-tasks.jsx:
--------------------------------------------------------------------------------
1 | import { useUserId } from 'meteor/react-meteor-accounts';
2 | import { useFind, useSubscribe } from 'meteor/react-meteor-data/suspense';
3 | import { useState } from 'react';
4 | import { Tasks } from '/api/tasks/tasks';
5 |
6 | export function useTasks() {
7 | useSubscribe('tasksByLoggedUser');
8 | const userId = useUserId();
9 | const [hideDone, setHideDone] = useState(false);
10 | const filter = hideDone ? { done: { $ne: true }, userId } : { userId };
11 |
12 | const tasks = useFind(
13 | Tasks,
14 | [filter, { sort: { createdAt: -1, description: -1 } }],
15 | [hideDone]
16 | );
17 | const count = useFind(Tasks, [{ userId }]).length;
18 | const pendingCount = useFind(Tasks, [{ done: { $ne: true }, userId }]).length;
19 |
20 | return {
21 | hideDone,
22 | setHideDone,
23 | tasks,
24 | pendingCount,
25 | count,
26 | };
27 | }
28 |
--------------------------------------------------------------------------------
/ui/main.html:
--------------------------------------------------------------------------------
1 |
2 | Simple Tasks - Charm (Chakra-UI, React, MeteorJS)
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/ui/pages/tasks/hooks/use-task-form.jsx:
--------------------------------------------------------------------------------
1 | import { useToast } from '@chakra-ui/react';
2 | import { zodResolver } from '@hookform/resolvers/zod/dist/zod';
3 | import { Meteor } from 'meteor/meteor';
4 | import { useForm } from 'react-hook-form';
5 | import * as z from 'zod';
6 |
7 | export function useTaskForm() {
8 | const toast = useToast();
9 | const schema = z.object({
10 | description: z.string().min(1, 'Task description is required'),
11 | });
12 |
13 | const { handleSubmit, register, reset, formState } = useForm({
14 | resolver: zodResolver(schema),
15 | });
16 |
17 | async function saveTask(values) {
18 | const description = values.description.trim();
19 | try {
20 | await Meteor.callAsync('insertTask', { description });
21 | reset();
22 | } catch (err) {
23 | const reason = err?.reason || 'Sorry, please try again.';
24 | toast({
25 | title: 'An error occurred.',
26 | description: reason,
27 | status: 'error',
28 | });
29 | }
30 | }
31 |
32 | return {
33 | saveTask,
34 | register,
35 | formState,
36 | handleSubmit,
37 | };
38 | }
39 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2021-2024 Frederico Maia
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/api/db/migrations.js:
--------------------------------------------------------------------------------
1 | import { Migrations } from 'meteor/quave:migrations';
2 | import { Accounts } from 'meteor/accounts-base';
3 | import { Tasks } from '../tasks/tasks';
4 |
5 | Migrations.add({
6 | version: 1,
7 | name: 'Add a seed username and password.',
8 | async up() {
9 | await Accounts.createUserAsync({
10 | username: 'fredmaia',
11 | password: 'abc123',
12 | });
13 | },
14 | });
15 |
16 | Migrations.add({
17 | version: 2,
18 | name: 'Add a few sample tasks.',
19 | async up() {
20 | const user = await Accounts.findUserByUsername('fredmaia');
21 | await Tasks.insertAsync({
22 | description: 'Install Node@20',
23 | done: false,
24 | userId: user._id,
25 | createdAt: new Date(2024, 1, 1),
26 | });
27 | await Tasks.insertAsync({
28 | description: 'Install Meteor.js 3.0',
29 | done: false,
30 | userId: user._id,
31 | createdAt: new Date(2024, 1, 2),
32 | });
33 | await Tasks.insertAsync({
34 | description: 'Clone this repository',
35 | done: false,
36 | userId: user._id,
37 | createdAt: new Date(2024, 1, 3),
38 | });
39 | },
40 | });
41 |
--------------------------------------------------------------------------------
/ui/routes.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { BrowserRouter, Routes as ReactRoutes, Route } from 'react-router-dom';
3 | import { Layout } from './common/components/layout';
4 |
5 | export const routes = {
6 | root: '/',
7 | notFound: '*',
8 | tasks: '/tasks',
9 | };
10 |
11 | const SignInPage = React.lazy(() => import('./pages/auth/sign-in-page'));
12 | const NotFoundPage = React.lazy(
13 | () => import('./pages/not-found/not-found-page')
14 | );
15 | const TasksPage = React.lazy(() => import('./pages/tasks/tasks-page'));
16 |
17 | export function Routes() {
18 | return (
19 |
20 |
21 |
24 |
25 |
26 | }
27 | index
28 | />
29 |
32 |
33 |
34 | }
35 | path={routes.tasks}
36 | />
37 |
40 |
41 |
42 | }
43 | path={routes.notFound}
44 | />
45 |
46 |
47 | );
48 | }
49 |
--------------------------------------------------------------------------------
/ui/pages/tasks/components/task-form.jsx:
--------------------------------------------------------------------------------
1 | import {
2 | Box,
3 | Button,
4 | FormControl,
5 | FormErrorMessage,
6 | Input,
7 | InputGroup,
8 | InputRightElement,
9 | } from '@chakra-ui/react';
10 | import React from 'react';
11 | import { useTaskForm } from '../hooks/use-task-form';
12 |
13 | export function TaskForm() {
14 | const {
15 | saveTask,
16 | handleSubmit,
17 | register,
18 | formState: { errors, isSubmitting },
19 | } = useTaskForm();
20 |
21 | return (
22 |
23 |
50 |
51 | );
52 | }
53 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 |
8 | private/settings.prod.json
9 | private/settings.dev.json
10 |
11 | # Runtime data
12 | pids
13 | *.pid
14 | *.seed
15 | *.pid.lock
16 |
17 | # Directory for instrumented libs generated by jscoverage/JSCover
18 | lib-cov
19 |
20 | # Coverage directory used by tools like istanbul
21 | coverage
22 |
23 | # nyc test coverage
24 | .nyc_output
25 |
26 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
27 | .grunt
28 |
29 | # Bower dependency directory (https://bower.io/)
30 | bower_components
31 |
32 | # node-waf configuration
33 | .lock-wscript
34 |
35 | # Compiled binary addons (https://nodejs.org/api/addons.html)
36 | build/Release
37 |
38 | # Dependency directories
39 | node_modules/
40 | jspm_packages/
41 |
42 | # TypeScript v1 declaration files
43 | typings/
44 |
45 | # Optional npm cache directory
46 | .npm
47 |
48 | # Optional REPL history
49 | .node_repl_history
50 |
51 | # Output of 'npm pack'
52 | *.tgz
53 |
54 | # Yarn Integrity file
55 | .yarn-integrity
56 |
57 | # dotenv environment variables file
58 | .env
59 |
60 | # webstorm
61 | **/.idea/*
62 | !.idea/runConfigurations
63 |
64 | **/.vscode/*
65 |
66 | jsconfig.json
67 |
68 | # Playwright
69 | /test-results/
70 | /playwright-report/
71 | /blob-report/
72 | /playwright/.cache/
73 |
74 | # Meteor Modern-Tools build context directories
75 | _build
76 | */build-assets
77 | */build-chunks
78 | .rsdoctor
79 |
80 | CLAUDE.md
--------------------------------------------------------------------------------
/.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.2.0-beta340.12 # The database Meteor supports right now
10 | reactive-var@1.0.13 # Reactive variable for tracker
11 |
12 | standard-minifier-css@1.9.3 # CSS minifier run for production mode
13 | es5-shim@4.8.1 # ECMAScript 5 compatibility for older browsers
14 | ecmascript@0.17.0-beta340.12 # Enable ECMAScript2015+ syntax in app code
15 | typescript@5.7.0-beta340.12 # Enable TypeScript syntax in .ts and .tsx modules
16 | shell-server@0.7.0-beta340.12 # Server-side component of the `meteor shell` command
17 | hot-module-replacement@0.5.4 # Update client in development without reloading the page
18 |
19 | static-html@1.4.0 # Define static page content in .html files
20 | dev-error-overlay@0.1.3
21 |
22 | accounts-password@3.2.2-beta340.12
23 | force-ssl@1.1.1
24 | meteortesting:mocha@3.2.0
25 | jam:easy-schema@1.4.0
26 | quave:migrations
27 | react-meteor-accounts@1.0.3
28 | react-meteor-data@3.0.0
29 | zodern:standard-minifier-js
30 | accounts-github@1.5.1
31 | service-configuration@1.3.5
32 | ddp-rate-limiter@1.2.2
33 | rspack@1.0.0-beta340.12
34 |
--------------------------------------------------------------------------------
/ui/pages/tasks/components/task-item.jsx:
--------------------------------------------------------------------------------
1 | import {
2 | Box,
3 | Button,
4 | Checkbox,
5 | HStack,
6 | Stack,
7 | Tooltip,
8 | } from '@chakra-ui/react';
9 | import React, { memo } from 'react';
10 | import { useTaskItem } from '../hooks/use-task-item';
11 |
12 | const formatDate = (date) => {
13 | return new Intl.DateTimeFormat('en-US', {
14 | year: 'numeric',
15 | month: 'long',
16 | day: '2-digit',
17 | hour: '2-digit',
18 | minute: '2-digit',
19 | second: '2-digit',
20 | hour12: false,
21 | }).format(date);
22 | };
23 |
24 | export const TaskItem = memo(({ task }) => {
25 | const { onDelete, onMarkAsDone } = useTaskItem();
26 |
27 | return (
28 |
29 |
30 | onMarkAsDone(task._id)}
34 | >
35 |
41 |
44 | {task.description}
45 |
46 |
47 |
48 |
49 |
50 |
58 |
59 |
60 | );
61 | });
62 |
--------------------------------------------------------------------------------
/api/tasks/tasks.publications.tests.js:
--------------------------------------------------------------------------------
1 | import { expect } from 'chai';
2 |
3 | import { Random } from 'meteor/random';
4 | import { Tasks } from './tasks';
5 | import '/api/tasks/tasks.publications';
6 | import { getMeteorPublication, mockLoggedUserId } from '../../tests/helpers';
7 |
8 | describe('Tasks', () => {
9 | describe('publications', () => {
10 | const userId = Random.id();
11 | const originalTask = {
12 | description: 'Groceries',
13 | createdAt: new Date(),
14 | done: false,
15 | userId,
16 | };
17 |
18 | beforeEach(async () => {
19 | mockLoggedUserId(userId);
20 | await Tasks.removeAsync({});
21 | await Tasks.insertAsync(originalTask);
22 | });
23 |
24 | it('should return tasks from the authenticated user', async () => {
25 | const publication = getMeteorPublication('tasksByLoggedUser');
26 | const tasks = await publication.fetchAsync();
27 |
28 | expect(tasks.length).to.be.equal(1);
29 | expect(tasks[0].description).to.be.equal(originalTask.description);
30 | });
31 |
32 | it('should not return any task to the user who does not have any', async () => {
33 | mockLoggedUserId(Random.id());
34 | const publication = getMeteorPublication('tasksByLoggedUser');
35 | const tasks = await publication.fetchAsync();
36 |
37 | expect(tasks.length).to.be.equal(0);
38 | });
39 |
40 | it('should not return any task if not authenticated', async () => {
41 | mockLoggedUserId(null);
42 | const publication = getMeteorPublication('tasksByLoggedUser');
43 | const tasks = await publication.fetchAsync();
44 |
45 | expect(tasks.length).to.be.equal(0);
46 | });
47 | });
48 | });
49 |
--------------------------------------------------------------------------------
/ui/common/components/navbar.jsx:
--------------------------------------------------------------------------------
1 | import { MoonIcon, SunIcon } from '@chakra-ui/icons';
2 | import {
3 | Box,
4 | Button,
5 | Flex,
6 | Stack,
7 | Text,
8 | useColorMode,
9 | useColorModeValue,
10 | } from '@chakra-ui/react';
11 | import React from 'react';
12 | import { Logout } from './logout';
13 |
14 | export function Navbar() {
15 | const { colorMode, toggleColorMode } = useColorMode();
16 |
17 | return (
18 |
19 |
30 |
31 |
39 | Simple Tasks
40 |
41 |
42 |
43 |
49 |
55 |
56 |
57 |
58 |
59 | );
60 | }
61 |
--------------------------------------------------------------------------------
/api/tasks/tasks.methods.js:
--------------------------------------------------------------------------------
1 | import { check } from 'meteor/check';
2 | import { Meteor } from 'meteor/meteor';
3 | import { checkLoggedIn, checkTaskOwner } from '../lib/auth';
4 | import { Tasks } from './tasks';
5 |
6 | /**
7 |
8 | Inserts a new task into the Tasks collection.
9 | @async
10 | @function insertTask
11 | @param {Object} taskData - The task data.
12 | @param {string} taskData.description - The description of the task.
13 | @returns {Promise} - The ID of the inserted task.
14 | */
15 | async function insertTask({ description }) {
16 | check(description, String);
17 | checkLoggedIn();
18 | const task = {
19 | description,
20 | done: false,
21 | userId: Meteor.userId(),
22 | createdAt: new Date(),
23 | };
24 | return Tasks.insertAsync(task);
25 | }
26 |
27 | /**
28 | Removes a task from the Tasks collection.
29 | @async
30 | @function removeTask
31 | @param {Object} taskData - The task data.
32 | @param {string} taskData.taskId - The ID of the task to remove.
33 | @returns {Promise}
34 | */
35 | async function removeTask({ taskId }) {
36 | check(taskId, String);
37 | await checkTaskOwner({ taskId });
38 | return Tasks.removeAsync(taskId);
39 | }
40 |
41 | /**
42 | Toggles the 'done' status of a task in the Tasks collection.
43 | @async
44 | @function toggleTaskDone
45 | @param {Object} taskData - The task data.
46 | @param {string} taskData.taskId - The ID of the task to toggle.
47 | @returns {Promise}
48 | */
49 | async function toggleTaskDone({ taskId }) {
50 | check(taskId, String);
51 | await checkTaskOwner({ taskId });
52 | const task = await Tasks.findOneAsync(taskId);
53 | return Tasks.updateAsync({ _id: taskId }, { $set: { done: !task.done } });
54 | }
55 |
56 | Meteor.methods({ insertTask, removeTask, toggleTaskDone });
57 |
--------------------------------------------------------------------------------
/ui/pages/auth/hooks/use-login.jsx:
--------------------------------------------------------------------------------
1 | import { useToast } from '@chakra-ui/react';
2 | import { zodResolver } from '@hookform/resolvers/zod/dist/zod';
3 | import { Accounts } from 'meteor/accounts-base';
4 | import { Meteor } from 'meteor/meteor';
5 | import { useState } from 'react';
6 | import { useForm } from 'react-hook-form';
7 | import { useNavigate } from 'react-router-dom';
8 | import * as z from 'zod';
9 |
10 | const schema = z.object({
11 | username: z.string().min(1, 'Username is required'),
12 | password: z.string().min(1, 'Password is required'),
13 | });
14 |
15 | const defaultValues = {
16 | username: 'fredmaia',
17 | password: 'abc123',
18 | };
19 |
20 | export function useLogin() {
21 | const toast = useToast();
22 | const [isSignup, setIsSignup] = useState(false);
23 | const [showPassword, setShowPassword] = useState(false);
24 | const navigate = useNavigate();
25 |
26 | const { handleSubmit, register, formState } = useForm({
27 | defaultValues,
28 | resolver: zodResolver(schema),
29 | });
30 |
31 | const handleError = (error) => {
32 | if (error) {
33 | const reason = error?.reason || 'Sorry, please try again.';
34 | toast({
35 | title: 'An error occurred.',
36 | description: reason,
37 | status: 'error',
38 | });
39 | return;
40 | }
41 | navigate('/tasks');
42 | };
43 |
44 | const loginOrCreateUser = (values) => {
45 | const { username, password } = values;
46 | if (isSignup) {
47 | Accounts.createUser({ username, password }, (error) => {
48 | handleError(error);
49 | });
50 | } else {
51 | Meteor.loginWithPassword(username, password, (error) => {
52 | handleError(error);
53 | });
54 | }
55 | };
56 |
57 | const handleGithubLogin = () => {
58 | Meteor.loginWithGithub(
59 | {
60 | requestPermissions: ['user'],
61 | loginStyle: 'popup',
62 | },
63 | (error) => {
64 | handleError(error);
65 | }
66 | );
67 | };
68 |
69 | return {
70 | loginOrCreateUser,
71 | isSignup,
72 | setIsSignup,
73 | showPassword,
74 | setShowPassword,
75 | handleSubmit,
76 | register,
77 | formState,
78 | handleGithubLogin,
79 | };
80 | }
81 |
--------------------------------------------------------------------------------
/ui/pages/tasks/tasks-page.jsx:
--------------------------------------------------------------------------------
1 | import {
2 | Box,
3 | Button,
4 | HStack,
5 | Heading,
6 | Spinner,
7 | Stack,
8 | Text,
9 | useColorModeValue,
10 | } from '@chakra-ui/react';
11 | import React, { Suspense } from 'react';
12 | import { TaskForm } from './components/task-form';
13 | import { TaskItem } from './components/task-item';
14 | import { useTasks } from './hooks/use-tasks';
15 | import '/api/tasks/tasks.methods';
16 |
17 | export default function TasksPage() {
18 | const { hideDone, setHideDone, tasks, count, pendingCount } = useTasks();
19 | return (
20 | <>
21 |
22 |
23 |
28 | Simple Tasks
29 |
30 |
31 |
32 |
33 | }>
34 |
44 |
45 |
46 |
51 | You have {count} {count === 1 ? 'task ' : 'tasks '}
52 | and {pendingCount || 0} pending.
53 |
54 |
55 |
56 |
65 |
66 |
67 | {tasks.map((task) => (
68 |
69 | ))}
70 |
71 |
72 | >
73 | );
74 | }
75 |
--------------------------------------------------------------------------------
/.meteor/versions:
--------------------------------------------------------------------------------
1 | accounts-base@3.2.0-beta340.12
2 | accounts-github@1.5.1
3 | accounts-oauth@1.4.6
4 | accounts-password@3.2.2-beta340.12
5 | allow-deny@2.1.0
6 | autoupdate@2.0.1
7 | babel-compiler@7.13.0-beta340.12
8 | babel-runtime@1.5.2
9 | base64@1.0.13
10 | binary-heap@1.0.12
11 | boilerplate-generator@2.1.0-beta340.12
12 | caching-compiler@2.0.1
13 | callback-hook@1.6.1
14 | check@1.4.4
15 | core-runtime@1.0.0
16 | ddp@1.4.2
17 | ddp-client@3.1.1
18 | ddp-common@1.4.4
19 | ddp-rate-limiter@1.2.2
20 | ddp-server@3.1.2
21 | dev-error-overlay@0.1.3
22 | diff-sequence@1.1.3
23 | dynamic-import@0.7.4
24 | ecmascript@0.17.0-beta340.12
25 | ecmascript-runtime@0.8.3
26 | ecmascript-runtime-client@0.12.3
27 | ecmascript-runtime-server@0.11.1
28 | ejson@1.1.5
29 | email@3.1.2
30 | es5-shim@4.8.1
31 | facts-base@1.0.2
32 | fetch@0.1.6
33 | force-ssl@1.1.1
34 | force-ssl-common@1.1.1
35 | geojson-utils@1.0.12
36 | github-oauth@1.4.2
37 | hot-code-push@1.0.5
38 | hot-module-replacement@0.5.4
39 | id-map@1.2.0
40 | inter-process-messaging@0.1.2
41 | jam:easy-schema@1.4.0
42 | launch-screen@2.0.1
43 | localstorage@1.2.1
44 | logging@1.3.6
45 | mdg:validation-error@0.5.1
46 | meteor@2.1.1
47 | meteor-base@1.5.2
48 | meteortesting:browser-tests@1.7.0
49 | meteortesting:mocha@3.2.0
50 | meteortesting:mocha-core@8.3.1-rc300.1
51 | minifier-css@2.0.1
52 | minimongo@2.0.5-beta340.12
53 | mobile-experience@1.1.2
54 | mobile-status-bar@1.1.1
55 | modern-browsers@0.2.3
56 | modules@0.20.3
57 | modules-runtime@0.13.2
58 | modules-runtime-hot@0.14.3
59 | mongo@2.2.0-beta340.12
60 | mongo-decimal@0.2.0
61 | mongo-dev-server@1.1.1
62 | mongo-id@1.0.9
63 | npm-mongo@6.16.1
64 | oauth@3.0.2
65 | oauth2@1.3.3
66 | ordered-dict@1.2.0
67 | promise@1.0.0
68 | quave:migrations@2.0.2
69 | random@1.2.2
70 | rate-limit@1.1.2
71 | react-fast-refresh@0.2.9
72 | react-meteor-accounts@1.0.3
73 | react-meteor-data@3.0.2
74 | reactive-var@1.0.13
75 | reload@1.3.2
76 | retry@1.1.1
77 | routepolicy@1.1.2
78 | rspack@1.0.0-beta340.12
79 | service-configuration@1.3.5
80 | sha@1.0.10
81 | shell-server@0.7.0-beta340.12
82 | socket-stream-client@0.6.1
83 | standard-minifier-css@1.9.3
84 | static-html@1.4.0
85 | static-html-tools@1.0.0
86 | tools-core@1.0.0-beta340.12
87 | tracker@1.3.4
88 | typescript@5.7.0-beta340.12
89 | url@1.3.5
90 | webapp@2.0.7
91 | webapp-hashing@1.1.2
92 | zodern:caching-minifier@0.5.0
93 | zodern:standard-minifier-js@5.3.1
94 | zodern:types@1.0.13
95 |
--------------------------------------------------------------------------------
/e2e/auth.spec.js:
--------------------------------------------------------------------------------
1 | // @ts-check
2 | import { test, expect } from '@playwright/test';
3 |
4 | async function loginWithUsernameAndPassword(page, username, password) {
5 | await page.goto('/');
6 | await page.getByPlaceholder('Enter your username').fill(username);
7 | await page.getByPlaceholder('Enter your password').fill(password);
8 | await page.getByRole('button', { name: 'Sign in' }).click();
9 | await page.waitForURL('**/tasks');
10 | }
11 |
12 | test('user can sign in with username and password and see Sign Out button', async ({ page }) => {
13 | await loginWithUsernameAndPassword(page, 'fredmaia', 'abc123');
14 | await expect(page.getByRole('button', { name: 'Sign Out' })).toBeVisible();
15 | });
16 |
17 | test('user can sign out with username and password', async ({ page }) => {
18 | await loginWithUsernameAndPassword(page, 'fredmaia', 'abc123');
19 | await page.getByRole('button', { name: 'Sign Out' }).click();
20 | await expect(page.getByRole('button', { name: 'Sign in' })).toBeVisible();
21 | });
22 |
23 | // GitHub keeps blocking the tests, so I'm disabling it for now
24 |
25 | // async function loginWithGithub(page, context) {
26 | // const [popup] = await Promise.all([
27 | // context.waitForEvent('page'),
28 | // page.getByRole('button', { name: /github/i }).click(),
29 | // ]);
30 | // await popup.waitForURL(/github\.com\/login/);
31 | // await popup.getByLabel('Username or email address').fill(process.env.GITHUB_USER);
32 | // await popup.getByLabel('Password').fill(process.env.GITHUB_PASSWORD);
33 | // await popup.getByRole('button', { name: 'Sign in' }).click();
34 | // // If your app asks for authorization, approve it
35 | // // await popup.getByRole('button', { name: /authorize/i }).click();
36 | // await popup.waitForEvent('close');
37 | // await page.waitForURL('**/tasks');
38 | // }
39 |
40 | // test('user can sign in with GitHub (full flow)', async ({ page, context }) => {
41 | // await page.goto('/');
42 | // await loginWithGithub(page, context);
43 | // await expect(page.getByRole('button', { name: 'Sign Out' })).toBeVisible();
44 | // });
45 |
46 | // test('user can sign out with GitHub', async ({ page, context }) => {
47 | // await page.goto('/');
48 | // await loginWithGithub(page, context);
49 | // await page.getByRole('button', { name: 'Sign Out' }).click();
50 | // await expect(page.getByRole('button', { name: 'Sign in' })).toBeVisible();
51 | // });
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "simpletasks",
3 | "author": "@fredmaiaarantes",
4 | "version": "1.0.3",
5 | "private": true,
6 | "scripts": {
7 | "start": "meteor --settings private/settings.dev.json",
8 | "test": "meteor test --once --driver-package meteortesting:mocha",
9 | "check": "npx @biomejs/biome check --write ./**/*.{js,jsx}",
10 | "visualize": "meteor --production --extra-packages bundle-visualizer",
11 | "profile": "meteor profile --exclude-archs web.browser.legacy,web.cordova",
12 | "test-e2e": "playwright test",
13 | "test-e2e-headed": "playwright test --headed",
14 | "test-e2e-report": "npx playwright show-report"
15 | },
16 | "meteor": {
17 | "mainModule": {
18 | "client": "ui/main.jsx",
19 | "server": "api/main.js"
20 | },
21 | "testModule": "tests/main.js",
22 | "modern": true
23 | },
24 | "dependencies": {
25 | "@babel/runtime": "^7.28.4",
26 | "@chakra-ui/icons": "^2.1.1",
27 | "@chakra-ui/react": "^2.10.9",
28 | "@emotion/react": "^11.14.0",
29 | "@emotion/styled": "^11.14.1",
30 | "@hookform/resolvers": "^2.9.11",
31 | "@react-icons/all-files": "^4.1.0",
32 | "@swc/helpers": "^0.5.17",
33 | "bcrypt": "^5.1.1",
34 | "framer-motion": "^6.5.1",
35 | "history": "^5.3.0",
36 | "meteor-node-stubs": "^1.2.24",
37 | "react": "^18.3.1",
38 | "react-dom": "^18.3.1",
39 | "react-helmet": "^6.1.0",
40 | "react-hook-form": "^7.65.0",
41 | "react-router-dom": "^6.25.1",
42 | "zod": "^3.25.76"
43 | },
44 | "devDependencies": {
45 | "@biomejs/biome": "1.8.3",
46 | "@meteorjs/rspack": "^0.0.60",
47 | "@playwright/test": "^1.52.0",
48 | "@rsdoctor/rspack-plugin": "^1.2.3",
49 | "@rspack/cli": "^1.5.3",
50 | "@rspack/core": "^1.5.3",
51 | "@rspack/plugin-react-refresh": "^1.4.3",
52 | "@types/meteor": "^2.9.8",
53 | "@types/mocha": "^9.1.1",
54 | "@types/node": "^22.15.3",
55 | "@types/react": "^18.3.3",
56 | "@types/react-dom": "^18.3.0",
57 | "chai": "^4.4.1",
58 | "dotenv": "^16.5.0",
59 | "react-refresh": "^0.17.0"
60 | },
61 | "husky": {
62 | "hooks": {
63 | "pre-commit": "meteor npm test && lint-staged",
64 | "post-commit": "git update-index --again"
65 | }
66 | },
67 | "lint-staged": {
68 | "*.{js,jsx}": [
69 | "biome check --write --unsafe --no-errors-on-unmatched"
70 | ]
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/ui/common/components/footer.jsx:
--------------------------------------------------------------------------------
1 | import {
2 | Box,
3 | ButtonGroup,
4 | IconButton,
5 | Stack,
6 | Text,
7 | useColorModeValue,
8 | } from '@chakra-ui/react';
9 | import { FaGithub } from '@react-icons/all-files/fa/FaGithub';
10 | import { FaLinkedin } from '@react-icons/all-files/fa/FaLinkedin';
11 | import { FaTwitter } from '@react-icons/all-files/fa/FaTwitter';
12 | import React from 'react';
13 |
14 | export function Footer() {
15 | return (
16 |
25 |
31 |
37 |
38 | }
44 | />
45 | }
51 | />
52 | }
58 | />
59 |
60 |
61 |
68 | © {new Date().getFullYear()} Charm (Chakra-UI, React,{' '}
69 |
70 | Meteor.js
71 |
72 | ) by{' '}
73 |
78 | @fredmaiaarantes
79 |
80 | .
81 |
82 |
83 |
84 | );
85 | }
86 |
--------------------------------------------------------------------------------
/playwright.config.js:
--------------------------------------------------------------------------------
1 | // @ts-check
2 | import { defineConfig, devices } from '@playwright/test';
3 | import dotenv from 'dotenv';
4 |
5 | dotenv.config();
6 | /**
7 | * Read environment variables from file.
8 | * https://github.com/motdotla/dotenv
9 | */
10 | // import dotenv from 'dotenv';
11 | // import path from 'path';
12 | // dotenv.config({ path: path.resolve(__dirname, '.env') });
13 |
14 | /**
15 | * @see https://playwright.dev/docs/test-configuration
16 | */
17 | export default defineConfig({
18 | testDir: './e2e',
19 | /* Run tests in files in parallel */
20 | fullyParallel: true,
21 | /* Fail the build on CI if you accidentally left test.only in the source code. */
22 | forbidOnly: !!process.env.CI,
23 | /* Retry on CI only */
24 | retries: process.env.CI ? 2 : 0,
25 | /* Opt out of parallel tests on CI. */
26 | workers: process.env.CI ? 1 : undefined,
27 | /* Reporter to use. See https://playwright.dev/docs/test-reporters */
28 | reporter: 'html',
29 | /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
30 | use: {
31 | /* Base URL to use in actions like `await page.goto('/')`. */
32 | baseURL: 'http://localhost:3000',
33 |
34 | /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
35 | trace: 'on-first-retry',
36 | // Add these settings to prevent browser closure issues
37 | headless: true,
38 | launchOptions: {
39 | slowMo: 50,
40 | },
41 | },
42 |
43 | /* Configure projects for major browsers */
44 | projects: [
45 | {
46 | name: 'chromium',
47 | use: { ...devices['Desktop Chrome'] },
48 | },
49 |
50 | {
51 | name: 'firefox',
52 | use: { ...devices['Desktop Firefox'] },
53 | },
54 |
55 | // WebKit is temporarily disabled due to macOS compatibility issues
56 | // {
57 | // name: 'webkit',
58 | // use: { ...devices['Desktop Safari'] },
59 | // },
60 |
61 | /* Test against mobile viewports. */
62 | // {
63 | // name: 'Mobile Chrome',
64 | // use: { ...devices['Pixel 5'] },
65 | // },
66 | // {
67 | // name: 'Mobile Safari',
68 | // use: { ...devices['iPhone 12'] },
69 | // },
70 |
71 | /* Test against branded browsers. */
72 | // {
73 | // name: 'Microsoft Edge',
74 | // use: { ...devices['Desktop Edge'], channel: 'msedge' },
75 | // },
76 | // {
77 | // name: 'Google Chrome',
78 | // use: { ...devices['Desktop Chrome'], channel: 'chrome' },
79 | // },
80 | ],
81 |
82 | /* Run your local dev server before starting the tests */
83 | webServer: {
84 | command: 'meteor --settings private/settings.dev.json',
85 | url: 'http://localhost:3000',
86 | reuseExistingServer: !process.env.CI,
87 | timeout: 120 * 1000,
88 | },
89 | });
90 |
91 |
--------------------------------------------------------------------------------
/api/tasks/tasks.methods.tests.js:
--------------------------------------------------------------------------------
1 | import { assert, expect } from 'chai';
2 | import { Meteor } from 'meteor/meteor';
3 | import { Random } from 'meteor/random';
4 | import { Tasks } from '/api/tasks/tasks';
5 | import '/api/tasks/tasks.methods';
6 | import { mockLoggedUserId } from '../../tests/helpers';
7 |
8 | if (Meteor.isServer) {
9 | describe('Tasks', () => {
10 | describe('methods', () => {
11 | const userId = Random.id();
12 | let taskId;
13 |
14 | beforeEach(async () => {
15 | mockLoggedUserId(userId);
16 | await Tasks.removeAsync({}).catch((error) => {
17 | console.error(error);
18 | });
19 | taskId = await Tasks.insertAsync({
20 | description: 'Test Task',
21 | done: false,
22 | createdAt: new Date(),
23 | userId,
24 | }).catch((error) => {
25 | console.error(error);
26 | });
27 | });
28 |
29 | it('can delete owned task', async () => {
30 | await Meteor.callAsync('removeTask', { taskId });
31 |
32 | assert.equal(await Tasks.find().countAsync(), 0);
33 | });
34 |
35 | it("can't delete task if not authenticated", async () => {
36 | mockLoggedUserId(null);
37 | try {
38 | await Meteor.callAsync('removeTask', { taskId });
39 | } catch (error) {
40 | expect(error).to.be.instanceof(Error);
41 | expect(error.reason).to.be.equal('Not authorized.');
42 | }
43 | assert.equal(await Tasks.find().countAsync(), 1);
44 | });
45 |
46 | it("can't delete task from another owner", async () => {
47 | mockLoggedUserId(Random.id());
48 | try {
49 | await Meteor.callAsync('removeTask', { taskId });
50 | } catch (error) {
51 | expect(error).to.be.instanceof(Error);
52 | expect(error.reason).to.be.equal('Access denied.');
53 | }
54 | assert.equal(await Tasks.find().countAsync(), 1);
55 | });
56 |
57 | it('can change the status of a task', async () => {
58 | const originalTask = await Tasks.findOneAsync(taskId);
59 | await Meteor.callAsync('toggleTaskDone', { taskId });
60 |
61 | const updatedTask = await Tasks.findOneAsync(taskId);
62 | assert.notEqual(updatedTask.done, originalTask.done);
63 | });
64 |
65 | it("can't change the status of a task from another owner", async () => {
66 | mockLoggedUserId(Random.id());
67 | const originalTask = await Tasks.findOneAsync(taskId);
68 |
69 | try {
70 | await Meteor.callAsync('toggleTaskDone', { taskId });
71 | } catch (error) {
72 | expect(error).to.be.instanceof(Error);
73 | expect(error.reason).to.be.equal('Access denied.');
74 | }
75 | const task = await Tasks.findOneAsync(taskId);
76 | assert.equal(task.done, originalTask.done);
77 | });
78 |
79 | it('can insert new tasks', async () => {
80 | const description = 'New Task';
81 | await Meteor.callAsync('insertTask', { description });
82 |
83 | const task = await Tasks.findOneAsync({ description });
84 | assert.isNotNull(task);
85 | assert.isTrue(task.description === description);
86 | });
87 |
88 | it("can't insert new tasks if not authenticated", async () => {
89 | mockLoggedUserId(null);
90 | const description = 'New Task';
91 | try {
92 | await Meteor.callAsync('insertTask', { description });
93 | } catch (error) {
94 | expect(error).to.be.instanceof(Error);
95 | expect(error.reason).to.be.equal('Not authorized.');
96 | }
97 | const task = await Tasks.findOneAsync({ description });
98 | assert.isUndefined(task);
99 | });
100 | });
101 | });
102 | }
103 |
--------------------------------------------------------------------------------
/ui/pages/auth/sign-in-page.jsx:
--------------------------------------------------------------------------------
1 | import {
2 | Box,
3 | Button,
4 | Flex,
5 | FormControl,
6 | FormErrorMessage,
7 | Heading,
8 | Input,
9 | InputGroup,
10 | InputRightElement,
11 | Stack,
12 | Text,
13 | useColorModeValue,
14 | } from '@chakra-ui/react';
15 | import { FaGithub } from '@react-icons/all-files/fa/FaGithub';
16 | import { useUserId } from 'meteor/react-meteor-accounts';
17 | import React from 'react';
18 | import { Navigate } from 'react-router-dom';
19 | import { routes } from '../../routes';
20 | import { useLogin } from './hooks/use-login';
21 |
22 | export default function SignInPage() {
23 | const userId = useUserId();
24 | const {
25 | loginOrCreateUser,
26 | isSignup,
27 | setIsSignup,
28 | showPassword,
29 | setShowPassword,
30 | register,
31 | formState: { errors, isSubmitting },
32 | handleSubmit,
33 | handleGithubLogin,
34 | } = useLogin();
35 |
36 | if (userId) {
37 | return ;
38 | }
39 |
40 | return (
41 |
42 |
43 |
44 |
49 | Sign in to your account
50 |
51 |
52 | to start creating your simple tasks
53 |
54 |
55 |
61 |
145 |
146 |
147 |
148 | );
149 | }
150 |
--------------------------------------------------------------------------------
/biome.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://biomejs.dev/schemas/1.8.3/schema.json",
3 | "files": {
4 | "ignore": ["node_modules/**", ".meteor/**", ".vscode/**", "README-Assets/**", "package*.json", "*tests*.js"]
5 | },
6 | "formatter": {
7 | "enabled": true,
8 | "formatWithErrors": false,
9 | "indentStyle": "space",
10 | "indentWidth": 2,
11 | "lineEnding": "lf",
12 | "lineWidth": 80,
13 | "attributePosition": "auto"
14 | },
15 | "organizeImports": { "enabled": true },
16 | "linter": {
17 | "enabled": true,
18 | "rules": {
19 | "recommended": false,
20 | "a11y": {
21 | "noAccessKey": "error",
22 | "noAriaUnsupportedElements": "error",
23 | "noAutofocus": "error",
24 | "noBlankTarget": "off",
25 | "noDistractingElements": "error",
26 | "noHeaderScope": "error",
27 | "noInteractiveElementToNoninteractiveRole": "error",
28 | "noNoninteractiveElementToInteractiveRole": "error",
29 | "noNoninteractiveTabindex": "error",
30 | "noPositiveTabindex": "error",
31 | "noRedundantAlt": "error",
32 | "noRedundantRoles": "error",
33 | "useAltText": "error",
34 | "useAnchorContent": "error",
35 | "useAriaActivedescendantWithTabindex": "error",
36 | "useAriaPropsForRole": "error",
37 | "useButtonType": "off",
38 | "useHeadingContent": "error",
39 | "useHtmlLang": "error",
40 | "useIframeTitle": "error",
41 | "useKeyWithClickEvents": "off",
42 | "useKeyWithMouseEvents": "error",
43 | "useMediaCaption": "error",
44 | "useValidAnchor": "off",
45 | "useValidAriaProps": "error",
46 | "useValidAriaRole": {
47 | "level": "error",
48 | "options": { "allowInvalidRoles": [], "ignoreNonDom": false }
49 | },
50 | "useValidAriaValues": "error",
51 | "useValidLang": "error"
52 | },
53 | "complexity": {
54 | "noExtraBooleanCast": "error",
55 | "noMultipleSpacesInRegularExpressionLiterals": "error",
56 | "noUselessCatch": "off",
57 | "noUselessConstructor": "error",
58 | "noUselessLabel": "error",
59 | "noUselessLoneBlockStatements": "error",
60 | "noUselessRename": "error",
61 | "noUselessTernary": "error",
62 | "noVoid": "error",
63 | "noWith": "error",
64 | "useLiteralKeys": "error"
65 | },
66 | "correctness": {
67 | "noChildrenProp": "error",
68 | "noConstAssign": "error",
69 | "noConstantCondition": "warn",
70 | "noEmptyCharacterClassInRegex": "error",
71 | "noEmptyPattern": "error",
72 | "noGlobalObjectCalls": "error",
73 | "noInnerDeclarations": "error",
74 | "noInvalidConstructorSuper": "error",
75 | "noInvalidUseBeforeDeclaration": "error",
76 | "noNewSymbol": "error",
77 | "noNodejsModules": "off",
78 | "noSelfAssign": "error",
79 | "noSwitchDeclarations": "error",
80 | "noUndeclaredVariables": "error",
81 | "noUnreachable": "error",
82 | "noUnreachableSuper": "error",
83 | "noUnsafeFinally": "error",
84 | "noUnusedLabels": "error",
85 | "noUnusedVariables": "error",
86 | "noVoidElementsWithChildren": "error",
87 | "useArrayLiterals": "error",
88 | "useIsNan": "error",
89 | "useJsxKeyInIterable": "off",
90 | "useValidForDirection": "error",
91 | "useYield": "error"
92 | },
93 | "security": {
94 | "noDangerouslySetInnerHtml": "warn",
95 | "noDangerouslySetInnerHtmlWithChildren": "error",
96 | "noGlobalEval": "error"
97 | },
98 | "style": {
99 | "noArguments": "error",
100 | "noCommaOperator": "error",
101 | "noDefaultExport": "info",
102 | "noNegationElse": "off",
103 | "noParameterAssign": "error",
104 | "noRestrictedGlobals": {
105 | "level": "error",
106 | "options": {
107 | "deniedGlobals": [
108 | "isFinite",
109 | "isNaN",
110 | "addEventListener",
111 | "blur",
112 | "close",
113 | "closed",
114 | "confirm",
115 | "defaultStatus",
116 | "defaultstatus",
117 | "event",
118 | "external",
119 | "find",
120 | "focus",
121 | "frameElement",
122 | "frames",
123 | "history",
124 | "innerHeight",
125 | "innerWidth",
126 | "length",
127 | "location",
128 | "locationbar",
129 | "menubar",
130 | "moveBy",
131 | "moveTo",
132 | "name",
133 | "onblur",
134 | "onerror",
135 | "onfocus",
136 | "onload",
137 | "onresize",
138 | "onunload",
139 | "open",
140 | "opener",
141 | "opera",
142 | "outerHeight",
143 | "outerWidth",
144 | "pageXOffset",
145 | "pageYOffset",
146 | "parent",
147 | "print",
148 | "removeEventListener",
149 | "resizeBy",
150 | "resizeTo",
151 | "screen",
152 | "screenLeft",
153 | "screenTop",
154 | "screenX",
155 | "screenY",
156 | "scroll",
157 | "scrollbars",
158 | "scrollBy",
159 | "scrollTo",
160 | "scrollX",
161 | "scrollY",
162 | "self",
163 | "status",
164 | "statusbar",
165 | "stop",
166 | "toolbar",
167 | "top"
168 | ]
169 | }
170 | },
171 | "noVar": "error",
172 | "useBlockStatements": "off",
173 | "useCollapsedElseIf": "error",
174 | "useConst": "error",
175 | "useFragmentSyntax": "off",
176 | "useNumericLiterals": "error",
177 | "useShorthandAssign": "error",
178 | "useSingleVarDeclarator": "error",
179 | "useTemplate": "error"
180 | },
181 | "suspicious": {
182 | "noArrayIndexKey": "error",
183 | "noAsyncPromiseExecutor": "off",
184 | "noCatchAssign": "error",
185 | "noClassAssign": "error",
186 | "noCommentText": "error",
187 | "noCompareNegZero": "error",
188 | "noControlCharactersInRegex": "error",
189 | "noDebugger": "error",
190 | "noDoubleEquals": "off",
191 | "noDuplicateCase": "error",
192 | "noDuplicateClassMembers": "error",
193 | "noDuplicateJsxProps": "error",
194 | "noDuplicateObjectKeys": "error",
195 | "noDuplicateParameters": "error",
196 | "noEmptyBlockStatements": "error",
197 | "noFallthroughSwitchClause": "error",
198 | "noFunctionAssign": "error",
199 | "noGlobalAssign": "error",
200 | "noLabelVar": "error",
201 | "noMisleadingCharacterClass": "off",
202 | "noPrototypeBuiltins": "error",
203 | "noRedeclare": "error",
204 | "noSelfCompare": "error",
205 | "noShadowRestrictedNames": "error",
206 | "noUnsafeNegation": "error",
207 | "useAwait": "off",
208 | "useGetterReturn": "error",
209 | "useValidTypeof": "error"
210 | }
211 | }
212 | },
213 | "javascript": {
214 | "formatter": {
215 | "jsxQuoteStyle": "double",
216 | "quoteProperties": "asNeeded",
217 | "trailingCommas": "es5",
218 | "semicolons": "always",
219 | "arrowParentheses": "always",
220 | "bracketSpacing": true,
221 | "bracketSameLine": false,
222 | "quoteStyle": "single",
223 | "attributePosition": "auto"
224 | }
225 | },
226 | "overrides": [{ "include": ["package*.json"] }]
227 | }
228 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Charm - Simple Tasks
2 | Running with **Meteor.js 3.4 Beta with RSPack** and Node 22.
3 | Built with the CHARM (Chakra-UI, React, Meteor) stack.
4 |
5 | Deployed to Galaxy: https://simpletasks2.meteorapp.com/
6 |
7 | ## What and why this stack?
8 | The main goal is to make development as quick and efficient as possible. To achieve this, I have selected these technologies:
9 |
10 | - [Meteor ](https://meteor.com/)- A full-stack framework focused on productivity that uses RPCs and Sockets for reactivity.
11 | - [React ](https://reactjs.org/)- A minimal UI library for building on the web.
12 | - [Chakra UI ](https://chakra-ui.com/)- A React library focused on simplicity and productivity.
13 | - [React Hook Form ](https://react-hook-form.com/)- Performant, flexible, and extensible forms with easy-to-use validation.
14 | - [MongoDB ](https://www.mongodb.com/)- A NoSQL database that is really powerful for prototyping and creating ready-to-use apps out of the box.
15 | - [Galaxy ](https://galaxycloud.app)- A cloud provider that makes deploying a server with a database included painless.
16 | - [Playwright ](https://playwright.dev/)- Reliable end-to-end testing.
17 |
18 | ### Features:
19 | - Sign In / Sign Up with Username and Password
20 | - Sign In / Sign Up with with GitHub
21 | - List Tasks by logged-in user
22 | - Add Tasks
23 | - Remove Tasks
24 | - Mark a Task as Done
25 | - Filter Tasks by Status
26 |
27 | Video demo:
28 | https://www.loom.com/share/50b9e1a513904b138fb772a332facbfb
29 |
30 | ## Running the template
31 |
32 | ### Install dependencies
33 |
34 | ```bash
35 | meteor npm install
36 | ```
37 |
38 | ### Configure GitHub Login (Optional)
39 |
40 | Create an OAuth App on [GitHub](https://github.com/settings/developers) by following this [tutorial](https://blog.meteor.com/meteor-social-login-with-github-1b48d04c332) and checking our [docs](https://v3-docs.meteor.com/api/accounts.html#Meteor-loginWith%3CExternalService%3E).
41 | Then, replace the GitHub `clientId` and `secret` in your `private/settings.json` file with your own.
42 |
43 | ### Running
44 |
45 | ```bash
46 | meteor npm run start
47 | ```
48 |
49 | ### Run tests
50 |
51 | ```bash
52 | meteor npm run test
53 | ```
54 |
55 | ### Cleaning up your local DB
56 |
57 | ```bash
58 | meteor reset --db
59 | ```
60 |
61 | ### Deploy to Galaxy with free MongoDB
62 | ```bash
63 | meteor deploy