├── src
└── journey
│ ├── public
│ ├── index.tsx
│ └── components
│ │ ├── app.tsx
│ │ └── entry.tsx
│ ├── types.mo
│ └── main.mo
├── .gitignore
├── dfx.json
├── package.json
├── README.md
├── webpack.config.js
└── tsconfig.json
/src/journey/public/index.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { render } from 'react-dom';
3 | import { JourneyApp } from './components/app';
4 |
5 | render(, document.getElementById('app'));
6 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Various IDEs and Editors
2 | .vscode/
3 | .idea/
4 | **/*~
5 |
6 | # Mac OSX temporary files
7 | .DS_Store
8 | **/.DS_Store
9 |
10 | # Build artifacts
11 | canisters/
12 |
13 | # dfx temporary files
14 | .dfx/
15 |
16 | # frontend code
17 | node_modules/
18 |
--------------------------------------------------------------------------------
/dfx.json:
--------------------------------------------------------------------------------
1 | {
2 | "canisters": {
3 | "journey": {
4 | "frontend": {
5 | "entrypoint": "src/journey/public/index.tsx"
6 | },
7 | "main": "src/journey/main.mo"
8 | }
9 | },
10 | "defaults": {
11 | "build": {
12 | "output": "canisters/"
13 | },
14 | "start": {
15 | "address": "127.0.0.1",
16 | "port": 8000,
17 | "serve_root": "canisters/journey/assets"
18 | }
19 | },
20 | "dfx": "0.5.5",
21 | "version": 1
22 | }
23 |
--------------------------------------------------------------------------------
/src/journey/public/components/app.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { EntryList, Entry, NewEntry } from './entry';
3 | import {
4 | HashRouter as Router,
5 | Switch,
6 | Route, Link,
7 | } from 'react-router-dom';
8 |
9 | export function JourneyApp() {
10 | return
11 |
12 | } />
13 |
14 |
15 |
16 | New Entry
17 |
18 |
19 | ;
20 | }
21 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "journey",
3 | "version": "0.1.0",
4 | "description": "",
5 | "keywords": [],
6 | "scripts": {
7 | "build": "webpack"
8 | },
9 | "devDependencies": {
10 | "@types/react": "^16.9.23",
11 | "@types/react-dom": "^16.9.5",
12 | "react": "^16.13.0",
13 | "react-dom": "^16.13.0",
14 | "terser-webpack-plugin": "2.2.2",
15 | "ts-loader": "^6.2.1",
16 | "typescript": "^3.8.3",
17 | "webpack": "4.41.3",
18 | "webpack-cli": "3.3.10"
19 | },
20 | "dependencies": {
21 | "@types/react-router-dom": "^5.1.3",
22 | "markdown": "^0.5.0",
23 | "react-router-dom": "^5.1.2"
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # journey
2 |
3 | Welcome to your new journey project and to the internet computer development community. By default, creating a new project adds this README and some template files to your project directory. You can edit these template files to customize your project and to include your own code to speed up the development cycle.
4 |
5 | To get started, you might want to explore the project directory structure and the default configuration file. Working with this project in your development environment will not affect any production deployment or identity tokens.
6 |
7 | To learn more before you start working with journey, see the following documentation available online:
8 |
9 | - [Quick Start](https://sdk.dfinity.org/developers-guide/quickstart.html)
10 | - [Developer's Guide](https://sdk.dfinity.org/developers-guide)
11 | - [Language Reference](https://sdk.dfinity.org/language-guide)
12 |
13 | If you want to start working on your project right away, you might want to try the following commands:
14 |
15 | ```bash
16 | cd journey/
17 | dfx help
18 | dfx config --help
19 | ```
20 |
--------------------------------------------------------------------------------
/src/journey/types.mo:
--------------------------------------------------------------------------------
1 | module {
2 | public type UserRole = {
3 | #admin;
4 | #editor;
5 | #base;
6 | };
7 |
8 | public type User = {
9 | id: Principal;
10 | name: Text;
11 | role: UserRole;
12 | description: Text;
13 | };
14 |
15 | // Externally we return Entry through our API.
16 | public type Entry = {
17 | // How to identify this entry.
18 | id: Nat;
19 |
20 | // The person who created the entry.
21 | author: ?User;
22 |
23 | // The title of the entry.
24 | title: Text;
25 |
26 | // The header (first paragraph) of the entry. In Markdown.
27 | header: Text;
28 |
29 | // The content (which is optional in list).
30 | content: ?Text;
31 | };
32 |
33 | // Internally, we store the JOIN key author to the author list.
34 | public type InternalEntry = {
35 | // How to identify this entry.
36 | id: Nat;
37 |
38 | // The person who created the entry.
39 | author: Principal;
40 |
41 | // The title of the entry.
42 | title: Text;
43 |
44 | // The content of the entry. In Markdown.
45 | content: Text;
46 | };
47 | }
48 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require("path");
2 | const TerserPlugin = require("terser-webpack-plugin");
3 | const dfxJson = require("./dfx.json");
4 |
5 | // List of all aliases for canisters. This creates the module alias for
6 | // the `import ... from "ic:canisters/xyz"` where xyz is the name of a
7 | // canister.
8 | const aliases = Object.entries(dfxJson.canisters).reduce((acc, [name,value]) => {
9 | const outputRoot = path.join(__dirname, dfxJson.defaults.build.output, name);
10 | const filename = path.basename(value.main, ".mo");
11 | return {
12 | ...acc,
13 | ["ic:canisters/" + name]: path.join(outputRoot, filename + ".js"),
14 | ["ic:idl/" + name]: path.join(outputRoot, filename + ".did.js"),
15 | };
16 | }, {
17 | // This will later point to the userlib from npm, when we publish the userlib.
18 | "ic:userlib": path.join(
19 | process.env["HOME"],
20 | ".cache/dfinity/versions",
21 | dfxJson.dfx || process.env["DFX_VERSION"],
22 | "js-user-library/",
23 | ),
24 | });
25 |
26 | /**
27 | * Generate a webpack configuration for a canister.
28 | */
29 | function generateWebpackConfigForCanister(name, info) {
30 | if (typeof info.frontend !== 'object') {
31 | return;
32 | }
33 |
34 | const outputRoot = path.join(__dirname, dfxJson.defaults.build.output, name);
35 | const inputRoot = __dirname;
36 |
37 | return {
38 | mode: "production",
39 | entry: {
40 | index: path.join(inputRoot, info.frontend.entrypoint),
41 | },
42 | devtool: "source-map",
43 | optimization: {
44 | minimize: true,
45 | minimizer: [new TerserPlugin()],
46 | },
47 | resolve: {
48 | alias: aliases,
49 | extensions: ['.tsx', '.ts', '.js']
50 | },
51 | output: {
52 | filename: "[name].js",
53 | path: path.join(outputRoot, "assets"),
54 | },
55 | module: {
56 | rules: [
57 | { test: /\.([jt]s)x?$/, loader: "ts-loader", exclude: /node_modules/ }
58 | ]
59 | },
60 | plugins: [
61 | ],
62 | };
63 | }
64 |
65 | // If you have webpack configurations you want to build as part of this
66 | // config, add them here.
67 | module.exports = [
68 | ...Object.entries(dfxJson.canisters).map(([name, info]) => {
69 | return generateWebpackConfigForCanister(name, info);
70 | }).filter(x => !!x),
71 | ];
72 |
--------------------------------------------------------------------------------
/src/journey/public/components/entry.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * Components for entries;
3 | * NewEntry
4 | * EntryList
5 | * EntrySummary
6 | * Entry
7 | */
8 |
9 | import journey from 'ic:canisters/journey';
10 | import * as React from 'react';
11 | import { useEffect, useState } from 'react';
12 | import { Link, Redirect, useParams } from 'react-router-dom';
13 |
14 | declare const require: any;
15 | const markdown = require('markdown').markdown;
16 |
17 | interface UserIdl {
18 | id: { toNumber(): number };
19 | name: string;
20 | description: string;
21 | }
22 |
23 | interface EntryIdl {
24 | author: [UserIdl?];
25 | header: string;
26 | content: [string?];
27 | title: string;
28 | id: { toNumber(): number };
29 | }
30 |
31 | export function NewEntry() {
32 | const [content, setContent] = useState('');
33 | const [title, setTitle] = useState('New Entry');
34 | const [done, setDone] = useState(false);
35 | const [saving, setSaving] = useState(false);
36 |
37 | async function submit() {
38 | setSaving(true);
39 |
40 | await journey.newEntry(title, content);
41 | setDone(true);
42 | }
43 |
44 | if (done) {
45 | return ()
46 | }
47 | if (saving) {
48 | return ();
49 | }
50 |
51 | return (
52 |
53 |
59 |
60 | cancel
61 |
62 |
63 | );
64 | }
65 |
66 | export function Entry() {
67 | let { id } = useParams();
68 | const [entry, setEntry] = useState({} as EntryIdl);
69 | const [loading, setLoading] = useState(true);
70 |
71 | useEffect(() => {
72 | let natId = parseInt('' + id, 10);
73 | if (!isFinite(natId)) {
74 | // Invalid string passed as number.
75 | throw new Error('Invalid ID: ' + JSON.stringify(id));
76 | }
77 |
78 | journey.getEntry(natId).then((optEntry: EntryIdl[]) => {
79 | let [entry] = optEntry;
80 | if (!entry) {
81 | // TODO: move this to a 404/403.
82 | throw new Error('ID not found.');
83 | } else {
84 | setEntry(entry);
85 | setLoading(false);
86 | }
87 | })
88 | });
89 |
90 | if (loading) {
91 | return ()
92 | } else {
93 | return (
94 |
95 |
96 |
97 | By {entry.author[0]?.name}. back
98 |
99 | );
100 | }
101 | }
102 |
103 | export function EntrySummary(props: { entry: EntryIdl }) {
104 | const entry = props.entry;
105 |
106 | return (
107 |
108 |
{entry.title}
109 |
110 | By {entry.author[0]?.name}. view
111 |
112 | );
113 | }
114 |
115 | export function EntryList() {
116 | const [entryList, setEntryList] = useState([] as EntryIdl[]);
117 |
118 | useEffect(() => {
119 | journey.listEntries(10).then((list: EntryIdl[]) => setEntryList(list));
120 | });
121 |
122 | return (
123 |
124 | {entryList.map(entry => {
125 | return ( );
126 | })}
127 |
128 | );
129 | }
130 |
--------------------------------------------------------------------------------
/src/journey/main.mo:
--------------------------------------------------------------------------------
1 | import Prim "mo:prim";
2 | import Array "mo:stdlib/array";
3 | import Types "./types";
4 |
5 | type Entry = Types.Entry;
6 | type InternalEntry = Types.InternalEntry;
7 | type User = Types.User;
8 |
9 |
10 | actor {
11 |
12 | var entries: [InternalEntry] = [];
13 | // TODO: Change this to a hash map.
14 | var users: [User] = [];
15 | var uniqueId: Nat = 0;
16 |
17 | func getUser(id: Principal): ?User {
18 | func predicate(u: User): Bool {
19 | u.id == id
20 | };
21 | Array.find(predicate, users)
22 | };
23 |
24 | func isAdmin(id: Principal): Bool {
25 | switch (getUser(id)) {
26 | case (null) {
27 | return false;
28 | };
29 | case (?u) {
30 | switch (u.role) {
31 | case (#admin) { return true; };
32 | case (_) { return false; };
33 | }
34 | }
35 | }
36 | };
37 |
38 | public shared(msg) func setUserRole(id: Principal, role0: Types.UserRole): async () {
39 | if (isAdmin(msg.caller)) {
40 | let u = getUser(id);
41 | switch (u) {
42 | case (null) {};
43 | case (?u) {
44 | let newUser: User = {
45 | id = u.id;
46 | name = u.name;
47 | description = u.description;
48 | role = role0;
49 | };
50 | func predicate(c: User): Bool {
51 | c.id != u.id
52 | };
53 | users := Array.append(Array.filter(predicate, users), [newUser]);
54 | };
55 | }
56 | }
57 | };
58 |
59 | public shared(msg) func createUser(name0: Text, desc0: Text): async () {
60 | let role0: Types.UserRole = if (entries.len() == 0) { #admin } else { #base };
61 | let user: User = {
62 | id = msg.caller;
63 | role = role0;
64 | name = name0;
65 | description = desc0;
66 | };
67 | users := Array.append(users, [user]);
68 | };
69 |
70 | public shared(msg) func getUserList(): async [User] {
71 | if (isAdmin(msg.caller)) {
72 | return users;
73 | } else {
74 | return [];
75 | }
76 | };
77 |
78 | func entryHeader(content: Text): Text {
79 | let chars = content.chars();
80 | var text = "";
81 | var first = false; // Indication that we saw a \n in the last character.
82 | label w for (c in chars) {
83 | if (c == '\r') {
84 | continue w;
85 | };
86 | if (c == '\n') {
87 | if (first) {
88 | break w;
89 | } else {
90 | first := true;
91 | }
92 | } else {
93 | first := false;
94 | };
95 | text := text # Prim.charToText(c);
96 | };
97 |
98 | text
99 | };
100 |
101 | public shared(msg) func newEntry(title0: Text, content0: Text): async () {
102 | let u = getUser(msg.caller);
103 | switch (u) {
104 | case (null) {
105 | return;
106 | };
107 | case (?u) {
108 | switch (u.role) {
109 | case (#admin or #editor) {};
110 | case (_) { return; }
111 | }
112 | }
113 | };
114 |
115 | uniqueId := uniqueId + 1;
116 | let entry: InternalEntry = {
117 | id = uniqueId;
118 | author = msg.caller;
119 | content = content0;
120 | title = title0;
121 | };
122 | entries := Array.append(entries, [entry]);
123 | };
124 |
125 | public func listUsers(): async [User] {
126 | users
127 | };
128 |
129 | public query func listEntries(max: Nat): async [Entry] {
130 | var m = max;
131 | if (entries.len() == 0) {
132 | return [];
133 | };
134 | if (m > entries.len()) {
135 | m := entries.len();
136 | };
137 |
138 | func gen(i: Nat): Entry {
139 | let e = entries[entries.len() - i - 1];
140 | let a = getUser(e.author);
141 |
142 | {
143 | id = e.id;
144 | author = a;
145 | title = e.title;
146 | header = entryHeader(e.content);
147 | content = null;
148 | }
149 | };
150 | Array.tabulate(m, gen)
151 | };
152 |
153 | public query func getEntry(id0: Nat): async ?Entry {
154 | func isEq(entry: InternalEntry): Bool {
155 | entry.id == id0
156 | };
157 |
158 | switch (Array.find(isEq, entries)) {
159 | case (null) {
160 | return null;
161 | };
162 | case (?e) {
163 | func predicate(u: User): Bool {
164 | u.id == e.author
165 | };
166 | let a = Array.find(predicate, users);
167 |
168 | return ?{
169 | id = e.id;
170 | author = a;
171 | title = e.title;
172 | header = entryHeader(e.content);
173 | content = ?e.content;
174 | };
175 | };
176 | };
177 | };
178 |
179 | };
180 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | /* Basic Options */
4 | // "incremental": true, /* Enable incremental compilation */
5 | "target": "es2015", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */
6 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
7 | // "lib": [], /* Specify library files to be included in the compilation. */
8 | "allowJs": true, /* Allow javascript files to be compiled. */
9 | // "checkJs": true, /* Report errors in .js files. */
10 | "jsx": "react", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
11 | // "declaration": true, /* Generates corresponding '.d.ts' file. */
12 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
13 | // "sourceMap": true, /* Generates corresponding '.map' file. */
14 | // "outFile": "./", /* Concatenate and emit output to single file. */
15 | // "outDir": "./", /* Redirect output structure to the directory. */
16 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
17 | // "composite": true, /* Enable project compilation */
18 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
19 | // "removeComments": true, /* Do not emit comments to output. */
20 | // "noEmit": true, /* Do not emit outputs. */
21 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */
22 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
23 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
24 |
25 | /* Strict Type-Checking Options */
26 | "strict": true, /* Enable all strict type-checking options. */
27 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
28 | // "strictNullChecks": true, /* Enable strict null checks. */
29 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */
30 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
31 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
32 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
33 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
34 |
35 | /* Additional Checks */
36 | // "noUnusedLocals": true, /* Report errors on unused locals. */
37 | // "noUnusedParameters": true, /* Report errors on unused parameters. */
38 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
39 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
40 |
41 | /* Module Resolution Options */
42 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
43 | "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
44 | "paths": {
45 | "ic:canisters/*": [
46 | "canisters/*/main.js"
47 | ],
48 | "idl:canisters/*": [
49 | "canisters/*/main.did.js"
50 | ]
51 | }, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
52 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
53 | // "typeRoots": [], /* List of folders to include type definitions from. */
54 | // "types": [], /* Type declaration files to be included in compilation. */
55 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
56 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
57 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
58 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
59 |
60 | /* Source Map Options */
61 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
62 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
63 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
64 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
65 |
66 | /* Experimental Options */
67 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
68 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
69 |
70 | /* Advanced Options */
71 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
72 | }
73 | }
74 |
--------------------------------------------------------------------------------