├── tests
├── functional
│ ├── all.ts
│ └── main.ts
└── unit
│ ├── all.ts
│ └── widgets
│ ├── all.ts
│ └── Tags.ts
├── src
├── main.css
├── config.ts
├── logo.png
├── main.tsx
├── widgets
│ ├── Banner.tsx
│ ├── Footer.tsx
│ ├── ErrorList.tsx
│ ├── Tags.tsx
│ ├── ArticleAuthorControls.tsx
│ ├── FeedPagination.tsx
│ ├── FeedList.tsx
│ ├── ArticleControls.tsx
│ ├── Comment.tsx
│ ├── ArticlePreview.tsx
│ ├── ArticleMeta.tsx
│ ├── Header.tsx
│ ├── Login.tsx
│ ├── Register.tsx
│ ├── Home.tsx
│ ├── Settings.tsx
│ ├── Editor.tsx
│ ├── Profile.tsx
│ └── Article.tsx
├── processes
│ ├── utils.ts
│ ├── tagProcesses.ts
│ ├── routeProcesses.ts
│ ├── interfaces.d.ts
│ ├── settingsProcesses.ts
│ ├── loginProcesses.ts
│ ├── profileProcesses.ts
│ ├── feedProcesses.ts
│ ├── editorProcesses.ts
│ └── articleProcesses.ts
├── routes.ts
├── index.html
├── App.tsx
├── store.ts
└── interfaces.d.ts
├── logo.png
├── .gitignore
├── .eslintrc.json
├── lighthouserc.json
├── firebase.json
├── .github
└── workflows
│ ├── merge-greetings.yml
│ └── ci-cd.yml
├── tsconfig.json
├── LICENSE
├── package.json
├── .dojorc
├── README.md
├── tslint.json
├── CODE_OF_CONDUCT.md
└── .firebase
└── hosting.0.cache
/tests/functional/all.ts:
--------------------------------------------------------------------------------
1 | import './main';
2 |
--------------------------------------------------------------------------------
/tests/unit/all.ts:
--------------------------------------------------------------------------------
1 | import './widgets/all';
2 |
--------------------------------------------------------------------------------
/tests/unit/widgets/all.ts:
--------------------------------------------------------------------------------
1 | import './Tags';
2 |
--------------------------------------------------------------------------------
/src/main.css:
--------------------------------------------------------------------------------
1 | /* Put your styles and imports here */
2 |
--------------------------------------------------------------------------------
/tests/functional/main.ts:
--------------------------------------------------------------------------------
1 | /* Write your app tests here */
2 |
--------------------------------------------------------------------------------
/src/config.ts:
--------------------------------------------------------------------------------
1 | export const baseUrl = 'https://api.realworld.io/api';
2 |
--------------------------------------------------------------------------------
/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gothinkster/dojo-realworld-example-app/HEAD/logo.png
--------------------------------------------------------------------------------
/src/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gothinkster/dojo-realworld-example-app/HEAD/src/logo.png
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | coverage/
3 | coverage-final.lcov
4 | _build/
5 | .vscode/
6 | dist/
7 | output/
8 |
--------------------------------------------------------------------------------
/src/main.tsx:
--------------------------------------------------------------------------------
1 | import { renderer, tsx } from "@dojo/framework/core/vdom";
2 | import { registry } from "./store";
3 |
4 | import { App } from "./App";
5 |
6 | const r = renderer(() => );
7 | r.mount({ domNode: document.getElementById("app")!, registry });
8 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "browser": true,
4 | "es2021": true
5 | },
6 | "parser": "@typescript-eslint/parser",
7 | "parserOptions": {
8 | "ecmaVersion": 13,
9 | "sourceType": "module"
10 | },
11 | "plugins": [
12 | "@typescript-eslint"
13 | ],
14 | "rules": {
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/lighthouserc.json:
--------------------------------------------------------------------------------
1 | {
2 | "ci": {
3 | "collect": {
4 | "numberOfRuns": 5
5 | },
6 | "assert": {
7 | "assertions": {
8 | "categories:performance": ["warn", {"minScore": 0.9}],
9 | "categories:accessibility": ["error", {"minScore": 0.7}]
10 | }
11 | },
12 | "upload": {},
13 | "server": {},
14 | "wizard": {}
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/widgets/Banner.tsx:
--------------------------------------------------------------------------------
1 | import { create, tsx } from "@dojo/framework/core/vdom";
2 |
3 | const factory = create();
4 |
5 | export const Banner = factory(function Banner() {
6 | return (
7 |
8 |
9 |
conduit
10 |
A place to share your knowledge.
11 |
12 |
13 | );
14 | });
15 |
16 | export default Banner;
17 |
--------------------------------------------------------------------------------
/src/processes/utils.ts:
--------------------------------------------------------------------------------
1 | import { createCommandFactory } from '@dojo/framework/stores/process';
2 | import { State } from '../interfaces';
3 |
4 | export function getHeaders(token?: string): any {
5 | const headers: { [key: string]: string } = {
6 | 'Content-Type': 'application/json'
7 | };
8 | if (token) {
9 | headers['Authorization'] = `Token ${token}`;
10 | }
11 | return headers;
12 | }
13 |
14 | export const commandFactory = createCommandFactory();
15 |
--------------------------------------------------------------------------------
/firebase.json:
--------------------------------------------------------------------------------
1 | {
2 | "hosting": {
3 | "public": "output/dist",
4 | "headers": [
5 | {
6 | "source" : "**/*.@(svg|css|js|jpg|jpeg|gif|png)",
7 | "headers" : [
8 | {
9 | "key" : "Cache-Control",
10 | "value" : "max-age=30672000"
11 | }
12 | ]
13 | }
14 | ],
15 | "ignore": [
16 | "firebase.json",
17 | "**/.*",
18 | "**/node_modules/**"
19 | ],
20 | "rewrites": [
21 | {
22 | "source": "**",
23 | "destination": "/index.html"
24 | }
25 | ]
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/processes/tagProcesses.ts:
--------------------------------------------------------------------------------
1 | import { createProcess } from '@dojo/framework/stores/process';
2 | import { replace } from '@dojo/framework/stores/state/operations';
3 | import { commandFactory } from './utils';
4 | import { baseUrl } from '../config';
5 |
6 | const getTagsCommand = commandFactory(async ({ path }) => {
7 | const response = await fetch(`${baseUrl}/tags`);
8 | const json = await response.json();
9 |
10 | return [replace(path('tags'), json.tags)];
11 | });
12 |
13 | export const getTagsProcess = createProcess('get-tags', [getTagsCommand]);
14 |
--------------------------------------------------------------------------------
/.github/workflows/merge-greetings.yml:
--------------------------------------------------------------------------------
1 | name: Merge greetings
2 |
3 | on:
4 | pull_request:
5 | types: closed
6 |
7 | jobs:
8 | contribution-greetings:
9 | if: github.event.pull_request.merged
10 | runs-on: ubuntu-latest
11 | steps:
12 | - name: greet the contributor
13 | uses: kerhub/saved-replies@v1.0.0
14 | with:
15 | token: ${{ secrets.GITHUB_TOKEN }}
16 | reply: |
17 | Thanks @${{ github.event.pull_request.user.login }}!
18 |
19 | Your contribution is now fully part of this project :rocket:
20 |
--------------------------------------------------------------------------------
/src/widgets/Footer.tsx:
--------------------------------------------------------------------------------
1 | import { create, tsx } from "@dojo/framework/core/vdom";
2 |
3 | const factory = create();
4 |
5 | export const Footer = factory(function Footer() {
6 | return (
7 |
18 | );
19 | });
20 |
21 | export default Footer;
22 |
--------------------------------------------------------------------------------
/src/routes.ts:
--------------------------------------------------------------------------------
1 | export default [
2 | {
3 | path: 'login',
4 | outlet: 'login'
5 | },
6 | {
7 | path: 'register',
8 | outlet: 'register'
9 | },
10 | {
11 | path: 'user/{username}',
12 | outlet: 'user'
13 | },
14 | {
15 | path: 'user/{username}/favorites',
16 | outlet: 'favorites'
17 | },
18 | {
19 | path: 'article/{slug}',
20 | outlet: 'article'
21 | },
22 | {
23 | path: 'settings',
24 | outlet: 'settings'
25 | },
26 | {
27 | path: 'editor',
28 | outlet: 'new-post',
29 | children: [
30 | {
31 | path: 'editor/{slug}',
32 | outlet: 'edit-post'
33 | }
34 | ]
35 | },
36 | {
37 | path: 'home',
38 | outlet: 'home',
39 | defaultRoute: true
40 | }
41 | ];
42 |
--------------------------------------------------------------------------------
/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | dojo-realworld
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "declaration": false,
4 | "experimentalDecorators": true,
5 | "jsx": "react",
6 | "jsxFactory": "tsx",
7 | "lib": [
8 | "dom",
9 | "es5",
10 | "es2015.promise",
11 | "es2015.iterable",
12 | "es2015.symbol",
13 | "es2015.symbol.wellknown"
14 | ],
15 | "module": "umd",
16 | "moduleResolution": "node",
17 | "noUnusedLocals": true,
18 | "noUnusedParameters": true,
19 | "outDir": "_build/",
20 | "removeComments": false,
21 | "importHelpers": true,
22 | "downlevelIteration": true,
23 | "sourceMap": true,
24 | "strict": true,
25 | "target": "es5",
26 | "types": [ "intern" ]
27 | },
28 | "include": [
29 | "./src/**/*.ts",
30 | "./tests/**/*.ts"
31 | ]
32 | }
33 |
--------------------------------------------------------------------------------
/src/widgets/ErrorList.tsx:
--------------------------------------------------------------------------------
1 | import { create, tsx } from "@dojo/framework/core/vdom";
2 | import { Errors } from "../interfaces";
3 |
4 | interface ErrorListProperties {
5 | errors: Errors;
6 | }
7 |
8 | const factory = create({}).properties();
9 |
10 | export const ErrorList = factory(function ErrorList({ properties }) {
11 | const { errors } = properties();
12 | const errorCategories = Object.keys(errors);
13 | let errorList: string[] = [];
14 | for (let i = 0; i < errorCategories.length; i++) {
15 | errorList = [
16 | ...errorList,
17 | ...errors[errorCategories[i]].map((error: string) => `${errorCategories[i]} ${error}`)
18 | ];
19 | }
20 | errorList;
21 |
22 | return (
23 |
24 | {errorList.map((error: string) => (
25 | - {error}
26 | ))}
27 |
28 | );
29 | });
30 |
31 | export default ErrorList;
32 |
--------------------------------------------------------------------------------
/src/widgets/Tags.tsx:
--------------------------------------------------------------------------------
1 | import { create, tsx } from "@dojo/framework/core/vdom";
2 | import { fetchFeedProcess } from "../processes/feedProcesses";
3 | import store from "../store";
4 |
5 | const factory = create({ store });
6 |
7 | export const Tags = factory(function Tags({ middleware: { store } }) {
8 | const { get, path, executor } = store;
9 | const tags = get(path("tags")) || [];
10 |
11 | return (
12 |
13 |
14 |
Popular Tags
15 |
30 |
31 |
32 | );
33 | });
34 |
35 | export default Tags;
36 |
--------------------------------------------------------------------------------
/src/widgets/ArticleAuthorControls.tsx:
--------------------------------------------------------------------------------
1 | import { create, tsx } from "@dojo/framework/core/vdom";
2 | import { Link } from "@dojo/framework/routing/Link";
3 | import { SlugPayload } from "../processes/interfaces";
4 |
5 | interface ArticleAuthorControlsProperties {
6 | slug: string;
7 | deleteArticle: (opts: SlugPayload) => void;
8 | }
9 |
10 | const factory = create({}).properties();
11 |
12 | export const ArticleAuthorControls = factory(function ArticleAuthorControls({ properties }) {
13 | const { slug, deleteArticle } = properties();
14 | return (
15 |
16 |
17 | Edit Article
18 |
19 |
27 |
28 | );
29 | });
30 |
31 | export default ArticleAuthorControls;
32 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Thinkster
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/processes/routeProcesses.ts:
--------------------------------------------------------------------------------
1 | import { createProcess } from '@dojo/framework/stores/process';
2 | import { replace } from '@dojo/framework/stores/state/operations';
3 | import { commandFactory } from './utils';
4 | import { ChangeRoutePayload } from './interfaces';
5 |
6 | function isProfile(currentOutlet: string, outlet: string) {
7 | const outlets = ['user', 'favorites'];
8 | return outlets.indexOf(currentOutlet) > -1 && outlets.indexOf(outlet) > -1;
9 | }
10 |
11 | const changeRouteCommand = commandFactory(({ get, path, payload: { outlet, context } }) => {
12 | const currentOutlet = get(path('routing', 'outlet'));
13 | return [
14 | replace(path('routing', 'outlet'), outlet),
15 | replace(path('routing', 'params'), context.params),
16 | replace(path('settings'), undefined),
17 | replace(path('editor'), undefined),
18 | isProfile(currentOutlet, outlet) ? replace(path('profile', 'feed'), {}) : replace(path('profile'), undefined),
19 | replace(path('feed'), undefined),
20 | replace(path('errors'), {})
21 | ];
22 | });
23 | export const changeRouteProcess = createProcess('change-route', [changeRouteCommand]);
24 |
--------------------------------------------------------------------------------
/src/widgets/FeedPagination.tsx:
--------------------------------------------------------------------------------
1 | import { create, tsx } from "@dojo/framework/core/vdom";
2 | import { DNode } from "@dojo/framework/core/interfaces";
3 |
4 | interface FeedPaginationProperties {
5 | total: number;
6 | currentPage: number;
7 | fetchFeed: (page: number) => void;
8 | }
9 |
10 | const factory = create({}).properties();
11 |
12 | export const FeedPagination = factory(function({ properties }) {
13 | const { total, currentPage, fetchFeed } = properties();
14 |
15 | let pageNumbers: DNode[] = [];
16 | for (let page = 0; page < Math.ceil(total / 10); page++) {
17 | const isActive = currentPage === page;
18 | const onclick = (event: MouseEvent) => {
19 | event.preventDefault();
20 | if (page !== currentPage) {
21 | fetchFeed(page);
22 | }
23 | };
24 | pageNumbers.push(
25 |
26 |
27 | {`${page + 1}`}
28 |
29 |
30 | );
31 | }
32 |
33 | return (
34 |
37 | );
38 | });
39 |
40 | export default FeedPagination;
41 |
--------------------------------------------------------------------------------
/src/widgets/FeedList.tsx:
--------------------------------------------------------------------------------
1 | import { create, tsx } from "@dojo/framework/core/vdom";
2 | import store from "../store";
3 |
4 | import { favoriteFeedArticleProcess } from "./../processes/feedProcesses";
5 | import { ArticlePreview } from "./ArticlePreview";
6 | import { ArticleItem } from "../interfaces";
7 |
8 | interface FeedListProperties {
9 | type: string;
10 | articles: ArticleItem[];
11 | }
12 |
13 | const factory = create({ store }).properties();
14 |
15 | export const FeedList = factory(function Tab({ middleware: { store }, properties }) {
16 | const { executor } = store;
17 | const { articles, type } = properties();
18 | if (articles.length) {
19 | return (
20 |
21 | {articles.map((article) => (
22 |
{
26 | executor(favoriteFeedArticleProcess)({
27 | slug: article.slug,
28 | favorited: article.favorited,
29 | type
30 | });
31 | }}
32 | />
33 | ))}
34 |
35 | );
36 | }
37 |
38 | return No articles here, yet!
;
39 | });
40 |
41 | export default FeedList;
42 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "dojo-realworld",
3 | "version": "1.0.0",
4 | "scripts": {
5 | "precommit": "lint-staged",
6 | "prettier": "prettier --write 'src/**/*.ts' 'tests/**/*.ts'",
7 | "test": "dojo build -m test && dojo test",
8 | "build": "dojo build",
9 | "dev": "dojo build -m dev -w -s"
10 | },
11 | "dependencies": {
12 | "@dojo/framework": "^6.0.0",
13 | "snarkdown": "^1.2.2",
14 | "tslib": "1.10.0"
15 | },
16 | "devDependencies": {
17 | "@dojo/cli": "^6.0.0",
18 | "@dojo/cli-build-app": "^6.0.0",
19 | "@dojo/cli-test-intern": "^6.0.0",
20 | "@types/sinon": "^1.16.35",
21 | "@typescript-eslint/eslint-plugin": "^5.6.0",
22 | "@typescript-eslint/parser": "^5.6.0",
23 | "eslint": "^8.4.1",
24 | "husky": "0.14.3",
25 | "lint-staged": "6.0.0",
26 | "prettier": "1.9.2",
27 | "sinon": "^2.0.0",
28 | "typescript": "~3.4.5"
29 | },
30 | "lint-staged": {
31 | "*.{ts,tsx}": [
32 | "prettier --write",
33 | "git add"
34 | ]
35 | },
36 | "prettier": {
37 | "singleQuote": true,
38 | "tabWidth": 4,
39 | "useTabs": true,
40 | "parser": "typescript",
41 | "printWidth": 120,
42 | "arrowParens": "always"
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/.dojorc:
--------------------------------------------------------------------------------
1 | {
2 | "build-app": {
3 | "pwa": {
4 | "manifest": {
5 | "name": "dojo-realworld",
6 | "short_name": "dojo-realworld",
7 | "description": "dojo-realworld",
8 | "background_color": "#ffffff",
9 | "theme_color": "#222127",
10 | "icons": [
11 | {
12 | "src": "src/logo.png",
13 | "sizes": [
14 | 128,
15 | 256,
16 | 512
17 | ]
18 | }
19 | ]
20 | },
21 | "serviceWorker": {
22 | "clientsClaim": true,
23 | "routes": [
24 | {
25 | "urlPattern": "https://fonts.googleapis.com/css",
26 | "strategy": "networkFirst",
27 | "expiration": {
28 | "maxEntries": 25
29 | }
30 | },
31 | {
32 | "urlPattern": "https://code.ionicframework.com/ionicons/2.0.1/css/ionicons.min.css",
33 | "strategy": "networkFirst",
34 | "expiration": {
35 | "maxEntries": 25
36 | }
37 | },
38 | {
39 | "urlPattern": "https://demo.productionready.io",
40 | "strategy": "networkFirst",
41 | "expiration": {
42 | "maxEntries": 25
43 | }
44 | },
45 | {
46 | "urlPattern": "https://conduit.productionready.io/api",
47 | "strategy": "networkFirst",
48 | "expiration": {
49 | "maxEntries": 25
50 | }
51 | }
52 | ]
53 | }
54 | },
55 | "legacy": false
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/src/widgets/ArticleControls.tsx:
--------------------------------------------------------------------------------
1 | import { create, tsx } from "@dojo/framework/core/vdom";
2 | import { FollowUserPayload, FavoriteArticlePayload } from "../processes/interfaces";
3 |
4 | interface ArticleControlsProperties {
5 | slug: string;
6 | favoritesCount: number;
7 | authorUsername: string;
8 | favorited: boolean;
9 | following: boolean;
10 | followUser: (opts: FollowUserPayload) => void;
11 | favoriteArticle: (opts: FavoriteArticlePayload) => void;
12 | }
13 |
14 | const factory = create({}).properties();
15 |
16 | export const ArticleControls = factory(function ArticleControls({ properties }) {
17 | const { favoritesCount, slug, favoriteArticle, favorited, following, authorUsername, followUser } = properties();
18 | return (
19 |
20 |
29 |
39 |
40 | );
41 | });
42 |
43 | export default ArticleControls;
44 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 
2 |
3 | > ### Dojo codebase containing real world examples (CRUD, auth, advanced patterns, etc) that adheres to the [RealWorld](https://dojo-2-realworld.firebaseapp.com) spec and API.
4 |
5 | ### [Demo](https://dojo-realworld.netlify.app) [RealWorld](https://github.com/gothinkster/realworld)
6 |
7 | This codebase was created to demonstrate a fully fledged full-stack progressive web application built with Dojo including automatic code splitting by route, CRUD operations, authentication, routing, pagination, build time rendering and more.
8 |
9 | We've gone to great lengths to adhere to the Dojo community style guides & best practices.
10 |
11 | For more information on how to this works with other front-ends/back-ends, head over to the [RealWorld](https://github.com/gothinkster/realworld) repository.
12 |
13 | # How it works
14 |
15 | Dojo RealWorld using @dojo/framework and @dojo/cli-build-app.
16 |
17 | For more information on Dojo visit [dojo.io](https://dojo.io)
18 |
19 | # Getting started
20 |
21 | You can view a live demo over at https://dojo-2-realworld.firebaseapp.com
22 |
23 | To get the frontend running locally:
24 |
25 | - Clone this repository
26 | - `npm install` to install all required dependencies
27 | - `npm run dev` to start the [local server](http://localhost:9999) with watch and hot reload
28 |
29 | The local web server uses port 9999.
30 |
31 | For a production build of the application:
32 |
33 | - `npm run build` to build the output into the `output/dist` directory.
34 |
35 | To run the tests run `npm run test`
36 |
--------------------------------------------------------------------------------
/src/widgets/Comment.tsx:
--------------------------------------------------------------------------------
1 | import { create, tsx } from "@dojo/framework/core/vdom";
2 | import Link from "@dojo/framework/routing/Link";
3 | import { DeleteCommentPayload } from "../processes/interfaces";
4 | import { Comment as CommentItem } from "./../interfaces";
5 |
6 | interface CommentProperties {
7 | slug: string;
8 | comment: CommentItem;
9 | loggedInUser: string;
10 | deleteComment: (opts: DeleteCommentPayload) => void;
11 | }
12 |
13 | const factory = create({}).properties();
14 |
15 | export const Comment = factory(function Comment({ properties }) {
16 | const { slug, comment, loggedInUser, deleteComment } = properties();
17 | return (
18 |
19 |
22 |
23 |
24 |

25 |
26 |
27 | {` ${comment.author.username}`}
28 |
29 |
{new Date(comment.createdAt).toDateString()}
30 | {loggedInUser === comment.author.username && (
31 |
32 | {
34 | deleteComment({ slug, id: comment.id });
35 | }}
36 | classes={["ion-trash-a"]}
37 | >
38 |
39 | )}
40 |
41 |
42 | );
43 | });
44 |
45 | export default Comment;
46 |
--------------------------------------------------------------------------------
/src/processes/interfaces.d.ts:
--------------------------------------------------------------------------------
1 | import { Session } from '../interfaces';
2 | import { OutletContext } from '@dojo/framework/routing/interfaces';
3 |
4 | export interface SlugPayload {
5 | slug: string;
6 | }
7 |
8 | export interface TitlePayload {
9 | title: string;
10 | }
11 |
12 | export interface DescriptionPayload {
13 | description: string;
14 | }
15 |
16 | export interface BodyPayload {
17 | body: string;
18 | }
19 |
20 | export interface TagPayload {
21 | tag: string;
22 | }
23 |
24 | export interface BioPayload {
25 | bio: string;
26 | }
27 |
28 | export interface ImagePayload {
29 | imageUrl: string;
30 | }
31 |
32 | export interface EmailPayload {
33 | email: string;
34 | }
35 |
36 | export interface UsernamePayload {
37 | username: string;
38 | }
39 |
40 | export interface PasswordPayload {
41 | password: string;
42 | }
43 |
44 | export interface FollowUserPayload {
45 | username: string;
46 | following: boolean;
47 | slug?: string;
48 | }
49 |
50 | export interface FavoriteArticlePayload extends SlugPayload {
51 | favorited: boolean;
52 | type?: string;
53 | }
54 |
55 | export interface NewCommentPayload {
56 | newComment: string;
57 | slug: string;
58 | }
59 |
60 | export interface AddCommentPayload extends SlugPayload, NewCommentPayload {}
61 |
62 | export interface DeleteCommentPayload extends SlugPayload {
63 | id: number;
64 | }
65 |
66 | export interface FetchFeedPayload {
67 | type: string;
68 | filter: string;
69 | page: number;
70 | }
71 |
72 | export interface SetSessionPayload {
73 | session: Session;
74 | }
75 |
76 | export interface ChangeRoutePayload {
77 | outlet: string;
78 | context: OutletContext;
79 | }
80 |
--------------------------------------------------------------------------------
/src/App.tsx:
--------------------------------------------------------------------------------
1 | import { create, tsx } from "@dojo/framework/core/vdom";
2 | import { Outlet } from "@dojo/framework/routing/Outlet";
3 |
4 | import Header from "./widgets/Header";
5 | import Settings from "./widgets/Settings";
6 | import Login from "./widgets/Login";
7 | import Register from "./widgets/Register";
8 | import Profile from "./widgets/Profile";
9 | import Editor from "./widgets/Editor";
10 | import Article from "./widgets/Article";
11 | import Home from "./widgets/Home";
12 | import Footer from "./widgets/Footer";
13 |
14 | const factory = create();
15 |
16 | export const App = factory(function App() {
17 | return (
18 |
19 |
20 |
} />
21 | } />
22 | {
25 | if (details.isExact()) {
26 | return ;
27 | }
28 | }}
29 | />
30 | }
33 | />
34 | } />
35 | {
38 | if (details.isExact()) {
39 | return ;
40 | }
41 | }}
42 | />
43 | } />
44 | } />
45 | } />
46 |
47 |
48 | );
49 | });
50 |
51 | export default App;
52 |
--------------------------------------------------------------------------------
/src/store.ts:
--------------------------------------------------------------------------------
1 | import has from '@dojo/framework/core/has';
2 | import global from '@dojo/framework/shim/global';
3 | import { createStoreMiddleware } from '@dojo/framework/core/middleware/store';
4 | import { registerRouterInjector } from '@dojo/framework/routing/RouterInjector';
5 |
6 | import { getTagsProcess } from './processes/tagProcesses';
7 | import { setSessionProcess } from './processes/loginProcesses';
8 | import { State } from './interfaces';
9 | import Store from '@dojo/framework/stores/Store';
10 | import config from './routes';
11 | import Registry from '@dojo/framework/core/Registry';
12 | import { changeRouteProcess } from './processes/routeProcesses';
13 |
14 | export const registry = new Registry();
15 | const router = registerRouterInjector(config, registry);
16 |
17 | const store = createStoreMiddleware((store: Store) => {
18 | let session: any;
19 | if (!has('build-time-render') && global.sessionStorage) {
20 | session = global.sessionStorage.getItem('conduit-session');
21 | }
22 | if (session) {
23 | setSessionProcess(store)({ session: JSON.parse(session) });
24 | }
25 | getTagsProcess(store)({});
26 | router.on('nav', ({ outlet, context }: any) => {
27 | changeRouteProcess(store)({ outlet, context });
28 | });
29 |
30 | function onRouteChange() {
31 | const outlet = store.get(store.path('routing', 'outlet'));
32 | const params = store.get(store.path('routing', 'params'));
33 | if (outlet) {
34 | const link = router.link(outlet, params);
35 | if (link !== undefined) {
36 | router.setPath(link);
37 | }
38 | }
39 | }
40 |
41 | store.onChange(store.path('routing', 'outlet'), onRouteChange);
42 | store.onChange(store.path('routing', 'params'), onRouteChange);
43 | });
44 |
45 | export default store;
46 |
--------------------------------------------------------------------------------
/src/widgets/ArticlePreview.tsx:
--------------------------------------------------------------------------------
1 | import { create, tsx } from "@dojo/framework/core/vdom";
2 | import { Link } from "@dojo/framework/routing/Link";
3 |
4 | import { ArticleItem } from "../interfaces";
5 |
6 | export interface ArticlePreviewProperties {
7 | article: ArticleItem;
8 | favoriteArticle: () => void;
9 | }
10 |
11 | const factory = create({}).properties();
12 |
13 | export const ArticlePreview = factory(function ArticlePreview({ properties }) {
14 | const {
15 | favoriteArticle,
16 | article,
17 | article: { author, favorited, slug }
18 | } = properties();
19 |
20 | let buttonClasses = ["btn", "btn-outline-primary", "btn-sm", "pull-xs-right"];
21 | if (favorited) {
22 | buttonClasses = ["btn", "btn-primary", "btn-sm", "pull-xs-right"];
23 | }
24 |
25 | return (
26 |
27 |
28 |
29 |

30 |
31 |
32 |
33 | {author.username}
34 |
35 | {new Date(article.createdAt).toDateString()}
36 |
37 |
46 |
47 |
48 |
{article.title}
49 |
{article.description}
50 |
Read more...
51 |
52 |
53 | );
54 | });
55 |
56 | export default ArticlePreview;
57 |
--------------------------------------------------------------------------------
/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "rules": {
3 | "align": false,
4 | "ban": [],
5 | "class-name": true,
6 | "comment-format": [ true, "check-space" ],
7 | "curly": true,
8 | "eofline": true,
9 | "forin": false,
10 | "indent": [ true, "tabs" ],
11 | "interface-name": [ true, "never-prefix" ],
12 | "jsdoc-format": true,
13 | "label-position": true,
14 | "max-line-length": 120,
15 | "member-access": false,
16 | "member-ordering": false,
17 | "no-any": false,
18 | "no-arg": true,
19 | "no-bitwise": false,
20 | "no-consecutive-blank-lines": true,
21 | "no-console": false,
22 | "no-construct": false,
23 | "no-debugger": true,
24 | "no-duplicate-variable": true,
25 | "no-empty": false,
26 | "no-eval": true,
27 | "no-inferrable-types": [ true, "ignore-params" ],
28 | "no-shadowed-variable": false,
29 | "no-string-literal": false,
30 | "no-switch-case-fall-through": false,
31 | "no-trailing-whitespace": true,
32 | "no-unused-expression": false,
33 | "no-use-before-declare": false,
34 | "no-var-keyword": true,
35 | "no-var-requires": false,
36 | "object-literal-sort-keys": false,
37 | "one-line": [ true, "check-open-brace", "check-whitespace" ],
38 | "radix": true,
39 | "trailing-comma": [ true, {
40 | "multiline": "never",
41 | "singleline": "never"
42 | } ],
43 | "triple-equals": [ true, "allow-null-check" ],
44 | "typedef": false,
45 | "typedef-whitespace": [ true, {
46 | "call-signature": "nospace",
47 | "index-signature": "nospace",
48 | "parameter": "nospace",
49 | "property-declaration": "nospace",
50 | "variable-declaration": "nospace"
51 | }, {
52 | "call-signature": "onespace",
53 | "index-signature": "onespace",
54 | "parameter": "onespace",
55 | "property-declaration": "onespace",
56 | "variable-declaration": "onespace"
57 | } ],
58 | "variable-name": [ true, "check-format", "allow-leading-underscore", "ban-keywords", "allow-pascal-case" ]
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/src/widgets/ArticleMeta.tsx:
--------------------------------------------------------------------------------
1 | import { create, tsx } from "@dojo/framework/core/vdom";
2 | import { Link } from "@dojo/framework/routing/Link";
3 | import { ArticleItem } from "../interfaces";
4 | import { ArticleControls } from "./ArticleControls";
5 | import { ArticleAuthorControls } from "./ArticleAuthorControls";
6 | import { FavoriteArticlePayload, FollowUserPayload, SlugPayload } from "../processes/interfaces";
7 |
8 | interface ArticleMetaProperties {
9 | currentUser: string;
10 | article: ArticleItem;
11 | isAuthenticated: boolean;
12 | favoriteArticle: (opts: FavoriteArticlePayload) => void;
13 | followUser: (opts: FollowUserPayload) => void;
14 | deleteArticle: (opts: SlugPayload) => void;
15 | }
16 |
17 | const factory = create({}).properties();
18 |
19 | export const ArticleMeta = factory(function ArticleMeta({ properties }) {
20 | const { article, isAuthenticated, currentUser, favoriteArticle, followUser, deleteArticle } = properties();
21 | const { author, slug, createdAt, favorited, favoritesCount } = article;
22 |
23 | return (
24 |
25 |
26 |

27 |
28 |
29 |
30 | {author.username}
31 |
32 | {new Date(createdAt).toDateString()}
33 |
34 | {isAuthenticated &&
35 | (currentUser === author.username ? (
36 |
37 | ) : (
38 |
47 | ))}
48 |
49 | );
50 | });
51 |
52 | export default ArticleMeta;
53 |
--------------------------------------------------------------------------------
/tests/unit/widgets/Tags.ts:
--------------------------------------------------------------------------------
1 | const { describe, it } = intern.getInterface('bdd');
2 | const { assert } = intern.getPlugin('chai');
3 | import { stub } from 'sinon';
4 |
5 | import { v, w } from '@dojo/framework/core/vdom';
6 | import harness from '@dojo/framework/testing/harness';
7 | import createMockStoreMiddleware from '@dojo/framework/testing/mocks/middleware/store';
8 |
9 | import { fetchFeedProcess } from '../../../src/processes/feedProcesses';
10 | import { Tags } from './../../../src/widgets/Tags';
11 | import store from './../../../src/store';
12 | import { replace } from '@dojo/framework/stores/state/operations';
13 |
14 | const mockEvent = {
15 | preventDefault() {}
16 | };
17 |
18 | describe('tags widget', () => {
19 | it('no tags', () => {
20 | const fetchFeed = stub();
21 | const mockStore = createMockStoreMiddleware([[fetchFeedProcess, fetchFeed]]);
22 | const h = harness(() => w(Tags, {}), { middleware: [[store, mockStore]] });
23 | const expected = v('div', { classes: ['col-md-3'] }, [
24 | v('div', { classes: ['sidebar'] }, [v('p', ['Popular Tags']), v('div', { classes: ['tag-list'] }, [])])
25 | ]);
26 |
27 | h.expect(() => expected);
28 | });
29 |
30 | it('with tags', () => {
31 | const fetchFeed = stub();
32 | const mockStore = createMockStoreMiddleware([[fetchFeedProcess, fetchFeed]]);
33 | const h = harness(() => w(Tags, {}), { middleware: [[store, mockStore]] });
34 | mockStore((path) => [replace(path('tags'), ['first', 'second'])]);
35 | h.expect(() =>
36 | v('div', { classes: ['col-md-3'] }, [
37 | v('div', { classes: ['sidebar'] }, [
38 | v('p', ['Popular Tags']),
39 | v('div', { classes: ['tag-list'] }, [
40 | v('a', { href: '', onclick: () => {}, key: '0', classes: ['tag-pill', 'tag-default'] }, [
41 | 'first'
42 | ]),
43 | v('a', { href: '', onclick: () => {}, key: '1', classes: ['tag-pill', 'tag-default'] }, [
44 | 'second'
45 | ])
46 | ])
47 | ])
48 | ])
49 | );
50 |
51 | h.trigger('@0', 'onclick', mockEvent);
52 | assert.isTrue(fetchFeed.calledOnce);
53 | });
54 | });
55 |
--------------------------------------------------------------------------------
/src/widgets/Header.tsx:
--------------------------------------------------------------------------------
1 | import { create, tsx } from "@dojo/framework/core/vdom";
2 | import ActiveLink from "@dojo/framework/routing/ActiveLink";
3 | import store from "../store";
4 |
5 | const factory = create({ store }).properties();
6 |
7 | export const Header = factory(function Factory({ middleware: { store } }) {
8 | const { get, path } = store;
9 | const isAuthenticated = !!get(path("session", "token"));
10 | const loggedInUser = get(path("session", "username"));
11 |
12 | return (
13 |
68 | );
69 | });
70 |
71 | export default Header;
72 |
--------------------------------------------------------------------------------
/src/interfaces.d.ts:
--------------------------------------------------------------------------------
1 | export type WithTarget = T & { target: E };
2 |
3 | export interface ResourceBased {
4 | isLoaded: boolean;
5 | isLoading: boolean;
6 | }
7 |
8 | export interface User {
9 | username: string;
10 | bio: string;
11 | image: string;
12 | }
13 |
14 | export interface AuthorProfile extends User {
15 | following: boolean;
16 | feed: Feed;
17 | }
18 |
19 | export interface Session extends User, ResourceBased {
20 | email: string;
21 | token: string;
22 | }
23 |
24 | export interface Comment {
25 | id: number;
26 | createdAt: string;
27 | updatedAt: string;
28 | body: string;
29 | author: AuthorProfile;
30 | }
31 |
32 | export interface Routing {
33 | outlet: string;
34 | params: { [index: string]: string };
35 | }
36 |
37 | export interface ArticleItem {
38 | slug: string;
39 | title: string;
40 | description: string;
41 | body: string;
42 | tagList: string[];
43 | createdAt: string;
44 | updatedAt: string;
45 | favorited: boolean;
46 | favoritesCount: number;
47 | author: AuthorProfile;
48 | }
49 |
50 | export interface Settings extends User, ResourceBased {
51 | email: string;
52 | password?: string;
53 | }
54 |
55 | export interface Article extends ResourceBased {
56 | item: ArticleItem;
57 | comments: Comment[];
58 | newComment: string;
59 | }
60 |
61 | export interface Feed extends ResourceBased {
62 | category: string;
63 | tagName: string;
64 | filter: string;
65 | items: ArticleItem[];
66 | offset: number;
67 | page: number;
68 | total: number;
69 | }
70 |
71 | export interface Errors {
72 | [index: string]: string[];
73 | }
74 |
75 | export interface Login extends ResourceBased {
76 | email: string;
77 | password: string;
78 | failed: boolean;
79 | }
80 |
81 | export interface Register extends ResourceBased {
82 | username: string;
83 | password: string;
84 | email: string;
85 | failed: boolean;
86 | }
87 |
88 | export interface Editor extends ResourceBased {
89 | slug: string;
90 | title: string;
91 | description: string;
92 | body: string;
93 | tag: string;
94 | tagList: string[];
95 | }
96 |
97 | export interface Profile {
98 | user: AuthorProfile & ResourceBased;
99 | feed: Feed;
100 | }
101 |
102 | interface State {
103 | settings: Settings;
104 | article: {
105 | [index: string]: Article;
106 | };
107 | feed: Feed;
108 | session: Session;
109 | profile: Profile;
110 | routing: Routing;
111 | tags: string[];
112 | errors: Errors;
113 | login: ResourceBased;
114 | register: ResourceBased;
115 | editor: Editor;
116 | }
117 |
--------------------------------------------------------------------------------
/src/widgets/Login.tsx:
--------------------------------------------------------------------------------
1 | import { create, tsx } from "@dojo/framework/core/vdom";
2 | import icache from "@dojo/framework/core/middleware/icache";
3 | import Link from "@dojo/framework/routing/Link";
4 |
5 | import store from "../store";
6 | import { loginProcess } from "../processes/loginProcesses";
7 | import ErrorList from "./ErrorList";
8 |
9 | const factory = create({ icache, store });
10 |
11 | export const Login = factory(function Login({ middleware: { store, icache } }) {
12 | const { get, path, executor } = store;
13 | const isLoading = get(path("login", "isLoading"));
14 | const errors = get(path("errors"));
15 | const email = icache.get("email") || "";
16 | const password = icache.get("password") || "";
17 | function setEmail(event: KeyboardEvent) {
18 | const target = event.target as HTMLInputElement;
19 | icache.set("email", target.value);
20 | }
21 | function setPassword(event: KeyboardEvent) {
22 | const target = event.target as HTMLInputElement;
23 | icache.set("password", target.value);
24 | }
25 |
26 | return (
27 |
28 |
29 |
30 |
31 |
Sign In
32 |
33 | Need an account?
34 |
35 | {errors &&
}
36 |
72 |
73 |
74 |
75 |
76 | );
77 | });
78 |
79 | export default Login;
80 |
--------------------------------------------------------------------------------
/src/processes/settingsProcesses.ts:
--------------------------------------------------------------------------------
1 | import { createProcess } from '@dojo/framework/stores/process';
2 | import { replace } from '@dojo/framework/stores/state/operations';
3 | import { getHeaders, commandFactory } from './utils';
4 | import { baseUrl } from '../config';
5 | import { EmailPayload, PasswordPayload, UsernamePayload, ImagePayload, BioPayload } from './interfaces';
6 |
7 | const emailInputCommand = commandFactory(({ payload: { email }, path }) => {
8 | return [replace(path('settings', 'email'), email)];
9 | });
10 |
11 | const passwordInputCommand = commandFactory(({ payload: { password }, path }) => {
12 | return [replace(path('settings', 'password'), password)];
13 | });
14 |
15 | const usernameInputCommand = commandFactory(({ payload: { username }, path }) => {
16 | return [replace(path('settings', 'username'), username)];
17 | });
18 |
19 | const bioInputCommand = commandFactory(({ payload: { bio }, path }) => {
20 | return [replace(path('settings', 'bio'), bio)];
21 | });
22 |
23 | const imageUrlInputCommand = commandFactory(({ payload: { imageUrl }, path }) => {
24 | return [replace(path('settings', 'image'), imageUrl)];
25 | });
26 |
27 | const startUserSettingsCommand = commandFactory(({ path }) => {
28 | return [replace(path('settings'), { isLoaded: false, isLoading: false })];
29 | });
30 |
31 | const getUserSettingsCommand = commandFactory(({ path, get }) => {
32 | return [replace(path('settings'), get(path('session')))];
33 | });
34 |
35 | const updateUserSettingsCommand = commandFactory(async ({ path, get }) => {
36 | const token = get(path('session', 'token'));
37 | const requestPayload = get(path('settings'));
38 | const response = await fetch(`${baseUrl}/user`, {
39 | method: 'put',
40 | headers: getHeaders(token),
41 | body: JSON.stringify(requestPayload)
42 | });
43 |
44 | const json = await response.json();
45 |
46 | return [
47 | replace(path('session'), json.user),
48 | replace(path('settings'), undefined),
49 | replace(path('routing', 'outlet'), 'user'),
50 | replace(path('routing', 'params'), { username: get(path('settings', 'username')) })
51 | ];
52 | });
53 |
54 | export const getUserSettingsProcess = createProcess('user-settings', [
55 | startUserSettingsCommand,
56 | getUserSettingsCommand
57 | ]);
58 | export const updateUserSettingsProcess = createProcess('update-user-settings', [updateUserSettingsCommand]);
59 | export const usernameInputProcess = createProcess('username-input', [usernameInputCommand]);
60 | export const emailInputProcess = createProcess('email-input', [emailInputCommand]);
61 | export const passwordInputProcess = createProcess('password-input', [passwordInputCommand]);
62 | export const bioInputProcess = createProcess('bio-input', [bioInputCommand]);
63 | export const imageUrlInputProcess = createProcess('image-url-input', [imageUrlInputCommand]);
64 |
--------------------------------------------------------------------------------
/.github/workflows/ci-cd.yml:
--------------------------------------------------------------------------------
1 | name: CI - CD
2 |
3 | on:
4 | push:
5 | branches: [main]
6 | pull_request:
7 | branches:
8 | - '**'
9 |
10 | jobs:
11 | first_interaction:
12 | if: github.event_name == 'pull_request'
13 | name: 'first interaction'
14 | runs-on: ubuntu-latest
15 | steps:
16 | - uses: actions/first-interaction@v1
17 | with:
18 | repo-token: ${{ secrets.GITHUB_TOKEN }}
19 | pr-message: |
20 | Thanks for your first pull request on this project!
21 |
22 | This is a kindly reminder to read the following resources:
23 | - [code of conduct]()
24 | - [contribution guidelines]()
25 |
26 | It'll help us to review your contribution and to ensure it's aligned with our standards.
27 | greetings:
28 | if: github.event_name == 'pull_request'
29 | runs-on: ubuntu-latest
30 | steps:
31 | - uses: kerhub/saved-replies@v1.0.0
32 | with:
33 | token: "${{ secrets.GITHUB_TOKEN }}"
34 | reply: |
35 | Hi @${{ github.event.pull_request.user.login }}, thanks for being part of the community :heart:
36 | We'll review your contribution as soon as possible!
37 | prettier:
38 | uses: kerhub/reusable-workflows/.github/workflows/node-job.yml@main
39 | with:
40 | command: npm run prettier --check \"**\"
41 | linter:
42 | uses: kerhub/reusable-workflows/.github/workflows/node-job.yml@main
43 | with:
44 | command: npx eslint --fix src/**/*.ts
45 | unit_tests:
46 | name: 'unit tests'
47 | uses: kerhub/reusable-workflows/.github/workflows/node-job.yml@main
48 | with:
49 | command: npm run test
50 | deploy_preview:
51 | name: 'deploy preview'
52 | if: github.event_name == 'pull_request'
53 | needs: [prettier, linter, unit_tests]
54 | uses: kerhub/reusable-workflows/.github/workflows/netlify-preview-deploy.yml@main
55 | with:
56 | build_directory: './output/dist'
57 | secrets:
58 | netlifyAuthToken: "${{ secrets.NETLIFY_AUTH_TOKEN }}"
59 | netlifySiteId: "${{ secrets.NETLIFY_SITE_ID }}"
60 | repoToken: "${{ secrets.GITHUB_TOKEN }}"
61 | deploy_live:
62 | name: 'deploy live'
63 | if: github.event_name == 'push'
64 | needs: [prettier, linter, unit_tests]
65 | uses: kerhub/reusable-workflows/.github/workflows/netlify-live-deploy.yml@main
66 | with:
67 | build_directory: './output/dist'
68 | secrets:
69 | netlifyAuthToken: "${{ secrets.NETLIFY_AUTH_TOKEN }}"
70 | netlifySiteId: "${{ secrets.NETLIFY_SITE_ID }}"
71 | lighthouse_preview:
72 | name: 'lighthouse preview'
73 | needs: deploy_preview
74 | uses: kerhub/reusable-workflows/.github/workflows/lighthouse-preview.yml@main
75 | with:
76 | siteName: 'dojo-realworld'
77 | secrets:
78 | netlifyAuthToken: "${{ secrets.NETLIFY_AUTH_TOKEN }}"
79 | lighthouse_live:
80 | name: 'lighthouse live'
81 | needs: deploy_live
82 | uses: kerhub/reusable-workflows/.github/workflows/lighthouse-live.yml@main
83 | with:
84 | siteUrl: 'https://dojo-realworld.netlify.app/'
85 |
--------------------------------------------------------------------------------
/src/widgets/Register.tsx:
--------------------------------------------------------------------------------
1 | import { create, tsx } from "@dojo/framework/core/vdom";
2 | import icache from "@dojo/framework/core/middleware/icache";
3 | import Link from "@dojo/framework/routing/Link";
4 |
5 | import store from "../store";
6 | import { registerProcess } from "../processes/loginProcesses";
7 | import ErrorList from "./ErrorList";
8 |
9 | const factory = create({ icache, store });
10 |
11 | export const Register = factory(function Register({ middleware: { store, icache } }) {
12 | const { get, path, executor } = store;
13 | const isLoading = get(path("register", "isLoading"));
14 | const errors = get(path("errors"));
15 | const email = icache.get("email") || "";
16 | const username = icache.get("username") || "";
17 | const password = icache.get("password") || "";
18 | function setEmail(event: KeyboardEvent) {
19 | const target = event.target as HTMLInputElement;
20 | icache.set("email", target.value);
21 | }
22 | function setPassword(event: KeyboardEvent) {
23 | const target = event.target as HTMLInputElement;
24 | icache.set("password", target.value);
25 | }
26 | function setUsername(event: KeyboardEvent) {
27 | const target = event.target as HTMLInputElement;
28 | icache.set("username", target.value);
29 | }
30 |
31 | return (
32 |
33 |
34 |
35 |
36 |
Sign In
37 |
38 | Have an account?
39 |
40 | {errors &&
}
41 |
86 |
87 |
88 |
89 |
90 | );
91 | });
92 |
93 | export default Register;
94 |
--------------------------------------------------------------------------------
/src/processes/loginProcesses.ts:
--------------------------------------------------------------------------------
1 | import global from '@dojo/framework/shim/global';
2 | import { createProcess } from '@dojo/framework/stores/process';
3 | import { replace } from '@dojo/framework/stores/state/operations';
4 | import { getHeaders, commandFactory } from './utils';
5 | import { baseUrl } from '../config';
6 | import { SetSessionPayload } from './interfaces';
7 |
8 | const startLoginCommand = commandFactory(({ path }) => {
9 | return [replace(path('login', 'isLoading'), true)];
10 | });
11 |
12 | const startRegisterCommand = commandFactory(({ path }) => {
13 | return [replace(path('register', 'isLoading'), true)];
14 | });
15 |
16 | const setSessionCommand = commandFactory(({ path, payload: { session } }) => {
17 | return [replace(path('session'), session)];
18 | });
19 |
20 | const loginCommand = commandFactory<{ email: string; password: string }>(async ({ path, payload }) => {
21 | const requestPayload = {
22 | user: {
23 | email: payload.email,
24 | password: payload.password
25 | }
26 | };
27 |
28 | const response = await fetch(`${baseUrl}/users/login`, {
29 | method: 'post',
30 | body: JSON.stringify(requestPayload),
31 | headers: getHeaders()
32 | });
33 |
34 | const json = await response.json();
35 | if (!response.ok) {
36 | return [
37 | replace(path('login', 'isLoading'), true),
38 | replace(path('errors'), json.errors),
39 | replace(path('session'), {})
40 | ];
41 | }
42 |
43 | global.sessionStorage.setItem('conduit-session', JSON.stringify(json.user));
44 |
45 | return [
46 | replace(path('routing', 'outlet'), 'home'),
47 | replace(path('login', 'isLoading'), false),
48 | replace(path('errors'), undefined),
49 | replace(path('session'), json.user)
50 | ];
51 | });
52 |
53 | const registerCommand = commandFactory<{ username: string; email: string; password: string }>(
54 | async ({ path, payload: { username, email, password } }) => {
55 | const requestPayload = {
56 | user: {
57 | username,
58 | email,
59 | password
60 | }
61 | };
62 |
63 | const response = await fetch(`${baseUrl}/users`, {
64 | method: 'post',
65 | body: JSON.stringify(requestPayload),
66 | headers: getHeaders()
67 | });
68 | const json = await response.json();
69 | if (!response.ok) {
70 | return [
71 | replace(path('register', 'isLoading'), false),
72 | replace(path('errors'), json.errors),
73 | replace(path('session'), {})
74 | ];
75 | }
76 |
77 | global.sessionStorage.setItem('conduit-session', JSON.stringify(json.user));
78 |
79 | return [
80 | replace(path('routing', 'outlet'), 'home'),
81 | replace(path('register', 'isLoading'), false),
82 | replace(path('errors'), undefined),
83 | replace(path('session'), json.user)
84 | ];
85 | }
86 | );
87 |
88 | const logoutCommand = commandFactory(({ path }) => {
89 | global.sessionStorage.removeItem('conduit-session');
90 | return [replace(path('session'), {}), replace(path('routing', 'outlet'), 'home')];
91 | });
92 |
93 | export const loginProcess = createProcess('login', [startLoginCommand, loginCommand]);
94 | export const registerProcess = createProcess('register', [startRegisterCommand, registerCommand]);
95 | export const setSessionProcess = createProcess('set-session', [setSessionCommand]);
96 | export const logoutProcess = createProcess('logout', [logoutCommand]);
97 |
--------------------------------------------------------------------------------
/src/processes/profileProcesses.ts:
--------------------------------------------------------------------------------
1 | import { createProcess } from '@dojo/framework/stores/process';
2 | import { replace } from '@dojo/framework/stores/state/operations';
3 | import { getHeaders, commandFactory } from './utils';
4 | import { baseUrl } from '../config';
5 | import { FollowUserPayload } from './interfaces';
6 |
7 | const startGetProfileCommand = commandFactory(({ path }) => {
8 | return [
9 | replace(path('profile', 'user', 'isLoading'), true),
10 | replace(path('profile', 'user', 'isLoaded'), false),
11 | replace(path('profile', 'feed', 'items'), undefined)
12 | ];
13 | });
14 |
15 | const startGetProfileFeedCommand = commandFactory(({ path, payload: { page, type } }) => {
16 | return [
17 | replace(path('profile', 'feed', 'isLoading'), true),
18 | replace(path('profile', 'feed', 'isLoaded'), false),
19 | replace(path('profile', 'feed', 'category'), type),
20 | replace(path('profile', 'feed', 'page'), page)
21 | ];
22 | });
23 |
24 | const followUserCommand = commandFactory(async ({ get, path, payload: { username, following } }) => {
25 | const token = get(path('session', 'token'));
26 | const response = await fetch(`${baseUrl}/profiles/${username}/follow`, {
27 | method: following ? 'delete' : 'post',
28 | headers: getHeaders(token)
29 | });
30 | const json = await response.json();
31 |
32 | return [replace(path('profile'), json.profile)];
33 | });
34 |
35 | const getProfileCommand = commandFactory<{ username: string; type: string }>(
36 | async ({ get, path, payload: { username } }) => {
37 | const token = get(path('session', 'token'));
38 | const response = await fetch(`${baseUrl}/profiles/${username}`, {
39 | headers: getHeaders(token)
40 | });
41 | const json = await response.json();
42 |
43 | return [
44 | replace(path('profile', 'user', 'username'), json.profile.username),
45 | replace(path('profile', 'user', 'image'), json.profile.image),
46 | replace(path('profile', 'user', 'bio'), json.profile.bio),
47 | replace(path('profile', 'user', 'following'), json.profile.following),
48 | replace(path('profile', 'user', 'isLoading'), false),
49 | replace(path('profile', 'user', 'isLoaded'), true)
50 | ];
51 | }
52 | );
53 |
54 | const getProfileFeedCommand = commandFactory<{ username: string; type: string; page: number }>(
55 | async ({ get, path, payload: { type, username, page } }) => {
56 | const token = get(path('session', 'token'));
57 | const offset = page * 10;
58 | let url = `${baseUrl}/articles?`;
59 |
60 | switch (type) {
61 | case 'favorites':
62 | url = `${url}favorited=${username}&`;
63 | break;
64 | case 'user':
65 | url = `${url}author=${username}&`;
66 | break;
67 | }
68 |
69 | const response = await fetch(`${url}limit=10&offset=${offset}`, { headers: getHeaders(token) });
70 | const json = await response.json();
71 | return [
72 | replace(path('profile', 'feed', 'items'), json.articles),
73 | replace(path('profile', 'feed', 'total'), json.articlesCount),
74 | replace(path('profile', 'feed', 'offset'), offset),
75 | replace(path('profile', 'feed', 'isLoading'), false),
76 | replace(path('profile', 'feed', 'isLoaded'), true)
77 | ];
78 | }
79 | );
80 |
81 | export const getProfileProcess = createProcess('get-profile', [
82 | [startGetProfileCommand, startGetProfileFeedCommand],
83 | [getProfileCommand, getProfileFeedCommand]
84 | ]);
85 | export const getProfileFeedProcess = createProcess('get-profile-feed', [
86 | startGetProfileFeedCommand,
87 | getProfileFeedCommand
88 | ]);
89 | export const followUserProcess = createProcess('follow-user', [followUserCommand]);
90 |
--------------------------------------------------------------------------------
/src/widgets/Home.tsx:
--------------------------------------------------------------------------------
1 | import { create, tsx } from "@dojo/framework/core/vdom";
2 | import icache from "@dojo/framework/core/middleware/icache";
3 | import store from "../store";
4 | import FeedList from "./FeedList";
5 | import { Tags } from "./Tags";
6 | import { Banner } from "./Banner";
7 | import { FeedPagination } from "./FeedPagination";
8 | import { fetchFeedProcess } from "../processes/feedProcesses";
9 |
10 | const factory = create({ store, icache });
11 |
12 | export const Home = factory(function Home({ middleware: { store } }) {
13 | const { get, path, executor } = store;
14 | const isAuthenticated = !!get(path("session", "token"));
15 | const username = get(path("session", "username"));
16 | const articles = get(path("feed", "items")) || [];
17 | const tagName = get(path("feed", "tagName"));
18 | const loaded = get(path("feed", "isLoaded")) || false;
19 | const loading = get(path("feed", "isLoading")) || false;
20 | const type = get(path("feed", "category")) || (isAuthenticated ? "feed" : "global");
21 |
22 | if (articles.length === 0 && !loading && !loaded) {
23 | executor(fetchFeedProcess)({ type, filter: username, page: 0 });
24 | return null;
25 | }
26 |
27 | const currentPage = get(path("feed", "page")) || 0;
28 | const total = get(path("feed", "total")) || 0;
29 |
30 | if (type)
31 | return (
32 |
33 |
34 |
35 |
36 |
37 |
81 |
82 | {loading ? (
83 |
Loading...
84 | ) : (
85 |
86 | )}
87 |
88 | {!loading && (
89 |
{
93 | executor(fetchFeedProcess)({
94 | type,
95 | filter: type === "tag" ? tagName : username,
96 | page
97 | });
98 | }}
99 | />
100 | )}
101 |
102 |
103 |
104 |
105 |
106 | );
107 | });
108 |
109 | export default Home;
110 |
--------------------------------------------------------------------------------
/src/processes/feedProcesses.ts:
--------------------------------------------------------------------------------
1 | import { createProcess } from '@dojo/framework/stores/process';
2 | import { replace } from '@dojo/framework/stores/state/operations';
3 | import { ArticleItem } from './../interfaces';
4 | import { getHeaders, commandFactory } from './utils';
5 | import { baseUrl } from '../config';
6 | import { FavoriteArticlePayload, FetchFeedPayload } from './interfaces';
7 |
8 | function getItemIndex(items: ArticleItem[], id: string) {
9 | let index = -1;
10 | for (let i = 0; i < items.length; i++) {
11 | if (items[i].slug === id) {
12 | index = i;
13 | break;
14 | }
15 | }
16 | return index;
17 | }
18 |
19 | const startFetchingFeedCommand = commandFactory(({ path, payload: { type, filter, page } }) => {
20 | return [
21 | replace(path('feed', 'isLoading'), true),
22 | replace(path('feed', 'isLoaded'), false),
23 | replace(path('feed', 'category'), type),
24 | replace(path('feed', 'filter'), filter),
25 | replace(path('feed', 'tagName'), type === 'tag' ? filter : undefined),
26 | replace(path('feed', 'page'), page),
27 | replace(path('feed', 'items'), undefined)
28 | ];
29 | });
30 |
31 | export const fetchFeedCommand = commandFactory(
32 | async ({ get, path, payload: { type, page, filter } }) => {
33 | const token = get(path('session', 'token'));
34 | const offset = page * 10;
35 | let url: string;
36 |
37 | switch (type) {
38 | case 'feed':
39 | url = `${baseUrl}/articles/feed?`;
40 | break;
41 | case 'tag':
42 | url = `${baseUrl}/articles?tag=${filter}&`;
43 | break;
44 | default:
45 | url = `${baseUrl}/articles/?`;
46 | break;
47 | }
48 |
49 | const response = await fetch(`${url}limit=10&offset=${offset}`, { headers: getHeaders(token) });
50 | const json = await response.json();
51 | return [
52 | replace(path('feed', 'items'), json.articles),
53 | replace(path('feed', 'total'), json.articlesCount),
54 | replace(path('feed', 'offset'), offset),
55 | replace(path('feed', 'page'), page),
56 | replace(path('feed', 'category'), type),
57 | replace(path('feed', 'filter'), filter),
58 | replace(path('feed', 'isLoading'), false),
59 | replace(path('feed', 'isLoaded'), true)
60 | ];
61 | }
62 | );
63 |
64 | const clearFeedCommand = commandFactory(({ path }) => {
65 | return [replace(path('feed'), undefined)];
66 | });
67 |
68 | const favoriteFeedArticleCommand = commandFactory(
69 | async ({ get, path, payload: { slug, favorited, type } }) => {
70 | const token = get(path('session', 'token'));
71 | const response = await fetch(`${baseUrl}/articles/${slug}/favorite`, {
72 | method: favorited ? 'delete' : 'post',
73 | headers: getHeaders(token)
74 | });
75 | const json = await response.json();
76 | let feedPath = path('feed', 'items');
77 | if (type === 'favorites' || type === 'user') {
78 | feedPath = path('profile', 'feed', 'items');
79 | }
80 | let articles = get(feedPath);
81 |
82 | const index = getItemIndex(articles, slug);
83 | articles = [...articles];
84 | articles[index] = json.article;
85 |
86 | if (index !== -1) {
87 | if (type === 'favorites') {
88 | articles.splice(index, 1);
89 | return [replace(feedPath, articles)];
90 | }
91 | articles[index] = json.article;
92 | return [replace(feedPath, articles)];
93 | }
94 | return [];
95 | }
96 | );
97 |
98 | export const fetchFeedProcess = createProcess('fetch-feed', [startFetchingFeedCommand, fetchFeedCommand]);
99 | export const clearFeedProcess = createProcess('clear-feed', [clearFeedCommand]);
100 | export const favoriteFeedArticleProcess = createProcess('fav-feed-article', [favoriteFeedArticleCommand]);
101 |
--------------------------------------------------------------------------------
/src/processes/editorProcesses.ts:
--------------------------------------------------------------------------------
1 | import { createProcess } from '@dojo/framework/stores/process';
2 | import { replace, add, remove } from '@dojo/framework/stores/state/operations';
3 | import { getHeaders, commandFactory } from './utils';
4 | import { baseUrl } from '../config';
5 | import { TitlePayload, DescriptionPayload, BodyPayload, TagPayload, SlugPayload } from './interfaces';
6 |
7 | const titleInputCommand = commandFactory(({ path, payload: { title } }) => {
8 | return [replace(path('editor', 'title'), title)];
9 | });
10 |
11 | const descriptionInputCommand = commandFactory(({ path, payload: { description } }) => {
12 | return [replace(path('editor', 'description'), description)];
13 | });
14 |
15 | const bodyInputCommand = commandFactory(({ path, payload: { body } }) => {
16 | return [replace(path('editor', 'body'), body)];
17 | });
18 |
19 | const tagInputCommand = commandFactory(({ path, payload: { tag } }) => {
20 | return [replace(path('editor', 'tag'), tag)];
21 | });
22 |
23 | const addTagCommand = commandFactory(({ get, at, path, payload: { tag } }) => {
24 | const length = (get(path('editor', 'tagList')) || []).length;
25 | return [add(at(path('editor', 'tagList'), length), tag)];
26 | });
27 |
28 | const clearTagInputCommand = commandFactory(({ path }) => {
29 | return [replace(path('editor', 'tag'), '')];
30 | });
31 |
32 | const removeTagCommand = commandFactory(({ get, at, path, payload: { tag } }) => {
33 | const tags = get(path('editor', 'tagList'));
34 | const index = tags.indexOf(tag);
35 | if (index !== -1) {
36 | return [remove(at(path('editor', 'tagList'), index))];
37 | }
38 | return [];
39 | });
40 |
41 | const getArticleForEditorCommand = commandFactory(async ({ path, payload: { slug } }) => {
42 | const response = await fetch(`${baseUrl}/articles/${slug}`);
43 | const json = await response.json();
44 | return [replace(path('editor'), json.article)];
45 | });
46 |
47 | const clearEditorCommand = commandFactory(({ path }) => {
48 | return [replace(path('editor'), {})];
49 | });
50 |
51 | const startPublishCommand = commandFactory(({ path }) => {
52 | return [replace(path('editor', 'isLoading'), true)];
53 | });
54 |
55 | const publishArticleCommand = commandFactory(async ({ get, path }) => {
56 | const token = get(path('session', 'token'));
57 | const slug = get(path('editor', 'slug'));
58 | const requestPayload = {
59 | article: get(path('editor'))
60 | };
61 |
62 | const url = slug ? `${baseUrl}/articles/${slug}` : `${baseUrl}/articles`;
63 | const response = await fetch(url, {
64 | method: slug ? 'put' : 'post',
65 | headers: getHeaders(token),
66 | body: JSON.stringify(requestPayload)
67 | });
68 | const json = await response.json();
69 |
70 | if (!response.ok) {
71 | return [replace(path('editor', 'isLoading'), false), replace(path('errors'), json.errors)];
72 | }
73 |
74 | return [
75 | replace(path('article', slug, 'item'), json.article),
76 | replace(path('article', slug, 'isLoading'), true),
77 | replace(path('editor'), undefined)
78 | ];
79 | });
80 |
81 | export const titleInputProcess = createProcess('title-input', [titleInputCommand]);
82 | export const descInputProcess = createProcess('desc-input', [descriptionInputCommand]);
83 | export const bodyInputProcess = createProcess('body-input', [bodyInputCommand]);
84 | export const tagInputProcess = createProcess('tag-input', [tagInputCommand]);
85 | export const addTagProcess = createProcess('add-tag', [addTagCommand, clearTagInputCommand]);
86 | export const removeTagProcess = createProcess('remove-tag', [removeTagCommand]);
87 | export const getEditorArticleProcess = createProcess('get-editor-article', [getArticleForEditorCommand]);
88 | export const publishArticleProcess = createProcess('publish-article', [startPublishCommand, publishArticleCommand]);
89 | export const clearEditorProcess = createProcess('clear-editor', [clearEditorCommand]);
90 |
--------------------------------------------------------------------------------
/src/widgets/Settings.tsx:
--------------------------------------------------------------------------------
1 | import { create, tsx } from "@dojo/framework/core/vdom";
2 | import store from "../store";
3 | import {
4 | bioInputProcess,
5 | emailInputProcess,
6 | passwordInputProcess,
7 | imageUrlInputProcess,
8 | getUserSettingsProcess,
9 | usernameInputProcess,
10 | updateUserSettingsProcess
11 | } from "./../processes/settingsProcesses";
12 | import { logoutProcess } from "../processes/loginProcesses";
13 |
14 | const factory = create({ store });
15 |
16 | export const Settings = factory(function Settings({ middleware: { store } }) {
17 | const { get, path, executor } = store;
18 | const settings = get(path("settings"));
19 |
20 | if (!settings) {
21 | executor(getUserSettingsProcess)({});
22 | return null;
23 | }
24 |
25 | return (
26 |
27 |
28 |
29 |
30 |
Your Settings
31 |
102 |
103 |
111 |
112 |
113 |
114 |
115 | );
116 | });
117 |
118 | export default Settings;
119 |
--------------------------------------------------------------------------------
/src/widgets/Editor.tsx:
--------------------------------------------------------------------------------
1 | import { create, tsx } from "@dojo/framework/core/vdom";
2 | import store from "../store";
3 | import {
4 | getEditorArticleProcess,
5 | titleInputProcess,
6 | descInputProcess,
7 | bodyInputProcess,
8 | tagInputProcess,
9 | addTagProcess,
10 | removeTagProcess,
11 | publishArticleProcess
12 | } from "../processes/editorProcesses";
13 | import ErrorList from "./ErrorList";
14 |
15 | export interface EditorProperties {
16 | slug?: string;
17 | }
18 |
19 | const factory = create({ store }).properties();
20 |
21 | export const Editor = factory(function Editor({ middleware: { store }, properties }) {
22 | const { get, path, executor } = store;
23 | const article = get(path("editor")) || {};
24 | const errors = get(path("errors"));
25 | const { slug } = properties();
26 |
27 | if (slug && (!article || (article.slug !== slug && !article.isLoading))) {
28 | executor(getEditorArticleProcess)({ slug });
29 | return null;
30 | }
31 |
32 | return (
33 |
122 | );
123 | });
124 |
125 | export default Editor;
126 |
--------------------------------------------------------------------------------
/src/widgets/Profile.tsx:
--------------------------------------------------------------------------------
1 | import { create, tsx } from "@dojo/framework/core/vdom";
2 | import { Link } from "@dojo/framework/routing/Link";
3 | import { ActiveLink } from "@dojo/framework/routing/ActiveLink";
4 | import FeedList from "./FeedList";
5 | import store from "../store";
6 | import { getProfileFeedProcess, getProfileProcess, followUserProcess } from "../processes/profileProcesses";
7 | import { FeedPagination } from "./FeedPagination";
8 |
9 | export interface ProfileProperties {
10 | username: string;
11 | type: string;
12 | }
13 |
14 | const factory = create({ store }).properties();
15 |
16 | export const Profile = factory(function Profile({ middleware: { store }, properties }) {
17 | const { get, path, executor } = store;
18 | const isLoading = get(path("profile", "user", "isLoading"));
19 | const profileUser = get(path("profile", "user", "username"));
20 | const feed = get(path("profile", "feed"));
21 | const currentUser = get(path("session", "username"));
22 | const { username, type } = properties();
23 |
24 | if (username !== profileUser && !isLoading) {
25 | executor(getProfileProcess)({ username, type, page: 0 });
26 | return null;
27 | } else if (type !== feed.category && !feed.isLoading) {
28 | executor(getProfileFeedProcess)({ username, type, page: 0 });
29 | }
30 | const isCurrentUser = currentUser === profileUser;
31 | const currentPage = get(path("profile", "feed", "page")) || 0;
32 | const total = get(path("profile", "feed", "total")) || 0;
33 | const image = get(path("profile", "user", "image"));
34 | const bio = get(path("profile", "user", "bio"));
35 | const following = get(path("profile", "user", "following"));
36 |
37 | return (
38 |
39 |
40 |
41 |
42 |
43 |

44 |
{username}
45 |
{bio}
46 | {isCurrentUser ? (
47 |
48 |
49 | {" Edit Profile Settings"}
50 |
51 | ) : (
52 |
70 | )}
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 | -
81 |
87 | My Articles
88 |
89 |
90 | -
91 |
97 | Favorited Articles
98 |
99 |
100 |
101 |
102 |
103 | {feed.isLoading ? (
104 |
Loading...
105 | ) : (
106 |
107 | )}
108 |
109 | {!feed.isLoading && (
110 |
{
114 | executor(getProfileFeedProcess)({
115 | type,
116 | username,
117 | page
118 | });
119 | }}
120 | />
121 | )}
122 |
123 |
124 |
125 |
126 | );
127 | });
128 |
129 | export default Profile;
130 |
--------------------------------------------------------------------------------
/src/widgets/Article.tsx:
--------------------------------------------------------------------------------
1 | import { create, tsx } from "@dojo/framework/core/vdom";
2 | const snarkdown = require("snarkdown");
3 | import store from "../store";
4 | import { Comment } from "./Comment";
5 | import { ArticleMeta } from "./ArticleMeta";
6 | import {
7 | getArticleProcess,
8 | favoriteArticleProcess,
9 | followUserProcess,
10 | deleteArticleProcess,
11 | addCommentProcess,
12 | newCommentInputProcess,
13 | deleteCommentProcess
14 | } from "../processes/articleProcesses";
15 |
16 | export interface ArticleProperties {
17 | slug: string;
18 | }
19 |
20 | const factory = create({ store }).properties();
21 |
22 | export const Article = factory(function Article({ middleware: { store }, properties }) {
23 | const { get, path, executor } = store;
24 | const { slug } = properties();
25 | const article = get(path("article", slug, "item"));
26 | const comments = get(path("article", slug, "comments")) || [];
27 | const newComment = get(path("article", slug, "newComment"));
28 | const isLoaded = get(path("article", slug, "isLoaded"));
29 | const isLoading = get(path("article", slug, "isLoading"));
30 | const isAuthenticated = !!get(path("session", "token"));
31 | const loggedInUser = get(path("session", "username"));
32 | const username = get(path("session", "username"));
33 |
34 | if (!article && !isLoading) {
35 | executor(getArticleProcess)({ slug });
36 | }
37 |
38 | if (!isLoaded) {
39 | return (
40 |
43 | );
44 | }
45 |
46 | const { favorited } = article;
47 |
48 | const articleMeta = (
49 | {
54 | executor(favoriteArticleProcess)({ favorited, slug });
55 | }}
56 | followUser={() => {
57 | executor(followUserProcess)({
58 | slug,
59 | username: article.author.username,
60 | following: article.author.following
61 | });
62 | }}
63 | deleteArticle={() => {
64 | executor(deleteArticleProcess)({ slug });
65 | }}
66 | />
67 | );
68 |
69 | return (
70 |
71 |
72 |
73 |
{article.title}
74 | {articleMeta}
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 | {article.tagList.map((tag) => (
83 | - {tag}
84 | ))}
85 |
86 |
87 |
88 |
89 |
90 | {articleMeta}
91 |
92 |
93 |
94 | {isAuthenticated ? (
95 |
120 | ) : (
121 |
122 | )}
123 |
124 | {comments.map((comment, i) => (
125 | {
130 | executor(deleteCommentProcess)({ slug, id: comment.id });
131 | }}
132 | slug={slug}
133 | />
134 | ))}
135 |
136 |
137 |
138 |
139 |
140 | );
141 | });
142 |
143 | export default Article;
144 |
--------------------------------------------------------------------------------
/src/processes/articleProcesses.ts:
--------------------------------------------------------------------------------
1 | import { createProcess } from '@dojo/framework/stores/process';
2 | import { remove, replace } from '@dojo/framework/stores/state/operations';
3 | import { getHeaders, commandFactory } from './utils';
4 | import { baseUrl } from '../config';
5 | import {
6 | SlugPayload,
7 | FavoriteArticlePayload,
8 | FollowUserPayload,
9 | AddCommentPayload,
10 | DeleteCommentPayload,
11 | NewCommentPayload
12 | } from './interfaces';
13 |
14 | const startLoadingArticleCommand = commandFactory(({ path, payload: { slug } }) => {
15 | return [
16 | replace(path('article', slug, 'item'), undefined),
17 | replace(path('article', slug, 'comments'), []),
18 | replace(path('article', slug, 'isLoading'), true),
19 | replace(path('article', slug, 'isLoaded'), false)
20 | ];
21 | });
22 |
23 | const loadArticleCommand = commandFactory(async ({ get, path, payload: { slug } }) => {
24 | const token = get(path('session', 'token'));
25 | const response = await fetch(`${baseUrl}/articles/${slug}`, {
26 | headers: getHeaders(token)
27 | });
28 | const json = await response.json();
29 |
30 | return [
31 | replace(path('article', slug, 'item'), json.article),
32 | replace(path('article', slug, 'isLoading'), false),
33 | replace(path('article', slug, 'isLoaded'), true)
34 | ];
35 | });
36 |
37 | const favoriteArticleCommand = commandFactory(
38 | async ({ get, path, payload: { slug, favorited } }) => {
39 | const token = get(path('session', 'token'));
40 | const response = await fetch(`${baseUrl}/articles/${slug}/favorite`, {
41 | method: favorited ? 'delete' : 'post',
42 | headers: getHeaders(token)
43 | });
44 | const json = await response.json();
45 | return [replace(path('article', slug, 'item'), json.article)];
46 | }
47 | );
48 |
49 | const followUserCommand = commandFactory>(
50 | async ({ get, path, payload: { slug, username, following } }) => {
51 | const token = get(path('session', 'token'));
52 | const response = await fetch(`${baseUrl}/profiles/${username}/follow`, {
53 | method: following ? 'delete' : 'post',
54 | headers: getHeaders(token)
55 | });
56 | const json = await response.json();
57 | const article = get(path('article', slug, 'item'));
58 | return [replace(path('article', slug, 'item'), { ...article, author: json.profile })];
59 | }
60 | );
61 |
62 | const loadCommentsCommand = commandFactory(async ({ get, path, payload: { slug } }) => {
63 | const token = get(path('session', 'token'));
64 | const response = await fetch(`${baseUrl}/articles/${slug}/comments`, {
65 | headers: getHeaders(token)
66 | });
67 | const json = await response.json();
68 |
69 | return [replace(path('article', slug, 'comments'), json.comments)];
70 | });
71 |
72 | const addCommentCommand = commandFactory(async ({ get, path, payload: { slug, newComment } }) => {
73 | const token = get(path('session', 'token'));
74 | const requestPayload = {
75 | comment: {
76 | body: newComment
77 | }
78 | };
79 | const response = await fetch(`${baseUrl}/articles/${slug}/comments`, {
80 | method: 'post',
81 | headers: getHeaders(token),
82 | body: JSON.stringify(requestPayload)
83 | });
84 | const json = await response.json();
85 | const comments = get(path('article', slug, 'comments'));
86 |
87 | return [
88 | replace(path('article', slug, 'comments'), [...comments, json.comment]),
89 | replace(path('article', slug, 'newComment'), '')
90 | ];
91 | });
92 |
93 | const deleteCommentCommand = commandFactory(async ({ at, get, path, payload: { slug, id } }) => {
94 | const token = get(path('session', 'token'));
95 | await fetch(`${baseUrl}/articles/${slug}/comments/${id}`, {
96 | method: 'delete',
97 | headers: getHeaders(token)
98 | });
99 | const comments = get(path('article', slug, 'comments'));
100 | let index = -1;
101 | for (let i = 0; i < comments.length; i++) {
102 | if (comments[i].id === id) {
103 | index = i;
104 | break;
105 | }
106 | }
107 |
108 | if (index !== -1) {
109 | return [remove(at(path('article', slug, 'comments'), index))];
110 | }
111 | return [];
112 | });
113 |
114 | const newCommentInputCommand = commandFactory(({ path, payload: { newComment, slug } }) => {
115 | return [replace(path('article', slug, 'newComment'), newComment)];
116 | });
117 |
118 | const deleteArticleCommand = commandFactory(async ({ get, path, payload: { slug } }) => {
119 | const token = get(path('session', 'token'));
120 | await fetch(`${baseUrl}/articles/${slug}`, {
121 | method: 'delete',
122 | headers: getHeaders(token)
123 | });
124 | return [replace(path('routing', 'outlet'), 'home')];
125 | });
126 |
127 | export const getArticleProcess = createProcess('get-article', [
128 | startLoadingArticleCommand,
129 | [loadArticleCommand, loadCommentsCommand]
130 | ]);
131 | export const deleteCommentProcess = createProcess('delete-comment', [deleteCommentCommand]);
132 | export const addCommentProcess = createProcess('add-comment', [addCommentCommand]);
133 | export const newCommentInputProcess = createProcess('new-comment-input', [newCommentInputCommand]);
134 | export const favoriteArticleProcess = createProcess('fav-article', [favoriteArticleCommand]);
135 | export const followUserProcess = createProcess('follow-user', [followUserCommand]);
136 | export const deleteArticleProcess = createProcess('delete-article', [deleteArticleCommand]);
137 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | We as members, contributors, and leaders pledge to make participation in our
6 | community a harassment-free experience for everyone, regardless of age, body
7 | size, visible or invisible disability, ethnicity, sex characteristics, gender
8 | identity and expression, level of experience, education, socio-economic status,
9 | nationality, personal appearance, race, religion, or sexual identity
10 | and orientation.
11 |
12 | We pledge to act and interact in ways that contribute to an open, welcoming,
13 | diverse, inclusive, and healthy community.
14 |
15 | ## Our Standards
16 |
17 | Examples of behavior that contributes to a positive environment for our
18 | community include:
19 |
20 | * Demonstrating empathy and kindness toward other people
21 | * Being respectful of differing opinions, viewpoints, and experiences
22 | * Giving and gracefully accepting constructive feedback
23 | * Accepting responsibility and apologizing to those affected by our mistakes,
24 | and learning from the experience
25 | * Focusing on what is best not just for us as individuals, but for the
26 | overall community
27 |
28 | Examples of unacceptable behavior include:
29 |
30 | * The use of sexualized language or imagery, and sexual attention or
31 | advances of any kind
32 | * Trolling, insulting or derogatory comments, and personal or political attacks
33 | * Public or private harassment
34 | * Publishing others' private information, such as a physical or email
35 | address, without their explicit permission
36 | * Other conduct which could reasonably be considered inappropriate in a
37 | professional setting
38 |
39 | ## Enforcement Responsibilities
40 |
41 | Community leaders are responsible for clarifying and enforcing our standards of
42 | acceptable behavior and will take appropriate and fair corrective action in
43 | response to any behavior that they deem inappropriate, threatening, offensive,
44 | or harmful.
45 |
46 | Community leaders have the right and responsibility to remove, edit, or reject
47 | comments, commits, code, wiki edits, issues, and other contributions that are
48 | not aligned to this Code of Conduct, and will communicate reasons for moderation
49 | decisions when appropriate.
50 |
51 | ## Scope
52 |
53 | This Code of Conduct applies within all community spaces, and also applies when
54 | an individual is officially representing the community in public spaces.
55 | Examples of representing our community include using an official e-mail address,
56 | posting via an official social media account, or acting as an appointed
57 | representative at an online or offline event.
58 |
59 | ## Enforcement
60 |
61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
62 | reported to the community leaders responsible for enforcement at
63 | hello@thinkster.io.
64 | All complaints will be reviewed and investigated promptly and fairly.
65 |
66 | All community leaders are obligated to respect the privacy and security of the
67 | reporter of any incident.
68 |
69 | ## Enforcement Guidelines
70 |
71 | Community leaders will follow these Community Impact Guidelines in determining
72 | the consequences for any action they deem in violation of this Code of Conduct:
73 |
74 | ### 1. Correction
75 |
76 | **Community Impact**: Use of inappropriate language or other behavior deemed
77 | unprofessional or unwelcome in the community.
78 |
79 | **Consequence**: A private, written warning from community leaders, providing
80 | clarity around the nature of the violation and an explanation of why the
81 | behavior was inappropriate. A public apology may be requested.
82 |
83 | ### 2. Warning
84 |
85 | **Community Impact**: A violation through a single incident or series
86 | of actions.
87 |
88 | **Consequence**: A warning with consequences for continued behavior. No
89 | interaction with the people involved, including unsolicited interaction with
90 | those enforcing the Code of Conduct, for a specified period of time. This
91 | includes avoiding interactions in community spaces as well as external channels
92 | like social media. Violating these terms may lead to a temporary or
93 | permanent ban.
94 |
95 | ### 3. Temporary Ban
96 |
97 | **Community Impact**: A serious violation of community standards, including
98 | sustained inappropriate behavior.
99 |
100 | **Consequence**: A temporary ban from any sort of interaction or public
101 | communication with the community for a specified period of time. No public or
102 | private interaction with the people involved, including unsolicited interaction
103 | with those enforcing the Code of Conduct, is allowed during this period.
104 | Violating these terms may lead to a permanent ban.
105 |
106 | ### 4. Permanent Ban
107 |
108 | **Community Impact**: Demonstrating a pattern of violation of community
109 | standards, including sustained inappropriate behavior, harassment of an
110 | individual, or aggression toward or disparagement of classes of individuals.
111 |
112 | **Consequence**: A permanent ban from any sort of public interaction within
113 | the community.
114 |
115 | ## Attribution
116 |
117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage],
118 | version 2.0, available at
119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
120 |
121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct
122 | enforcement ladder](https://github.com/mozilla/diversity).
123 |
124 | [homepage]: https://www.contributor-covenant.org
125 |
126 | For answers to common questions about this code of conduct, see the FAQ at
127 | https://www.contributor-covenant.org/faq. Translations are available at
128 | https://www.contributor-covenant.org/translations.
129 |
--------------------------------------------------------------------------------
/.firebase/hosting.0.cache:
--------------------------------------------------------------------------------
1 | icon_128x128.3789c4a4f67429a0cd993c52d2c0d2a0.png,1539638548844,2e6f4085a0c98c8b49bb62e9cca366a8bfe03dd14c8cee4c869819d9a6fdb537
2 | index.html,1539638548843,ca2e23f0827ea64ee701a356eabca4dd06bbceca5f072275553fd8960dc7e23c
3 | main.752063a49ff926e7e7f2ee01b3c20679.bundle.css,1539638548843,6180c951e6f6e8487f4d016a690276d77a6b170b14e3b9143cf352badc43dca0
4 | manifest.7fee97f97950cbec60aeaa36ba945fa7.json,1539638548844,1ba7f34db6b434b0515f97e9dcd14ef514c1a2a41c7165626b45828de8f3acab
5 | manifest.json,1539638548843,1d98fa5400c21059495a263a81eea35c4d0c81fc51695fe9d2bd0e9463501bd4
6 | main.752063a49ff926e7e7f2ee01b3c20679.bundle.css.map,1539638548843,c79c19fc1ed6b74f6ee890ee8d013a7c2ba9040d94c471e171d5ea1c9a7d6188
7 | precache-manifest.c0a6a5a338d93ffaeed7725cb36642e2.js,1539638548844,9b803b24ccfeb9d85bdfae4c11cbde74a1288a5e3af51178759e3a0546cbcee2
8 | runtime.d41d8cd98f00b204e980.bundle.js,1539638548843,775491ca914c3b9e2cd1903ff831c3b61fb60e1825fd85a1efff513c2faa81f2
9 | service-worker.js,1539638548844,64479a21fef7ab7944bd97ec757dfc4024b2f6c21fe710016be7f89d10be2513
10 | src/containers/ArticleContainer.a75c45d0a493e8d38a77.bundle.js,1539638548845,3cf6d8c77d64bfae65b36fdd97e0c7d00c0495ef2648d8564f36c7eb0d94787b
11 | runtime.d41d8cd98f00b204e980.bundle.js.map,1539638548843,3cd4097a9f2bee760820ac3280f5107daca809a7ef781d93fc4ff20df9953883
12 | src/containers/EditorContainer.fb52aa2bc8801e320320.bundle.js,1539638548846,d07d02a91a562b5a1b0e10137ca1971566c0aff9625d37921229241bf71c9d31
13 | src/containers/EditorContainer.fb52aa2bc8801e320320.bundle.js.map,1539638548846,34dda41a5cadbbe1e366aade6a2ff9ae615967052cb69d4e3417e95d661d357b
14 | src/containers/LoginContainer.f19b4e06dab68be7854b.bundle.js,1539638548845,16001301f139e2589aef0d87b3acef4ed33b708d7bd8ee355ddeb3a250cd903b
15 | src/containers/LoginContainer.f19b4e06dab68be7854b.bundle.js.map,1539638548846,d713946571c72aaaf9fd94c35899ea0f83874858833e0a8596f15b4c49d6430f
16 | src/containers/ProfileContainer.838ecc429e897ab963d9.bundle.js,1539638548845,52c54544f11cbcb46847faa6e2660c01d14bfe5db206908edae0a13345fac860
17 | src/containers/ArticleContainer.a75c45d0a493e8d38a77.bundle.js.map,1539638548846,693a086490ef2bce7fd7789a3bdd0e591ea26c539f7fd68738b9777a1f0fdd5d
18 | src/containers/ProfileContainer.838ecc429e897ab963d9.bundle.js.map,1539638548846,7b304575ef719a9dfbe3f4967ad8743b2723bb562d412b7d21c70589a3234911
19 | src/containers/RegisterContainer.7fb4b43a0df323469012.bundle.js,1539638548845,df6934aa0e175d22858899f22761d2a9978264035f47682b73e08838aa9ea288
20 | src/containers/SettingsContainer.310c36a050e454c8ad75.bundle.js,1539638548845,0e1e857374346e0d409da943b1c2f7cb4e37d68955fe519de9215c34259799fa
21 | src/containers/RegisterContainer.7fb4b43a0df323469012.bundle.js.map,1539638548846,43d8346c55569d051361a5277e423cb33b1fde00ac0a802c26d3cb972556b4f0
22 | src/containers/SettingsContainer.310c36a050e454c8ad75.bundle.js.map,1539638548846,1049ba3f9adf540f5be248d805ea423e75e38e1fa2c27917774a49085ef6647f
23 | workbox-v3.6.2/workbox-background-sync.dev.js,1539638548819,2b3bed776a8d9de01afdc376af9999e5db3883a8fe0e7e7708387b0096155fa7
24 | workbox-v3.6.2/workbox-background-sync.prod.js,1539638548819,a5f12c536107e6cdf6e525a6d23ee129b16e64bdc4eb691cb42fd582593812e6
25 | workbox-v3.6.2/workbox-background-sync.prod.js.map,1539638548819,5a719d74bca001d82bd8f50cacc59bddff790efc3e510b8d476770f6559cfe2d
26 | workbox-v3.6.2/workbox-background-sync.dev.js.map,1539638548819,5ed5d6b10d6b5186f483cb35491fa4800f36f7e0a14e0a5043606915d6cc16cd
27 | workbox-v3.6.2/workbox-broadcast-cache-update.prod.js,1539638548819,4e7f4fb64baf643cc8d00842805dadf3f07a64105083bff7755765f3ea9de060
28 | workbox-v3.6.2/workbox-broadcast-cache-update.prod.js.map,1539638548819,2df760683a539dabc54537ad031a548c39cb04c5989dd0b179c0212d268adb3b
29 | workbox-v3.6.2/workbox-broadcast-cache-update.dev.js,1539638548819,facd801dd5a8cb211ab159db273fa03a1d4eea6b40cfde6c7c1cedc667d1d04e
30 | workbox-v3.6.2/workbox-broadcast-cache-update.dev.js.map,1539638548819,0eea78848e03dc9704e2a718a7f81bae7bcc208938a8026a44908a4c3028cde7
31 | workbox-v3.6.2/workbox-cache-expiration.prod.js,1539638548819,5c4bb501c90e7b798c3cfce034bbc6ee54bc4989afa3a9463fb698397c1a46b1
32 | workbox-v3.6.2/workbox-cache-expiration.prod.js.map,1539638548819,c938eb45b79cbf02c2d48295af14e0db07aa8a234df2ce033c76bef3b0067779
33 | workbox-v3.6.2/workbox-cacheable-response.dev.js,1539638548820,a095457d1b4cabb2f48445ef69bc54cd217ab6ffc9322ac32be5592147e0c5f8
34 | workbox-v3.6.2/workbox-cacheable-response.dev.js.map,1539638548820,737fd206a872a2a3777ae57b771f0999abe7810c465e747c6fd2bbb1163b5964
35 | workbox-v3.6.2/workbox-cache-expiration.dev.js.map,1539638548820,13f77271a06e8596b5f11886320bc97408e6d0270ad1bab5527b10ab6641d3ac
36 | workbox-v3.6.2/workbox-cacheable-response.prod.js,1539638548820,65ba94b486a1531d2b8db3aaebe8f6bcd0d850db71ff5d6e62145ca44c4d7ed4
37 | workbox-v3.6.2/workbox-cacheable-response.prod.js.map,1539638548820,f359503ef47c195beba4a62e9236a7b35084f465cdc3dd7b3288059bab96d800
38 | workbox-v3.6.2/workbox-core.prod.js,1539638548820,4e1c1466117692feb89f6aa5b9af1329d480403cb114cd532c5fc150dbf623ac
39 | workbox-v3.6.2/workbox-cache-expiration.dev.js,1539638548820,8a27261df122adfe6be8dfc088ef112933d9a7dea3405b9aab3f27f470bf05a6
40 | workbox-v3.6.2/workbox-core.prod.js.map,1539638548820,11dee702fd581320db94b8ae42f2b755ce7bf7e10a090dd5ac1154f9ce49434a
41 | workbox-v3.6.2/workbox-google-analytics.dev.js.map,1539638548821,70c84bf1c9f4513e5ca542399e3fe024d40447e279c60fd57caccfcaaac52c74
42 | workbox-v3.6.2/workbox-google-analytics.dev.js,1539638548820,1ac1c0ecd35b2d02772373c29e61c8ef4618e311c590de8a4ed82aa6a3b80286
43 | workbox-v3.6.2/workbox-google-analytics.prod.js,1539638548820,425d50e5327e195598ca3517e643eea1ef7fb85cbf4eef4c142187eec4f0d9f5
44 | workbox-v3.6.2/workbox-google-analytics.prod.js.map,1539638548821,3e7aa4b5c32af571cc97e2bfbe6ef4d18e4cf174545f0357a43fbb9ae64d75d7
45 | icon_256x256.c3399fc140e7b7ce69760eb55c9e94cd.png,1539638548844,e10fb10265c8ca9d9f9407c0ea726affa931160648a43e9c5d8d69bd620aea09
46 | workbox-v3.6.2/workbox-core.dev.js.map,1539638548820,9dba4789901ac974692ecc3a73b46fb8439ccdbfbaf976fc9aa533c31ae9dc90
47 | workbox-v3.6.2/workbox-core.dev.js,1539638548820,cd66b965353504d4fdd687f79ada7593fabdf21800edde6066e8bd4b29f765c0
48 | main.0780a9ccc973dcd53ac9.bundle.js,1539638548843,fc109365a429492a1a24d2ae06b4e4a77a062ff9fbe7677194549753d06b5551
49 | workbox-v3.6.2/workbox-navigation-preload.prod.js,1539638548821,db1e30a887d07c588bea56f0c2982342d7f6704f389d4c9f46d1f524f4eb35be
50 | workbox-v3.6.2/workbox-navigation-preload.prod.js.map,1539638548821,bb3279acc8e3c8b3a7fb95ecf5291291c59aa8cf7a2615ed8b609071839371d2
51 | workbox-v3.6.2/workbox-navigation-preload.dev.js.map,1539638548821,19a84079b127fcce0869ab5adadce8cee20943a2eddf66ac6e7d5a076fc5f57b
52 | workbox-v3.6.2/workbox-navigation-preload.dev.js,1539638548821,be537a218a0619cb5fd8cfebca7bb075410a92bdb91eaf3f20e42abc8aa88bb5
53 | workbox-v3.6.2/workbox-precaching.prod.js,1539638548821,b09b5f03fb827b82d60efc8dc81297bdee744dbc55526ac38887db595a8e7df4
54 | workbox-v3.6.2/workbox-precaching.prod.js.map,1539638548821,a09a04ad3382f9d9934f0befb44079e37164cfebe4f8ac535b8c8fd8a38a8607
55 | workbox-v3.6.2/workbox-range-requests.dev.js,1539638548822,d86d37203f19b6bc6fce8da77307c140e0f734cda8cdafe2b168c4c7e3f7d2f1
56 | workbox-v3.6.2/workbox-range-requests.dev.js.map,1539638548821,fe8d42f3f43c792e11af0ebf5b0a9cce7d9efa06ebd2713fde7d421fde9d5977
57 | workbox-v3.6.2/workbox-range-requests.prod.js,1539638548821,c6bfcc6722102eb4c5d21d04d4080852a4e904038fe76fbd59c7409f43bddd2f
58 | workbox-v3.6.2/workbox-range-requests.prod.js.map,1539638548822,04e1b43b1379017ea685d029cc496e6ab1c057407396425d1c79161008fbc934
59 | workbox-v3.6.2/workbox-routing.prod.js,1539638548822,2eb1daf1c118c10796e640f03e09243cb808472f72b9401869d5acbf53875779
60 | workbox-v3.6.2/workbox-routing.prod.js.map,1539638548822,2a39811fc0f7448f0223a7c3c216d93c9dde10973f1c69bf00b1add9faa1cc65
61 | workbox-v3.6.2/workbox-precaching.dev.js.map,1539638548821,f6da7d92f4544b1480808e6493d14a516fbd4f861f0390fb91936e6e2b9e4e64
62 | workbox-v3.6.2/workbox-precaching.dev.js,1539638548821,cfa8a847dda6aee80c0ec7b229977396d02a509bcef5034ed30b5a3285acf294
63 | workbox-v3.6.2/workbox-routing.dev.js.map,1539638548822,9c52a709908ae275c898d1dbe6c75af4341de272f0deb3f4c21f46d001ded60e
64 | workbox-v3.6.2/workbox-routing.dev.js,1539638548822,da4eb86a9bedf88b1aa691fa8a86faf6ef0b3f3f521154e809dc84ce4aca7c13
65 | workbox-v3.6.2/workbox-strategies.prod.js,1539638548822,615af6d2e2a035c06016133d0b94bc9cf16e516e9f68d4fb6b1d398d10780a4c
66 | workbox-v3.6.2/workbox-strategies.prod.js.map,1539638548822,a88aabc2c7feb7533674e00127dede7b8914379b94e462b2fb1bff7ac3f8a9d8
67 | workbox-v3.6.2/workbox-streams.prod.js,1539638548822,b564339695b4cd5e393079dac211185eca58f132d5555633789a9cd319b22501
68 | workbox-v3.6.2/workbox-streams.dev.js,1539638548822,641624d5fe544f7fb19c50494c4fca98f1634a6b2e425cd79ad684aa9709d5e1
69 | workbox-v3.6.2/workbox-streams.dev.js.map,1539638548822,73e7784aa9f3391951b50ad4119c971ee3092ca77d402ed6e59a98a9727406b7
70 | workbox-v3.6.2/workbox-strategies.dev.js.map,1539638548822,0257f74894a07d47226500314d41432cd864c753a3f42fade8a54c55496d10cc
71 | workbox-v3.6.2/workbox-streams.prod.js.map,1539638548822,8f783731d302d663adcc6d66a60445a7623bf917d1f2b0f55b06f3f2be78835c
72 | workbox-v3.6.2/workbox-sw.js,1539638548822,dc6b21b0d81b8aeee8a910d804d22caba0367c65fcb2715b1febe4c9834a9021
73 | workbox-v3.6.2/workbox-sw.js.map,1539638548823,cd9ce41cca889b674801639777852ed74ccbc22cb0f68fd36c165abb789255e6
74 | workbox-v3.6.2/workbox-strategies.dev.js,1539638548822,32cc94a0874fcd6fed50c366c69a4ffc7bb532c945fe10453be2442576641da5
75 | icon_512x512.4b938ed405cd7a04e8c706a0097e2bc5.png,1539638548844,d78a5defe10ca93edec8f9f2e5add49a021a1f2bfe2cd0d9a5b141d30ce7bfbc
76 | main.0780a9ccc973dcd53ac9.bundle.js.map,1539638548843,183164f1dce8e90e5b1b6eab299649108f86c2cc1aa75fe241d101d9ea708f67
77 |
--------------------------------------------------------------------------------