├── api
├── items
│ ├── sample.dat
│ ├── function.json
│ └── index.js
├── lists
│ ├── sample.dat
│ ├── function.json
│ └── index.js
├── .funcignore
├── proxies.json
├── .vscode
│ └── extensions.json
├── config
│ ├── default.json
│ ├── custom-environment-variables.json
│ └── index.js
├── package.json
├── host.json
├── models
│ ├── todoList.js
│ ├── todoItem.js
│ └── store.js
├── core.js
├── .gitignore
└── package-lock.json
├── public
├── robots.txt
├── favicon.ico
├── logo192.png
├── logo512.png
├── manifest.json
└── index.html
├── src
├── build
│ ├── robots.txt
│ ├── favicon.ico
│ ├── logo192.png
│ ├── logo512.png
│ └── manifest.json
├── features
│ ├── lists
│ │ ├── listAPI.js
│ │ ├── Lists.js
│ │ └── listSlice.js
│ ├── items
│ │ ├── itemAPI.js
│ │ ├── itemSlice.js
│ │ └── Items.js
│ └── user
│ │ └── userSlice.js
├── index.css
├── reportWebVitals.js
├── app
│ └── store.js
├── App.js
├── index.js
├── TopBar.js
└── Display.js
├── .vscode
├── extensions.json
├── launch.json
├── settings.json
└── tasks.json
├── .gitignore
├── package.json
├── .devcontainer
├── devcontainer.json
├── docker-compose.yml
└── Dockerfile
├── local-setup.md
├── .github
└── workflows
│ └── azure-static-web-apps-kind-plant-02300391e.yml
└── README.md
/api/items/sample.dat:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Azure"
3 | }
--------------------------------------------------------------------------------
/api/lists/sample.dat:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Azure"
3 | }
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 |
--------------------------------------------------------------------------------
/src/build/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/staticwebdev/mongoose-starter/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/staticwebdev/mongoose-starter/HEAD/public/logo192.png
--------------------------------------------------------------------------------
/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/staticwebdev/mongoose-starter/HEAD/public/logo512.png
--------------------------------------------------------------------------------
/api/.funcignore:
--------------------------------------------------------------------------------
1 | *.js.map
2 | *.ts
3 | .git*
4 | .vscode
5 | local.settings.json
6 | test
7 | tsconfig.json
--------------------------------------------------------------------------------
/api/proxies.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "http://json.schemastore.org/proxies",
3 | "proxies": {}
4 | }
5 |
--------------------------------------------------------------------------------
/src/build/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/staticwebdev/mongoose-starter/HEAD/src/build/favicon.ico
--------------------------------------------------------------------------------
/src/build/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/staticwebdev/mongoose-starter/HEAD/src/build/logo192.png
--------------------------------------------------------------------------------
/src/build/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/staticwebdev/mongoose-starter/HEAD/src/build/logo512.png
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": [
3 | "ms-azuretools.vscode-azurefunctions"
4 | ]
5 | }
6 |
--------------------------------------------------------------------------------
/api/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": [
3 | "ms-azuretools.vscode-azurefunctions"
4 | ]
5 | }
--------------------------------------------------------------------------------
/api/config/default.json:
--------------------------------------------------------------------------------
1 | {
2 | "database": {
3 | "connectionString": "mongodb://localhost",
4 | "databaseName": "todo"
5 | }
6 | }
--------------------------------------------------------------------------------
/api/config/custom-environment-variables.json:
--------------------------------------------------------------------------------
1 | {
2 | "database": {
3 | "connectionString": "AZURE_COSMOS_CONNECTION_STRING",
4 | "databaseName": "AZURE_COSMOS_DATABASE_NAME"
5 | }
6 | }
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "0.2.0",
3 | "configurations": [
4 | {
5 | "name": "Attach to Node Functions",
6 | "type": "node",
7 | "request": "attach",
8 | "port": 9229,
9 | "preLaunchTask": "func: host start"
10 | }
11 | ]
12 | }
13 |
--------------------------------------------------------------------------------
/api/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "api",
3 | "version": "1.0.0",
4 | "description": "",
5 | "scripts": {
6 | "start": "func start",
7 | "test": "echo \"No tests yet...\""
8 | },
9 | "dependencies": {
10 | "config": "^3.3.7",
11 | "dotenv": "^16.0.1",
12 | "mongoose": "^5.10.11"
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/api/host.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "2.0",
3 | "logging": {
4 | "applicationInsights": {
5 | "samplingSettings": {
6 | "isEnabled": true,
7 | "excludedTypes": "Request"
8 | }
9 | }
10 | },
11 | "extensionBundle": {
12 | "id": "Microsoft.Azure.Functions.ExtensionBundle",
13 | "version": "[2.*, 3.0.0)"
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/features/lists/listAPI.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 | const baseUrl = '/api/lists';
3 |
4 | export const listService = {
5 | list: async () => {
6 | const response = await axios.get(baseUrl);
7 | return response.data;
8 | },
9 | save: async (list) => {
10 | const response = await axios.post(baseUrl, list);
11 | return response.data;
12 | }
13 | }
--------------------------------------------------------------------------------
/api/lists/function.json:
--------------------------------------------------------------------------------
1 | {
2 | "bindings": [
3 | {
4 | "authLevel": "anonymous",
5 | "type": "httpTrigger",
6 | "direction": "in",
7 | "name": "req",
8 | "methods": [
9 | "get",
10 | "post"
11 | ],
12 | "route": "lists/{listId?}"
13 | },
14 | {
15 | "type": "http",
16 | "direction": "out",
17 | "name": "res"
18 | }
19 | ]
20 | }
21 |
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
5 | sans-serif;
6 | -webkit-font-smoothing: antialiased;
7 | -moz-osx-font-smoothing: grayscale;
8 | }
9 |
10 | code {
11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
12 | monospace;
13 | }
14 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 | .env
21 |
22 | npm-debug.log*
23 | yarn-debug.log*
24 | yarn-error.log*
25 |
26 |
--------------------------------------------------------------------------------
/src/reportWebVitals.js:
--------------------------------------------------------------------------------
1 | const reportWebVitals = onPerfEntry => {
2 | if (onPerfEntry && onPerfEntry instanceof Function) {
3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
4 | getCLS(onPerfEntry);
5 | getFID(onPerfEntry);
6 | getFCP(onPerfEntry);
7 | getLCP(onPerfEntry);
8 | getTTFB(onPerfEntry);
9 | });
10 | }
11 | };
12 |
13 | export default reportWebVitals;
14 |
--------------------------------------------------------------------------------
/api/items/function.json:
--------------------------------------------------------------------------------
1 | {
2 | "bindings": [
3 | {
4 | "authLevel": "anonymous",
5 | "type": "httpTrigger",
6 | "direction": "in",
7 | "name": "req",
8 | "methods": [
9 | "get",
10 | "post",
11 | "put",
12 | "delete"
13 | ],
14 | "route": "lists/{listId}/items/{itemId?}"
15 | },
16 | {
17 | "type": "http",
18 | "direction": "out",
19 | "name": "res"
20 | }
21 | ]
22 | }
23 |
--------------------------------------------------------------------------------
/src/app/store.js:
--------------------------------------------------------------------------------
1 | import { configureStore } from '@reduxjs/toolkit';
2 | // import counterReducer from '../features/counter/counterSlice';
3 | import itemsReducer from '../features/items/itemSlice';
4 | import listsReducer from '../features/lists/listSlice';
5 | import userReducer from '../features/user/userSlice';
6 |
7 | export const store = configureStore({
8 | reducer: {
9 | items: itemsReducer,
10 | lists: listsReducer,
11 | user: userReducer
12 | },
13 | });
14 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "azureFunctions.deploySubpath": "api",
3 | "azureFunctions.postDeployTask": "npm install",
4 | "azureFunctions.projectLanguage": "JavaScript",
5 | "azureFunctions.projectRuntime": "~3",
6 | "debug.internalConsoleOptions": "neverOpen",
7 | "azureFunctions.preDeployTask": "npm prune",
8 | "markdownlint.config": {
9 | "MD028": false,
10 | "MD025": {
11 | "front_matter_title": ""
12 | }
13 | }
14 | }
--------------------------------------------------------------------------------
/api/models/todoList.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose');
2 |
3 | const schema = new mongoose.Schema({
4 | name: {
5 | type: String,
6 | required: true,
7 | },
8 | description: String,
9 | userId: {
10 | type: String,
11 | required: true,
12 | },
13 | }, {
14 | timestamps: {
15 | createdAt: "createdDate",
16 | updatedAt: "updatedDate"
17 | }
18 | });
19 |
20 | module.exports.TodoListModel = mongoose.model("TodoList", schema, "TodoList");
--------------------------------------------------------------------------------
/api/core.js:
--------------------------------------------------------------------------------
1 | // Get current user
2 | function getUserId(req) {
3 | // Retrieve client info from request header
4 | const header = req.headers['x-ms-client-principal'];
5 | // The header is encoded in Base64, so we need to convert it
6 | const encoded = Buffer.from(header, 'base64');
7 | // Convert from Base64 to ascii
8 | const decoded = encoded.toString('ascii');
9 | // Convert to a JSON object and return the userId
10 | return JSON.parse(decoded).userId;
11 | }
12 | exports.getUserId = getUserId;
13 |
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/src/build/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/api/config/index.js:
--------------------------------------------------------------------------------
1 | const dotenv = require("dotenv");
2 | const config = require("config");
3 |
4 | module.exports.getConfig = async (log) => {
5 | // Load any ENV vars from local .env file
6 | if (process.env.NODE_ENV !== "production") {
7 | dotenv.config();
8 | }
9 |
10 | // load database configuration
11 | const databaseConfig = config.get("database");
12 | log("Database config loaded");
13 | log(databaseConfig);
14 | return {
15 | database: {
16 | connectionString: databaseConfig.connectionString,
17 | databaseName: databaseConfig.databaseName,
18 | },
19 | };
20 | };
21 |
--------------------------------------------------------------------------------
/src/App.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react';
2 | import { useDispatch, useSelector } from 'react-redux';
3 | import Display from './Display';
4 | import { getUserAsync, selectUserDetails } from './features/user/userSlice';
5 | import TopBar from './TopBar';
6 |
7 | function App() {
8 | const dispatch = useDispatch();
9 |
10 | useEffect(() => {
11 | dispatch(getUserAsync());
12 | }, [dispatch]);
13 |
14 | const userDetails = useSelector(selectUserDetails);
15 |
16 | return (
17 |
18 |
19 |
20 | {userDetails === ''
21 | ? Login to see todo items
22 | :
23 | }
24 |
25 | );
26 | }
27 |
28 | export default App;
29 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { createRoot } from 'react-dom/client';
3 | import { Provider } from 'react-redux';
4 | import { store } from './app/store';
5 | import App from './App';
6 | import reportWebVitals from './reportWebVitals';
7 | import './index.css';
8 |
9 | const container = document.getElementById('root');
10 | const root = createRoot(container);
11 |
12 | root.render(
13 |
14 |
15 |
16 |
17 |
18 | );
19 |
20 | // If you want to start measuring performance in your app, pass a function
21 | // to log results (for example: reportWebVitals(console.log))
22 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
23 | reportWebVitals();
24 |
--------------------------------------------------------------------------------
/.vscode/tasks.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "2.0.0",
3 | "tasks": [
4 | {
5 | "type": "func",
6 | "command": "host start",
7 | "problemMatcher": "$func-node-watch",
8 | "isBackground": true,
9 | "dependsOn": "npm install",
10 | "options": {
11 | "cwd": "${workspaceFolder}/api"
12 | }
13 | },
14 | {
15 | "type": "shell",
16 | "label": "npm install",
17 | "command": "npm install",
18 | "options": {
19 | "cwd": "${workspaceFolder}/api"
20 | }
21 | },
22 | {
23 | "type": "shell",
24 | "label": "npm prune",
25 | "command": "npm prune --production",
26 | "problemMatcher": [],
27 | "options": {
28 | "cwd": "${workspaceFolder}/api"
29 | }
30 | }
31 | ]
32 | }
33 |
--------------------------------------------------------------------------------
/api/models/todoItem.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose');
2 |
3 | const schema = new mongoose.Schema({
4 | id: mongoose.Types.ObjectId,
5 | listId: {
6 | type: mongoose.Schema.Types.ObjectId,
7 | required: true
8 | },
9 | name: {
10 | type: String,
11 | required: true
12 | },
13 | userId: {
14 | type: String,
15 | required: true
16 | },
17 | description: String,
18 | state: {
19 | type: String,
20 | required: true,
21 | default: 'active'
22 | },
23 | dueDate: Date,
24 | completedDate: Date,
25 | }, {
26 | timestamps: {
27 | createdAt: "createdDate",
28 | updatedAt: "updatedDate"
29 | }
30 | });
31 |
32 | module.exports.TodoItemModel = mongoose.model("TodoItem", schema, "TodoItem");
--------------------------------------------------------------------------------
/src/TopBar.js:
--------------------------------------------------------------------------------
1 | import { useSelector } from "react-redux";
2 | import { selectUserDetails } from "./features/user/userSlice";
3 |
4 | function TopBar() {
5 | const userDetails = useSelector(selectUserDetails);
6 |
7 | return (
8 |
9 |
10 | Todo manager
11 |
12 | {
13 | userDetails !== '' // user logged in
14 | ? Welcome, {userDetails} Logout
15 | : Login
16 | }
17 |
18 |
19 |
20 | )
21 | }
22 |
23 | export default TopBar;
--------------------------------------------------------------------------------
/src/features/items/itemAPI.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 | const baseUrl = '/api/lists';
3 |
4 | export const itemService = {
5 | list: async (listId) => {
6 | const response = await axios.get(`${baseUrl}/${listId}/items`);
7 | return response.data;
8 | },
9 | save: async (item) => {
10 | if (!item.id) {
11 | // New item, post/save
12 | item.state = 'active';
13 | const response = await axios.post(`${baseUrl}/${item.listId}/items`, item);
14 | return response.data;
15 | } else {
16 | // Existing item updating, put/save
17 | const response = await axios.put(`${baseUrl}/${item.listId}/items/${item.id}`, item);
18 | return response.data;
19 | }
20 | },
21 | delete: async (item) => {
22 | await axios.delete(`${baseUrl}/${item.listId}/items/${item.id}`);
23 | return item;
24 | }
25 | }
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "dependencies": {
3 | "@reduxjs/toolkit": "^1.8.1",
4 | "ajv": "^8.11.0",
5 | "axios": "^0.27.2",
6 | "ci": "^2.2.0",
7 | "react": "^18.1.0",
8 | "react-dom": "^18.1.0",
9 | "react-redux": "^8.0.1",
10 | "react-scripts": "^5.0.1",
11 | "web-vitals": "^2.1.4"
12 | },
13 | "scripts": {
14 | "start": "swa start http://localhost:3000 --api-location ./api --run 'react-scripts start'",
15 | "dev": "swa start http://localhost:3000 --api-location ./api --run 'react-scripts start'",
16 | "dev:install": "npm install && cd api && npm install && func init --language javascript --worker-runtime node",
17 | "build": "react-scripts build"
18 | },
19 | "browserslist": {
20 | "production": [
21 | ">0.2%",
22 | "not dead",
23 | "not op_mini all"
24 | ],
25 | "development": [
26 | "last 1 chrome version",
27 | "last 1 firefox version",
28 | "last 1 safari version"
29 | ]
30 | },
31 | "devDependencies": {
32 | "@azure/static-web-apps-cli": "^1.0.1"
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/features/user/userSlice.js:
--------------------------------------------------------------------------------
1 | import { createAsyncThunk, createSlice } from "@reduxjs/toolkit"
2 |
3 | const initialState = {
4 | userDetails: ''
5 | }
6 |
7 | export const getUserAsync = createAsyncThunk(
8 | 'user/get',
9 | async () => {
10 | // Retrieve response from /.auth/me
11 | const response = await fetch('/.auth/me');
12 | // Convert to JSON
13 | const payload = await response.json();
14 | // Retrieve the clientPrincipal (current user)
15 | const { clientPrincipal } = payload;
16 | if (clientPrincipal) return clientPrincipal.userDetails;
17 | else return '';
18 | }
19 | )
20 |
21 | export const userSlice = createSlice({
22 | name: 'user',
23 | initialState,
24 | reducers: {},
25 | extraReducers: (builder) => {
26 | builder.addCase(getUserAsync.fulfilled, (state, action) => {
27 | // put user into state
28 | state.userDetails = action.payload;
29 | });
30 | }
31 | });
32 |
33 | export const selectUserDetails = (state) => {
34 | return state.user.userDetails;
35 | }
36 |
37 | export default userSlice.reducer;
--------------------------------------------------------------------------------
/src/Display.js:
--------------------------------------------------------------------------------
1 | import { useEffect } from 'react';
2 | import { useDispatch, useSelector } from 'react-redux';
3 | import { Items } from './features/items/Items';
4 | import { listAsync as getItems } from './features/items/itemSlice';
5 | import { Lists } from './features/lists/Lists';
6 | import { listAsync, selectActiveList } from './features/lists/listSlice';
7 |
8 | function Display() {
9 | const dispatch = useDispatch();
10 |
11 | // Load all lists
12 | useEffect(() => {
13 | dispatch(listAsync());
14 | }, [dispatch]);
15 |
16 | // get the currently selected list
17 | const activeList = useSelector(selectActiveList);
18 |
19 | // update items for new list
20 | useEffect(() => {
21 | if (activeList && activeList.id) dispatch(getItems(activeList.id));
22 | }, [activeList, dispatch])
23 |
24 | return (
25 |
26 |
30 |
31 | {activeList.name ? {activeList.name} items
: }
32 |
33 |
34 |
35 | )
36 | }
37 |
38 | export default Display;
--------------------------------------------------------------------------------
/.devcontainer/devcontainer.json:
--------------------------------------------------------------------------------
1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the README at:
2 | // https://github.com/microsoft/vscode-dev-containers/tree/v0.217.4/containers/javascript-node-mongo
3 | // Update the VARIANT arg in docker-compose.yml to pick a Node.js version
4 | {
5 | "name": "Node.js & Mongo DB",
6 | "dockerComposeFile": "docker-compose.yml",
7 | "service": "app",
8 | "workspaceFolder": "/workspace",
9 |
10 | // Set *default* container specific settings.json values on container create.
11 | "settings": {},
12 |
13 | // Add the IDs of extensions you want installed when the container is created.
14 | "extensions": [
15 | "ms-vscode.azure-account",
16 | "dbaeumer.vscode-eslint",
17 | "mongodb.mongodb-vscode",
18 | "ms-azuretools.vscode-azurefunctions",
19 | "ms-azuretools.vscode-cosmosdb",
20 | "ms-azuretools.vscode-azurestaticwebapps"
21 | ],
22 |
23 | // Use 'forwardPorts' to make a list of ports inside the container available locally.
24 | "forwardPorts": [4280, 7071, 3000],
25 |
26 | // Use 'postCreateCommand' to run commands after the container is created.
27 | // "postCreateCommand": "yarn install",
28 |
29 | // Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root.
30 | "remoteUser": "node"
31 | }
32 |
--------------------------------------------------------------------------------
/.devcontainer/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3.8'
2 |
3 | services:
4 | app:
5 | build:
6 | context: .
7 | dockerfile: Dockerfile
8 | args:
9 | # Update 'VARIANT' to pick an LTS version of Node.js: 16, 14, 12.
10 | # Append -bullseye or -buster to pin to an OS version.
11 | # Use -bullseye variants on local arm64/Apple Silicon.
12 | VARIANT: 14-buster
13 | volumes:
14 | - ..:/workspace:cached
15 |
16 | # Overrides default command so things don't shut down after the process ends.
17 | command: sleep infinity
18 |
19 | # Runs app on the same network as the database container, allows "forwardPorts" in devcontainer.json function.
20 | network_mode: service:db
21 |
22 | # Uncomment the next line to use a non-root user for all processes.
23 | # user: node
24 |
25 | # Use "forwardPorts" in **devcontainer.json** to forward an app port locally.
26 | # (Adding the "ports" property to this file will not forward from a Codespace.)
27 |
28 | db:
29 | image: mongo:latest
30 | restart: unless-stopped
31 | volumes:
32 | - mongodb-data:/data/db
33 |
34 | # Uncomment to change startup options
35 | # environment:
36 | # MONGO_INITDB_ROOT_USERNAME: root
37 | # MONGO_INITDB_ROOT_PASSWORD: example
38 | # MONGO_INITDB_DATABASE: your-database-here
39 |
40 | # Add "forwardPorts": ["27017"] to **devcontainer.json** to forward MongoDB locally.
41 | # (Adding the "ports" property to this file will not forward from a Codespace.)
42 |
43 | volumes:
44 | mongodb-data:
--------------------------------------------------------------------------------
/api/lists/index.js:
--------------------------------------------------------------------------------
1 | const { getUserId } = require('../core.js');
2 | const store = require('../models/store.js');
3 |
4 | module.exports = async function (context, req) {
5 | context.log('List function called.');
6 | let userId = null;
7 |
8 | try {
9 | userId = getUserId(req);
10 | // If no current user, return 401
11 | if(!userId) {
12 | context.res.status = 401;
13 | return;
14 | }
15 | } catch {
16 | // Error, return unauthorized
17 | context.res.status = 401;
18 | return;
19 | }
20 |
21 |
22 | // set return value to JSON
23 | context.res = {
24 | header: {
25 | "Content-Type": "application/json"
26 | }
27 | }
28 |
29 | // connect to the database
30 | await store.connect(context.log);
31 |
32 | // Read the method and determine requested action
33 | switch (req.method) {
34 | case 'GET':
35 | // return all lists
36 | await getLists(context, userId);
37 | break;
38 | case 'POST':
39 | // create new list
40 | await createList(context, userId);
41 | break;
42 | }
43 | }
44 |
45 | async function getLists(context, userId) {
46 | const lists = await store.listStore.list(userId);
47 | context.res.body = lists;
48 | }
49 |
50 | async function createList(context, userId) {
51 | let newList = context.req.body;
52 | newList.userId = userId;
53 | newList = await store.listStore.create(newList);
54 | context.res.status = 201;
55 | context.res.body = newList;
56 | }
57 |
--------------------------------------------------------------------------------
/api/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | lerna-debug.log*
8 |
9 | # Diagnostic reports (https://nodejs.org/api/report.html)
10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
11 |
12 | # Runtime data
13 | pids
14 | *.pid
15 | *.seed
16 | *.pid.lock
17 |
18 | # Directory for instrumented libs generated by jscoverage/JSCover
19 | lib-cov
20 |
21 | # Coverage directory used by tools like istanbul
22 | coverage
23 |
24 | # nyc test coverage
25 | .nyc_output
26 |
27 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
28 | .grunt
29 |
30 | # Bower dependency directory (https://bower.io/)
31 | bower_components
32 |
33 | # node-waf configuration
34 | .lock-wscript
35 |
36 | # Compiled binary addons (https://nodejs.org/api/addons.html)
37 | build/Release
38 |
39 | # Dependency directories
40 | node_modules/
41 | jspm_packages/
42 |
43 | # TypeScript v1 declaration files
44 | typings/
45 |
46 | # Optional npm cache directory
47 | .npm
48 |
49 | # Optional eslint cache
50 | .eslintcache
51 |
52 | # Optional REPL history
53 | .node_repl_history
54 |
55 | # Output of 'npm pack'
56 | *.tgz
57 |
58 | # Yarn Integrity file
59 | .yarn-integrity
60 |
61 | # dotenv environment variables file
62 | .env
63 | .env.test
64 |
65 | # parcel-bundler cache (https://parceljs.org/)
66 | .cache
67 |
68 | # next.js build output
69 | .next
70 |
71 | # nuxt.js build output
72 | .nuxt
73 |
74 | # vuepress build output
75 | .vuepress/dist
76 |
77 | # Serverless directories
78 | .serverless/
79 |
80 | # FuseBox cache
81 | .fusebox/
82 |
83 | # DynamoDB Local files
84 | .dynamodb/
85 |
86 | # TypeScript output
87 | dist
88 | out
89 |
90 | # Azure Functions artifacts
91 | bin
92 | obj
93 | appsettings.json
94 | local.settings.json
--------------------------------------------------------------------------------
/local-setup.md:
--------------------------------------------------------------------------------
1 | # Local setup
2 |
3 | To support running locally, the starter project is configured with a [dev container](https://code.visualstudio.com/docs/remote/create-dev-container?WT.mc_id=academic-45074-chrhar). The container has the following resources:
4 |
5 | - Node.js
6 | - Azure Functions Core Tools
7 | - MongoDB
8 |
9 | To run the project, you will need the following:
10 |
11 | - [Docker](https://docs.docker.com/engine/install/)
12 | - [Visual Studio Code](https://code.visualstudio.com?WT.mc_id=academic-45074-chrhar)
13 | - [Remote - Containers extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers&WT.mc_id=academic-45074-chrhar)
14 |
15 | ## Setup
16 |
17 | 1. Clone the repository you created earlier when [deploying to Azure](https://docs.microsoft.com/azure/static-web-apps/add-mongoose?WT.mc_id=academic-45074-chrhar), or [create a copy from the template](https://github.com/login?return_to=/staticwebdev/mongoose-starter/generate) and clone your copy
18 |
19 | ```bash
20 | git clone
21 | cd
22 | ```
23 |
24 | 1. Open the project in Visual Studio Code
25 |
26 | ```bash
27 | code .
28 | ```
29 |
30 | 1. When prompted inside Visual Studio Code, select **Reopen in Container**. The container will build, and Visual Studio Code wil refresh.
31 |
32 | 1. Inside Visual Studio Code, open a terminal window by selecting **View** > **Terminal**, and execute the following code to install the packages and run the site
33 |
34 | ```bash
35 | npm dev:install
36 | npm run dev
37 | ```
38 |
39 | Your project will now start!
40 |
41 | 1. Navigate to [http://localhost:4280](http://localhost:4280) to use your site
42 |
43 | > **NOTE** You might be prompted to open a different port. The Azure Static Web Apps CLI will host the project on port [4280](http://localhost:4280).
44 |
--------------------------------------------------------------------------------
/src/features/lists/Lists.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from "react";
2 | import { useDispatch, useSelector } from "react-redux";
3 | import { saveAsync, selectLists, selectActiveList, activateList } from "./listSlice";
4 |
5 | function DisplayList(props) {
6 | const dispatch = useDispatch();
7 |
8 | const activeList = useSelector(selectActiveList);
9 | const [active, setActive] = useState('');
10 | const list = props.list;
11 |
12 | useEffect(() => {
13 | setActive(list.id === activeList.id ? 'active' : '');
14 | }, [activeList, list.id]);
15 |
16 | return (
17 |
18 | )
19 | }
20 |
21 | export function Lists() {
22 | const dispatch = useDispatch();
23 | const lists = useSelector(selectLists);
24 |
25 | const [newList, setNewList] = useState({ name: '' });
26 |
27 | function onSubmitNewList(event) {
28 | // prevent form from submitting
29 | event.preventDefault();
30 | // save data
31 | dispatch(saveAsync(newList));
32 | // reset item
33 | setNewList({ name: '' });
34 | }
35 |
36 | return (
37 |
38 |
42 |
43 |
44 | { lists.map(list => ) }
45 |
46 |
47 | )
48 | }
--------------------------------------------------------------------------------
/.github/workflows/azure-static-web-apps-kind-plant-02300391e.yml:
--------------------------------------------------------------------------------
1 | name: Azure Static Web Apps CI/CD
2 |
3 | on:
4 | push:
5 | branches:
6 | - add-react-deploy
7 | pull_request:
8 | types: [opened, synchronize, reopened, closed]
9 | branches:
10 | - add-react-deploy
11 |
12 | jobs:
13 | build_and_deploy_job:
14 | if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.action != 'closed')
15 | runs-on: ubuntu-latest
16 | name: Build and Deploy Job
17 | steps:
18 | - uses: actions/checkout@v2
19 | with:
20 | submodules: true
21 | - name: Build And Deploy
22 | id: builddeploy
23 | uses: Azure/static-web-apps-deploy@v1
24 | with:
25 | azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_KIND_PLANT_02300391E }}
26 | repo_token: ${{ secrets.GITHUB_TOKEN }} # Used for Github integrations (i.e. PR comments)
27 | action: "upload"
28 | ###### Repository/Build Configurations - These values can be configured to match your app requirements. ######
29 | # For more information regarding Static Web App workflow configurations, please visit: https://aka.ms/swaworkflowconfig
30 | app_location: "/" # App source code path
31 | api_location: "api" # Api source code path - optional
32 | output_location: "build" # Built app content directory - optional
33 | ###### End of Repository/Build Configurations ######
34 |
35 | close_pull_request_job:
36 | if: github.event_name == 'pull_request' && github.event.action == 'closed'
37 | runs-on: ubuntu-latest
38 | name: Close Pull Request Job
39 | steps:
40 | - name: Close Pull Request
41 | id: closepullrequest
42 | uses: Azure/static-web-apps-deploy@v1
43 | with:
44 | azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_KIND_PLANT_02300391E }}
45 | action: "close"
46 |
--------------------------------------------------------------------------------
/src/features/lists/listSlice.js:
--------------------------------------------------------------------------------
1 | import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
2 | import { listService } from "./listAPI";
3 |
4 | const initialState = {
5 | data: [],
6 | activeList: {},
7 | status: 'idle'
8 | }
9 |
10 | export const listAsync = createAsyncThunk(
11 | 'lists/get',
12 | async () => {
13 | return await listService.list();
14 | }
15 | )
16 |
17 | export const saveAsync = createAsyncThunk(
18 | 'lists/save',
19 | async (list) => {
20 | return await listService.save(list);
21 | }
22 | )
23 |
24 | export const listSlice = createSlice({
25 | name: 'list',
26 | initialState,
27 | reducers: {
28 | activateList(state, action) {
29 | state.activeList = action.payload
30 | }
31 | },
32 | extraReducers: (builder) => {
33 | builder
34 | .addCase(listAsync.fulfilled, (state, action) => {
35 | // all lists loaded
36 | // put lists into state
37 | state.data = action.payload
38 | })
39 | .addCase(saveAsync.fulfilled, (state, action) => {
40 | // list saved
41 | // See if current list exists
42 | const existingListIndex = state.data.findIndex(l => l.id === action.payload.id);
43 | if (existingListIndex > -1) {
44 | // list exists, replace with updated list
45 | state.data[existingListIndex] = action.payload.action;
46 | } else {
47 | // list does not exist, add to end
48 | state.data.push(action.payload);
49 | state.activeList = action.payload;
50 | }
51 | })
52 | }
53 | })
54 |
55 | export const selectLists = (state) => {
56 | return state.lists.data;
57 | }
58 | export const selectActiveList = (state) => {
59 | return state.lists.activeList;
60 | }
61 |
62 | export default listSlice.reducer;
63 | export const { activateList } = listSlice.actions;
--------------------------------------------------------------------------------
/src/features/items/itemSlice.js:
--------------------------------------------------------------------------------
1 | import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
2 | import { itemService } from "./itemAPI";
3 |
4 | const initialState = {
5 | data: [],
6 | status: 'idle'
7 | }
8 |
9 | export const listAsync = createAsyncThunk(
10 | 'items/getList',
11 | async (data) => {
12 | return await itemService.list(data);
13 | }
14 | )
15 |
16 | export const loadAsync = createAsyncThunk(
17 | 'items/load',
18 | async (data) => {
19 | return await itemService.load(data);
20 | }
21 | )
22 |
23 | export const saveAsync = createAsyncThunk(
24 | 'items/save',
25 | async (data) => {
26 | return await itemService.save(data);
27 | }
28 | )
29 |
30 | export const removeAsync = createAsyncThunk(
31 | 'items/remove',
32 | async (data) => {
33 | return await itemService.delete(data);
34 | }
35 | )
36 |
37 | export const itemSlice = createSlice({
38 | name: 'item',
39 | initialState,
40 | reducers: {
41 |
42 | },
43 | extraReducers: (builder) => {
44 | builder
45 | .addCase(listAsync.fulfilled, (state, action) => {
46 | // put items into state
47 | state.data = action.payload;
48 | })
49 | .addCase(removeAsync.fulfilled, (state, action) => {
50 | // remove item from state
51 | state.data = state.data.filter(i => i.id !== action.payload.id);
52 | })
53 | .addCase(saveAsync.fulfilled, (state, action) => {
54 | // See if current item exists
55 | const existingItemIndex = state.data.findIndex(i => i.id === action.payload.id);
56 | if (existingItemIndex > -1) {
57 | // item exists, replace with updated item
58 | state.data[existingItemIndex] = action.payload;
59 | }
60 | else {
61 | // item does not exist, add new item to end
62 | state.data.push(action.payload);
63 | }
64 | })
65 | },
66 | })
67 |
68 | export const selectListItems = (state) => {
69 | return state.items.data;
70 | }
71 | export default itemSlice.reducer;
--------------------------------------------------------------------------------
/.devcontainer/Dockerfile:
--------------------------------------------------------------------------------
1 | # [Choice] Node.js version (use -bullseye variants on local arm64/Apple Silicon): 16, 14, 12, 16-bullseye, 14-bullseye, 12-bullseye, 16-buster, 14-buster, 12-buster
2 | ARG VARIANT=16-bullseye
3 | FROM mcr.microsoft.com/vscode/devcontainers/javascript-node:0-${VARIANT}
4 |
5 | # Install MongoDB command line tools if on buster and x86_64 (arm64 not supported)
6 | ARG MONGO_TOOLS_VERSION=5.0
7 | RUN . /etc/os-release \
8 | && if [ "${VERSION_CODENAME}" = "buster" ] && [ "$(dpkg --print-architecture)" = "amd64" ]; then \
9 | curl -sSL "https://www.mongodb.org/static/pgp/server-${MONGO_TOOLS_VERSION}.asc" | gpg --dearmor > /usr/share/keyrings/mongodb-archive-keyring.gpg \
10 | && echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/mongodb-archive-keyring.gpg] http://repo.mongodb.org/apt/debian $(lsb_release -cs)/mongodb-org/${MONGO_TOOLS_VERSION} main" | tee /etc/apt/sources.list.d/mongodb-org-${MONGO_TOOLS_VERSION}.list \
11 | && apt-get update && export DEBIAN_FRONTEND=noninteractive \
12 | && apt-get install -y mongodb-database-tools mongodb-mongosh \
13 | && apt-get clean -y && rm -rf /var/lib/apt/lists/*; \
14 | fi
15 |
16 | RUN curl https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor > microsoft.gpg
17 | RUN mv microsoft.gpg /etc/apt/trusted.gpg.d/microsoft.gpg
18 | RUN sudo sh -c 'echo "deb [arch=amd64] https://packages.microsoft.com/debian/$(lsb_release -rs | cut -d'.' -f 1)/prod $(lsb_release -cs) main" > /etc/apt/sources.list.d/dotnetdev.list'
19 | RUN apt-get update && export DEBIAN_FRONTEND=noninteractive && apt-get install azure-functions-core-tools-4
20 | # RUN sudo apt-get install azure-functions-core-tools-4
21 |
22 | # [Optional] Uncomment this section to install additional OS packages.
23 | # RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
24 | # && apt-get -y install --no-install-recommends
25 |
26 | # [Optional] Uncomment if you want to install an additional version of node using nvm
27 | # ARG EXTRA_NODE_VERSION=10
28 | # RUN su node -c "source /usr/local/share/nvm/nvm.sh && nvm install ${EXTRA_NODE_VERSION}"
29 |
30 | # [Optional] Uncomment if you want to install more global node modules
31 | # RUN su node -c "npm install -g "
32 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
17 |
18 |
27 |
28 |
29 | React Redux App
30 |
31 |
32 |
33 |
34 |
44 |
45 |
46 |
47 |
--------------------------------------------------------------------------------
/api/models/store.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose');
2 | const { getConfig } = require("../config/index.js");
3 | const { TodoItemModel } = require("./todoItem.js");
4 | const { TodoListModel } = require("./todoList.js");
5 |
6 | async function configureMongoose(log) {
7 | // Configure JSON output to client
8 | // Removes version, sets _id to id
9 | mongoose.set("toJSON", {
10 | virtuals: true,
11 | versionKey: false,
12 | transform: (_, converted) => {
13 | converted.id = converted._id;
14 | delete converted._id;
15 | }
16 | });
17 |
18 | try {
19 | const db = mongoose.connection;
20 | db.on("connecting", () => log("Mongoose connecting..."));
21 | db.on("connected", () => log("Mongoose connected successfully!"));
22 | db.on("disconnecting", () => log("Mongoose disconnecting..."));
23 | db.on("disconnected", () => log("Mongoose disconnected successfully!"));
24 | db.on("error", (err) => log("Mongoose database error:", err));
25 |
26 | // Load configuration information
27 | const config = await getConfig(log);
28 |
29 | await mongoose.connect(
30 | config.database.connectionString,
31 | { dbName: config.database.databaseName, useNewUrlParser: true }
32 | );
33 | }
34 | catch (err) {
35 | log(`Mongoose database error: ${err}`);
36 | throw err;
37 | }
38 | };
39 |
40 | const itemStore = {
41 | list: async(userId, listId) => {
42 | return await TodoItemModel.find({ userId, listId });
43 | },
44 | create: async(item) => {
45 | return await TodoItemModel.create(item);
46 | },
47 | update: async(id, userId, item) => {
48 | return await TodoItemModel.updateOne({ _id: id, userId}, item);
49 | },
50 | delete: async(id, userId) => {
51 | return await TodoItemModel.deleteOne({ _id: id, userId });
52 | }
53 | }
54 |
55 | const listStore = {
56 | list: async(userId) => {
57 | return await TodoListModel.find({ userId }).exec();
58 | },
59 | create: async(list) => {
60 | return await TodoListModel.create(list);
61 | },
62 | update: async(id, userId, list) => {
63 | return await TodoListModel.updateOne({ _id: id, userId}, list).exec();
64 | },
65 | delete: async(id, userId) => {
66 | return await TodoListModel.deleteOne({ _id: id, userId }).exec();
67 | }
68 | }
69 |
70 | module.exports = {
71 | connect: async(log) => {
72 | if (mongoose.connection.readyState === 1) {
73 | log('Connection already established.');
74 | return;
75 | }
76 |
77 | await configureMongoose(log);
78 | },
79 | itemStore,
80 | listStore
81 | }
82 |
--------------------------------------------------------------------------------
/api/items/index.js:
--------------------------------------------------------------------------------
1 | const { getUserId } = require('../core.js');
2 | const store = require('../models/store.js');
3 |
4 | // Export our function
5 | module.exports = async function (context, req) {
6 | // Get the current user
7 | const userId = getUserId(req);
8 |
9 | // If no current user, return 401
10 | if(!userId) {
11 | context.res.status = 401;
12 | return;
13 | }
14 |
15 | // setup our default content type (we always return JSON)
16 | context.res = {
17 | header: {
18 | "Content-Type": "application/json"
19 | }
20 | }
21 |
22 | // Connect to the database
23 | await store.connect(context.log);
24 |
25 | // Read the method and determine the requested action
26 | switch (req.method) {
27 | // If get, return all tasks
28 | case 'GET':
29 | await getItems(context, userId);
30 | break;
31 | // If post, create new task
32 | case 'POST':
33 | await createItem(context, userId);
34 | break;
35 | // If put, update task
36 | case 'PUT':
37 | await updateItem(context, userId);
38 | break;
39 | case 'DELETE':
40 | await deleteItem(context, userId);
41 | break;
42 | }
43 | };
44 |
45 | // Return all items
46 | async function getItems(context, userId) {
47 | // Get the list ID from the URL
48 | const listId = context.bindingData.listId;
49 | // load all items from database filtered by userId
50 | const items = await store.itemStore.list(userId, listId);
51 | // return all items
52 | context.res.body = items;
53 | }
54 |
55 | // Create new item
56 | async function createItem(context, userId) {
57 | // Read the uploaded item
58 | let item = context.req.body;
59 | // Add the userId
60 | item.userId = userId;
61 | // Add the listId
62 | item.listId = context.bindingData.listId;
63 | // Save to database
64 | item = await store.itemStore.create(item);
65 | context.log(item);
66 | // Set the HTTP status to created
67 | context.res.status = 201;
68 | // return new object
69 | context.res.body = item;
70 | }
71 |
72 | // Update an existing function
73 | async function updateItem(context, userId) {
74 | // Grab the id from the URL (stored in bindingData)
75 | const id = context.bindingData.id;
76 | // Get the item from the body
77 | const item = context.req.body;
78 | // Get the listId from the requeest
79 | item.listId = context.bindingData.listId;
80 | // Add the userId
81 | item.userId = userId;
82 | // Update the item in the database
83 | const result = await store.itemStore.update(id, userId, item);
84 | // Check to ensure an item was modified
85 | if (result.nModified === 1) {
86 | context.res.body = item;
87 | } else {
88 | // Item not found, status 404
89 | context.res.status = 404;
90 | }
91 | }
92 |
93 | async function deleteItem(context, userId) {
94 | const id = context.bindingData.id;
95 | const result = await store.itemStore.delete(id, userId);
96 | context.res = {id};
97 | }
98 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Static Web Apps - Mongoose starter
2 |
3 | This template is designed to be a starter for creating [React](https://reactjs.org) apps using [Azure Static Web Apps](https://docs.microsoft.com/azure/static-web-apps/overview?WT.mc_id=academic-45074-chrhar), with [Azure Cosmos DB API for Mongo DB](https://docs.microsoft.com/azure/cosmos-db/mongodb/mongodb-introduction?WT.mc_id=academic-45074-chrhar) as a database and a [Mongoose](https://mongoosejs.com/) client. It's built with the following:
4 |
5 | - Azure resources
6 | - [Azure Static Web Apps](https://docs.microsoft.com/azure/static-web-apps/overview?WT.mc_id=academic-45074-chrhar)
7 | - [Azure Cosmos DB API for Mongo DB](https://docs.microsoft.com/azure/cosmos-db/mongodb/mongodb-introduction?WT.mc_id=academic-45074-chrhar)
8 | - Application libraries
9 | - [React](https://reactjs.org/) and [Redux Toolkit](https://redux-toolkit.js.org/)
10 | - [Mongoose](https://mongoosejs.com/)
11 | - [Azure Functions](https://docs.microsoft.com/azure/azure-functions/functions-overview?WT.mc_id=academic-45074-chrhar)
12 | - Development libraries
13 | - [Azure Static Web Apps CLI](https://docs.microsoft.com/azure/static-web-apps/local-development?WT.mc_id=academic-45074-chrhar)
14 |
15 | ## Azure deployment
16 |
17 | Please refer to the [documentation](https://docs.microsoft.com/azure/static-web-apps/add-mongoose?WT.mc_id=academic-45074-chrhar) for information on creating the appropriate server resources and deploying the project.
18 |
19 | > **Important**
20 | >
21 | > Two environmental variables are required for the project:
22 | >
23 | > - `AZURE_COSMOS_CONNECTION_STRING`: The connection string to the database server
24 | > - `AZURE_COSMOS_DATABASE_NAME`: The name of the database
25 | >
26 | > These can be stored in [application settings](https://docs.microsoft.com/azure/static-web-apps/add-mongoose?WT.mc_id=academic-45074-chrhar#configure-database-connection-string) in Azure Static Web Apps. When developing locally, the project will default to using MongoDB running on localhost.
27 |
28 | ## Local development
29 |
30 | You can run the project locally using containers by following the [local setup instructions](./local-setup.md).
31 |
32 | ## Project structure
33 |
34 | This starter project is designed to be a template for React with Redux and hosted on Azure Static Web Apps. It uses the [Redux Toolkit](https://redux-toolkit.js.org/). The project is a Todo application using authentication for Azure Static Web Apps. Todo items are collected in lists, and are scoped to individual users. To use the application:
35 |
36 | 1. Login using GitHub by clicking the login link.
37 | 1. Create a list.
38 | 1. Create todo items for the list.
39 |
40 | ### package.json scripts
41 |
42 | - **dev**: Starts the SWA CLI, Azure Functions and the React app. The application will be available on [http://localhost:4280](http://localhost:4280)
43 |
44 | ### src/app
45 |
46 | Contains [store.js](./src/app/store.js), which manages global state for the application.
47 |
48 | ### src/features
49 |
50 | Contains three "features", one each for [items](./src/features/items/), [lists](./src/features/lists/) and [user](./src/features/user/). *lists* and *items* contain a [slice](https://redux-toolkit.js.org/api/createSlice) to manage their respective state and a React component.
51 |
52 | ### api
53 |
54 | Root folder for Azure Functions. All [new serverless functions](https://docs.microsoft.com/azure/static-web-apps/add-api?tabs=react#create-the-api?WT.mc_id=academic-45074-chrhar) are added to this directory.
55 |
56 | #### api/config
57 |
58 | Contains the configuration for the database. Two environmental variables are required for the project:
59 |
60 | - `AZURE_COSMOS_CONNECTION_STRING`: The connection string to the database server
61 | - `AZURE_COSMOS_DATABASE_NAME`: The name of the database
62 |
63 | These can be stored in [application settings](https://docs.microsoft.com/azure/static-web-apps/add-mongoose?WT.mc_id=academic-45074-chrhar#configure-database-connection-string) in Azure Static Web Apps. When developing locally, the project will default to using MongoDB running on localhost. You can change the default values by updating *default.json*.
64 |
65 | #### api/models
66 |
67 | Contains the two Mongoose models, TodoItemModel and TodoListModel. It also contains *store.js*, which exposes helper functions for [CRUD](https://en.wikipedia.org/wiki/Create,_read,_update_and_delete) operations.
68 |
--------------------------------------------------------------------------------
/src/features/items/Items.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import { useDispatch, useSelector } from "react-redux";
3 | import { selectActiveList } from "../lists/listSlice";
4 | import { removeAsync, saveAsync, selectListItems } from "./itemSlice";
5 |
6 | function ItemsDisplay(props) {
7 | const dispatch = useDispatch();
8 | const [newItem, setNewItem] = useState({ name: '' });
9 |
10 | function onSubmitNewItem(event) {
11 | // prevent form from submitting
12 | event.preventDefault();
13 | // set the listId
14 | newItem.listId = props.listId;
15 | // save data
16 | dispatch(saveAsync(newItem));
17 | // reset item
18 | setNewItem({ name: '' });
19 | }
20 |
21 | return (
22 |
23 |
27 |
28 |
29 |
30 |
31 |
34 |
35 |
36 |
37 |
38 | {props.activeItems.map(item => )}
39 |
40 |
41 |
42 |
43 |
44 |
45 |
48 |
49 |
50 |
51 |
52 | {props.doneItems.map(item => )}
53 |
54 |
55 |
56 |
57 |
58 |
59 | )
60 | }
61 |
62 | function ItemDisplay(props) {
63 | const dispatch = useDispatch();
64 |
65 | const [editItem, setEditItem] = useState(null);
66 |
67 | function onRemove(item) {
68 | dispatch(removeAsync(item));
69 | }
70 |
71 | function onToggleState(item) {
72 | const updatedItem = { ...item };
73 | updatedItem.state = item.state === 'active' ? 'done' : 'active';
74 | dispatch(saveAsync(updatedItem));
75 | }
76 |
77 | function onStartEdit(item) {
78 | setEditItem({ ...item });
79 | }
80 |
81 | function onEndEdit() {
82 | setEditItem(null);
83 | }
84 |
85 | function onSubmitEditForm(event) {
86 | // prevent form from submitting
87 | event.preventDefault();
88 | // call parent event handler
89 | dispatch(saveAsync(editItem));
90 | // end editing
91 | onEndEdit();
92 | }
93 |
94 | if (editItem && editItem.id === props.item.id) {
95 | // Editing this item
96 | return (
97 |
98 |
103 |
104 | )
105 | } else {
106 | // No edit item, just display it
107 | return (
108 |
109 | onStartEdit(props.item)}>{props.item.name}
110 |
111 |
112 |
113 |
114 |
115 |
116 | )
117 | }
118 | }
119 |
120 | export function Items() {
121 | const items = useSelector(selectListItems);
122 | const activeList = useSelector(selectActiveList);
123 |
124 | const activeItems = items.filter(i => i.state === 'active');
125 | const doneItems = items.filter(i => i.state === 'done');
126 | if (activeList.name) {
127 | // A list is selected, display items
128 | return (
129 |
130 | )
131 | } else {
132 | // No list selected, display message
133 | return (
134 | Select a list from the left to display items
135 | )
136 | }
137 | }
--------------------------------------------------------------------------------
/api/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "api",
3 | "version": "1.0.0",
4 | "lockfileVersion": 1,
5 | "requires": true,
6 | "dependencies": {
7 | "@types/bson": {
8 | "version": "4.0.5",
9 | "resolved": "https://registry.npmjs.org/@types/bson/-/bson-4.0.5.tgz",
10 | "integrity": "sha512-vVLwMUqhYJSQ/WKcE60eFqcyuWse5fGH+NMAXHuKrUAPoryq3ATxk5o4bgYNtg5aOM4APVg7Hnb3ASqUYG0PKg==",
11 | "requires": {
12 | "@types/node": "*"
13 | }
14 | },
15 | "@types/mongodb": {
16 | "version": "3.6.20",
17 | "resolved": "https://registry.npmjs.org/@types/mongodb/-/mongodb-3.6.20.tgz",
18 | "integrity": "sha512-WcdpPJCakFzcWWD9juKoZbRtQxKIMYF/JIAM4JrNHrMcnJL6/a2NWjXxW7fo9hxboxxkg+icff8d7+WIEvKgYQ==",
19 | "requires": {
20 | "@types/bson": "*",
21 | "@types/node": "*"
22 | }
23 | },
24 | "@types/node": {
25 | "version": "16.10.3",
26 | "resolved": "https://registry.npmjs.org/@types/node/-/node-16.10.3.tgz",
27 | "integrity": "sha512-ho3Ruq+fFnBrZhUYI46n/bV2GjwzSkwuT4dTf0GkuNFmnb8nq4ny2z9JEVemFi6bdEJanHLlYfy9c6FN9B9McQ=="
28 | },
29 | "bl": {
30 | "version": "2.2.1",
31 | "resolved": "https://registry.npmjs.org/bl/-/bl-2.2.1.tgz",
32 | "integrity": "sha512-6Pesp1w0DEX1N550i/uGV/TqucVL4AM/pgThFSN/Qq9si1/DF9aIHs1BxD8V/QU0HoeHO6cQRTAuYnLPKq1e4g==",
33 | "requires": {
34 | "readable-stream": "^2.3.5",
35 | "safe-buffer": "^5.1.1"
36 | }
37 | },
38 | "bluebird": {
39 | "version": "3.5.1",
40 | "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.1.tgz",
41 | "integrity": "sha512-MKiLiV+I1AA596t9w1sQJ8jkiSr5+ZKi0WKrYGUn6d1Fx+Ij4tIj+m2WMQSGczs5jZVxV339chE8iwk6F64wjA=="
42 | },
43 | "bson": {
44 | "version": "1.1.5",
45 | "resolved": "https://registry.npmjs.org/bson/-/bson-1.1.5.tgz",
46 | "integrity": "sha512-kDuEzldR21lHciPQAIulLs1LZlCXdLziXI6Mb/TDkwXhb//UORJNPXgcRs2CuO4H0DcMkpfT3/ySsP3unoZjBg=="
47 | },
48 | "config": {
49 | "version": "3.3.7",
50 | "resolved": "https://registry.npmjs.org/config/-/config-3.3.7.tgz",
51 | "integrity": "sha512-mX/n7GKDYZMqvvkY6e6oBY49W8wxdmQt+ho/5lhwFDXqQW9gI+Ahp8EKp8VAbISPnmf2+Bv5uZK7lKXZ6pf1aA==",
52 | "requires": {
53 | "json5": "^2.1.1"
54 | }
55 | },
56 | "core-util-is": {
57 | "version": "1.0.2",
58 | "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
59 | "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac="
60 | },
61 | "debug": {
62 | "version": "3.1.0",
63 | "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
64 | "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
65 | "requires": {
66 | "ms": "2.0.0"
67 | },
68 | "dependencies": {
69 | "ms": {
70 | "version": "2.0.0",
71 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
72 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
73 | }
74 | }
75 | },
76 | "denque": {
77 | "version": "1.4.1",
78 | "resolved": "https://registry.npmjs.org/denque/-/denque-1.4.1.tgz",
79 | "integrity": "sha512-OfzPuSZKGcgr96rf1oODnfjqBFmr1DVoc/TrItj3Ohe0Ah1C5WX5Baquw/9U9KovnQ88EqmJbD66rKYUQYN1tQ=="
80 | },
81 | "dotenv": {
82 | "version": "16.0.1",
83 | "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.1.tgz",
84 | "integrity": "sha512-1K6hR6wtk2FviQ4kEiSjFiH5rpzEVi8WW0x96aztHVMhEspNpc4DVOUTEHtEva5VThQ8IaBX1Pe4gSzpVVUsKQ=="
85 | },
86 | "inherits": {
87 | "version": "2.0.4",
88 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
89 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
90 | },
91 | "isarray": {
92 | "version": "1.0.0",
93 | "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
94 | "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE="
95 | },
96 | "json5": {
97 | "version": "2.2.1",
98 | "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz",
99 | "integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA=="
100 | },
101 | "kareem": {
102 | "version": "2.3.2",
103 | "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.3.2.tgz",
104 | "integrity": "sha512-STHz9P7X2L4Kwn72fA4rGyqyXdmrMSdxqHx9IXon/FXluXieaFA6KJ2upcHAHxQPQ0LeM/OjLrhFxifHewOALQ=="
105 | },
106 | "memory-pager": {
107 | "version": "1.5.0",
108 | "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz",
109 | "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==",
110 | "optional": true
111 | },
112 | "mongodb": {
113 | "version": "3.6.11",
114 | "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-3.6.11.tgz",
115 | "integrity": "sha512-4Y4lTFHDHZZdgMaHmojtNAlqkvddX2QQBEN0K//GzxhGwlI9tZ9R0vhbjr1Decw+TF7qK0ZLjQT292XgHRRQgw==",
116 | "requires": {
117 | "bl": "^2.2.1",
118 | "bson": "^1.1.4",
119 | "denque": "^1.4.1",
120 | "optional-require": "^1.0.3",
121 | "safe-buffer": "^5.1.2",
122 | "saslprep": "^1.0.0"
123 | }
124 | },
125 | "mongoose": {
126 | "version": "5.13.10",
127 | "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-5.13.10.tgz",
128 | "integrity": "sha512-h1tkPu/3grQjdWjoIA3uUNWvDFDcaNGwcmBLEkK9t/kzKq4L4fB2EoA/VLjQ7WGTL/pDKH6OElBTK7x/QPRvbg==",
129 | "requires": {
130 | "@types/bson": "1.x || 4.0.x",
131 | "@types/mongodb": "^3.5.27",
132 | "bson": "^1.1.4",
133 | "kareem": "2.3.2",
134 | "mongodb": "3.6.11",
135 | "mongoose-legacy-pluralize": "1.0.2",
136 | "mpath": "0.8.4",
137 | "mquery": "3.2.5",
138 | "ms": "2.1.2",
139 | "optional-require": "1.0.x",
140 | "regexp-clone": "1.0.0",
141 | "safe-buffer": "5.2.1",
142 | "sift": "13.5.2",
143 | "sliced": "1.0.1"
144 | },
145 | "dependencies": {
146 | "optional-require": {
147 | "version": "1.0.3",
148 | "resolved": "https://registry.npmjs.org/optional-require/-/optional-require-1.0.3.tgz",
149 | "integrity": "sha512-RV2Zp2MY2aeYK5G+B/Sps8lW5NHAzE5QClbFP15j+PWmP+T9PxlJXBOOLoSAdgwFvS4t0aMR4vpedMkbHfh0nA=="
150 | }
151 | }
152 | },
153 | "mongoose-legacy-pluralize": {
154 | "version": "1.0.2",
155 | "resolved": "https://registry.npmjs.org/mongoose-legacy-pluralize/-/mongoose-legacy-pluralize-1.0.2.tgz",
156 | "integrity": "sha512-Yo/7qQU4/EyIS8YDFSeenIvXxZN+ld7YdV9LqFVQJzTLye8unujAWPZ4NWKfFA+RNjh+wvTWKY9Z3E5XM6ZZiQ=="
157 | },
158 | "mpath": {
159 | "version": "0.8.4",
160 | "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.8.4.tgz",
161 | "integrity": "sha512-DTxNZomBcTWlrMW76jy1wvV37X/cNNxPW1y2Jzd4DZkAaC5ZGsm8bfGfNOthcDuRJujXLqiuS6o3Tpy0JEoh7g=="
162 | },
163 | "mquery": {
164 | "version": "3.2.5",
165 | "resolved": "https://registry.npmjs.org/mquery/-/mquery-3.2.5.tgz",
166 | "integrity": "sha512-VjOKHHgU84wij7IUoZzFRU07IAxd5kWJaDmyUzQlbjHjyoeK5TNeeo8ZsFDtTYnSgpW6n/nMNIHvE3u8Lbrf4A==",
167 | "requires": {
168 | "bluebird": "3.5.1",
169 | "debug": "3.1.0",
170 | "regexp-clone": "^1.0.0",
171 | "safe-buffer": "5.1.2",
172 | "sliced": "1.0.1"
173 | },
174 | "dependencies": {
175 | "safe-buffer": {
176 | "version": "5.1.2",
177 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
178 | "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
179 | }
180 | }
181 | },
182 | "ms": {
183 | "version": "2.1.2",
184 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
185 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
186 | },
187 | "optional-require": {
188 | "version": "1.1.8",
189 | "resolved": "https://registry.npmjs.org/optional-require/-/optional-require-1.1.8.tgz",
190 | "integrity": "sha512-jq83qaUb0wNg9Krv1c5OQ+58EK+vHde6aBPzLvPPqJm89UQWsvSuFy9X/OSNJnFeSOKo7btE0n8Nl2+nE+z5nA==",
191 | "requires": {
192 | "require-at": "^1.0.6"
193 | }
194 | },
195 | "process-nextick-args": {
196 | "version": "2.0.1",
197 | "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
198 | "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="
199 | },
200 | "readable-stream": {
201 | "version": "2.3.7",
202 | "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",
203 | "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==",
204 | "requires": {
205 | "core-util-is": "~1.0.0",
206 | "inherits": "~2.0.3",
207 | "isarray": "~1.0.0",
208 | "process-nextick-args": "~2.0.0",
209 | "safe-buffer": "~5.1.1",
210 | "string_decoder": "~1.1.1",
211 | "util-deprecate": "~1.0.1"
212 | },
213 | "dependencies": {
214 | "safe-buffer": {
215 | "version": "5.1.2",
216 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
217 | "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
218 | }
219 | }
220 | },
221 | "regexp-clone": {
222 | "version": "1.0.0",
223 | "resolved": "https://registry.npmjs.org/regexp-clone/-/regexp-clone-1.0.0.tgz",
224 | "integrity": "sha512-TuAasHQNamyyJ2hb97IuBEif4qBHGjPHBS64sZwytpLEqtBQ1gPJTnOaQ6qmpET16cK14kkjbazl6+p0RRv0yw=="
225 | },
226 | "require-at": {
227 | "version": "1.0.6",
228 | "resolved": "https://registry.npmjs.org/require-at/-/require-at-1.0.6.tgz",
229 | "integrity": "sha512-7i1auJbMUrXEAZCOQ0VNJgmcT2VOKPRl2YGJwgpHpC9CE91Mv4/4UYIUm4chGJaI381ZDq1JUicFii64Hapd8g=="
230 | },
231 | "safe-buffer": {
232 | "version": "5.2.1",
233 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
234 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="
235 | },
236 | "saslprep": {
237 | "version": "1.0.3",
238 | "resolved": "https://registry.npmjs.org/saslprep/-/saslprep-1.0.3.tgz",
239 | "integrity": "sha512-/MY/PEMbk2SuY5sScONwhUDsV2p77Znkb/q3nSVstq/yQzYJOH/Azh29p9oJLsl3LnQwSvZDKagDGBsBwSooag==",
240 | "optional": true,
241 | "requires": {
242 | "sparse-bitfield": "^3.0.3"
243 | }
244 | },
245 | "sift": {
246 | "version": "13.5.2",
247 | "resolved": "https://registry.npmjs.org/sift/-/sift-13.5.2.tgz",
248 | "integrity": "sha512-+gxdEOMA2J+AI+fVsCqeNn7Tgx3M9ZN9jdi95939l1IJ8cZsqS8sqpJyOkic2SJk+1+98Uwryt/gL6XDaV+UZA=="
249 | },
250 | "sliced": {
251 | "version": "1.0.1",
252 | "resolved": "https://registry.npmjs.org/sliced/-/sliced-1.0.1.tgz",
253 | "integrity": "sha1-CzpmK10Ewxd7GSa+qCsD+Dei70E="
254 | },
255 | "sparse-bitfield": {
256 | "version": "3.0.3",
257 | "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz",
258 | "integrity": "sha1-/0rm5oZWBWuks+eSqzM004JzyhE=",
259 | "optional": true,
260 | "requires": {
261 | "memory-pager": "^1.0.2"
262 | }
263 | },
264 | "string_decoder": {
265 | "version": "1.1.1",
266 | "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
267 | "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
268 | "requires": {
269 | "safe-buffer": "~5.1.0"
270 | },
271 | "dependencies": {
272 | "safe-buffer": {
273 | "version": "5.1.2",
274 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
275 | "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
276 | }
277 | }
278 | },
279 | "util-deprecate": {
280 | "version": "1.0.2",
281 | "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
282 | "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
283 | }
284 | }
285 | }
286 |
--------------------------------------------------------------------------------