├── database
├── .gitignore
├── mongo-init.js
└── start.js
├── .prettierignore
├── .gitignore
├── .vscode
└── extensions.json
├── frontend
├── src
│ ├── assets
│ │ └── layr-favicon-20201027.immutable.png
│ ├── index.html
│ ├── index.js
│ └── components
│ │ ├── common.js
│ │ ├── frontend.js
│ │ ├── movie-list.js
│ │ └── movie.js
├── jsconfig.json
├── babel.config.js
├── simple-deployment.config.js
├── package.json
└── webpack.config.js
├── backend
├── jsconfig.json
├── src
│ ├── handler.js
│ ├── server.js
│ ├── http-server.js
│ └── components
│ │ └── movie.js
├── babel.config.js
├── simple-deployment.config.js
└── package.json
├── .editorconfig
├── package.json
└── README.md
/database/.gitignore:
--------------------------------------------------------------------------------
1 | /data
2 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dist
3 | build
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | *.log
3 | node_modules
4 | dist
5 | build
6 | _private
7 | *.private.*
8 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": ["esbenp.prettier-vscode", "editorconfig.editorconfig"]
3 | }
4 |
--------------------------------------------------------------------------------
/database/mongo-init.js:
--------------------------------------------------------------------------------
1 | db.createUser({
2 | user: 'test',
3 | pwd: 'test',
4 | roles: [{role: 'readWrite', db: 'test'}]
5 | });
6 |
--------------------------------------------------------------------------------
/frontend/src/assets/layr-favicon-20201027.immutable.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/layrjs/crud-example-app-js-webpack/HEAD/frontend/src/assets/layr-favicon-20201027.immutable.png
--------------------------------------------------------------------------------
/backend/jsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2017",
4 | "module": "CommonJS",
5 | "checkJs": false,
6 | "experimentalDecorators": true
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/frontend/jsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2017",
4 | "module": "CommonJS",
5 | "checkJs": false,
6 | "experimentalDecorators": true
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/backend/src/handler.js:
--------------------------------------------------------------------------------
1 | import {createAWSLambdaHandlerForComponentServer} from '@layr/aws-integration';
2 |
3 | import {server} from './server';
4 |
5 | export const handler = createAWSLambdaHandlerForComponentServer(server);
6 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # editorconfig.org
2 | root = true
3 |
4 | [*]
5 | indent_style = space
6 | indent_size = 2
7 | end_of_line = lf
8 | charset = utf-8
9 | trim_trailing_whitespace = true
10 | insert_final_newline = true
11 |
12 | [*.md]
13 | trim_trailing_whitespace = false
14 |
--------------------------------------------------------------------------------
/backend/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = (api) => {
2 | api.cache(true);
3 |
4 | const presets = [
5 | [
6 | '@babel/preset-env',
7 | {
8 | targets: {node: '10'},
9 | loose: true
10 | }
11 | ]
12 | ];
13 |
14 | const plugins = [
15 | ['@babel/plugin-proposal-decorators', {legacy: true}],
16 | ['@babel/plugin-proposal-class-properties', {loose: true}]
17 | ];
18 |
19 | return {presets, plugins};
20 | };
21 |
--------------------------------------------------------------------------------
/backend/src/server.js:
--------------------------------------------------------------------------------
1 | import {ComponentServer} from '@layr/component-server';
2 | import {MongoDBStore} from '@layr/mongodb-store';
3 |
4 | import {Movie} from './components/movie';
5 |
6 | const connectionString = process.env.MONGODB_STORE_CONNECTION_STRING;
7 |
8 | if (!connectionString) {
9 | throw new Error(`'MONGODB_STORE_CONNECTION_STRING' environment variable is missing`);
10 | }
11 |
12 | const store = new MongoDBStore(connectionString);
13 | store.registerRootComponent(Movie);
14 |
15 | export const server = new ComponentServer(Movie);
16 |
--------------------------------------------------------------------------------
/backend/src/http-server.js:
--------------------------------------------------------------------------------
1 | import {ComponentHTTPServer} from '@layr/component-http-server';
2 |
3 | import {server} from './server';
4 |
5 | const backendURL = process.env.BACKEND_URL;
6 |
7 | if (!backendURL) {
8 | throw new Error(`'BACKEND_URL' environment variable is missing`);
9 | }
10 |
11 | const port = Number(new URL(backendURL).port);
12 |
13 | if (!port) {
14 | throw new Error(`'BACKEND_URL' environment variable should include a port`);
15 | }
16 |
17 | const httpServer = new ComponentHTTPServer(server, {port});
18 | httpServer.start();
19 |
--------------------------------------------------------------------------------
/frontend/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CRUD Example App
6 |
7 |
8 | <%= htmlWebpackPlugin.tags.headTags %>
9 |
10 |
11 |
12 |
13 | <%= htmlWebpackPlugin.tags.bodyTags %>
14 |
15 |
16 |
--------------------------------------------------------------------------------
/frontend/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = (api) => {
2 | api.cache(true);
3 |
4 | const presets = [
5 | [
6 | '@babel/preset-env',
7 | {
8 | targets: {chrome: '55', safari: '11', firefox: '54'},
9 | loose: true,
10 | modules: false
11 | }
12 | ],
13 | ['@babel/preset-react']
14 | ];
15 |
16 | const plugins = [
17 | ['@babel/plugin-proposal-decorators', {legacy: true}],
18 | ['@babel/plugin-proposal-class-properties', {loose: true}]
19 | ];
20 |
21 | return {presets, plugins};
22 | };
23 |
--------------------------------------------------------------------------------
/frontend/simple-deployment.config.js:
--------------------------------------------------------------------------------
1 | module.exports = () => {
2 | const frontendURL = process.env.FRONTEND_URL;
3 |
4 | if (!frontendURL) {
5 | throw new Error(`'FRONTEND_URL' environment variable is missing`);
6 | }
7 |
8 | const domainName = new URL(frontendURL).hostname;
9 |
10 | return {
11 | type: 'website',
12 | provider: 'aws',
13 | domainName,
14 | files: ['./build'],
15 | customErrors: [{errorCode: 404, responseCode: 200, responsePage: 'index.html'}],
16 | aws: {
17 | region: 'us-west-2',
18 | cloudFront: {
19 | priceClass: 'PriceClass_100'
20 | }
21 | }
22 | };
23 | };
24 |
--------------------------------------------------------------------------------
/backend/src/components/movie.js:
--------------------------------------------------------------------------------
1 | import {Component, expose, validators} from '@layr/component';
2 | import {Storable, primaryIdentifier, attribute} from '@layr/storable';
3 |
4 | const {notEmpty} = validators;
5 |
6 | @expose({
7 | find: {call: true},
8 | prototype: {
9 | load: {call: true},
10 | save: {call: true},
11 | delete: {call: true}
12 | }
13 | })
14 | export class Movie extends Storable(Component) {
15 | @expose({get: true, set: true}) @primaryIdentifier() id;
16 |
17 | @expose({get: true, set: true}) @attribute('string', {validators: [notEmpty()]}) title = '';
18 |
19 | @expose({get: true, set: true}) @attribute('number?') year;
20 |
21 | @expose({get: true, set: true}) @attribute('string') country = '';
22 | }
23 |
--------------------------------------------------------------------------------
/frontend/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 |
4 | import {getFrontend} from './components/frontend';
5 |
6 | const backendURL = process.env.BACKEND_URL;
7 |
8 | if (!backendURL) {
9 | throw new Error(`'BACKEND_URL' environment variable is missing`);
10 | }
11 |
12 | (async () => {
13 | let content;
14 |
15 | try {
16 | const Frontend = await getFrontend({backendURL});
17 |
18 | if (process.env.NODE_ENV !== 'production') {
19 | window.Frontend = Frontend; // For debugging
20 | }
21 |
22 | content = ;
23 | } catch (err) {
24 | console.error(err);
25 |
26 | content = {err.stack};
27 | }
28 |
29 | ReactDOM.render(content, document.getElementById('root'));
30 | })();
31 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "crud-example-app",
3 | "version": "1.0.0",
4 | "private": true,
5 | "author": "Manuel Vila ",
6 | "license": "MIT",
7 | "scripts": {
8 | "deploy": "(cd ./backend && npm run deploy) && (cd ./frontend && npm run deploy)",
9 | "postinstall": "(cd ./frontend && npm install) && (cd ./backend && npm install)",
10 | "start": "concurrently --names=frontend,backend,database --prefix-colors=green,blue,gray --kill-others \"(cd ./frontend && npm run start)\" \"(cd ./backend && npm run start)\" \"(cd ./database && node ./start.js)\"",
11 | "update": "(cd ./frontend && npm update) && (cd ./backend && npm update)"
12 | },
13 | "prettier": "@mvila/prettierrc",
14 | "devDependencies": {
15 | "@mvila/prettierrc": "^1.1.0",
16 | "concurrently": "^5.3.0",
17 | "prettier": "^2.1.1"
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/database/start.js:
--------------------------------------------------------------------------------
1 | const {execFileSync} = require('child_process');
2 |
3 | const connectionString = process.env.MONGODB_STORE_CONNECTION_STRING;
4 |
5 | if (!connectionString) {
6 | throw new Error(`'MONGODB_STORE_CONNECTION_STRING' environment variable is missing`);
7 | }
8 |
9 | const port = Number(new URL(connectionString).port || '27017');
10 |
11 | process.on('SIGINT', () => {});
12 |
13 | execFileSync(
14 | 'docker',
15 | [
16 | 'run',
17 | '--name',
18 | 'crud-example-app-database',
19 | '--rm',
20 | '--volume',
21 | `${__dirname}/data:/data/db`,
22 | '--volume',
23 | `${__dirname}/mongo-init.js:/docker-entrypoint-initdb.d/mongo-init.js:ro`,
24 | '--publish',
25 | `127.0.0.1:${port}:27017`,
26 | '--env',
27 | 'MONGO_INITDB_ROOT_USERNAME=test',
28 | '--env',
29 | 'MONGO_INITDB_ROOT_PASSWORD=test',
30 | 'mongo:4'
31 | ],
32 | {cwd: __dirname, stdio: 'inherit'}
33 | );
34 |
--------------------------------------------------------------------------------
/backend/simple-deployment.config.js:
--------------------------------------------------------------------------------
1 | module.exports = () => {
2 | const backendURL = process.env.BACKEND_URL;
3 |
4 | if (!backendURL) {
5 | throw new Error(`'BACKEND_URL' environment variable is missing`);
6 | }
7 |
8 | const domainName = new URL(backendURL).hostname;
9 |
10 | const connectionString = process.env.MONGODB_STORE_CONNECTION_STRING;
11 |
12 | if (!connectionString) {
13 | throw new Error(`'MONGODB_STORE_CONNECTION_STRING' environment variable is missing`);
14 | }
15 |
16 | return {
17 | type: 'function',
18 | provider: 'aws',
19 | domainName,
20 | files: ['./build'],
21 | main: './build/handler.js',
22 | includeDependencies: true,
23 | environment: {
24 | MONGODB_STORE_CONNECTION_STRING: connectionString
25 | },
26 | aws: {
27 | region: 'us-west-2',
28 | lambda: {
29 | memorySize: 1024,
30 | timeout: 15
31 | }
32 | }
33 | };
34 | };
35 |
--------------------------------------------------------------------------------
/frontend/src/components/common.js:
--------------------------------------------------------------------------------
1 | import {Component} from '@layr/component';
2 | import React from 'react';
3 | import {view, useDelay} from '@layr/react-integration';
4 |
5 | export class Common extends Component {
6 | @view() static LoadingMessage() {
7 | return (
8 |
9 | Loading...
10 |
11 | );
12 | }
13 |
14 | @view() static ErrorMessage({message = 'Sorry, something went wrong.', onRetry}) {
15 | return (
16 |
17 |
{message}
18 |
19 |
20 |
21 |
22 | );
23 | }
24 |
25 | @view() static RouteNotFound() {
26 | return Sorry, there is nothing here.
;
27 | }
28 |
29 | @view() static Delayed({duration = 200, children}) {
30 | const [isElapsed] = useDelay(duration);
31 |
32 | if (isElapsed) {
33 | return children;
34 | }
35 |
36 | return null;
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/backend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "crud-example-app-backend",
3 | "version": "1.0.0",
4 | "private": true,
5 | "author": "Manuel Vila ",
6 | "license": "MIT",
7 | "scripts": {
8 | "build": "rm -rf ./build && babel ./src --out-dir ./build",
9 | "deploy": "npm run build && simple-deployment",
10 | "start": "nodemon --watch ./src --exec babel-node ./src/http-server.js"
11 | },
12 | "dependencies": {
13 | "@layr/aws-integration": "^1.0.20",
14 | "@layr/component": "^1.0.20",
15 | "@layr/component-server": "^1.0.18",
16 | "@layr/mongodb-store": "^1.1.14",
17 | "@layr/storable": "^1.1.4"
18 | },
19 | "devDependencies": {
20 | "@babel/cli": "^7.11.5",
21 | "@babel/core": "^7.11.5",
22 | "@babel/node": "^7.10.5",
23 | "@babel/plugin-proposal-class-properties": "^7.10.4",
24 | "@babel/plugin-proposal-decorators": "^7.12.12",
25 | "@babel/preset-env": "^7.11.5",
26 | "@layr/component-http-server": "^1.0.18",
27 | "nodemon": "^2.0.4",
28 | "simple-deployment": "^0.1.46"
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # CRUD Example App
2 |
3 | > **This Layr example app is deprecated.**
4 |
5 | A simple example showing how to build a full-stack CRUD app with Layr.
6 |
7 | ## Install
8 |
9 | Install the npm dependencies with:
10 |
11 | ```sh
12 | npm install
13 | ```
14 |
15 | Make sure you have [Docker](https://www.docker.com/) installed as it is used to run the database (MongoDB) when running the app in development mode.
16 |
17 | ## Usage
18 |
19 | ### Running the app in development mode
20 |
21 | Execute the following command:
22 |
23 | ```sh
24 | FRONTEND_URL=http://localhost:16577 \
25 | BACKEND_URL=http://localhost:16578 \
26 | MONGODB_STORE_CONNECTION_STRING=mongodb://test:test@localhost:16579/test \
27 | npm run start
28 | ```
29 |
30 | The app should then be available at http://localhost:16577.
31 |
32 | ### Debugging
33 |
34 | #### Client
35 |
36 | Add the following entry in the local storage of your browser:
37 |
38 | ```
39 | | Key | Value |
40 | | ----- | --------- |
41 | | debug | layr:* |
42 | ```
43 |
44 | #### Server
45 |
46 | Add the following environment variables when starting the app:
47 |
48 | ```sh
49 | DEBUG=layr:* DEBUG_DEPTH=10
50 | ```
51 |
--------------------------------------------------------------------------------
/frontend/src/components/frontend.js:
--------------------------------------------------------------------------------
1 | import {Component, provide} from '@layr/component';
2 | import {Storable} from '@layr/storable';
3 | import {ComponentHTTPClient} from '@layr/component-http-client';
4 | import React from 'react';
5 | import {view, useBrowserRouter} from '@layr/react-integration';
6 |
7 | import {MovieList} from './movie-list';
8 | import {Movie} from './movie';
9 | import {Common} from './common';
10 |
11 | export const getFrontend = async ({backendURL}) => {
12 | const client = new ComponentHTTPClient(backendURL, {mixins: [Storable]});
13 |
14 | const BackendMovie = await client.getComponent();
15 |
16 | class Frontend extends Component {
17 | @provide() static MovieList = MovieList;
18 | @provide() static Movie = Movie(BackendMovie);
19 | @provide() static Common = Common;
20 |
21 | @view() static Main() {
22 | const [router, isReady] = useBrowserRouter(this);
23 |
24 | if (!isReady) {
25 | return null;
26 | }
27 |
28 | const content = router.callCurrentRoute({fallback: this.Common.RouteNotFound});
29 |
30 | return (
31 |
32 |
CRUD example app
33 | {content}
34 |
35 | );
36 | }
37 | }
38 |
39 | return Frontend;
40 | };
41 |
--------------------------------------------------------------------------------
/frontend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "crud-example-app-frontend",
3 | "version": "1.0.0",
4 | "private": true,
5 | "author": "Manuel Vila ",
6 | "license": "MIT",
7 | "scripts": {
8 | "build": "webpack --mode=production",
9 | "deploy": "npm run build && simple-deployment",
10 | "start": "webpack serve --mode=development"
11 | },
12 | "dependencies": {
13 | "@layr/component": "^1.0.20",
14 | "@layr/component-http-client": "^1.0.21",
15 | "@layr/react-integration": "^1.0.20",
16 | "@layr/routable": "^1.0.19",
17 | "@layr/storable": "^1.1.4",
18 | "react": "^16.13.1",
19 | "react-dom": "^16.13.1"
20 | },
21 | "devDependencies": {
22 | "@babel/core": "^7.12.10",
23 | "@babel/plugin-proposal-class-properties": "^7.10.4",
24 | "@babel/plugin-proposal-decorators": "^7.12.12",
25 | "@babel/preset-env": "^7.12.11",
26 | "@babel/preset-react": "^7.12.10",
27 | "babel-loader": "^8.2.2",
28 | "clean-webpack-plugin": "^3.0.0",
29 | "html-loader": "^1.3.0",
30 | "html-webpack-plugin": "^4.4.1",
31 | "simple-deployment": "^0.1.46",
32 | "terser-webpack-plugin": "^3.1.0",
33 | "webpack": "^5.11.1",
34 | "webpack-cli": "^4.3.1",
35 | "webpack-dev-server": "^3.11.1"
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/frontend/src/components/movie-list.js:
--------------------------------------------------------------------------------
1 | import {Component, attribute, consume} from '@layr/component';
2 | import {Routable, route} from '@layr/routable';
3 | import React from 'react';
4 | import {view, useAsyncMemo} from '@layr/react-integration';
5 |
6 | export class MovieList extends Routable(Component) {
7 | @consume() static Movie;
8 | @consume() static Common;
9 |
10 | @attribute('Movie[]?') items;
11 |
12 | @view() static Layout({children}) {
13 | return (
14 |
15 |
Movies
16 | {children}
17 |
18 | );
19 | }
20 |
21 | @route('/movies', {aliases: ['/']}) @view() static Main() {
22 | const [movieList, isLoading, loadingError, retryLoading] = useAsyncMemo(async () => {
23 | const movieList = new this();
24 |
25 | movieList.items = await this.Movie.find(
26 | {},
27 | {title: true, year: true},
28 | {sort: {year: 'desc', title: 'asc'}}
29 | );
30 |
31 | return movieList;
32 | }, []);
33 |
34 | if (isLoading) {
35 | return ;
36 | }
37 |
38 | if (loadingError) {
39 | return (
40 |
44 | );
45 | }
46 |
47 | return (
48 |
49 |
50 |
51 | );
52 | }
53 |
54 | @view() Main() {
55 | const {Movie} = this.constructor;
56 |
57 | return (
58 | <>
59 |
60 | {this.items.map((movie) => (
61 |
62 | ))}
63 |
64 |
65 |
66 |
67 | >
68 | );
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/frontend/webpack.config.js:
--------------------------------------------------------------------------------
1 | const webpack = require('webpack');
2 | const HtmlWebPackPlugin = require('html-webpack-plugin');
3 | const {CleanWebpackPlugin} = require('clean-webpack-plugin');
4 | const TerserPlugin = require('terser-webpack-plugin');
5 | const path = require('path');
6 |
7 | module.exports = (env, argv) => {
8 | const isProduction = argv.mode === 'production';
9 |
10 | let port;
11 |
12 | if (!isProduction) {
13 | const frontendURL = process.env.FRONTEND_URL;
14 |
15 | if (!frontendURL) {
16 | throw new Error(`'FRONTEND_URL' environment variable is missing`);
17 | }
18 |
19 | port = Number(new URL(frontendURL).port);
20 |
21 | if (!port) {
22 | throw new Error(`'FRONTEND_PORT' environment variable should include a port`);
23 | }
24 | }
25 |
26 | return {
27 | entry: './src/index.js',
28 | output: {
29 | path: path.join(__dirname, 'build'),
30 | filename: isProduction ? '[name].[contenthash].immutable.js' : 'bundle.js',
31 | publicPath: '/'
32 | },
33 | module: {
34 | rules: [
35 | {
36 | test: /\.js$/,
37 | include: path.join(__dirname, 'src'),
38 | loader: 'babel-loader'
39 | }
40 | ]
41 | },
42 | resolve: {
43 | alias: {
44 | 'react': path.resolve('./node_modules/react'),
45 | 'react-dom': path.resolve('./node_modules/react-dom')
46 | }
47 | },
48 | plugins: [
49 | new CleanWebpackPlugin(),
50 | new HtmlWebPackPlugin({
51 | template: './src/index.html',
52 | favicon: './src/assets/layr-favicon-20201027.immutable.png',
53 | inject: false
54 | }),
55 | new webpack.EnvironmentPlugin(['BACKEND_URL'])
56 | ],
57 | ...(isProduction
58 | ? {
59 | optimization: {
60 | minimizer: [new TerserPlugin({terserOptions: {keep_classnames: true}})]
61 | }
62 | }
63 | : {
64 | devtool: 'eval-cheap-module-source-map',
65 | devServer: {
66 | contentBase: './build/dev',
67 | port,
68 | historyApiFallback: true
69 | }
70 | })
71 | };
72 | };
73 |
--------------------------------------------------------------------------------
/frontend/src/components/movie.js:
--------------------------------------------------------------------------------
1 | import {consume} from '@layr/component';
2 | import {Routable, route} from '@layr/routable';
3 | import React, {useMemo} from 'react';
4 | import {view, useAsyncMemo, useAsyncCallback} from '@layr/react-integration';
5 |
6 | export function Movie(Base) {
7 | class Movie extends Routable(Base) {
8 | @consume() static MovieList;
9 | @consume() static Common;
10 |
11 | @view() static Layout({children}) {
12 | return (
13 |
14 |
Movie
15 | {children}
16 |
17 | );
18 | }
19 |
20 | @view() static Loader({id, children}) {
21 | const [movie, isLoading, loadingError, retryLoading] = useAsyncMemo(async () => {
22 | return await this.get(id, {title: true, year: true, country: true});
23 | }, [id]);
24 |
25 | if (isLoading) {
26 | return ;
27 | }
28 |
29 | if (loadingError) {
30 | return (
31 |
35 | );
36 | }
37 |
38 | return children(movie);
39 | }
40 |
41 | @route('/movies/:id') @view() static Main({id}) {
42 | return (
43 |
44 | {(movie) => }
45 |
46 | ‹ Back
47 |
48 |
49 | );
50 | }
51 |
52 | @view() Main() {
53 | const {MovieList, Editor} = this.constructor;
54 |
55 | const [handleDelete, isDeleting, deletingError] = useAsyncCallback(async () => {
56 | await this.delete();
57 |
58 | MovieList.Main.navigate();
59 | }, []);
60 |
61 | return (
62 |
63 | {deletingError &&
Sorry, something went wrong while deleting the movie.
}
64 |
65 |
66 |
67 | | Title: |
68 | {this.title} |
69 |
70 |
71 | | Year: |
72 | {this.year} |
73 |
74 |
75 | | Country: |
76 | {this.country} |
77 |
78 |
79 |
80 |
81 |
84 |
85 |
88 |
89 |
90 | );
91 | }
92 |
93 | @route('/movies/-/create') @view() static Creator() {
94 | const movie = useMemo(() => {
95 | return new this();
96 | }, []);
97 |
98 | return (
99 |
100 |
101 |
102 | );
103 | }
104 |
105 | @view() Creator() {
106 | const {MovieList} = this.constructor;
107 |
108 | const [handleSave, , savingError] = useAsyncCallback(async () => {
109 | await this.save();
110 |
111 | MovieList.Main.navigate();
112 | }, []);
113 |
114 | return (
115 |
116 | {savingError &&
Sorry, something went wrong while saving the movie.
}
117 |
118 |
119 | ‹ Back
120 |
121 |
122 | );
123 | }
124 |
125 | @route('/movies/:id/edit') @view() static Editor({id}) {
126 | return (
127 |
128 | {(movie) => }
129 |
130 | );
131 | }
132 |
133 | @view() Editor() {
134 | const {Main} = this.constructor;
135 |
136 | const forkedMovie = useMemo(() => this.fork(), []);
137 |
138 | const [handleSave, , savingError] = useAsyncCallback(async () => {
139 | await forkedMovie.save();
140 |
141 | this.merge(forkedMovie);
142 |
143 | Main.navigate(this);
144 | }, [forkedMovie]);
145 |
146 | return (
147 |
148 | {savingError &&
Sorry, something went wrong while saving the movie.
}
149 |
150 |
151 | ‹ Back
152 |
153 |
154 | );
155 | }
156 |
157 | @view() Form({onSubmit}) {
158 | const [handleSubmit, isSubmitting] = useAsyncCallback(
159 | async (event) => {
160 | event.preventDefault();
161 | await onSubmit();
162 | },
163 | [onSubmit]
164 | );
165 |
166 | return (
167 |
212 | );
213 | }
214 |
215 | @view() ListItem() {
216 | const {Main} = this.constructor;
217 |
218 | return (
219 |
220 | {this.title}
221 | {this.year !== undefined ? ` (${this.year})` : ''}
222 |
223 | );
224 | }
225 | }
226 |
227 | return Movie;
228 | }
229 |
--------------------------------------------------------------------------------