├── VERSION ├── tests ├── __init__.py └── test_bake_project.py ├── pytest.ini ├── requirements-dev.txt ├── {{cookiecutter.project_slug}} ├── .formatter.exs ├── .dockerignore ├── apps │ ├── {{cookiecutter.phoenix_app_slug}} │ │ ├── README.md │ │ ├── priv │ │ │ └── repo │ │ │ │ ├── migrations │ │ │ │ └── .formatter.exs │ │ │ │ └── seeds.exs │ │ ├── coveralls.json │ │ ├── test │ │ │ ├── test_helper.exs │ │ │ └── support │ │ │ │ └── data_case.ex │ │ ├── .formatter.exs │ │ ├── lib │ │ │ ├── {{cookiecutter.phoenix_app_slug}} │ │ │ │ ├── repo.ex │ │ │ │ └── application.ex │ │ │ └── {{cookiecutter.phoenix_app_slug}}.ex │ │ ├── .gitignore │ │ └── mix.exs │ └── {{cookiecutter.phoenix_app_slug}}_web │ │ ├── assets │ │ ├── .stylelintrc │ │ ├── css │ │ │ ├── app.css │ │ │ └── phoenix.css │ │ ├── .prettierignore │ │ ├── .eslintignore │ │ ├── src │ │ │ ├── helpers │ │ │ │ ├── components │ │ │ │ │ ├── index.ts │ │ │ │ │ └── loading-page.tsx │ │ │ │ ├── index.ts │ │ │ │ └── hooks.ts │ │ │ ├── app │ │ │ │ ├── reducer-context-demo │ │ │ │ │ ├── types.ts │ │ │ │ │ ├── components │ │ │ │ │ │ ├── todo-list.tsx │ │ │ │ │ │ ├── todo.tsx │ │ │ │ │ │ ├── todo-item.tsx │ │ │ │ │ │ └── options.tsx │ │ │ │ │ ├── reducer-context-demo-container.tsx │ │ │ │ │ ├── reducer-context-demo.tsx │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── reducers │ │ │ │ │ │ └── todo-ducks.ts │ │ │ │ ├── home │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── home.tsx │ │ │ │ └── subscription-demo │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── subscription-demo.tsx │ │ │ ├── graphql │ │ │ │ └── index.ts │ │ │ ├── app-router.tsx │ │ │ ├── index.tsx │ │ │ └── app.tsx │ │ ├── static │ │ │ ├── favicon.ico │ │ │ ├── images │ │ │ │ └── phoenix.png │ │ │ └── robots.txt │ │ ├── .editorconfig │ │ ├── prettier.config.js │ │ ├── pwa │ │ │ └── manifest.json │ │ ├── js │ │ │ ├── app.js │ │ │ └── socket.js │ │ ├── tsconfig.json │ │ ├── apollo-links.ts │ │ ├── .eslintrc.js │ │ ├── package.json │ │ └── webpack.config.js │ │ ├── .formatter.exs │ │ ├── coveralls.json │ │ ├── test │ │ ├── test_helper.exs │ │ ├── {{cookiecutter.phoenix_app_slug}}_web │ │ │ ├── views │ │ │ │ ├── page_view_test.exs │ │ │ │ ├── layout_view_test.exs │ │ │ │ └── error_view_test.exs │ │ │ └── controllers │ │ │ │ └── page_controller_test.exs │ │ └── support │ │ │ ├── channel_case.ex │ │ │ └── conn_case.ex │ │ ├── lib │ │ ├── {{cookiecutter.phoenix_app_slug}}_web │ │ │ ├── views │ │ │ │ ├── page_view.ex │ │ │ │ ├── layout_view.ex │ │ │ │ ├── error_view.ex │ │ │ │ ├── webpack_entrypoint.ex │ │ │ │ └── error_helpers.ex │ │ │ ├── controllers │ │ │ │ └── page_controller.ex │ │ │ ├── gettext.ex │ │ │ ├── graphql │ │ │ │ └── schema.ex │ │ │ ├── templates │ │ │ │ ├── layout │ │ │ │ │ └── app.html.eex │ │ │ │ └── page │ │ │ │ │ └── index.html.eex │ │ │ ├── router.ex │ │ │ ├── application.ex │ │ │ ├── channels │ │ │ │ └── user_socket.ex │ │ │ └── endpoint.ex │ │ └── {{cookiecutter.phoenix_app_slug}}_web.ex │ │ ├── README.md │ │ ├── .gitignore │ │ ├── priv │ │ └── gettext │ │ │ ├── en │ │ │ └── LC_MESSAGES │ │ │ │ └── errors.po │ │ │ └── errors.pot │ │ └── mix.exs ├── coveralls.json ├── rel │ ├── env.bat.eex │ ├── vm.args.eex │ └── env.sh.eex ├── docker-compose.yml ├── README.md ├── config │ ├── test.exs │ ├── releases.exs │ ├── config.exs │ ├── prod.exs │ └── dev.exs ├── .gitignore ├── Makefile ├── mix.exs ├── Dockerfile ├── .gitlab-ci.yml └── .credo.exs ├── .travis.yml ├── CHANGELOG.md ├── cookiecutter.json ├── LICENSE └── README.md /VERSION: -------------------------------------------------------------------------------- 1 | 0.1.0 -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | testpaths = tests/ 3 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | cookiecutter==1.6.0 2 | flake8==3.5.0 3 | pytest==3.3.2 4 | pytest-cookies==0.3.0 5 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/.formatter.exs: -------------------------------------------------------------------------------- 1 | [ 2 | inputs: ["mix.exs", "config/*.exs"], 3 | subdirectories: ["apps/*"] 4 | ] 5 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/.dockerignore: -------------------------------------------------------------------------------- 1 | **/.* 2 | **/_build/ 3 | **/deps/ 4 | **/README* 5 | **/test/ 6 | **/priv/static/ 7 | Dockerfile 8 | Makefile 9 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/apps/{{cookiecutter.phoenix_app_slug}}/README.md: -------------------------------------------------------------------------------- 1 | # {{cookiecutter.phoenix_app_module}} 2 | 3 | **TODO: Add description** 4 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/apps/{{cookiecutter.phoenix_app_slug}}/priv/repo/migrations/.formatter.exs: -------------------------------------------------------------------------------- 1 | [ 2 | import_deps: [:ecto_sql], 3 | inputs: ["*.exs"] 4 | ] 5 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/apps/{{cookiecutter.phoenix_app_slug}}_web/assets/.stylelintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "stylelint-config-recommended", 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/apps/{{cookiecutter.phoenix_app_slug}}_web/assets/css/app.css: -------------------------------------------------------------------------------- 1 | /* This file is for your main application css. */ 2 | 3 | @import "./phoenix.css"; 4 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/apps/{{cookiecutter.phoenix_app_slug}}/coveralls.json: -------------------------------------------------------------------------------- 1 | { 2 | "skip_files": [ 3 | "lib/{{cookiecutter.phoenix_app_module}}/application.ex" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/apps/{{cookiecutter.phoenix_app_slug}}/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | Ecto.Adapters.SQL.Sandbox.mode({{cookiecutter.phoenix_app_module}}.Repo, :manual) 3 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/apps/{{cookiecutter.phoenix_app_slug}}_web/.formatter.exs: -------------------------------------------------------------------------------- 1 | [ 2 | import_deps: [:phoenix], 3 | inputs: ["*.{ex,exs}", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/apps/{{cookiecutter.phoenix_app_slug}}_web/assets/.prettierignore: -------------------------------------------------------------------------------- 1 | /coverage 2 | /dist 3 | /node_modules 4 | package-lock.json 5 | .eslintcache 6 | .stylelintcache 7 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/apps/{{cookiecutter.phoenix_app_slug}}_web/coveralls.json: -------------------------------------------------------------------------------- 1 | { 2 | "skip_files": [ 3 | "lib/{{cookiecutter.phoenix_app_module}}_web/application.ex" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/apps/{{cookiecutter.phoenix_app_slug}}_web/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | Ecto.Adapters.SQL.Sandbox.mode({{cookiecutter.phoenix_app_module}}.Repo, :manual) 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.7" 4 | - "3.4" 5 | - "3.5" 6 | - "3.6" 7 | - "pypy" 8 | install: 9 | - "pip install -r requirements-dev.txt" 10 | script: pytest 11 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/apps/{{cookiecutter.phoenix_app_slug}}_web/assets/.eslintignore: -------------------------------------------------------------------------------- 1 | /coverage 2 | /dist 3 | /examples 4 | /node_modules 5 | webpack.config.js 6 | **/*.test.ts 7 | **/*.test.tsx 8 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/coveralls.json: -------------------------------------------------------------------------------- 1 | { 2 | "skip_files": [ 3 | "apps/{{cookiecutter.phoenix_app_module}}/coveralls.json", 4 | "apps/{{cookiecutter.phoenix_app_module}}_web/coveralls.json" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/apps/{{cookiecutter.phoenix_app_slug}}_web/assets/src/helpers/components/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Components 3 | */ 4 | 5 | import { LoadingPage } from "./loading-page" 6 | 7 | export { LoadingPage } 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | All notable changes to this project will be documented in this file. 3 | This project adheres to [Semantic Versioning](http://semver.org/). 4 | 5 | ### [Unreleased][unreleased] 6 | 7 | #### Added 8 | - package files 9 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/apps/{{cookiecutter.phoenix_app_slug}}/.formatter.exs: -------------------------------------------------------------------------------- 1 | [ 2 | import_deps: [:ecto], 3 | inputs: ["*.{ex,exs}", "priv/*/seeds.exs", "{config,lib,test}/**/*.{ex,exs}"], 4 | subdirectories: ["priv/*/migrations"] 5 | ] 6 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/apps/{{cookiecutter.phoenix_app_slug}}_web/lib/{{cookiecutter.phoenix_app_slug}}_web/views/page_view.ex: -------------------------------------------------------------------------------- 1 | defmodule {{cookiecutter.phoenix_app_module}}Web.PageView do 2 | use {{cookiecutter.phoenix_app_module}}Web, :view 3 | end 4 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/apps/{{cookiecutter.phoenix_app_slug}}_web/assets/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ibakami/cookiecutter-elixir-phoenix/HEAD/{{cookiecutter.project_slug}}/apps/{{cookiecutter.phoenix_app_slug}}_web/assets/static/favicon.ico -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/apps/{{cookiecutter.phoenix_app_slug}}_web/lib/{{cookiecutter.phoenix_app_slug}}_web/views/layout_view.ex: -------------------------------------------------------------------------------- 1 | defmodule {{cookiecutter.phoenix_app_module}}Web.LayoutView do 2 | use {{cookiecutter.phoenix_app_module}}Web, :view 3 | end 4 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/apps/{{cookiecutter.phoenix_app_slug}}_web/assets/static/images/phoenix.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ibakami/cookiecutter-elixir-phoenix/HEAD/{{cookiecutter.project_slug}}/apps/{{cookiecutter.phoenix_app_slug}}_web/assets/static/images/phoenix.png -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/apps/{{cookiecutter.phoenix_app_slug}}_web/test/{{cookiecutter.phoenix_app_slug}}_web/views/page_view_test.exs: -------------------------------------------------------------------------------- 1 | defmodule {{cookiecutter.phoenix_app_module}}Web.PageViewTest do 2 | use {{cookiecutter.phoenix_app_module}}Web.ConnCase, async: true 3 | end 4 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/apps/{{cookiecutter.phoenix_app_slug}}_web/assets/src/app/reducer-context-demo/types.ts: -------------------------------------------------------------------------------- 1 | import { State, Actions } from "./reducers/todo-ducks" 2 | 3 | export interface ComponentWithReducers { 4 | state: State 5 | dispatch: React.Dispatch 6 | } 7 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/apps/{{cookiecutter.phoenix_app_slug}}_web/test/{{cookiecutter.phoenix_app_slug}}_web/views/layout_view_test.exs: -------------------------------------------------------------------------------- 1 | defmodule {{cookiecutter.phoenix_app_module}}Web.LayoutViewTest do 2 | use {{cookiecutter.phoenix_app_module}}Web.ConnCase, async: true 3 | end 4 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/apps/{{cookiecutter.phoenix_app_slug}}/lib/{{cookiecutter.phoenix_app_slug}}/repo.ex: -------------------------------------------------------------------------------- 1 | defmodule {{cookiecutter.phoenix_app_module}}.Repo do 2 | use Ecto.Repo, 3 | otp_app: :{{cookiecutter.phoenix_app_slug}}, 4 | adapter: Ecto.Adapters.Postgres 5 | end 6 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/apps/{{cookiecutter.phoenix_app_slug}}_web/assets/static/robots.txt: -------------------------------------------------------------------------------- 1 | # See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file 2 | # 3 | # To ban all spiders from the entire site uncomment the next two lines: 4 | # User-agent: * 5 | # Disallow: / 6 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/rel/env.bat.eex: -------------------------------------------------------------------------------- 1 | @echo off 2 | rem Set the release to work across nodes. If using the long name format like 3 | rem the one below (my_app@127.0.0.1), you need to also uncomment the 4 | rem RELEASE_DISTRIBUTION variable below. 5 | rem set RELEASE_DISTRIBUTION=name 6 | rem set RELEASE_NODE=<%= @release.name %>@127.0.0.1 7 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | volumes: 3 | postgres-data: 4 | services: 5 | postgres: 6 | image: postgres:10.5-alpine 7 | ports: 8 | - 5432:5432 9 | environment: 10 | POSTGRES_USER: postgres 11 | POSTGRES_PASSWORD: postgres 12 | POSTGRES_DB: {{cookiecutter.phoenix_app_slug}}_dev 13 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/apps/{{cookiecutter.phoenix_app_slug}}_web/assets/.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | indent_style = space 7 | indent_size = 2 8 | end_of_line = lf 9 | charset = utf-8 10 | trim_trailing_whitespace = true 11 | insert_final_newline = true 12 | 13 | [*.md] 14 | trim_trailing_whitespace = false 15 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/apps/{{cookiecutter.phoenix_app_slug}}_web/assets/src/helpers/index.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | export const normalizeArrayData = function(array: any, idProperty = "id") { 3 | return array.reduce((map: any, obj: any) => { 4 | map[idProperty] = { ...obj } // eslint-disable-line no-param-reassign 5 | return map 6 | }, {}) 7 | } 8 | /* tslint:enable */ 9 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/apps/{{cookiecutter.phoenix_app_slug}}_web/test/{{cookiecutter.phoenix_app_slug}}_web/controllers/page_controller_test.exs: -------------------------------------------------------------------------------- 1 | defmodule {{cookiecutter.phoenix_app_module}}Web.PageControllerTest do 2 | use {{cookiecutter.phoenix_app_module}}Web.ConnCase 3 | 4 | test "GET /", %{conn: conn} do 5 | conn = get(conn, "/") 6 | assert conn.status == 200 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/apps/{{cookiecutter.phoenix_app_slug}}_web/lib/{{cookiecutter.phoenix_app_slug}}_web/controllers/page_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule {{cookiecutter.phoenix_app_module}}Web.PageController do 2 | use {{cookiecutter.phoenix_app_module}}Web, :controller 3 | 4 | @spec index(Plug.Conn.t(), map()) :: Plug.Conn.t() 5 | def index(conn, _params) do 6 | render(conn, "index.html") 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/rel/vm.args.eex: -------------------------------------------------------------------------------- 1 | ## Customize flags given to the VM: http://erlang.org/doc/man/erl.html 2 | ## -mode/-name/-sname/-setcookie are configured via env vars, do not set them here 3 | 4 | ## Number of dirty schedulers doing IO work (file, sockets, etc) 5 | ##+SDio 5 6 | 7 | ## Increase number of concurrent ports/sockets 8 | ##+Q 65536 9 | 10 | ## Tweak GC to run more often 11 | ##-env ERL_FULLSWEEP_AFTER 10 12 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/apps/{{cookiecutter.phoenix_app_slug}}/lib/{{cookiecutter.phoenix_app_slug}}.ex: -------------------------------------------------------------------------------- 1 | defmodule {{cookiecutter.phoenix_app_module}} do 2 | @moduledoc """ 3 | {{cookiecutter.phoenix_app_module}} keeps the contexts that define your domain 4 | and business logic. 5 | 6 | Contexts are also responsible for managing your data, regardless 7 | if it comes from the database, an external API or others. 8 | """ 9 | end 10 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/apps/{{cookiecutter.phoenix_app_slug}}_web/assets/prettier.config.js: -------------------------------------------------------------------------------- 1 | // Some settings automatically inherited from .editorconfig 2 | 3 | module.exports = { 4 | semi: false, 5 | trailingComma: "all", 6 | overrides: [ 7 | { 8 | files: ".editorconfig", 9 | options: { parser: "yaml" }, 10 | }, 11 | { 12 | files: "LICENSE", 13 | options: { parser: "markdown" }, 14 | }, 15 | ], 16 | } 17 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/apps/{{cookiecutter.phoenix_app_slug}}_web/assets/src/app/reducer-context-demo/components/todo-list.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { List } from "semantic-ui-react" 3 | 4 | import { TodoItem } from "./todo-item" 5 | 6 | export const TodoList: React.SFC = () => { 7 | return ( 8 | 9 | 10 | 11 | 12 | 13 | 14 | ) 15 | } 16 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/apps/{{cookiecutter.phoenix_app_slug}}_web/assets/pwa/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "{{cookiecutter.phoenix_app_module}}", 3 | "name": "{{cookiecutter.phoenix_app_slug}}", 4 | "icons": [ 5 | { 6 | "src": "logo.png", 7 | "sizes": "512x512", 8 | "type": "image/png" 9 | } 10 | ], 11 | "start_url": "/", 12 | "display": "standalone", 13 | "theme_color": "#ffffff", 14 | "background_color": "#333333" 15 | } 16 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/apps/{{cookiecutter.phoenix_app_slug}}_web/assets/src/app/reducer-context-demo/components/todo.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { Grid, Header, Icon, Input, Label, List } from "semantic-ui-react" 3 | 4 | import { TodoList } from "./todo-list" 5 | 6 | export const Todo: React.SFC = () => { 7 | return ( 8 | 9 |
Tasks:
10 | 11 |
12 | ) 13 | } 14 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/apps/{{cookiecutter.phoenix_app_slug}}_web/assets/src/helpers/hooks.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react" 2 | 3 | export const useRootTitle = (pageName?: string): void => { 4 | const UnmountBehavior = (): void => { 5 | document.title = "Phoenix CookieCutter" 6 | } 7 | 8 | useEffect( 9 | (): typeof UnmountBehavior => { 10 | document.title = pageName ? `${pageName}` : "Phoenix CookieCutter" 11 | return UnmountBehavior 12 | }, 13 | ) 14 | } 15 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/README.md: -------------------------------------------------------------------------------- 1 | # {{cookiecutter.phoenix_app_module}} 2 | 3 | To start your Phoenix server: 4 | 5 | - Start Postgres `docker-compose up -d` 6 | - Install dependencies,setup db with `mix app.setup` 7 | - Start Phoenix endpoint with `mix phx.server` 8 | 9 | Now you can visit [`localhost:4000`](http://localhost:4000) from your browser. 10 | 11 | - Open graphiql (A graphical interactive in-browser GraphQL IDE) `http://localhost:4000/graphiql` 12 | 13 | ``` 14 | $ make 15 | ``` 16 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/apps/{{cookiecutter.phoenix_app_slug}}/priv/repo/seeds.exs: -------------------------------------------------------------------------------- 1 | # Script for populating the database. You can run it as: 2 | # 3 | # mix run priv/repo/seeds.exs 4 | # 5 | # Inside the script, you can read and write to any of your 6 | # repositories directly: 7 | # 8 | # {{cookiecutter.phoenix_app_module}}.Repo.insert!(%{{cookiecutter.phoenix_app_module}}.SomeSchema{}) 9 | # 10 | # We recommend using the bang functions (`insert!`, `update!` 11 | # and so on) as they will fail if something goes wrong. 12 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/apps/{{cookiecutter.phoenix_app_slug}}_web/assets/src/graphql/index.ts: -------------------------------------------------------------------------------- 1 | import gql from "graphql-tag" 2 | 3 | export const ADD_USER = gql` 4 | mutation($name: String!, $age: Int!) { 5 | addUser(name: $name, age: $age) { 6 | name 7 | } 8 | } 9 | ` 10 | 11 | export const LIST_USERS = gql` 12 | query { 13 | listUsers { 14 | name 15 | age 16 | } 17 | } 18 | ` 19 | 20 | export const USER_ADDED = gql` 21 | subscription { 22 | userAdded { 23 | name 24 | age 25 | } 26 | } 27 | ` 28 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/apps/{{cookiecutter.phoenix_app_slug}}_web/assets/src/helpers/components/loading-page.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { Segment, Dimmer, Loader, Image } from "semantic-ui-react" 3 | 4 | interface Props { 5 | message: string 6 | } 7 | 8 | export const LoadingPage: React.SFC = ({ message }) => ( 9 | 10 | 11 | {message} 12 | 13 | 14 | 15 | 16 | ) 17 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/apps/{{cookiecutter.phoenix_app_slug}}_web/assets/src/app/reducer-context-demo/components/todo-item.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { Icon, Input, Label, List } from "semantic-ui-react" 3 | 4 | export const TodoItem: React.SFC = () => { 5 | return ( 6 | 7 | } 10 | label={ 15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/config/test.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | # Configure your database 4 | config :{{cookiecutter.phoenix_app_slug}}, {{cookiecutter.phoenix_app_module}}.Repo, 5 | username: "postgres", 6 | password: "postgres", 7 | database: "{{cookiecutter.phoenix_app_slug}}_test", 8 | hostname: "localhost", 9 | pool: Ecto.Adapters.SQL.Sandbox 10 | 11 | # We don't run a server during test. If one is required, 12 | # you can enable the server option below. 13 | config :{{cookiecutter.phoenix_app_slug}}_web, {{cookiecutter.phoenix_app_module}}Web.Endpoint, 14 | http: [port: 4002], 15 | server: false 16 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/apps/{{cookiecutter.phoenix_app_slug}}_web/assets/src/app-router.tsx: -------------------------------------------------------------------------------- 1 | import { Router } from "@reach/router" 2 | import * as React from "react" 3 | 4 | import { HomeRoot } from "./app/home" 5 | import { ReducerContextDemoRoot } from "./app/reducer-context-demo" 6 | import { SubscriptionDemoRoot } from "./app/subscription-demo" 7 | 8 | export const AppRouter: React.SFC = (): JSX.Element => ( 9 | 10 | 11 | 12 | 13 | 14 | ) 15 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/apps/{{cookiecutter.phoenix_app_slug}}_web/assets/js/app.js: -------------------------------------------------------------------------------- 1 | // We need to import the CSS so that webpack will load it. 2 | // The MiniCssExtractPlugin is used to separate it out into 3 | // its own CSS file. 4 | import css from "../css/app.css" 5 | 6 | // webpack automatically bundles all modules in your 7 | // entry points. Those entry points can be configured 8 | // in "webpack.config.js". 9 | // 10 | // Import dependencies 11 | // 12 | import "phoenix_html" 13 | 14 | // Import local files 15 | // 16 | // Local files can be imported directly using relative paths, for example: 17 | // import socket from "./socket" 18 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where 3rd-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | .eslintcache 23 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/rel/env.sh.eex: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Sets and enables heart (recommended only in daemon mode) 4 | # if [ "$RELEASE_COMMAND" = "daemon" ] || [ "$RELEASE_COMMAND" = "daemon_iex" ]; then 5 | # HEART_COMMAND="$RELEASE_ROOT/bin/$RELEASE_NAME $RELEASE_COMMAND" 6 | # export HEART_COMMAND 7 | # export ELIXIR_ERL_OPTIONS="-heart" 8 | # fi 9 | 10 | # Set the release to work across nodes. If using the long name format like 11 | # the one below (my_app@127.0.0.1), you need to also uncomment the 12 | # RELEASE_DISTRIBUTION variable below. 13 | # export RELEASE_DISTRIBUTION=name 14 | # export RELEASE_NODE=<%= @release.name %>@127.0.0.1 15 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/apps/{{cookiecutter.phoenix_app_slug}}_web/assets/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "allowSyntheticDefaultImports": true, 5 | "sourceMap": true, 6 | "noImplicitAny": true, 7 | "module": "commonjs", 8 | "strict": true, 9 | "skipLibCheck": true, 10 | // "types": ["react", "jest"], 11 | "types": ["react"], 12 | "target": "esnext", 13 | "jsx": "react" 14 | }, 15 | "exclude": [ 16 | "/node_modules/**/*", 17 | "apollo-links.ts", 18 | "**/*.d.ts", 19 | "**/*.spec.ts", 20 | "**/*.scss", 21 | "**/*.sass", 22 | "**/*.css" 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /cookiecutter.json: -------------------------------------------------------------------------------- 1 | { 2 | "full_name": "Your name", 3 | "email": "Your address email (eq. you@example.com)", 4 | "github_username": "Your github username", 5 | "project_name": "Name of the project", 6 | "project_slug": "{{ cookiecutter.project_name.lower().replace(' ', '-') }}", 7 | "phoenix_app_module": "{{cookiecutter.project_name.split() | join('') }}", 8 | "phoenix_app_slug": "{{cookiecutter.project_name.lower().replace(' ', '_') }}", 9 | "project_short_description": "A short description of the project", 10 | "release_date": "{% now 'local' %}", 11 | "version": "0.1.0", 12 | "_extensions": [ "jinja2_time.TimeExtension" ], 13 | "_copy_without_render": [ "*.ts", "*.tsx" ] 14 | } 15 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/apps/{{cookiecutter.phoenix_app_slug}}_web/assets/src/app/reducer-context-demo/reducer-context-demo-container.tsx: -------------------------------------------------------------------------------- 1 | import { RouteComponentProps } from "@reach/router" 2 | import { Field, Formik, Form as FormikForm } from "formik" 3 | import React, { useReducer } from "react" 4 | import { Reducers, initialState } from "./reducers/todo-ducks" 5 | 6 | import { ReducerContextDemo } from "./reducer-context-demo" 7 | 8 | export const ReducerContextDemoContainer: React.SFC< 9 | RouteComponentProps 10 | > = () => { 11 | const [state, dispatch] = useReducer(Reducers, initialState) 12 | 13 | return 14 | } 15 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/apps/{{cookiecutter.phoenix_app_slug}}_web/test/{{cookiecutter.phoenix_app_slug}}_web/views/error_view_test.exs: -------------------------------------------------------------------------------- 1 | defmodule {{cookiecutter.phoenix_app_module}}Web.ErrorViewTest do 2 | use {{cookiecutter.phoenix_app_module}}Web.ConnCase, async: true 3 | 4 | # Bring render/3 and render_to_string/3 for testing custom views 5 | import Phoenix.View 6 | 7 | test "renders 404.html" do 8 | assert render_to_string({{cookiecutter.phoenix_app_module}}Web.ErrorView, "404.html", []) == "Not Found" 9 | end 10 | 11 | test "renders 500.html" do 12 | assert render_to_string({{cookiecutter.phoenix_app_module}}Web.ErrorView, "500.html", []) == "Internal Server Error" 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/apps/{{cookiecutter.phoenix_app_slug}}_web/assets/src/app/reducer-context-demo/components/options.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { Button, Grid, Header, Input, Segment } from "semantic-ui-react" 3 | 4 | export const Options: React.SFC = () => { 5 | return ( 6 | 7 |
Add Tasks:
8 | 9 | 19 | 20 |
21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/apps/{{cookiecutter.phoenix_app_slug}}/lib/{{cookiecutter.phoenix_app_slug}}/application.ex: -------------------------------------------------------------------------------- 1 | defmodule {{cookiecutter.phoenix_app_module}}.Application do 2 | # See https://hexdocs.pm/elixir/Application.html 3 | # for more information on OTP Applications 4 | @moduledoc false 5 | 6 | use Application 7 | 8 | @spec start(Application.start_type(), term()) :: 9 | {:error, reason :: term()} | {:ok, pid()} | {:ok, pid(), Application.state()} 10 | def start(_type, _args) do 11 | children = [ 12 | {{cookiecutter.phoenix_app_module}}.Repo 13 | ] 14 | 15 | Supervisor.start_link(children, strategy: :one_for_one, name: {{cookiecutter.phoenix_app_module}}.Supervisor) 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/apps/{{cookiecutter.phoenix_app_slug}}/.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where 3rd-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Ignore package tarball (built via "mix hex.build"). 23 | {{cookiecutter.phoenix_app_slug}}-*.tar 24 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/apps/{{cookiecutter.phoenix_app_slug}}_web/lib/{{cookiecutter.phoenix_app_slug}}_web/views/error_view.ex: -------------------------------------------------------------------------------- 1 | defmodule {{cookiecutter.phoenix_app_module}}Web.ErrorView do 2 | use {{cookiecutter.phoenix_app_module}}Web, :view 3 | 4 | # If you want to customize a particular status code 5 | # for a certain format, you may uncomment below. 6 | # def render("500.html", _assigns) do 7 | # "Internal Server Error" 8 | # end 9 | 10 | # By default, Phoenix returns the status message from 11 | # the template name. For example, "404.html" becomes 12 | # "Not Found". 13 | @spec template_not_found(String.t(), String.t()) :: %{errors: any} 14 | def template_not_found(template, _assigns) do 15 | Phoenix.Controller.status_message_from_template(template) 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/apps/{{cookiecutter.phoenix_app_slug}}_web/README.md: -------------------------------------------------------------------------------- 1 | # {{cookiecutter.phoenix_app_module}}Web 2 | 3 | To start your Phoenix server: 4 | 5 | * Install dependencies with `mix deps.get` 6 | * Create and migrate your database with `mix ecto.setup` 7 | * Install Node.js dependencies with `cd assets && npm install` 8 | * Start Phoenix endpoint with `mix phx.server` 9 | 10 | Now you can visit [`localhost:4000`](http://localhost:4000) from your browser. 11 | 12 | Ready to run in production? Please [check our deployment guides](https://hexdocs.pm/phoenix/deployment.html). 13 | 14 | ## Learn more 15 | 16 | * Official website: http://www.phoenixframework.org/ 17 | * Guides: https://hexdocs.pm/phoenix/overview.html 18 | * Docs: https://hexdocs.pm/phoenix 19 | * Mailing list: http://groups.google.com/group/phoenix-talk 20 | * Source: https://github.com/phoenixframework/phoenix 21 | -------------------------------------------------------------------------------- /tests/test_bake_project.py: -------------------------------------------------------------------------------- 1 | from contextlib import contextmanager 2 | 3 | import os 4 | import subprocess 5 | 6 | 7 | @contextmanager 8 | def inside_dir(dirpath): 9 | """ 10 | Execute code from inside the given directory 11 | :param dirpath: String, path of the directory the command is being run. 12 | """ 13 | old_path = os.getcwd() 14 | try: 15 | os.chdir(dirpath) 16 | yield 17 | finally: 18 | os.chdir(old_path) 19 | 20 | 21 | def test_project_tree(cookies): 22 | result = cookies.bake(extra_context={'project_slug': 'test_project'}) 23 | assert result.exit_code == 0 24 | assert result.exception is None 25 | assert result.project.basename == 'test_project' 26 | 27 | 28 | def test_run_flake8(cookies): 29 | result = cookies.bake(extra_context={'project_slug': 'flake8_compat'}) 30 | with inside_dir(str(result.project)): 31 | subprocess.check_call(['flake8']) 32 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/apps/{{cookiecutter.phoenix_app_slug}}_web/lib/{{cookiecutter.phoenix_app_slug}}_web/gettext.ex: -------------------------------------------------------------------------------- 1 | defmodule {{cookiecutter.phoenix_app_module}}Web.Gettext do 2 | @moduledoc """ 3 | A module providing Internationalization with a gettext-based API. 4 | 5 | By using [Gettext](https://hexdocs.pm/gettext), 6 | your module gains a set of macros for translations, for example: 7 | 8 | import {{cookiecutter.phoenix_app_module}}Web.Gettext 9 | 10 | # Simple translation 11 | gettext("Here is the string to translate") 12 | 13 | # Plural translation 14 | ngettext("Here is the string to translate", 15 | "Here are the strings to translate", 16 | 3) 17 | 18 | # Domain-based translation 19 | dgettext("errors", "Here is the error message to translate") 20 | 21 | See the [Gettext Docs](https://hexdocs.pm/gettext) for detailed usage. 22 | """ 23 | use Gettext, otp_app: :{{cookiecutter.phoenix_app_slug}}_web 24 | end 25 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: help 2 | 3 | APP_NAME ?= `grep 'app:' apps/{{cookiecutter.phoenix_app_slug}}/mix.exs | sed -e 's/\[//g' -e 's/ //g' -e 's/app://' -e 's/[:,]//g'` 4 | APP_VSN ?= `grep 'version:' mix.exs | cut -d '"' -f2` 5 | BUILD ?= `git rev-parse --short HEAD` 6 | ALPINE_VERSION ?= '3.9' 7 | TZ ?= 'Asia/Manila' 8 | IMAGE_REPO ?= 'registry.gitlab.com/ibakami/medilink' 9 | IMAGE_NAME = $(IMAGE_REPO)/$(APP_NAME) 10 | 11 | help: 12 | @echo "$(APP_NAME):$(BUILD)" 13 | @perl -nle'print $& if m{^[a-zA-Z_-]+:.*?## .*$$}' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' 14 | 15 | build: ## Build the Docker image 16 | docker build \ 17 | --build-arg TZ=${TZ} \ 18 | --build-arg ALPINE_VERSION=${ALPINE_VERSION} \ 19 | --build-arg APP_NAME=$(APP_NAME) \ 20 | --build-arg APP_VSN=$(APP_VSN) \ 21 | -t $(IMAGE_NAME):$(BUILD) \ 22 | -t $(IMAGE_NAME):latest . 23 | 24 | run: ## Run the app in Docker 25 | docker run --env-file config/docker.env \ 26 | --expose 4000 -p 4000:4000 \ 27 | --rm -it $(IMAGE_NAME):latest 28 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/apps/{{cookiecutter.phoenix_app_slug}}_web/lib/{{cookiecutter.phoenix_app_slug}}_web/graphql/schema.ex: -------------------------------------------------------------------------------- 1 | defmodule {{cookiecutter.phoenix_app_module}}Web.GraphQL.Schema do 2 | @moduledoc """ 3 | The root schema 4 | """ 5 | 6 | use Absinthe.Schema 7 | import_types(Absinthe.Type.Custom) 8 | 9 | object :user do 10 | field(:name, :string) 11 | field(:age, :integer) 12 | end 13 | 14 | query do 15 | field :list_users, list_of(:user) do 16 | resolve(fn _, _, _ -> 17 | {:ok, [%{name: "Rafael", age: 22}]} 18 | end) 19 | end 20 | end 21 | 22 | mutation do 23 | field :add_user, :user do 24 | arg(:name, :string) 25 | arg(:age, :integer) 26 | 27 | resolve(fn _, %{name: name, age: age}, _ -> 28 | {:ok, %{name: name, age: age}} 29 | end) 30 | end 31 | end 32 | 33 | subscription do 34 | field :user_added, :user do 35 | config(fn _args, _info -> 36 | {:ok, topic: "*"} 37 | end) 38 | 39 | trigger(:add_user, topic: fn _ -> ["*"] end) 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/apps/{{cookiecutter.phoenix_app_slug}}_web/.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where 3rd-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Ignore package tarball (built via "mix hex.build"). 23 | {{cookiecutter.phoenix_app_slug}}_web-*.tar 24 | 25 | # If NPM crashes, it generates a log, let's ignore it too. 26 | npm-debug.log 27 | 28 | # The directory NPM downloads your dependencies sources to. 29 | /assets/node_modules/ 30 | 31 | # Since we are building assets from web/static, 32 | # we ignore priv/static. You may want to comment 33 | # this depending on your deployment strategy. 34 | /priv/static/ 35 | 36 | .eslintcache -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/apps/{{cookiecutter.phoenix_app_slug}}_web/assets/src/app/reducer-context-demo/reducer-context-demo.tsx: -------------------------------------------------------------------------------- 1 | import { RouteComponentProps } from "@reach/router" 2 | import { Field, Formik, Form as FormikForm } from "formik" 3 | import React from "react" 4 | import { ComponentWithReducers } from "./types" 5 | import { 6 | Button, 7 | Checkbox, 8 | Container, 9 | Divider, 10 | Form, 11 | Grid, 12 | Header, 13 | Icon, 14 | Input, 15 | Label, 16 | List, 17 | Segment, 18 | } from "semantic-ui-react" 19 | 20 | import { Options } from "./components/options" 21 | import { Todo } from "./components/todo" 22 | 23 | export const ReducerContextDemo: React.SFC = ({ 24 | state, 25 | dispatch, 26 | }) => { 27 | return ( 28 | 29 |
30 | Reducer and Context Demo (Hooks) 31 |
32 |
TODO List App:
33 | 34 | 35 | 36 | 37 | 38 | 39 |
40 | ) 41 | } 42 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/apps/{{cookiecutter.phoenix_app_slug}}_web/lib/{{cookiecutter.phoenix_app_slug}}_web/templates/layout/app.html.eex: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | {{cookiecutter.phoenix_app_module}} · Phoenix Framework 8 | 9 | 10 | 11 | 14 | <% npm_chunks = get_npm_chunk_list() %> 15 | <%= for key <- npm_chunks do %> 16 | 17 | <% end %> 18 | 19 | 20 |
21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright 2019 Alistair A. Israel . 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/apps/{{cookiecutter.phoenix_app_slug}}_web/assets/src/app/home/index.tsx: -------------------------------------------------------------------------------- 1 | import { RouteComponentProps } from "@reach/router" 2 | import * as React from "react" 3 | 4 | import { LoadingPage } from "../../helpers/components" 5 | import { useRootTitle } from "../../helpers/hooks" 6 | 7 | const PAGE_NAME = "Home" 8 | 9 | /** 10 | * ? React lazy isn't supported yet on typings, 11 | * ? and it doesn't allow named export yet, which sucks 12 | * 13 | * ? Also, we can't do something like an HoC for this to save lines of code 14 | * ? We are doing dynamic import and we need to separate the page to its own 15 | */ 16 | 17 | // ! IMPORTANT! 18 | // ! Change the webpackChunkName comment below, so the dynamic file will appear with a proper name 19 | 20 | const LazyComponent = React.lazy(() => 21 | import(/* webpackChunkName: "HomeComponent" */ "./home").then(module => ({ 22 | default: module.Home, 23 | })), 24 | ) 25 | 26 | export const HomeRoot: React.FC = (props): JSX.Element => { 27 | useRootTitle(PAGE_NAME) 28 | return ( 29 | } 31 | > 32 | 33 | 34 | ) 35 | } 36 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/apps/{{cookiecutter.phoenix_app_slug}}_web/assets/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import ReactDOM from "react-dom" 3 | 4 | import { App } from "./app" 5 | import { ApolloClient } from "apollo-client" 6 | import { apolloLinks } from "../apollo-links" 7 | import { InMemoryCache } from "apollo-cache-inmemory" 8 | import { ApolloProvider } from "@apollo/react-hooks" 9 | 10 | const appRoot: HTMLDivElement | null = document.querySelector("#root") 11 | 12 | const cache = new InMemoryCache() 13 | 14 | const client = new ApolloClient({ 15 | link: apolloLinks, 16 | cache, 17 | }) 18 | 19 | const AppComponent: React.SFC = () => ( 20 | 21 | 22 | 23 | ) 24 | 25 | ReactDOM.render(, appRoot) 26 | 27 | if ("serviceWorker" in navigator) { 28 | window.addEventListener("load", () => { 29 | navigator.serviceWorker 30 | .register("/service-worker.js") 31 | .then(registration => { 32 | console.log("SW registered:", registration) // eslint-disable-line no-console 33 | return null 34 | }) 35 | .catch(error => { 36 | console.log("SW registration failed:", error) // eslint-disable-line no-console 37 | }) 38 | }) 39 | } 40 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/apps/{{cookiecutter.phoenix_app_slug}}_web/test/support/channel_case.ex: -------------------------------------------------------------------------------- 1 | defmodule {{cookiecutter.phoenix_app_module}}Web.ChannelCase do 2 | @moduledoc """ 3 | This module defines the test case to be used by 4 | channel tests. 5 | 6 | Such tests rely on `Phoenix.ChannelTest` and also 7 | import other functionality to make it easier 8 | to build common data structures and query the data layer. 9 | 10 | Finally, if the test case interacts with the database, 11 | it cannot be async. For this reason, every test runs 12 | inside a transaction which is reset at the beginning 13 | of the test unless the test case is marked as async. 14 | """ 15 | 16 | use ExUnit.CaseTemplate 17 | 18 | alias Ecto.Adapters.SQL.Sandbox 19 | 20 | using do 21 | quote do 22 | # Import conveniences for testing with channels 23 | use Phoenix.ChannelTest 24 | 25 | # The default endpoint for testing 26 | @endpoint {{cookiecutter.phoenix_app_module}}Web.Endpoint 27 | end 28 | end 29 | 30 | setup tags do 31 | :ok = Sandbox.checkout({{cookiecutter.phoenix_app_module}}.Repo) 32 | 33 | unless tags[:async] do 34 | Sandbox.mode({{cookiecutter.phoenix_app_module}}.Repo, {:shared, self()}) 35 | end 36 | 37 | :ok 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/apps/{{cookiecutter.phoenix_app_slug}}_web/lib/{{cookiecutter.phoenix_app_slug}}_web/templates/page/index.html.eex: -------------------------------------------------------------------------------- 1 |
2 |

<%= gettext "Welcome to %{name}!", name: "Phoenix" %>

3 |

A productive web framework that
does not compromise speed or maintainability.

4 |
5 | 6 |
7 | 21 | 35 |
36 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/config/releases.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | database_url = 4 | System.get_env("DATABASE_URL") || 5 | raise """ 6 | environment variable DATABASE_URL is missing. 7 | For example: ecto://USER:PASS@HOST/DATABASE 8 | """ 9 | 10 | config :{{cookiecutter.phoenix_app_slug}}, {{cookiecutter.phoenix_app_module}}.Repo, 11 | # ssl: true, 12 | url: database_url, 13 | pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10") 14 | 15 | secret_key_base = 16 | System.get_env("SECRET_KEY_BASE") || 17 | raise """ 18 | environment variable SECRET_KEY_BASE is missing. 19 | You can generate one by calling: mix phx.gen.secret 20 | """ 21 | 22 | config :{{cookiecutter.phoenix_app_slug}}_web, {{cookiecutter.phoenix_app_module}}Web.Endpoint, 23 | http: [:inet6, port: String.to_integer(System.get_env("PORT") || "4000")], 24 | secret_key_base: secret_key_base 25 | 26 | # ## Using releases (Elixir v1.9+) 27 | # 28 | # If you are doing OTP releases, you need to instruct Phoenix 29 | # to start each relevant endpoint: 30 | # 31 | config :{{cookiecutter.phoenix_app_slug}}_web, {{cookiecutter.phoenix_app_module}}Web.Endpoint, server: true 32 | # 33 | # Then you can assemble a release by calling `mix release`. 34 | # See `mix help release` for more information. 35 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/apps/{{cookiecutter.phoenix_app_slug}}_web/lib/{{cookiecutter.phoenix_app_slug}}_web/router.ex: -------------------------------------------------------------------------------- 1 | defmodule {{cookiecutter.phoenix_app_module}}Web.Router do 2 | use {{cookiecutter.phoenix_app_module}}Web, :router 3 | 4 | pipeline :browser do 5 | plug :accepts, ["html"] 6 | plug :fetch_session 7 | plug :fetch_flash 8 | plug :protect_from_forgery 9 | plug :put_secure_browser_headers 10 | end 11 | 12 | pipeline :api do 13 | plug :accepts, ["json"] 14 | end 15 | 16 | scope "/", {{cookiecutter.phoenix_app_module}}Web do 17 | pipe_through :browser 18 | 19 | get "/", PageController, :index 20 | get "/subscription-demo", PageController, :index 21 | end 22 | 23 | # Other scopes may use custom stacks. 24 | # scope "/api", {{cookiecutter.phoenix_app_module}}Web do 25 | # pipe_through :api 26 | # end 27 | 28 | scope "/" do 29 | forward("/graphql", Absinthe.Plug, schema: {{cookiecutter.phoenix_app_module}}Web.GraphQL.Schema, json_codec: Jason, socket: {{cookiecutter.phoenix_app_module}}Web.UserSocket) 30 | 31 | forward("/graphiql", Absinthe.Plug.GraphiQL, 32 | schema: {{cookiecutter.phoenix_app_module}}Web.GraphQL.Schema, 33 | json_codec: Jason, 34 | socket: {{cookiecutter.phoenix_app_module}}Web.UserSocket 35 | ) 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/apps/{{cookiecutter.phoenix_app_slug}}_web/assets/src/app/subscription-demo/index.tsx: -------------------------------------------------------------------------------- 1 | import { RouteComponentProps } from "@reach/router" 2 | import * as React from "react" 3 | 4 | import { LoadingPage } from "../../helpers/components" 5 | import { useRootTitle } from "../../helpers/hooks" 6 | 7 | const PAGE_NAME = "Subscription Demo" 8 | 9 | /** 10 | * ? React lazy isn't supported yet on typings, 11 | * ? and it doesn't allow named export yet, which sucks 12 | * 13 | * ? Also, we can't do something like an HoC for this to save lines of code 14 | * ? We are doing dynamic import and we need to separate the page to its own 15 | */ 16 | 17 | // ! IMPORTANT! 18 | // ! Change the webpackChunkName comment below, so the dynamic file will appear with a proper name 19 | 20 | const LazyComponent = React.lazy(() => 21 | import( 22 | /* webpackChunkName: "SubscriptionDemoComponent" */ "./subscription-demo" 23 | ).then(module => ({ 24 | default: module.SubscriptionDemo, 25 | })), 26 | ) 27 | 28 | export const SubscriptionDemoRoot: React.FC = ( 29 | props, 30 | ): JSX.Element => { 31 | useRootTitle(PAGE_NAME) 32 | return ( 33 | } 35 | > 36 | 37 | 38 | ) 39 | } 40 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/apps/{{cookiecutter.phoenix_app_slug}}_web/assets/src/app/reducer-context-demo/index.tsx: -------------------------------------------------------------------------------- 1 | import { RouteComponentProps } from "@reach/router" 2 | import * as React from "react" 3 | 4 | import { LoadingPage } from "../../helpers/components" 5 | import { useRootTitle } from "../../helpers/hooks" 6 | 7 | const PAGE_NAME = "Reducer/Context Demo" 8 | 9 | /** 10 | * ? React lazy isn't supported yet on typings, 11 | * ? and it doesn't allow named export yet, which sucks 12 | * 13 | * ? Also, we can't do something like an HoC for this to save lines of code 14 | * ? We are doing dynamic import and we need to separate the page to its own 15 | */ 16 | 17 | // ! IMPORTANT! 18 | // ! Change the webpackChunkName comment below, so the dynamic file will appear with a proper name 19 | 20 | const LazyComponent = React.lazy(() => 21 | import( 22 | /* webpackChunkName: "ReducerContextDemo" */ "./reducer-context-demo-container" 23 | ).then(module => ({ 24 | default: module.ReducerContextDemoContainer, 25 | })), 26 | ) 27 | 28 | export const ReducerContextDemoRoot: React.FC = ( 29 | props, 30 | ): JSX.Element => { 31 | useRootTitle(PAGE_NAME) 32 | return ( 33 | } 35 | > 36 | 37 | 38 | ) 39 | } 40 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/apps/{{cookiecutter.phoenix_app_slug}}_web/test/support/conn_case.ex: -------------------------------------------------------------------------------- 1 | defmodule {{cookiecutter.phoenix_app_module}}Web.ConnCase do 2 | @moduledoc """ 3 | This module defines the test case to be used by 4 | tests that require setting up a connection. 5 | 6 | Such tests rely on `Phoenix.ConnTest` and also 7 | import other functionality to make it easier 8 | to build common data structures and query the data layer. 9 | 10 | Finally, if the test case interacts with the database, 11 | it cannot be async. For this reason, every test runs 12 | inside a transaction which is reset at the beginning 13 | of the test unless the test case is marked as async. 14 | """ 15 | 16 | use ExUnit.CaseTemplate 17 | 18 | alias Ecto.Adapters.SQL.Sandbox 19 | 20 | using do 21 | quote do 22 | # Import conveniences for testing with connections 23 | use Phoenix.ConnTest 24 | alias {{cookiecutter.phoenix_app_module}}Web.Router.Helpers, as: Routes 25 | 26 | # The default endpoint for testing 27 | @endpoint {{cookiecutter.phoenix_app_module}}Web.Endpoint 28 | end 29 | end 30 | 31 | setup tags do 32 | :ok = Sandbox.checkout({{cookiecutter.phoenix_app_module}}.Repo) 33 | 34 | unless tags[:async] do 35 | Sandbox.mode({{cookiecutter.phoenix_app_module}}.Repo, {:shared, self()}) 36 | end 37 | 38 | {:ok, conn: Phoenix.ConnTest.build_conn()} 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/apps/{{cookiecutter.phoenix_app_slug}}_web/lib/{{cookiecutter.phoenix_app_slug}}_web/application.ex: -------------------------------------------------------------------------------- 1 | defmodule {{cookiecutter.phoenix_app_module}}Web.Application do 2 | # See https://hexdocs.pm/elixir/Application.html 3 | # for more information on OTP Applications 4 | @moduledoc false 5 | 6 | use Application 7 | 8 | @spec start(Application.start_type(), term()) :: 9 | {:error, reason :: term()} | {:ok, pid()} | {:ok, pid(), Application.state()} 10 | def start(_type, _args) do 11 | # List all child processes to be supervised 12 | children = [ 13 | # Start the endpoint when the application starts 14 | {{cookiecutter.phoenix_app_module}}Web.Endpoint, 15 | {Absinthe.Subscription, [{{cookiecutter.phoenix_app_module}}Web.Endpoint]} 16 | # Starts a worker by calling: {{cookiecutter.phoenix_app_module}}Web.Worker.start_link(arg) 17 | # {{'{'}}{{cookiecutter.phoenix_app_module}}Web.Worker, arg}, 18 | ] 19 | 20 | # See https://hexdocs.pm/elixir/Supervisor.html 21 | # for other strategies and supported options 22 | opts = [strategy: :one_for_one, name: {{cookiecutter.phoenix_app_module}}Web.Supervisor] 23 | Supervisor.start_link(children, opts) 24 | end 25 | 26 | # Tell Phoenix to update the endpoint configuration 27 | # whenever the application is updated. 28 | @spec config_change(list(tuple), list(tuple), list(any)) :: :ok 29 | def config_change(changed, _new, removed) do 30 | {{cookiecutter.phoenix_app_module}}Web.Endpoint.config_change(changed, removed) 31 | :ok 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/apps/{{cookiecutter.phoenix_app_slug}}_web/assets/src/app/home/home.tsx: -------------------------------------------------------------------------------- 1 | import { RouteComponentProps } from "@reach/router" 2 | import React from "react" 3 | import { Container, Header, Image, Label } from "semantic-ui-react" 4 | 5 | export const Home: React.SFC = () => { 6 | return ( 7 | 8 |
9 | Elixir Phoenix Cookie Cutter 10 |
11 |

Phoenix Umbrella Project with React/TypeScript

12 | 16 | 20 | 24 | 28 | 32 | 36 | 40 |
41 | ) 42 | } 43 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/apps/{{cookiecutter.phoenix_app_slug}}_web/lib/{{cookiecutter.phoenix_app_slug}}_web/channels/user_socket.ex: -------------------------------------------------------------------------------- 1 | defmodule {{cookiecutter.phoenix_app_module}}Web.UserSocket do 2 | use Phoenix.Socket 3 | use Absinthe.Phoenix.Socket, schema: {{cookiecutter.phoenix_app_module}}Web.GraphQL.Schema 4 | 5 | 6 | ## Channels 7 | # channel "room:*", {{cookiecutter.phoenix_app_module}}Web.RoomChannel 8 | 9 | # Socket params are passed from the client and can 10 | # be used to verify and authenticate a user. After 11 | # verification, you can put default assigns into 12 | # the socket that will be set for all channels, ie 13 | # 14 | # {:ok, assign(socket, :user_id, verified_user_id)} 15 | # 16 | # To deny connection, return `:error`. 17 | # 18 | # See `Phoenix.Token` documentation for examples in 19 | # performing token verification on connect. 20 | @spec connect(map(), Phoenix.Socket.t(), map()) :: {:ok, Phoenix.Socket.t()} | :error 21 | def connect(_params, socket, _connect_info) do 22 | {:ok, socket} 23 | end 24 | 25 | # Socket id's are topics that allow you to identify all sockets for a given user: 26 | # 27 | # def id(socket), do: "user_socket:#{socket.assigns.user_id}" 28 | # 29 | # Would allow you to broadcast a "disconnect" event and terminate 30 | # all active sockets and channels for a given user: 31 | # 32 | # {{cookiecutter.phoenix_app_module}}Web.Endpoint.broadcast("user_socket:#{user.id}", "disconnect", %{}) 33 | # 34 | # Returning `nil` makes this socket anonymous. 35 | @spec id(any()) :: nil 36 | def id(_socket), do: nil 37 | end 38 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/apps/{{cookiecutter.phoenix_app_slug}}_web/lib/{{cookiecutter.phoenix_app_slug}}_web/views/webpack_entrypoint.ex: -------------------------------------------------------------------------------- 1 | defmodule {{cookiecutter.phoenix_app_module}}Web.WebpackEntrypoint do 2 | @moduledoc """ 3 | Webpack Entrypoint 4 | 5 | This particular module is responsible for making code-splitting on phoenix possible 6 | 7 | Webpack generates assets-manifest.json file for us to know what assets should be included in the template. 8 | Then we'll manually map through it in the corresponding template 9 | 10 | Import this module on the main entrypoint (e.g.: cpt_web.ex), just below the `use Phoenix.HTML` part 11 | """ 12 | 13 | # credo:disable-for-this-file 14 | 15 | # Read the manifest from static directory 16 | def get_manifest do 17 | Path.join(:code.priv_dir(:{{cookiecutter.phoenix_app_slug}}_web), "static/assets-manifest.json") 18 | |> File.read!() 19 | |> Jason.decode!() 20 | end 21 | 22 | # Create a list of npm chunks based on the manifest. Will be needing this to populate the chunks on templates 23 | def get_npm_chunk_list do 24 | get_manifest() 25 | |> Enum.map(fn {key, _value} -> key end) 26 | |> Enum.filter(fn entry -> Regex.match?(~r/^npm\..*$/, entry) end) 27 | end 28 | 29 | if Mix.env() == :prod do 30 | # Phoenix uses @manifest to recognize our manifest assets as a valid route on production 31 | @manifest Path.join(:code.priv_dir(:{{cookiecutter.phoenix_app_slug}}_web), "static/assets-manifest.json") 32 | |> File.read!() 33 | |> Jason.decode!() 34 | def webpack_entrypoint_path(_conn, name), do: @manifest[name] 35 | else 36 | def webpack_entrypoint_path(_conn, name) do 37 | manifest = get_manifest() 38 | manifest[name] 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/apps/{{cookiecutter.phoenix_app_slug}}/test/support/data_case.ex: -------------------------------------------------------------------------------- 1 | defmodule {{cookiecutter.phoenix_app_module}}.DataCase do 2 | @moduledoc """ 3 | This module defines the setup for tests requiring 4 | access to the application's data layer. 5 | 6 | You may define functions here to be used as helpers in 7 | your tests. 8 | 9 | Finally, if the test case interacts with the database, 10 | it cannot be async. For this reason, every test runs 11 | inside a transaction which is reset at the beginning 12 | of the test unless the test case is marked as async. 13 | """ 14 | 15 | use ExUnit.CaseTemplate 16 | 17 | alias Ecto.Adapters.SQL.Sandbox 18 | 19 | using do 20 | quote do 21 | alias {{cookiecutter.phoenix_app_module}}.Repo 22 | 23 | import Ecto 24 | import Ecto.Changeset 25 | import Ecto.Query 26 | import {{cookiecutter.phoenix_app_module}}.DataCase 27 | end 28 | end 29 | 30 | setup tags do 31 | :ok = Sandbox.checkout({{cookiecutter.phoenix_app_module}}.Repo) 32 | 33 | unless tags[:async] do 34 | Sandbox.mode({{cookiecutter.phoenix_app_module}}.Repo, {:shared, self()}) 35 | end 36 | 37 | :ok 38 | end 39 | 40 | @doc """ 41 | A helper that transforms changeset errors into a map of messages. 42 | 43 | assert {:error, changeset} = Accounts.create_user(%{password: "short"}) 44 | assert "password is too short" in errors_on(changeset).password 45 | assert %{password: ["password is too short"]} = errors_on(changeset) 46 | 47 | """ 48 | @spec errors_on(%Ecto.Changeset{}) :: map 49 | def errors_on(changeset) do 50 | Ecto.Changeset.traverse_errors(changeset, fn {message, opts} -> 51 | Regex.replace(~r"%{(\w+)}", message, fn _, key -> 52 | opts |> Keyword.get(String.to_existing_atom(key), key) |> to_string() 53 | end) 54 | end) 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/apps/{{cookiecutter.phoenix_app_slug}}_web/lib/{{cookiecutter.phoenix_app_slug}}_web/views/error_helpers.ex: -------------------------------------------------------------------------------- 1 | defmodule {{cookiecutter.phoenix_app_module}}Web.ErrorHelpers do 2 | @moduledoc """ 3 | Conveniences for translating and building error messages. 4 | """ 5 | 6 | use Phoenix.HTML 7 | 8 | @doc """ 9 | Generates tag for inlined form input errors. 10 | """ 11 | @spec error_tag(atom, atom) :: list() 12 | def error_tag(form, field) do 13 | Enum.map(Keyword.get_values(form.errors, field), fn error -> 14 | content_tag(:span, translate_error(error), class: "help-block") 15 | end) 16 | end 17 | 18 | @doc """ 19 | Translates an error message using gettext. 20 | """ 21 | @spec translate_error({String.t(), map}) :: String.t() 22 | def translate_error({msg, opts}) do 23 | # When using gettext, we typically pass the strings we want 24 | # to translate as a static argument: 25 | # 26 | # # Translate "is invalid" in the "errors" domain 27 | # dgettext("errors", "is invalid") 28 | # 29 | # # Translate the number of files with plural rules 30 | # dngettext("errors", "1 file", "%{count} files", count) 31 | # 32 | # Because the error messages we show in our forms and APIs 33 | # are defined inside Ecto, we need to translate them dynamically. 34 | # This requires us to call the Gettext module passing our gettext 35 | # backend as first argument. 36 | # 37 | # Note we use the "errors" domain, which means translations 38 | # should be written to the errors.po file. The :count option is 39 | # set by Ecto and indicates we should also apply plural rules. 40 | if count = opts[:count] do 41 | Gettext.dngettext({{cookiecutter.phoenix_app_module}}Web.Gettext, "errors", msg, msg, count, opts) 42 | else 43 | Gettext.dgettext({{cookiecutter.phoenix_app_module}}Web.Gettext, "errors", msg, opts) 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your umbrella 2 | # and **all applications** and their dependencies with the 3 | # help of Mix.Config. 4 | # 5 | # Note that all applications in your umbrella share the 6 | # same configuration and dependencies, which is why they 7 | # all use the same configuration file. If you want different 8 | # configurations or dependencies per app, it is best to 9 | # move said applications out of the umbrella. 10 | use Mix.Config 11 | 12 | # Configure Mix tasks and generators 13 | config :{{cookiecutter.phoenix_app_slug}}, 14 | ecto_repos: [{{cookiecutter.phoenix_app_module}}.Repo] 15 | 16 | config :{{cookiecutter.phoenix_app_slug}}_web, 17 | ecto_repos: [{{cookiecutter.phoenix_app_module}}.Repo], 18 | generators: [context_app: :{{cookiecutter.phoenix_app_slug}}] 19 | 20 | # Configures the endpoint 21 | config :{{cookiecutter.phoenix_app_slug}}_web, {{cookiecutter.phoenix_app_module}}Web.Endpoint, 22 | url: [host: "localhost"], 23 | secret_key_base: "6Dy4Xya3S+t/vgNTqbxtrmqifo+Gkaecc9IZBbfk7qfKJ6z7J34ZUxRF1HBP8bcU", 24 | render_errors: [view: {{cookiecutter.phoenix_app_module}}Web.ErrorView, accepts: ~w(html json)], 25 | pubsub: [name: {{cookiecutter.phoenix_app_module}}Web.PubSub, adapter: Phoenix.PubSub.PG2] 26 | 27 | # Configures Elixir's Logger 28 | elixir_logger_level = System.get_env("ELIXIR_LOGGER_LEVEL") || "info" 29 | 30 | level = 31 | case String.downcase(elixir_logger_level) do 32 | s when s == "1" or s == "debug" -> 33 | :debug 34 | 35 | s when s == "3" or s == "warn" -> 36 | :warn 37 | 38 | _ -> 39 | :info 40 | end 41 | 42 | # Configures Elixir's Logger 43 | config :logger, :console, 44 | format: "$time $metadata[$level] $message\n", 45 | metadata: [:request_id], 46 | level: level 47 | 48 | # Use Jason for JSON parsing in Phoenix 49 | config :phoenix, :json_library, Jason 50 | 51 | # Import environment specific config. This must remain at the bottom 52 | # of this file so it overrides the configuration defined above. 53 | import_config "#{Mix.env()}.exs" 54 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule {{cookiecutter.phoenix_app_module}}.Umbrella.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | apps_path: "apps", 7 | version: "{{cookiecutter.version}}", 8 | start_permanent: Mix.env() == :prod, 9 | deps: deps(), 10 | test_coverage: [tool: ExCoveralls], 11 | aliases: aliases(), 12 | preferred_cli_env: [ 13 | coveralls: :test, 14 | "coveralls.detail": :test, 15 | "coveralls.post": :test, 16 | "coveralls.html": :test 17 | ], 18 | releases: [ 19 | {{cookiecutter.phoenix_app_slug}}: [ 20 | include_executables_for: [:unix], 21 | applications: [{{cookiecutter.phoenix_app_slug}}: :permanent, {{cookiecutter.phoenix_app_slug}}_web: :permanent] 22 | ] 23 | ] 24 | ] 25 | end 26 | 27 | # Dependencies can be Hex packages: 28 | # 29 | # {:mydep, "~> 0.3.0"} 30 | # 31 | # Or git/path repositories: 32 | # 33 | # {:mydep, git: "https://github.com/elixir-lang/mydep.git", tag: "0.1.0"} 34 | # 35 | # Type "mix help deps" for more examples and options. 36 | # 37 | # Dependencies listed here are available only for this project 38 | # and cannot be accessed from applications inside the apps folder 39 | defp deps do 40 | [ 41 | {:dialyxir, "~> 1.0.0-rc.6", only: [:dev, :test], runtime: false}, 42 | {:excoveralls, "~> 0.10", only: :test}, 43 | {:credo, "~> 1.0.0", only: [:dev, :test], runtime: false} 44 | ] 45 | end 46 | 47 | defp aliases do 48 | [ 49 | "app.setup": [ 50 | "deps.get", 51 | "ecto.setup", 52 | &assets/1 53 | ], 54 | "ecto.setup": [ 55 | "ecto.create", 56 | "ecto.migrate", 57 | ], 58 | "ecto.reset": ["ecto.drop", "ecto.setup"] 59 | ] 60 | end 61 | 62 | defp assets(args) do 63 | {_, res} = 64 | System.cmd("yarn", args, into: IO.binstream(:stdio, :line), cd: "apps/{{cookiecutter.phoenix_app_slug}}_web/assets") 65 | 66 | if res > 0 do 67 | System.at_exit(fn _ -> exit({:shutdown, res}) end) 68 | end 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/config/prod.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | # For production, don't forget to configure the url host 4 | # to something meaningful, Phoenix uses this information 5 | # when generating URLs. 6 | # 7 | # Note we also include the path to a cache manifest 8 | # containing the digested version of static files. This 9 | # manifest is generated by the `mix phx.digest` task, 10 | # which you should run after static files are built and 11 | # before starting your production server. 12 | config :{{cookiecutter.phoenix_app_slug}}_web, {{cookiecutter.phoenix_app_module}}Web.Endpoint, 13 | url: [host: "example.com", port: 80], 14 | cache_static_manifest: "priv/static/cache_manifest.json" 15 | 16 | # ## SSL Support 17 | # 18 | # To get SSL working, you will need to add the `https` key 19 | # to the previous section and set your `:url` port to 443: 20 | # 21 | # config :{{cookiecutter.phoenix_app_slug}}_web, {{cookiecutter.phoenix_app_module}}Web.Endpoint, 22 | # ... 23 | # url: [host: "example.com", port: 443], 24 | # https: [ 25 | # :inet6, 26 | # port: 443, 27 | # cipher_suite: :strong, 28 | # keyfile: System.get_env("SOME_APP_SSL_KEY_PATH"), 29 | # certfile: System.get_env("SOME_APP_SSL_CERT_PATH") 30 | # ] 31 | # 32 | # The `cipher_suite` is set to `:strong` to support only the 33 | # latest and more secure SSL ciphers. This means old browsers 34 | # and clients may not be supported. You can set it to 35 | # `:compatible` for wider support. 36 | # 37 | # `:keyfile` and `:certfile` expect an absolute path to the key 38 | # and cert in disk or a relative path inside priv, for example 39 | # "priv/ssl/server.key". For all supported SSL configuration 40 | # options, see https://hexdocs.pm/plug/Plug.SSL.html#configure/1 41 | # 42 | # We also recommend setting `force_ssl` in your endpoint, ensuring 43 | # no data is ever sent via http, always redirecting to https: 44 | # 45 | # config :{{cookiecutter.phoenix_app_slug}}_web, {{cookiecutter.phoenix_app_module}}Web.Endpoint, 46 | # force_ssl: [hsts: true] 47 | # 48 | # Check `Plug.SSL` for all available options in `force_ssl`. 49 | 50 | # Do not print debug messages in production 51 | config :logger, level: :info 52 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/apps/{{cookiecutter.phoenix_app_slug}}_web/lib/{{cookiecutter.phoenix_app_slug}}_web/endpoint.ex: -------------------------------------------------------------------------------- 1 | defmodule {{cookiecutter.phoenix_app_module}}Web.Endpoint do 2 | use Phoenix.Endpoint, otp_app: :{{cookiecutter.phoenix_app_slug}}_web 3 | use Absinthe.Phoenix.Endpoint 4 | 5 | socket "/socket", {{cookiecutter.phoenix_app_module}}Web.UserSocket, 6 | websocket: true, 7 | longpoll: false 8 | 9 | # Serve at "/" the static files from "priv/static" directory. 10 | # 11 | # You should set gzip to true if you are running phx.digest 12 | # when deploying your static files in production. 13 | plug Plug.Static, 14 | # This will enable us to fetch the manifest + sw on the root of the project 15 | at: "/", 16 | from: :{{cookiecutter.phoenix_app_slug}}_web, 17 | gzip: false, 18 | only: ~w(manifest.json css fonts images js favicon.ico robots.txt service-worker.js) 19 | 20 | plug Plug.Static, 21 | # All our frontend static files will go on assets folder 22 | at: "/assets", 23 | from: :{{cookiecutter.phoenix_app_slug}}_web, 24 | gzip: true, 25 | only: ~w(css fonts images js favicon.ico service-worker.js robots.txt assets-manifest.json), 26 | only_matching: ~w(precache-manifest) 27 | 28 | # Code reloading can be explicitly enabled under the 29 | # :code_reloader configuration of your endpoint. 30 | if code_reloading? do 31 | socket "/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket 32 | plug Phoenix.LiveReloader 33 | plug Phoenix.CodeReloader 34 | end 35 | 36 | plug Plug.RequestId 37 | plug Plug.Telemetry, event_prefix: [:phoenix, :endpoint] 38 | 39 | plug Plug.Parsers, 40 | parsers: [:urlencoded, :multipart, :json], 41 | pass: ["*/*"], 42 | json_decoder: Phoenix.json_library() 43 | 44 | plug Plug.MethodOverride 45 | plug Plug.Head 46 | 47 | # The session will be stored in the cookie and signed, 48 | # this means its contents can be read but not tampered with. 49 | # Set :encryption_salt if you would also like to encrypt it. 50 | plug Plug.Session, 51 | store: :cookie, 52 | key: "_{{cookiecutter.phoenix_app_slug}}_web_key", 53 | signing_salt: "jMyK4u8L" 54 | 55 | plug {{cookiecutter.phoenix_app_module}}Web.Router 56 | end 57 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/apps/{{cookiecutter.phoenix_app_slug}}_web/assets/src/app/reducer-context-demo/reducers/todo-ducks.ts: -------------------------------------------------------------------------------- 1 | /* @typescript-eslint/no-angle-bracket-type-assertion */ 2 | import { normalizeArrayData } from "../../../helpers" 3 | 4 | export interface ItemType { 5 | id: string 6 | task: string 7 | isCompleted: boolean 8 | } 9 | 10 | export interface State { 11 | items: { 12 | [key: string]: ItemType 13 | } 14 | itemIds: (keyof State["items"])[] 15 | completedItems: (keyof State["items"])[] 16 | } 17 | 18 | type ItemIds = keyof State["items"] 19 | 20 | export const initialState: State = { 21 | items: {}, 22 | itemIds: [], 23 | completedItems: [], 24 | } 25 | 26 | export const todoActions = { 27 | addItems: (items: ItemType | ItemType[]) => 28 | { 29 | type: "ADD_ITEMS", 30 | items: Array.isArray(items) ? items : [items], 31 | }, 32 | removeItems: (itemIds: ItemIds[]) => 33 | { 34 | type: "REMOVE_ITEMS", 35 | itemIds, 36 | }, 37 | completeItem: (item: ItemIds) => 38 | { 39 | type: "COMPLETE_ITEM", 40 | item, 41 | }, 42 | uncompleteItem: (item: ItemIds) => 43 | { 44 | type: "UNCOMPLETE_ITEM", 45 | item, 46 | }, 47 | } 48 | 49 | export type Actions = ReturnType< 50 | | typeof todoActions.addItems 51 | | typeof todoActions.removeItems 52 | | typeof todoActions.completeItem 53 | | typeof todoActions.uncompleteItem 54 | > 55 | 56 | export const Reducers: React.Reducer = (state, action) => { 57 | switch (action.type) { 58 | case "ADD_ITEMS": 59 | return { 60 | ...state, 61 | items: { 62 | ...state.items, 63 | ...normalizeArrayData(action.items), 64 | }, 65 | } 66 | case "REMOVE_ITEMS": 67 | return { 68 | ...state, 69 | itemIds: state.itemIds.filter(id => !action.itemIds.includes(id)), 70 | } 71 | case "COMPLETE_ITEM": 72 | return { 73 | ...state, 74 | completedItems: [...state.completedItems, action.item], 75 | } 76 | case "UNCOMPLETE_ITEM": 77 | return { 78 | ...state, 79 | completedItems: state.completedItems.filter( 80 | item => action.item !== item, 81 | ), 82 | } 83 | default: 84 | return state 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/apps/{{cookiecutter.phoenix_app_slug}}/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule {{cookiecutter.phoenix_app_module}}.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :{{cookiecutter.phoenix_app_slug}}, 7 | version: "{{cookiecutter.version}}", 8 | build_path: "../../_build", 9 | config_path: "../../config/config.exs", 10 | deps_path: "../../deps", 11 | lockfile: "../../mix.lock", 12 | elixir: "~> 1.9", 13 | elixirc_paths: elixirc_paths(Mix.env()), 14 | start_permanent: Mix.env() == :prod, 15 | aliases: aliases(), 16 | deps: deps(), 17 | test_coverage: [tool: ExCoveralls], 18 | preferred_cli_env: [ 19 | coveralls: :test, 20 | "coveralls.detail": :test, 21 | "coveralls.post": :test, 22 | "coveralls.html": :test 23 | ] 24 | ] 25 | end 26 | 27 | # Configuration for the OTP application. 28 | # 29 | # Type `mix help compile.app` for more information. 30 | def application do 31 | [ 32 | mod: {{'{'}}{{cookiecutter.phoenix_app_module}}.Application, []}, 33 | extra_applications: [:logger, :runtime_tools] 34 | ] 35 | end 36 | 37 | # Specifies which paths to compile per environment. 38 | defp elixirc_paths(:test), do: ["lib", "test/support"] 39 | defp elixirc_paths(_), do: ["lib"] 40 | 41 | # Specifies your project dependencies. 42 | # 43 | # Type `mix help deps` for examples and options. 44 | defp deps do 45 | [ 46 | {:dialyxir, "~> 1.0.0-rc.6", only: [:dev, :test], runtime: false}, 47 | {:excoveralls, "~> 0.10", only: :test}, 48 | {:credo, "~> 1.0.0", only: [:dev, :test], runtime: false}, 49 | {:ecto_sql, "~> 3.1.6"}, 50 | {:postgrex, ">= 0.0.0"}, 51 | {:jason, "~> 1.0"}, 52 | {:ex_machina, "~> 2.3", only: :test} 53 | ] 54 | end 55 | 56 | # Aliases are shortcuts or tasks specific to the current project. 57 | # For example, to create, migrate and run the seeds file at once: 58 | # 59 | # $ mix ecto.setup 60 | # 61 | # See the documentation for `Mix` for more info on aliases. 62 | defp aliases do 63 | [ 64 | "ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"], 65 | "ecto.reset": ["ecto.drop", "ecto.setup"], 66 | test: ["ecto.create --quiet", "ecto.migrate", "test"] 67 | ] 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/apps/{{cookiecutter.phoenix_app_slug}}_web/lib/{{cookiecutter.phoenix_app_slug}}_web.ex: -------------------------------------------------------------------------------- 1 | defmodule {{cookiecutter.phoenix_app_module}}Web do 2 | @moduledoc """ 3 | The entrypoint for defining your web interface, such 4 | as controllers, views, channels and so on. 5 | 6 | This can be used in your application as: 7 | 8 | use {{cookiecutter.phoenix_app_module}}Web, :controller 9 | use {{cookiecutter.phoenix_app_module}}Web, :view 10 | 11 | The definitions below will be executed for every view, 12 | controller, etc, so keep them short and clean, focused 13 | on imports, uses and aliases. 14 | 15 | Do NOT define functions inside the quoted expressions 16 | below. Instead, define any helper function in modules 17 | and import those modules here. 18 | """ 19 | 20 | def controller do 21 | quote do 22 | use Phoenix.Controller, namespace: {{cookiecutter.phoenix_app_module}}Web 23 | import Plug.Conn 24 | import {{cookiecutter.phoenix_app_module}}Web.Gettext 25 | alias {{cookiecutter.phoenix_app_module}}Web.Router.Helpers, as: Routes 26 | end 27 | end 28 | 29 | def view do 30 | quote do 31 | use Phoenix.View, 32 | root: "lib/{{cookiecutter.phoenix_app_slug}}_web/templates", 33 | namespace: {{cookiecutter.phoenix_app_module}}Web 34 | 35 | # Import convenience functions from controllers 36 | import Phoenix.Controller, only: [get_flash: 1, get_flash: 2, view_module: 1] 37 | 38 | # Use all HTML functionality (forms, tags, etc) 39 | use Phoenix.HTML 40 | import {{cookiecutter.phoenix_app_module}}Web.WebpackEntrypoint 41 | import {{cookiecutter.phoenix_app_module}}Web.ErrorHelpers 42 | import {{cookiecutter.phoenix_app_module}}Web.Gettext 43 | alias {{cookiecutter.phoenix_app_module}}Web.Router.Helpers, as: Routes 44 | end 45 | end 46 | 47 | def router do 48 | quote do 49 | use Phoenix.Router 50 | import Plug.Conn 51 | import Phoenix.Controller 52 | end 53 | end 54 | 55 | def channel do 56 | quote do 57 | use Phoenix.Channel 58 | import {{cookiecutter.phoenix_app_module}}Web.Gettext 59 | end 60 | end 61 | 62 | @doc """ 63 | When used, dispatch to the appropriate controller/view/etc. 64 | """ 65 | defmacro __using__(which) when is_atom(which) do 66 | apply(__MODULE__, which, []) 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/apps/{{cookiecutter.phoenix_app_slug}}_web/assets/js/socket.js: -------------------------------------------------------------------------------- 1 | // NOTE: The contents of this file will only be executed if 2 | // you uncomment its entry in "assets/js/app.js". 3 | 4 | // To use Phoenix channels, the first step is to import Socket, 5 | // and connect at the socket path in "lib/web/endpoint.ex". 6 | // 7 | // Pass the token on params as below. Or remove it 8 | // from the params if you are not using authentication. 9 | import {Socket} from "phoenix" 10 | 11 | let socket = new Socket("/socket", {params: {token: window.userToken}}) 12 | 13 | // When you connect, you'll often need to authenticate the client. 14 | // For example, imagine you have an authentication plug, `MyAuth`, 15 | // which authenticates the session and assigns a `:current_user`. 16 | // If the current user exists you can assign the user's token in 17 | // the connection for use in the layout. 18 | // 19 | // In your "lib/web/router.ex": 20 | // 21 | // pipeline :browser do 22 | // ... 23 | // plug MyAuth 24 | // plug :put_user_token 25 | // end 26 | // 27 | // defp put_user_token(conn, _) do 28 | // if current_user = conn.assigns[:current_user] do 29 | // token = Phoenix.Token.sign(conn, "user socket", current_user.id) 30 | // assign(conn, :user_token, token) 31 | // else 32 | // conn 33 | // end 34 | // end 35 | // 36 | // Now you need to pass this token to JavaScript. You can do so 37 | // inside a script tag in "lib/web/templates/layout/app.html.eex": 38 | // 39 | // 40 | // 41 | // You will need to verify the user token in the "connect/3" function 42 | // in "lib/web/channels/user_socket.ex": 43 | // 44 | // def connect(%{"token" => token}, socket, _connect_info) do 45 | // # max_age: 1209600 is equivalent to two weeks in seconds 46 | // case Phoenix.Token.verify(socket, "user socket", token, max_age: 1209600) do 47 | // {:ok, user_id} -> 48 | // {:ok, assign(socket, :user, user_id)} 49 | // {:error, reason} -> 50 | // :error 51 | // end 52 | // end 53 | // 54 | // Finally, connect to the socket: 55 | socket.connect() 56 | 57 | // Now that you are connected, you can join channels with a topic: 58 | let channel = socket.channel("topic:subtopic", {}) 59 | channel.join() 60 | .receive("ok", resp => { console.log("Joined successfully", resp) }) 61 | .receive("error", resp => { console.log("Unable to join", resp) }) 62 | 63 | export default socket 64 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/apps/{{cookiecutter.phoenix_app_slug}}_web/assets/apollo-links.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | import * as AbsintheSocket from "@absinthe/socket" 3 | import { InMemoryCache } from "apollo-cache-inmemory" 4 | import { ApolloLink } from "apollo-link" 5 | import { onError } from "apollo-link-error" 6 | import { HttpLink } from "apollo-link-http" 7 | import { RetryLink } from "apollo-link-retry" 8 | import { withClientState } from "apollo-link-state" 9 | import { WebSocketLink } from "apollo-link-ws" 10 | // @ts-ignore 11 | import { createAbsintheSocketLink } from "@absinthe/socket-apollo-link" 12 | import { Socket as PhoenixSocket } from "phoenix" 13 | 14 | const GQL_URI = "/graphql" 15 | const WEB_SOCKET_URI = "ws://127.0.0.1:4000/socket" 16 | 17 | const cache = new InMemoryCache() 18 | 19 | const absintheSocketLink = createAbsintheSocketLink( 20 | AbsintheSocket.create(new PhoenixSocket("ws://localhost:4000/socket")), 21 | ) 22 | 23 | console.log("AbsintheSocketLink", absintheSocketLink) 24 | 25 | /* eslint-disable no-console, @typescript-eslint/no-explicit-any */ 26 | export const apolloLinks = ApolloLink.from([ 27 | onError(({ graphQLErrors, networkError }): void => { 28 | if (graphQLErrors) 29 | graphQLErrors.map(({ message, locations, path }): undefined => { 30 | console.log( 31 | `[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`, 32 | ) 33 | return undefined 34 | }) 35 | if (networkError) console.log(`[Network error]: ${networkError}`) 36 | }), 37 | withClientState({ 38 | defaults: { 39 | isConnected: true, 40 | }, 41 | resolvers: { 42 | Mutation: { 43 | updateNetworkStatus: ( 44 | _: any, 45 | { isConnected }: any, 46 | { thisCache }: any, 47 | ): null => { 48 | thisCache.writeData({ data: { isConnected } }) 49 | return null 50 | }, 51 | }, 52 | }, 53 | cache, 54 | }), 55 | new RetryLink({ 56 | attempts: (_count, operation, error): boolean => { 57 | return !!error && operation.operationName !== "specialCase" 58 | }, 59 | delay: (count): number => { 60 | return count * 1000 * Math.random() 61 | }, 62 | }), 63 | // new HttpLink({ 64 | // uri: GQL_URI, 65 | // credentials: "same-origin", 66 | // }), 67 | // new WebSocketLink({ 68 | // uri: WEB_SOCKET_URI, 69 | // options: { 70 | // reconnect: true, 71 | // }, 72 | // }), 73 | createAbsintheSocketLink( 74 | AbsintheSocket.create(new PhoenixSocket(WEB_SOCKET_URI)), 75 | ), 76 | ]) 77 | 78 | /* eslint-enable */ 79 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/apps/{{cookiecutter.phoenix_app_slug}}_web/priv/gettext/en/LC_MESSAGES/errors.po: -------------------------------------------------------------------------------- 1 | ## `msgid`s in this file come from POT (.pot) files. 2 | ## 3 | ## Do not add, change, or remove `msgid`s manually here as 4 | ## they're tied to the ones in the corresponding POT file 5 | ## (with the same domain). 6 | ## 7 | ## Use `mix gettext.extract --merge` or `mix gettext.merge` 8 | ## to merge POT files into PO files. 9 | msgid "" 10 | msgstr "" 11 | "Language: en\n" 12 | 13 | ## From Ecto.Changeset.cast/4 14 | msgid "can't be blank" 15 | msgstr "" 16 | 17 | ## From Ecto.Changeset.unique_constraint/3 18 | msgid "has already been taken" 19 | msgstr "" 20 | 21 | ## From Ecto.Changeset.put_change/3 22 | msgid "is invalid" 23 | msgstr "" 24 | 25 | ## From Ecto.Changeset.validate_acceptance/3 26 | msgid "must be accepted" 27 | msgstr "" 28 | 29 | ## From Ecto.Changeset.validate_format/3 30 | msgid "has invalid format" 31 | msgstr "" 32 | 33 | ## From Ecto.Changeset.validate_subset/3 34 | msgid "has an invalid entry" 35 | msgstr "" 36 | 37 | ## From Ecto.Changeset.validate_exclusion/3 38 | msgid "is reserved" 39 | msgstr "" 40 | 41 | ## From Ecto.Changeset.validate_confirmation/3 42 | msgid "does not match confirmation" 43 | msgstr "" 44 | 45 | ## From Ecto.Changeset.no_assoc_constraint/3 46 | msgid "is still associated with this entry" 47 | msgstr "" 48 | 49 | msgid "are still associated with this entry" 50 | msgstr "" 51 | 52 | ## From Ecto.Changeset.validate_length/3 53 | msgid "should be %{count} character(s)" 54 | msgid_plural "should be %{count} character(s)" 55 | msgstr[0] "" 56 | msgstr[1] "" 57 | 58 | msgid "should have %{count} item(s)" 59 | msgid_plural "should have %{count} item(s)" 60 | msgstr[0] "" 61 | msgstr[1] "" 62 | 63 | msgid "should be at least %{count} character(s)" 64 | msgid_plural "should be at least %{count} character(s)" 65 | msgstr[0] "" 66 | msgstr[1] "" 67 | 68 | msgid "should have at least %{count} item(s)" 69 | msgid_plural "should have at least %{count} item(s)" 70 | msgstr[0] "" 71 | msgstr[1] "" 72 | 73 | msgid "should be at most %{count} character(s)" 74 | msgid_plural "should be at most %{count} character(s)" 75 | msgstr[0] "" 76 | msgstr[1] "" 77 | 78 | msgid "should have at most %{count} item(s)" 79 | msgid_plural "should have at most %{count} item(s)" 80 | msgstr[0] "" 81 | msgstr[1] "" 82 | 83 | ## From Ecto.Changeset.validate_number/3 84 | msgid "must be less than %{number}" 85 | msgstr "" 86 | 87 | msgid "must be greater than %{number}" 88 | msgstr "" 89 | 90 | msgid "must be less than or equal to %{number}" 91 | msgstr "" 92 | 93 | msgid "must be greater than or equal to %{number}" 94 | msgstr "" 95 | 96 | msgid "must be equal to %{number}" 97 | msgstr "" 98 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/apps/{{cookiecutter.phoenix_app_slug}}_web/priv/gettext/errors.pot: -------------------------------------------------------------------------------- 1 | ## This is a PO Template file. 2 | ## 3 | ## `msgid`s here are often extracted from source code. 4 | ## Add new translations manually only if they're dynamic 5 | ## translations that can't be statically extracted. 6 | ## 7 | ## Run `mix gettext.extract` to bring this file up to 8 | ## date. Leave `msgstr`s empty as changing them here has no 9 | ## effect: edit them in PO (`.po`) files instead. 10 | 11 | ## From Ecto.Changeset.cast/4 12 | msgid "can't be blank" 13 | msgstr "" 14 | 15 | ## From Ecto.Changeset.unique_constraint/3 16 | msgid "has already been taken" 17 | msgstr "" 18 | 19 | ## From Ecto.Changeset.put_change/3 20 | msgid "is invalid" 21 | msgstr "" 22 | 23 | ## From Ecto.Changeset.validate_acceptance/3 24 | msgid "must be accepted" 25 | msgstr "" 26 | 27 | ## From Ecto.Changeset.validate_format/3 28 | msgid "has invalid format" 29 | msgstr "" 30 | 31 | ## From Ecto.Changeset.validate_subset/3 32 | msgid "has an invalid entry" 33 | msgstr "" 34 | 35 | ## From Ecto.Changeset.validate_exclusion/3 36 | msgid "is reserved" 37 | msgstr "" 38 | 39 | ## From Ecto.Changeset.validate_confirmation/3 40 | msgid "does not match confirmation" 41 | msgstr "" 42 | 43 | ## From Ecto.Changeset.no_assoc_constraint/3 44 | msgid "is still associated with this entry" 45 | msgstr "" 46 | 47 | msgid "are still associated with this entry" 48 | msgstr "" 49 | 50 | ## From Ecto.Changeset.validate_length/3 51 | msgid "should be %{count} character(s)" 52 | msgid_plural "should be %{count} character(s)" 53 | msgstr[0] "" 54 | msgstr[1] "" 55 | 56 | msgid "should have %{count} item(s)" 57 | msgid_plural "should have %{count} item(s)" 58 | msgstr[0] "" 59 | msgstr[1] "" 60 | 61 | msgid "should be at least %{count} character(s)" 62 | msgid_plural "should be at least %{count} character(s)" 63 | msgstr[0] "" 64 | msgstr[1] "" 65 | 66 | msgid "should have at least %{count} item(s)" 67 | msgid_plural "should have at least %{count} item(s)" 68 | msgstr[0] "" 69 | msgstr[1] "" 70 | 71 | msgid "should be at most %{count} character(s)" 72 | msgid_plural "should be at most %{count} character(s)" 73 | msgstr[0] "" 74 | msgstr[1] "" 75 | 76 | msgid "should have at most %{count} item(s)" 77 | msgid_plural "should have at most %{count} item(s)" 78 | msgstr[0] "" 79 | msgstr[1] "" 80 | 81 | ## From Ecto.Changeset.validate_number/3 82 | msgid "must be less than %{number}" 83 | msgstr "" 84 | 85 | msgid "must be greater than %{number}" 86 | msgstr "" 87 | 88 | msgid "must be less than or equal to %{number}" 89 | msgstr "" 90 | 91 | msgid "must be greater than or equal to %{number}" 92 | msgstr "" 93 | 94 | msgid "must be equal to %{number}" 95 | msgstr "" 96 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/apps/{{cookiecutter.phoenix_app_slug}}_web/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule {{cookiecutter.phoenix_app_module}}Web.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :{{cookiecutter.phoenix_app_slug}}_web, 7 | version: "{{cookiecutter.version}}", 8 | build_path: "../../_build", 9 | config_path: "../../config/config.exs", 10 | deps_path: "../../deps", 11 | lockfile: "../../mix.lock", 12 | elixir: "~> 1.9", 13 | elixirc_paths: elixirc_paths(Mix.env()), 14 | compilers: [:phoenix, :gettext] ++ Mix.compilers(), 15 | start_permanent: Mix.env() == :prod, 16 | aliases: aliases(), 17 | deps: deps(), 18 | test_coverage: [tool: ExCoveralls], 19 | preferred_cli_env: [ 20 | coveralls: :test, 21 | "coveralls.detail": :test, 22 | "coveralls.post": :test, 23 | "coveralls.html": :test 24 | ] 25 | ] 26 | end 27 | 28 | # Configuration for the OTP application. 29 | # 30 | # Type `mix help compile.app` for more information. 31 | def application do 32 | [ 33 | mod: {{'{'}}{{cookiecutter.phoenix_app_module}}Web.Application, []}, 34 | extra_applications: [:logger, :runtime_tools] 35 | ] 36 | end 37 | 38 | # Specifies which paths to compile per environment. 39 | defp elixirc_paths(:test), do: ["lib", "test/support"] 40 | defp elixirc_paths(_), do: ["lib"] 41 | 42 | # Specifies your project dependencies. 43 | # 44 | # Type `mix help deps` for examples and options. 45 | defp deps do 46 | [ 47 | {:dialyxir, "~> 1.0.0-rc.6", only: [:dev, :test], runtime: false}, 48 | {:excoveralls, "~> 0.10", only: :test}, 49 | {:credo, "~> 1.0.0", only: [:dev, :test], runtime: false}, 50 | {:phoenix, "~> 1.4.7"}, 51 | {:phoenix_pubsub, "~> 1.1"}, 52 | {:phoenix_ecto, "~> 4.0"}, 53 | {:phoenix_html, "~> 2.11"}, 54 | {:phoenix_live_reload, "~> 1.2", only: :dev}, 55 | {:gettext, "~> 0.11"}, 56 | {:{{cookiecutter.phoenix_app_slug}}, in_umbrella: true}, 57 | {:jason, "~> 1.0"}, 58 | {:plug_cowboy, "~> 2.0"}, 59 | {:cowboy, "< 2.8.0", override: true}, 60 | {:absinthe, "~> 1.4"}, 61 | {:absinthe_plug, "~> 1.4"}, 62 | {:absinthe_ecto, "~> 0.1.3"}, 63 | {:absinthe_phoenix, "~> 1.4.0"}, 64 | {:ex_machina, "~> 2.3", only: :test} 65 | ] 66 | end 67 | 68 | # Aliases are shortcuts or tasks specific to the current project. 69 | # For example, we extend the test task to create and migrate the database. 70 | # 71 | # See the documentation for `Mix` for more info on aliases. 72 | defp aliases do 73 | [test: ["ecto.create --quiet", "ecto.migrate", "test"]] 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/config/dev.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | # Configure your database 4 | config :{{cookiecutter.phoenix_app_slug}}, {{cookiecutter.phoenix_app_module}}.Repo, 5 | username: "postgres", 6 | password: "postgres", 7 | database: "{{cookiecutter.phoenix_app_slug}}_dev", 8 | hostname: "localhost", 9 | show_sensitive_data_on_connection_error: true, 10 | pool_size: 10 11 | 12 | # For development, we disable any cache and enable 13 | # debugging and code reloading. 14 | # 15 | # The watchers configuration can be used to run external 16 | # watchers to your application. For example, we use it 17 | # with webpack to recompile .js and .css sources. 18 | config :{{cookiecutter.phoenix_app_slug}}_web, {{cookiecutter.phoenix_app_module}}Web.Endpoint, 19 | http: [port: 4000], 20 | debug_errors: true, 21 | code_reloader: true, 22 | check_origin: false, 23 | watchers: [ 24 | node: [ 25 | "node_modules/webpack/bin/webpack.js", 26 | "--mode", 27 | "development", 28 | "--watch-stdin", 29 | cd: Path.expand("../apps/{{cookiecutter.phoenix_app_slug}}_web/assets", __DIR__) 30 | ] 31 | ] 32 | 33 | # ## SSL Support 34 | # 35 | # In order to use HTTPS in development, a self-signed 36 | # certificate can be generated by running the following 37 | # Mix task: 38 | # 39 | # mix phx.gen.cert 40 | # 41 | # Note that this task requires Erlang/OTP 20 or later. 42 | # Run `mix help phx.gen.cert` for more information. 43 | # 44 | # The `http:` config above can be replaced with: 45 | # 46 | # https: [ 47 | # port: 4001, 48 | # cipher_suite: :strong, 49 | # keyfile: "priv/cert/selfsigned_key.pem", 50 | # certfile: "priv/cert/selfsigned.pem" 51 | # ], 52 | # 53 | # If desired, both `http:` and `https:` keys can be 54 | # configured to run both http and https servers on 55 | # different ports. 56 | 57 | # Watch static and templates for browser reloading. 58 | config :{{cookiecutter.phoenix_app_slug}}_web, {{cookiecutter.phoenix_app_module}}Web.Endpoint, 59 | live_reload: [ 60 | patterns: [ 61 | ~r"priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$", 62 | ~r"priv/gettext/.*(po)$", 63 | ~r"lib/{{cookiecutter.phoenix_app_slug}}_web/{live,views}/.*(ex)$", 64 | ~r"lib/{{cookiecutter.phoenix_app_slug}}_web/templates/.*(eex)$" 65 | ] 66 | ] 67 | 68 | # Do not include metadata nor timestamps in development logs 69 | config :logger, :console, format: "[$level] $message\n" 70 | 71 | # Initialize plugs at runtime for faster development compilation 72 | config :phoenix, :plug_init_mode, :runtime 73 | 74 | # Set a higher stacktrace during development. Avoid configuring such 75 | # in production as building large stacktraces may be expensive. 76 | config :phoenix, :stacktrace_depth, 20 77 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/Dockerfile: -------------------------------------------------------------------------------- 1 | 2 | # The version of Alpine to use for the final image 3 | ARG ALPINE_VERSION=3.9 4 | 5 | FROM elixir:1.9.0-alpine AS builder 6 | 7 | # The following are build arguments used to change variable parts of the image. 8 | # The name of your application/release (required) 9 | ARG APP_NAME 10 | # The version of the application we are building (required) 11 | ARG APP_VSN 12 | # The environment to build with 13 | ARG MIX_ENV=prod 14 | # Set this to true if this release is not a Phoenix app 15 | ARG SKIP_PHOENIX=false 16 | # If you are using an umbrella project, you can change this 17 | # argument to the directory the Phoenix app is in so that the assets 18 | # can be built 19 | ARG PHOENIX_SUBDIR=apps/{{cookiecutter.phoenix_app_slug}}_web 20 | 21 | ENV SKIP_PHOENIX=${SKIP_PHOENIX} \ 22 | APP_NAME=${APP_NAME} \ 23 | APP_VSN=${APP_VSN} \ 24 | MIX_ENV=${MIX_ENV} 25 | 26 | # By convention, /opt is typically used for applications 27 | RUN mkdir -p /opt/app 28 | WORKDIR /opt/app 29 | 30 | # This step installs all the build tools we'll need 31 | RUN apk update && \ 32 | apk upgrade --no-cache && \ 33 | apk add --no-cache \ 34 | nodejs \ 35 | yarn \ 36 | git \ 37 | build-base && \ 38 | mix local.rebar --force && \ 39 | mix local.hex --force 40 | 41 | # install mix dependencies 42 | COPY mix.exs mix.lock ./ 43 | COPY config config 44 | COPY apps apps 45 | 46 | RUN mix deps.get --only production 47 | 48 | # This step builds assets for the Phoenix app (if there is one) 49 | # If you aren't building a Phoenix app, pass `--build-arg SKIP_PHOENIX=true` 50 | # This is mostly here for demonstration purposes 51 | RUN if [ ! "$SKIP_PHOENIX" = "true" ]; then \ 52 | mix deps.clean --all && \ 53 | mix deps.get --only prod && \ 54 | cd ${PHOENIX_SUBDIR}/assets && \ 55 | yarn install && \ 56 | yarn deploy && \ 57 | cd .. && \ 58 | mix phx.digest.clean; \ 59 | fi 60 | 61 | COPY rel rel 62 | RUN MIX_ENV=${MIX_ENV} mix release 63 | 64 | # From this line onwards, we're in a new image, which will be the image used in production 65 | FROM alpine:${ALPINE_VERSION} 66 | 67 | # The name of your application/release (required) 68 | ARG APP_NAME 69 | # The environment to build with 70 | ARG MIX_ENV=prod 71 | 72 | # The time zone (needed by Timex) 73 | ARG TZ=Asia/Manila 74 | 75 | EXPOSE 4000 76 | 77 | RUN apk update && \ 78 | apk add --no-cache \ 79 | bash \ 80 | openssl-dev 81 | 82 | ENV TZ=${TZ} \ 83 | APP_NAME=${APP_NAME} \ 84 | HOSTNAME=localhost \ 85 | PORT=4000 \ 86 | HOME=/app 87 | 88 | WORKDIR /opt/app 89 | 90 | COPY --from=builder /opt/app/_build/${MIX_ENV}/rel/${APP_NAME} . 91 | RUN chown -R nobody: /opt/app 92 | USER nobody 93 | 94 | ENV HOME=/opt/app 95 | 96 | CMD trap 'exit' INT; /opt/app/bin/${APP_NAME} start 97 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/apps/{{cookiecutter.phoenix_app_slug}}_web/assets/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | "@typescript-eslint", 4 | "eslint-comments", 5 | "jest", 6 | "promise", 7 | "unicorn", 8 | "react", 9 | "react-hooks", 10 | ], 11 | extends: [ 12 | "airbnb-typescript", 13 | "plugin:@typescript-eslint/recommended", 14 | "plugin:eslint-comments/recommended", 15 | "plugin:jest/recommended", 16 | "plugin:react/recommended", 17 | "plugin:promise/recommended", 18 | "plugin:unicorn/recommended", 19 | "prettier", 20 | "prettier/react", 21 | "prettier/@typescript-eslint", 22 | ], 23 | env: { 24 | node: true, 25 | browser: true, 26 | jest: true, 27 | }, 28 | globals: { 29 | location: "readonly", 30 | }, 31 | rules: { 32 | // Too restrictive, writing ugly code to defend against a very unlikely scenario: https://eslint.org/docs/rules/no-prototype-builtins 33 | "no-prototype-builtins": "off", 34 | // https://basarat.gitbooks.io/typescript/docs/tips/defaultIsBad.html 35 | "import/prefer-default-export": "off", 36 | "import/no-default-export": "error", 37 | // Too restrictive: https://github.com/yannickcr/eslint-plugin-react/blob/master/docs/rules/destructuring-assignment.md 38 | "react/destructuring-assignment": "off", 39 | "jsx-a11y/label-has-associated-control": [ 40 | "error", 41 | { 42 | required: { 43 | some: ["nesting", "id"], 44 | }, 45 | }, 46 | ], 47 | "jsx-a11y/label-has-for": [ 48 | "error", 49 | { 50 | required: { 51 | some: ["nesting", "id"], 52 | }, 53 | }, 54 | ], 55 | "import/no-extraneous-dependencies": [ 56 | "error", 57 | { 58 | devDependencies: true, 59 | }, 60 | ], 61 | "@typescript-eslint/no-explicit-any": 0, 62 | "@typescript-eslint/explicit-function-return-type": [ 63 | "warn", 64 | { 65 | allowExpressions: true, 66 | allowTypedFunctionExpressions: true, 67 | }, 68 | ], 69 | "react/prop-types": 0, 70 | "react/prefer-stateless-function": 2, 71 | // very unlikely you'll stumble with the radix issue, unless you're trying to be clever with parseInt 72 | radix: 0, 73 | // we don't need to return or catch promises for our usage 74 | // see more here: https://gist.github.com/mwickett/2aecfdeea40daa07d39e11922ae1fe20#gistcomment-2153642 75 | "promise/always-return": 0, 76 | "promise/catch-or-return": 0, 77 | "unicorn/prevent-abbreviations": 0, 78 | "import/no-cycle": 0, // This option acts weird when using resolvers 79 | // "react-hooks/exhaustive-deps": "warn", // idk, this rule is highly unstable 80 | "react-hooks/rules-of-hooks": "error", 81 | }, 82 | overrides: [ 83 | { 84 | files: ["*.test.ts", "*.test.tsx"], 85 | }, 86 | ], 87 | settings: { 88 | react: { 89 | version: "detect", // Tells eslint-plugin-react to automatically detect the version of React to use 90 | }, 91 | "import/resolver": "webpack", 92 | }, 93 | } 94 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/apps/{{cookiecutter.phoenix_app_slug}}_web/assets/src/app/subscription-demo/subscription-demo.tsx: -------------------------------------------------------------------------------- 1 | import { useMutation, useQuery, useSubscription } from "@apollo/react-hooks" 2 | import { RouteComponentProps } from "@reach/router" 3 | import { Field, Formik, Form as FormikForm } from "formik" 4 | import React from "react" 5 | import { 6 | Button, 7 | Container, 8 | Divider, 9 | Form, 10 | Grid, 11 | Header, 12 | Input, 13 | Label, 14 | Segment, 15 | } from "semantic-ui-react" 16 | 17 | import { ADD_USER, LIST_USERS, USER_ADDED } from "../../graphql" 18 | 19 | export const SubscriptionDemo: React.SFC = () => { 20 | const { loading, error, data } = useSubscription(USER_ADDED) 21 | const [mutate, mutationData] = useMutation(ADD_USER) 22 | 23 | return ( 24 | 25 |
26 | Subscription Demo 27 |
28 |

29 | The following examples are demo for working Subscription and Query 30 | GraphQL operations in the frontend 31 |

32 | 33 | 34 | 35 | { 41 | mutate({ 42 | variables: values, 43 | }).then(() => { 44 | console.log("User Added") 45 | }) 46 | }} 47 | render={formikProps => { 48 | const { handleChange, handleBlur, values } = formikProps 49 | return ( 50 |
51 | 52 | 53 |
Add User
54 |
55 | 56 | 57 | 65 | 66 | 67 | 68 | 76 | 77 | 80 |
81 |
82 | ) 83 | }} 84 | /> 85 |
86 | 87 | {data ? ( 88 | <> 89 |
{data.userAdded.name}
90 | 91 | 92 | ) : ( 93 |
No user yet
94 | )} 95 |
96 |
97 |
98 |
Current Users:
99 | {data ? ( 100 | 101 |

{data.userAdded.name}

102 | Age: {data.userAdded.age} 103 |
104 | ) : ( 105 | 106 |

No User

107 |
108 | )} 109 |
110 | ) 111 | } 112 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cookiecutter-elixir-phoenix 2 | 3 | An Elixir + Phoenix umbrella project template 4 | 5 | ## Sections 6 | 7 | - [Features](#features) 8 | - [Cookiecutter Requirements](#cookiecutter-requirements) 9 | - [Cookiecutter Usage](#cookiecutter-usage) 10 | - [Setting Everything Up](#setting-everything-up) 11 | - [Running the project](#running-the-project) 12 | - [Recommended VS Code Plugins](#recommended-vs-code-plugins) 13 | 14 | ## Features 15 | - Elixir (1.9.0) 16 | - Phoenix (1.4) 17 | - Absinthe (Elixir GraphQL library) 18 | - Credo 19 | - Dialyzer 20 | - Excoveralls 21 | - Dockerfile 22 | - Gitlab CI Configurations 23 | - React (latest) 24 | - TypeScript 25 | - ESLint with customized rules for Typescript and React (loosely based on Airbnb rules) 26 | - Prettier + Editorconfig 27 | - Single Page Application Routing: Reach Router with Lazy Loading 28 | - Apollo GraphQL 29 | - [React Apollo with `@apollo/react-hooks`](https://blog.apollographql.com/apollo-client-now-with-react-hooks-676d116eeae2) 30 | - Apollo Client and Links setup readily available for you 31 | - `websocket` and `subscription` working with example 32 | - Frontend optimizations with webpack configuration out of the box. Includes: 33 | - [Split Chunks Optimization with `WebpackAssetsManifest`](https://medium.com/hackernoon/the-100-correct-way-to-split-your-chunks-with-webpack-f8a9df5b7758) 34 | - Terser Minifier 35 | - [Code Splitting and Dynamic Imports](https://medium.com/@ohsiwon/code-splitting-with-dynamic-import-test-and-learn-28bc2a06d1b8) 36 | - OptimizeCSSAssetsPlugin 37 | - GZIP (Compression Plugin) 38 | - Workbox (Service Worker) 39 | - Cache Busting with Service Worker 40 | - Progressive Web App Capable 41 | 42 | ## Cookiecutter Requirements 43 | 44 | [Install python first](https://www.python.org/downloads/). We'll need `pip` or `pip3` to use `cookiecutter`. 45 | Install `cookiecutter` command line: `pip install cookiecutter` 46 | 47 | ## Cookiecutter Usage 48 | 49 | Generate a new Elixir + Phoenix + GraphQL + ReactJS + Docker project: 50 | 51 | Stable (`master`) 52 | 53 | ``` 54 | cookiecutter gh:ibakami/cookiecutter-elixir-phoenix 55 | ``` 56 | 57 | Latest (`develop`) 58 | 59 | ``` 60 | cookiecutter gh:ibakami/cookiecutter-elixir-phoenix --checkout develop 61 | ``` 62 | 63 | 70 | 71 | ## Setting everything up 72 | 73 | ### Requirements 74 | 75 | Please install the latest versions to avoid problems 76 | 77 | - [Elixir](https://elixir-lang.org/install.html) 78 | - [Phoenix](https://hexdocs.pm/phoenix/installation.html) 79 | - [Docker](https://docs.docker.com/install/) 80 | - [Docker Compose](https://docs.docker.com/compose/install/) 81 | - [Node.js](https://nodejs.org/en/) 82 | - [Yarn](https://yarnpkg.com/lang/en/) 83 | 84 | #### To avoid problems, it is recommended to do the steps in order. We also assume that you have the required application installed in your machine 85 | 86 | Initialize Git Flow (default setting). Also, do the first commit so gitignore and git hooks will be initialized too. 87 | [Git Flow Cheatsheet](https://danielkummer.github.io/git-flow-cheatsheet/) 88 | 89 | ``` 90 | git flow init 91 | git add . 92 | git commit -am "Initial Commit" 93 | ``` 94 | 95 | Build docker for postgres 96 | 97 | ``` 98 | docker-compose up -d 99 | ``` 100 | 101 | Install phoenix dependencies 102 | 103 | ``` 104 | mix deps.get 105 | ``` 106 | 107 | Setting up the database and compiling stuffs 108 | 109 | ``` 110 | mix ecto.setup 111 | ``` 112 | 113 | Getting the npm packages with yarn (avoid using npm). Change `` to your project slug name 114 | 115 | ``` 116 | (cd apps/_web/assets && yarn) 117 | ``` 118 | 119 | ## Running the project 120 | 121 | Make the phoenix project work locally 122 | 123 | ``` 124 | mix phx.server 125 | ``` 126 | 127 | ## Recommended VS Code Plugins 128 | - Better Comments 129 | - EditorConfig for VS Code 130 | - Eslint 131 | - GraphQL 132 | - Prettier - Code Formatter 133 | - vscode-elixir 134 | 135 | ## License 136 | 137 | This project is licensed under the terms of the [MIT License](/LICENSE) 138 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/apps/{{cookiecutter.phoenix_app_slug}}_web/assets/src/app.tsx: -------------------------------------------------------------------------------- 1 | import { Link } from "@reach/router" 2 | import React from "react" 3 | import { 4 | Container, 5 | Divider, 6 | Dropdown, 7 | Grid, 8 | Header, 9 | Image, 10 | List, 11 | Menu, 12 | Segment, 13 | } from "semantic-ui-react" 14 | 15 | import { AppRouter } from "./app-router" 16 | 17 | const SegmentMargin = { margin: "5em 0em 0em", padding: "5em 0em" } 18 | const ImageMargin = { marginRight: "1.5em" } 19 | const containerMargin = { margin: "5em 0", paddingTop: "2.5em" } 20 | 21 | export const App = () => ( 22 |
23 | 24 | 25 | 26 | 31 | Project Name 32 | 33 | 34 | Home 35 | 36 | 37 | Subscription Demo 38 | 39 | 40 | Reducer/Context Demo 41 | 42 | 43 | 44 | 45 | List Item 46 | List Item 47 | 48 | Header Item 49 | 50 | 51 | Submenu 52 | 53 | List Item 54 | List Item 55 | 56 | 57 | List Item 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 |
70 | 71 | Link One 72 | Link Two 73 | Link Three 74 | Link Four 75 | 76 | 77 | 78 |
79 | 80 | Link One 81 | Link Two 82 | Link Three 83 | Link Four 84 | 85 | 86 | 87 |
88 | 89 | Link One 90 | Link Two 91 | Link Three 92 | Link Four 93 | 94 | 95 | 96 |
97 |

98 | Extra space for a call to action inside the footer that could help 99 | re-engage users. 100 |

101 | 102 | 103 | 104 | 105 | 110 | 111 | 112 | Site Map 113 | 114 | 115 | Contact Us 116 | 117 | 118 | Terms and Conditions 119 | 120 | 121 | Privacy Policy 122 | 123 | 124 | 125 | 126 |
127 | ) 128 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | stages: 2 | - test 3 | - build 4 | - deploy 5 | 6 | variables: 7 | IMAGE_REPO: registry.gitlab.com/ibakami/medilink 8 | 9 | # Cache deps in between jobs 10 | cache: 11 | key: ${CI_COMMIT_REF_SLUG} 12 | paths: 13 | - _build/test 14 | 15 | # Base elixir job: 16 | # 17 | # * elixir-1.9.0-alpine 18 | # * only run on certain changes 19 | .elixir: &elixir 20 | stage: test 21 | only: 22 | changes: 23 | - .gitlab-ci.yml 24 | - mix.exs 25 | - mix.lock 26 | - apps/*/mix.exs 27 | - apps/*/mix.lock 28 | - apps/*/lib/**/*.{ex,exs} 29 | - apps/*/priv/**/*.{ex,exs} 30 | - apps/*/test/**/*.{ex,exs} 31 | image: elixir:1.9.1-alpine 32 | variables: 33 | MIX_ENV: test 34 | before_script: 35 | - mix local.rebar --force 36 | - mix local.hex --force 37 | - mix deps.get 38 | 39 | credo: 40 | <<: *elixir 41 | script: 42 | - mix credo --strict 43 | 44 | dialyzer: 45 | <<: *elixir 46 | when: manual 47 | script: 48 | - mix dialyzer 49 | 50 | mix_test: 51 | <<: *elixir 52 | variables: 53 | DATABASE_URL: ecto://postgres:postgres@postgres/{{cookiecutter.phoenix_app_module}}_test 54 | POSTGRES_DB: {{cookiecutter.phoenix_app_module}}_test 55 | POSTGRES_USER: postgres 56 | POSTGRES_PASSWORD: postgres 57 | POSTGRES_HOST: postgres 58 | services: 59 | - postgres:10.5-alpine 60 | script: 61 | - mix test 62 | 63 | coveralls: 64 | <<: *elixir 65 | image: elixir:1.9.1 66 | variables: 67 | DATABASE_URL: ecto://postgres:postgres@postgres/{{cookiecutter.phoenix_app_module}}_test 68 | POSTGRES_DB: {{cookiecutter.phoenix_app_module}}_test 69 | POSTGRES_USER: postgres 70 | POSTGRES_PASSWORD: postgres 71 | POSTGRES_HOST: postgres 72 | services: 73 | - postgres:10.5-alpine 74 | script: 75 | - mix coveralls --umbrella 76 | - > 77 | mix coveralls --umbrella | 78 | perl -n -e '/\[TOTAL\]\s+([0-9.]+)%/ && exit ($1 < 80 ? 1 : 0)' || 79 | (echo 'Code coverage below 80%!' && false) 80 | 81 | # Base docker job 82 | .docker: &docker 83 | stage: build 84 | image: docker:18.09.7 85 | variables: 86 | DOCKER_HOST: tcp://docker:2375/ 87 | # When using dind, it's wise to use the overlayfs driver for 88 | # improved performance. 89 | DOCKER_DRIVER: overlay2 90 | services: 91 | - docker:dind 92 | before_script: 93 | - docker login -u $CI_REGISTRY_USER -p $CI_JOB_TOKEN $CI_REGISTRY 94 | - docker info 95 | 96 | docker_build_develop: 97 | <<: *docker 98 | only: 99 | refs: 100 | - develop 101 | changes: 102 | - .gitlab-ci.yml 103 | - .dockerignore 104 | - Dockerfile 105 | - mix.exs 106 | - mix.lock 107 | - apps/*/config/* 108 | - apps/*/mix.exs 109 | - apps/*/mix.lock 110 | - apps/*/lib/**/*.{ex,exs} 111 | - apps/*/priv/**/*.{ex,exs} 112 | script: 113 | - docker build 114 | --build-arg APP_VSN=$(grep 'version:' apps/{{cookiecutter.phoenix_app_module}}/mix.exs | cut -d '"' -f2) 115 | -t $IMAGE_REPO/$CI_PROJECT_NAME:latest 116 | -t $IMAGE_REPO/$CI_PROJECT_NAME:${CI_COMMIT_SHORT_SHA} 117 | . 118 | - docker push $IMAGE_REPO/$CI_PROJECT_NAME:latest 119 | - docker push $IMAGE_REPO/$CI_PROJECT_NAME:${CI_COMMIT_SHORT_SHA} 120 | 121 | docker_build_features: 122 | <<: *docker 123 | when: manual 124 | only: 125 | - /^feature\/.*$/ 126 | script: 127 | - docker build 128 | --build-arg APP_VSN=$(grep 'version:' apps/{{cookiecutter.phoenix_app_module}}/mix.exs | cut -d '"' -f2) 129 | -t $IMAGE_REPO/$CI_PROJECT_NAME:latest 130 | -t $IMAGE_REPO/$CI_PROJECT_NAME:${CI_COMMIT_SHORT_SHA} 131 | . 132 | - docker push $IMAGE_REPO/$CI_PROJECT_NAME:latest 133 | - docker push $IMAGE_REPO/$CI_PROJECT_NAME:${CI_COMMIT_SHORT_SHA} 134 | 135 | docker_build_release_candidate: 136 | <<: *docker 137 | when: manual 138 | only: 139 | - /^release\/.*$/ 140 | script: 141 | - export APP_VSN=$(grep 'version:' apps/{{cookiecutter.phoenix_app_module}}/mix.exs | cut -d '"' -f2) 142 | - docker build 143 | --build-arg APP_VSN=${APP_VSN} 144 | -t $IMAGE_REPO/$CI_PROJECT_NAME:latest 145 | -t $IMAGE_REPO/$CI_PROJECT_NAME:${APP_VSN}.${CI_COMMIT_SHORT_SHA} 146 | . 147 | - docker push $IMAGE_REPO/$CI_PROJECT_NAME:latest 148 | - docker push $IMAGE_REPO/$CI_PROJECT_NAME:${APP_VSN}.${CI_COMMIT_SHORT_SHA} 149 | 150 | docker_build_tags: 151 | <<: *docker 152 | only: 153 | - tags 154 | script: 155 | - docker build 156 | --build-arg APP_VSN=$(grep 'version:' apps/{{cookiecutter.phoenix_app_module}}/mix.exs | cut -d '"' -f2) 157 | -t $IMAGE_REPO/$CI_PROJECT_NAME:latest 158 | -t $IMAGE_REPO/$CI_PROJECT_NAME:$CI_COMMIT_TAG 159 | . 160 | - docker push $IMAGE_REPO/$CI_PROJECT_NAME:latest 161 | - docker push $IMAGE_REPO/$CI_PROJECT_NAME:$CI_COMMIT_TAG 162 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/apps/{{cookiecutter.phoenix_app_slug}}_web/assets/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "repository": {}, 3 | "license": "MIT", 4 | "scripts": { 5 | "deploy": "webpack --mode production", 6 | "watch": "webpack --mode development --watch", 7 | "format:prettier": "prettier --write '**/*.{css,gql,graphql,html,js,jsx,json,less,md,mdx,scss,ts,tsx,vue,yaml,yml}' '.editorconfig' 'LICENSE'", 8 | "lint:scripts": "eslint --cache --ext .js,.jsx,.ts,.tsx --format=pretty ./src/", 9 | "typecheck": "tsc --noEmit" 10 | }, 11 | "importSort": { 12 | ".js, .jsx": { 13 | "parser": "babylon", 14 | "style": "module" 15 | }, 16 | ".ts, .tsx": { 17 | "parser": "typescript", 18 | "style": "module" 19 | } 20 | }, 21 | "husky": { 22 | "hooks": { 23 | "pre-commit": "tsc --noEmit && lint-staged" 24 | } 25 | }, 26 | "lint-staged": { 27 | "**/*.{css,gql,graphql,html,json,less,md,mdx,scss,vue,yaml,yml}": [ 28 | "prettier --write", 29 | "git add" 30 | ], 31 | "**/*.{js,jsx,ts,tsx}": [ 32 | "import-sort --write", 33 | "prettier --write", 34 | "eslint --cache --ext .js,.jsx,.ts,.tsx --fix", 35 | "git add" 36 | ] 37 | }, 38 | "dependencies": { 39 | "@absinthe/socket-apollo-link": "^0.2.1", 40 | "@apollo/react-hooks": "^3.0.0", 41 | "@types/styled-components": "^4.1.18", 42 | "apollo-boost": "^0.4.3", 43 | "apollo-client": "^2.6.4", 44 | "apollo-link-error": "^1.1.11", 45 | "apollo-link-http": "^1.5.15", 46 | "apollo-link-retry": "^2.2.14", 47 | "apollo-link-state": "^0.4.2", 48 | "apollo-link-ws": "^1.0.18", 49 | "axios": "^0.19.0", 50 | "formik": "^1.5.8", 51 | "graphql": "^14.4.2", 52 | "graphql-tag": "^2.10.1", 53 | "lodash": "^4.17.15", 54 | "phoenix": "file:../../../deps/phoenix", 55 | "phoenix_html": "file:../../../deps/phoenix_html", 56 | "react": "^16.8.6", 57 | "react-dom": "^16.8.6", 58 | "semantic-ui-react": "^0.87.3", 59 | "styled-components": "^4.3.2", 60 | "subscriptions-transport-ws": "^0.9.16", 61 | "yup": "^0.27.0" 62 | }, 63 | "babel": { 64 | "presets": [ 65 | "@babel/env", 66 | "@babel/preset-typescript", 67 | "@babel/preset-react" 68 | ], 69 | "plugins": [ 70 | "lodash", 71 | "@babel/plugin-transform-runtime", 72 | "@babel/syntax-dynamic-import" 73 | ] 74 | }, 75 | "devDependencies": { 76 | "@babel/core": "^7.5.5", 77 | "@babel/plugin-proposal-class-properties": "^7.5.5", 78 | "@babel/plugin-proposal-object-rest-spread": "^7.5.5", 79 | "@babel/plugin-syntax-dynamic-import": "^7.2.0", 80 | "@babel/plugin-transform-runtime": "^7.5.5", 81 | "@babel/preset-env": "^7.5.5", 82 | "@babel/preset-react": "^7.0.0", 83 | "@babel/preset-typescript": "^7.3.3", 84 | "@reach/router": "^1.2.1", 85 | "@types/lodash": "^4.14.136", 86 | "@types/phoenix": "^1.4.2", 87 | "@types/reach__router": "^1.2.4", 88 | "@types/react": "^16.8.25", 89 | "@types/react-dom": "^16.8.5", 90 | "@typescript-eslint/eslint-plugin": "1.13.0", 91 | "@typescript-eslint/parser": "1.13.0", 92 | "babel-loader": "^8.0.6", 93 | "babel-plugin-lodash": "^3.3.4", 94 | "clean-webpack-plugin": "^3.0.0", 95 | "compression-webpack-plugin": "^3.0.0", 96 | "copy-webpack-plugin": "^5.0.4", 97 | "css-loader": "^3.2.0", 98 | "eslint": "5.16.0", 99 | "eslint-config-airbnb-typescript": "^4.0.1", 100 | "eslint-config-prettier": "^6.0.0", 101 | "eslint-formatter-pretty": "^2.1.1", 102 | "eslint-import-resolver-webpack": "^0.11.1", 103 | "eslint-plugin-eslint-comments": "^3.1.2", 104 | "eslint-plugin-import": "^2.18.2", 105 | "eslint-plugin-jest": "^22.15.0", 106 | "eslint-plugin-jsx-a11y": "^6.2.3", 107 | "eslint-plugin-promise": "^4.2.1", 108 | "eslint-plugin-react": "^7.14.3", 109 | "eslint-plugin-react-hooks": "^1.6.1", 110 | "eslint-plugin-unicorn": "9.1.1", 111 | "file-loader": "^4.2.0", 112 | "husky": "^3.0.2", 113 | "image-loader": "^0.0.1", 114 | "import-sort": "^6.0.0", 115 | "import-sort-cli": "^6.0.0", 116 | "import-sort-parser-babylon": "^6.0.0", 117 | "import-sort-parser-typescript": "^6.0.0", 118 | "import-sort-style-module": "^6.0.0", 119 | "lint-staged": "^9.2.1", 120 | "lodash-webpack-plugin": "^0.11.5", 121 | "mini-css-extract-plugin": "^0.8.0", 122 | "node-sass": "^4.12.0", 123 | "optimize-css-assets-webpack-plugin": "^5.0.3", 124 | "prettier": "^1.18.2", 125 | "sass-loader": "^7.1.0", 126 | "stylelint": "^10.1.0", 127 | "stylelint-config-recommended": "^2.2.0", 128 | "terser-webpack-plugin": "^1.4.1", 129 | "typescript": "^3.5.3", 130 | "uglifyjs-webpack-plugin": "^2.2.0", 131 | "url-loader": "^2.1.0", 132 | "webpack": "^4.39.1", 133 | "webpack-assets-manifest": "^3.1.1", 134 | "webpack-cli": "^3.3.6", 135 | "workbox-webpack-plugin": "^4.3.1" 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/.credo.exs: -------------------------------------------------------------------------------- 1 | # This file contains the configuration for Credo and you are probably reading 2 | # this after creating it with `mix credo.gen.config`. 3 | # 4 | # If you find anything wrong or unclear in this file, please report an 5 | # issue on GitHub: https://github.com/rrrene/credo/issues 6 | # 7 | %{ 8 | # 9 | # You can have as many configs as you like in the `configs:` field. 10 | configs: [ 11 | %{ 12 | # 13 | # Run any exec using `mix credo -C `. If no exec name is given 14 | # "default" is used. 15 | # 16 | name: "default", 17 | # 18 | # These are the files included in the analysis: 19 | files: %{ 20 | # 21 | # You can give explicit globs or simply directories. 22 | # In the latter case `**/*.{ex,exs}` will be used. 23 | # 24 | included: ["lib/", "src/", "test/", "web/", "apps/"], 25 | excluded: [~r"/_build/", ~r"/deps/", ~r"/node_modules/"] 26 | }, 27 | # 28 | # If you create your own checks, you must specify the source files for 29 | # them here, so they can be loaded by Credo before running the analysis. 30 | # 31 | requires: [], 32 | # 33 | # If you want to enforce a style guide and need a more traditional linting 34 | # experience, you can change `strict` to `true` below: 35 | # 36 | strict: false, 37 | # 38 | # If you want to use uncolored output by default, you can change `color` 39 | # to `false` below: 40 | # 41 | color: true, 42 | # 43 | # You can customize the parameters of any check by adding a second element 44 | # to the tuple. 45 | # 46 | # To disable a check put `false` as second element: 47 | # 48 | # {Credo.Check.Design.DuplicatedCode, false} 49 | # 50 | checks: [ 51 | # 52 | ## Consistency Checks 53 | # 54 | {Credo.Check.Consistency.ExceptionNames, []}, 55 | {Credo.Check.Consistency.LineEndings, []}, 56 | {Credo.Check.Consistency.ParameterPatternMatching, []}, 57 | {Credo.Check.Consistency.SpaceAroundOperators, []}, 58 | {Credo.Check.Consistency.SpaceInParentheses, []}, 59 | {Credo.Check.Consistency.TabsOrSpaces, []}, 60 | 61 | # 62 | ## Design Checks 63 | # 64 | # You can customize the priority of any check 65 | # Priority values are: `low, normal, high, higher` 66 | # 67 | {Credo.Check.Design.AliasUsage, 68 | [priority: :low, if_nested_deeper_than: 2, if_called_more_often_than: 0]}, 69 | # You can also customize the exit_status of each check. 70 | # If you don't want TODO comments to cause `mix credo` to fail, just 71 | # set this value to 0 (zero). 72 | # 73 | {Credo.Check.Design.TagTODO, [exit_status: 2]}, 74 | {Credo.Check.Design.TagFIXME, []}, 75 | 76 | # 77 | ## Readability Checks 78 | # 79 | {Credo.Check.Readability.AliasOrder, []}, 80 | {Credo.Check.Readability.FunctionNames, []}, 81 | {Credo.Check.Readability.LargeNumbers, []}, 82 | {Credo.Check.Readability.MaxLineLength, [priority: :low, max_length: 120]}, 83 | {Credo.Check.Readability.ModuleAttributeNames, []}, 84 | {Credo.Check.Readability.ModuleDoc, []}, 85 | {Credo.Check.Readability.ModuleNames, []}, 86 | {Credo.Check.Readability.ParenthesesInCondition, []}, 87 | {Credo.Check.Readability.ParenthesesOnZeroArityDefs, []}, 88 | {Credo.Check.Readability.PredicateFunctionNames, []}, 89 | {Credo.Check.Readability.PreferImplicitTry, []}, 90 | {Credo.Check.Readability.RedundantBlankLines, []}, 91 | {Credo.Check.Readability.Semicolons, []}, 92 | {Credo.Check.Readability.SpaceAfterCommas, []}, 93 | {Credo.Check.Readability.StringSigils, []}, 94 | {Credo.Check.Readability.TrailingBlankLine, []}, 95 | {Credo.Check.Readability.TrailingWhiteSpace, []}, 96 | # TODO: enable by default in Credo 1.1 97 | {Credo.Check.Readability.UnnecessaryAliasExpansion, false}, 98 | {Credo.Check.Readability.VariableNames, []}, 99 | 100 | # 101 | ## Refactoring Opportunities 102 | # 103 | {Credo.Check.Refactor.CondStatements, []}, 104 | {Credo.Check.Refactor.CyclomaticComplexity, []}, 105 | {Credo.Check.Refactor.FunctionArity, []}, 106 | {Credo.Check.Refactor.LongQuoteBlocks, []}, 107 | {Credo.Check.Refactor.MapInto, false}, 108 | {Credo.Check.Refactor.MatchInCondition, []}, 109 | {Credo.Check.Refactor.NegatedConditionsInUnless, []}, 110 | {Credo.Check.Refactor.NegatedConditionsWithElse, []}, 111 | {Credo.Check.Refactor.Nesting, []}, 112 | {Credo.Check.Refactor.PipeChainStart, 113 | [ 114 | excluded_argument_types: [:atom, :binary, :fn, :keyword, :number], 115 | excluded_functions: [] 116 | ]}, 117 | {Credo.Check.Refactor.UnlessWithElse, []}, 118 | 119 | # 120 | ## Warnings 121 | # 122 | {Credo.Check.Warning.BoolOperationOnSameValues, []}, 123 | {Credo.Check.Warning.ExpensiveEmptyEnumCheck, []}, 124 | {Credo.Check.Warning.IExPry, []}, 125 | {Credo.Check.Warning.IoInspect, []}, 126 | {Credo.Check.Warning.LazyLogging, false}, 127 | {Credo.Check.Warning.OperationOnSameValues, []}, 128 | {Credo.Check.Warning.OperationWithConstantResult, []}, 129 | {Credo.Check.Warning.RaiseInsideRescue, []}, 130 | {Credo.Check.Warning.UnusedEnumOperation, []}, 131 | {Credo.Check.Warning.UnusedFileOperation, []}, 132 | {Credo.Check.Warning.UnusedKeywordOperation, []}, 133 | {Credo.Check.Warning.UnusedListOperation, []}, 134 | {Credo.Check.Warning.UnusedPathOperation, []}, 135 | {Credo.Check.Warning.UnusedRegexOperation, []}, 136 | {Credo.Check.Warning.UnusedStringOperation, []}, 137 | {Credo.Check.Warning.UnusedTupleOperation, []}, 138 | 139 | # 140 | # Controversial and experimental checks (opt-in, just replace `false` with `[]`) 141 | # 142 | {Credo.Check.Consistency.MultiAliasImportRequireUse, false}, 143 | {Credo.Check.Design.DuplicatedCode, false}, 144 | {Credo.Check.Readability.MultiAlias, false}, 145 | {Credo.Check.Readability.Specs, []}, 146 | {Credo.Check.Refactor.ABCSize, false}, 147 | {Credo.Check.Refactor.AppendSingleItem, false}, 148 | {Credo.Check.Refactor.DoubleBooleanNegation, false}, 149 | {Credo.Check.Refactor.ModuleDependencies, false}, 150 | {Credo.Check.Refactor.VariableRebinding, false}, 151 | {Credo.Check.Warning.MapGetUnsafePass, false}, 152 | {Credo.Check.Warning.UnsafeToAtom, false} 153 | 154 | # 155 | # Custom checks can be created using `mix credo.gen.check`. 156 | # 157 | ] 158 | } 159 | ] 160 | } 161 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/apps/{{cookiecutter.phoenix_app_slug}}_web/assets/webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require("webpack") 2 | 3 | const path = require("path") 4 | const TerserPlugin = require("terser-webpack-plugin") 5 | const glob = require("glob") 6 | const MiniCssExtractPlugin = require("mini-css-extract-plugin") 7 | const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin") 8 | const CopyWebpackPlugin = require("copy-webpack-plugin") 9 | const WebpackAssetsManifest = require("webpack-assets-manifest") 10 | const CompressionPlugin = require("compression-webpack-plugin") 11 | const WorkboxPlugin = require("workbox-webpack-plugin") 12 | const LodashModuleReplacementPlugin = require("lodash-webpack-plugin") 13 | 14 | const { CleanWebpackPlugin } = require("clean-webpack-plugin") 15 | 16 | const distFolder = path.resolve(__dirname, "../priv/static") 17 | 18 | const externalAssetsURI = [ 19 | "http://cdn.jsdelivr.net/", 20 | "https://fonts.gstatic.com/", 21 | ] 22 | 23 | const productionPlugin = [ 24 | // Generates GZIP version of each file generated, Remember to enable gzip on static plug 25 | new CompressionPlugin(), 26 | ] 27 | 28 | module.exports = (env, argv) => { 29 | const dev = argv.mode !== "production" 30 | 31 | return { 32 | entry: { 33 | // the [glob.sync()] part enables us to get everything that we imported in the project 34 | "js/index": ["./src/index.tsx"].concat(glob.sync("./vendor/**/*.js")), 35 | }, 36 | output: { 37 | /** 38 | * This will produce the hashing + the naming of our bundles. 39 | * Everything in this file will go to /assets folder in our static folder 40 | * 41 | * This certain pattern will make the cache busting possible. 42 | * 43 | * We'll need to configure the assets to have cache-control header to max age 44 | * Webpack will remember the hashes we use on each assets, the hashes will change if a certain asset is modified. 45 | * The client, by default, will load the assets from the memory cache. 46 | * The client will only download a certain asset again if the hashes changed, as the client's assets with old hash won't be used anymore 47 | * 48 | * It will save a lot of network bandwidth when the user visited the app again. 49 | * And will also make your page load much faster and consistent for the first time, with HTTP2's multiplexing 50 | * 51 | * https://developers.google.com/web/fundamentals/performance/http2/ 52 | */ 53 | path: distFolder, // the base path 54 | filename: dev ? "[name].js" : "[name]_[contenthash].bundle.js", // the hash magic 55 | publicPath: "/assets/", // should match the endpoint.ex' Plug.Static 56 | chunkFilename: dev ? "js/[id].chunk" : "js/[id].[chunkhash].chunk", // the chunks (relevant file to the entrypoint) with hashing 57 | }, 58 | module: { 59 | rules: [ 60 | { 61 | test: /\.(jsx?|tsx?)$/i, 62 | exclude: "/node_modules/", 63 | loader: "babel-loader", 64 | }, 65 | { 66 | test: /\.s?css|less$/i, 67 | use: [MiniCssExtractPlugin.loader, "css-loader", "sass-loader"], 68 | }, 69 | // Some Apollo links and other libraries uses mjs extensions. We'll need a loader for that 70 | { 71 | test: /\.mjs$/, 72 | include: /node_modules/, 73 | type: "javascript/auto", 74 | }, 75 | ], 76 | }, 77 | resolve: { 78 | extensions: ["*", ".js", ".jsx", ".json", ".ts", ".tsx", ".mjs"], 79 | }, 80 | optimization: { 81 | // This will make the chunk names we produce to be human readable 82 | namedChunks: true, 83 | /** 84 | * This particular optimization splits all npm package we use into individual chunks. 85 | * This is very useful for cache busting + HTTP2's multiplexing. See why we did it here: 86 | * https://hackernoon.com/the-100-correct-way-to-split-your-chunks-with-webpack-f8a9df5b7758 87 | * 88 | * Also, we'll need manually import the scripts on phoenix' layouts because html-webpack-plugin 89 | * (the one that generates the import scripts for us) won't work in a phoenix project's frontend 90 | */ 91 | splitChunks: { 92 | chunks: "all", 93 | maxInitialRequests: Infinity, 94 | minSize: 0, 95 | cacheGroups: { 96 | vendor: { 97 | test: /[\\/]node_modules[\\/]/, 98 | name(module) { 99 | const packageName = module.context.match( 100 | /[\\/]node_modules[\\/](.*?)([\\/]|$)/, 101 | )[1] 102 | return `npm.${packageName.replace("@", "")}` 103 | }, 104 | }, 105 | }, 106 | }, 107 | minimizer: [ 108 | // we'll use terser because it's better and faster than uglifyjs 109 | new TerserPlugin({ 110 | test: /\.(js|chunk)(\?.*)?$/i, 111 | cache: true, 112 | }), 113 | new OptimizeCSSAssetsPlugin({}), 114 | ], 115 | }, 116 | // The plugin's order is important, it executes everything asynchronously. 117 | plugins: [ 118 | // Clears the priv/static for us, to avoid bloating during dev and prod 119 | new CleanWebpackPlugin(), 120 | // Copies everything on the static folder 121 | new CopyWebpackPlugin([ 122 | { from: "static/**/*", to: path.resolve(distFolder, "../") }, 123 | { from: "pwa", distFolder }, 124 | ]), 125 | // new LodashModuleReplacementPlugin(), 126 | // This certain plugin remembers the hashes we use in every assets, 127 | // so it won't change the hash of a certain asset if it's not modified 128 | new webpack.HashedModuleIdsPlugin(), 129 | // Copies the CSS, functions like webpack entrypoint 130 | new MiniCssExtractPlugin({ filename: "../css/app.scss" }), 131 | // The plugin will only be accepted 132 | ...(dev ? [] : productionPlugin), 133 | /** 134 | * Workbox is the one resposible for making PWA app possible. 135 | * Handles the caching, offline mode, installable feature, manifest, ect. 136 | * We don't need to worry about it being in dev mode, workbox handles it for us 137 | */ 138 | new WorkboxPlugin.GenerateSW({ 139 | navigationPreload: true, 140 | clientsClaim: true, 141 | skipWaiting: true, 142 | exclude: [ 143 | /^assets-manifest.*?\.json$/, 144 | /^manifest.*?\.json$/, 145 | /favicon/, 146 | /.gz$/, 147 | ], 148 | // navigateFallback: "/", 149 | runtimeCaching: [ 150 | { 151 | urlPattern: /assets\/.*/, 152 | handler: "CacheFirst", 153 | }, 154 | { 155 | // Will join each URI in the array 156 | // It will match the URI even if it's only partial 157 | urlPattern: new RegExp(`(${externalAssetsURI.join("|")})`), 158 | handler: "CacheFirst", 159 | }, 160 | { 161 | urlPattern: /.*/, 162 | handler: "NetworkFirst", 163 | }, 164 | ], 165 | }), 166 | // Generates a manifest that lists all the directories. We need it to manually import the scripts to phoenix' template 167 | // This should be the last in the plugins list 168 | new WebpackAssetsManifest({ 169 | output: "assets-manifest.json", 170 | publicPath: true, 171 | writeToDisk: true, 172 | merge: dev ? true : false, // eslint-disable-line no-unneeded-ternary 173 | }), 174 | ], 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/apps/{{cookiecutter.phoenix_app_slug}}_web/assets/css/phoenix.css: -------------------------------------------------------------------------------- 1 | /* Includes some default style for the starter application. 2 | * This can be safely deleted to start fresh. 3 | */ 4 | 5 | /* Milligram v1.3.0 https://milligram.github.io 6 | * Copyright (c) 2017 CJ Patoilo Licensed under the MIT license 7 | */ 8 | 9 | *,*:after,*:before{box-sizing:inherit}html{box-sizing:border-box;font-size:62.5%}body{color:#000000;font-family:'Helvetica', 'Arial', sans-serif;font-size:1.6em;font-weight:300;line-height:1.6}blockquote{border-left:0.3rem solid #d1d1d1;margin-left:0;margin-right:0;padding:1rem 1.5rem}blockquote *:last-child{margin-bottom:0}.button,button,input[type='button'],input[type='reset'],input[type='submit']{background-color:#0069d9;border:0.1rem solid #0069d9;border-radius:.4rem;color:#fff;cursor:pointer;display:inline-block;font-size:1.1rem;font-weight:700;height:3.8rem;letter-spacing:.1rem;line-height:3.8rem;padding:0 3.0rem;text-align:center;text-decoration:none;text-transform:uppercase;white-space:nowrap}.button:focus,.button:hover,button:focus,button:hover,input[type='button']:focus,input[type='button']:hover,input[type='reset']:focus,input[type='reset']:hover,input[type='submit']:focus,input[type='submit']:hover{background-color:#606c76;border-color:#606c76;color:#fff;outline:0}.button[disabled],button[disabled],input[type='button'][disabled],input[type='reset'][disabled],input[type='submit'][disabled]{cursor:default;opacity:.5}.button[disabled]:focus,.button[disabled]:hover,button[disabled]:focus,button[disabled]:hover,input[type='button'][disabled]:focus,input[type='button'][disabled]:hover,input[type='reset'][disabled]:focus,input[type='reset'][disabled]:hover,input[type='submit'][disabled]:focus,input[type='submit'][disabled]:hover{background-color:#0069d9;border-color:#0069d9}.button.button-outline,button.button-outline,input[type='button'].button-outline,input[type='reset'].button-outline,input[type='submit'].button-outline{background-color:transparent;color:#0069d9}.button.button-outline:focus,.button.button-outline:hover,button.button-outline:focus,button.button-outline:hover,input[type='button'].button-outline:focus,input[type='button'].button-outline:hover,input[type='reset'].button-outline:focus,input[type='reset'].button-outline:hover,input[type='submit'].button-outline:focus,input[type='submit'].button-outline:hover{background-color:transparent;border-color:#606c76;color:#606c76}.button.button-outline[disabled]:focus,.button.button-outline[disabled]:hover,button.button-outline[disabled]:focus,button.button-outline[disabled]:hover,input[type='button'].button-outline[disabled]:focus,input[type='button'].button-outline[disabled]:hover,input[type='reset'].button-outline[disabled]:focus,input[type='reset'].button-outline[disabled]:hover,input[type='submit'].button-outline[disabled]:focus,input[type='submit'].button-outline[disabled]:hover{border-color:inherit;color:#0069d9}.button.button-clear,button.button-clear,input[type='button'].button-clear,input[type='reset'].button-clear,input[type='submit'].button-clear{background-color:transparent;border-color:transparent;color:#0069d9}.button.button-clear:focus,.button.button-clear:hover,button.button-clear:focus,button.button-clear:hover,input[type='button'].button-clear:focus,input[type='button'].button-clear:hover,input[type='reset'].button-clear:focus,input[type='reset'].button-clear:hover,input[type='submit'].button-clear:focus,input[type='submit'].button-clear:hover{background-color:transparent;border-color:transparent;color:#606c76}.button.button-clear[disabled]:focus,.button.button-clear[disabled]:hover,button.button-clear[disabled]:focus,button.button-clear[disabled]:hover,input[type='button'].button-clear[disabled]:focus,input[type='button'].button-clear[disabled]:hover,input[type='reset'].button-clear[disabled]:focus,input[type='reset'].button-clear[disabled]:hover,input[type='submit'].button-clear[disabled]:focus,input[type='submit'].button-clear[disabled]:hover{color:#0069d9}code{background:#f4f5f6;border-radius:.4rem;font-size:86%;margin:0 .2rem;padding:.2rem .5rem;white-space:nowrap}pre{background:#f4f5f6;border-left:0.3rem solid #0069d9;overflow-y:hidden}pre>code{border-radius:0;display:block;padding:1rem 1.5rem;white-space:pre}hr{border:0;border-top:0.1rem solid #f4f5f6;margin:3.0rem 0}input[type='email'],input[type='number'],input[type='password'],input[type='search'],input[type='tel'],input[type='text'],input[type='url'],textarea,select{-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:transparent;border:0.1rem solid #d1d1d1;border-radius:.4rem;box-shadow:none;box-sizing:inherit;height:3.8rem;padding:.6rem 1.0rem;width:100%}input[type='email']:focus,input[type='number']:focus,input[type='password']:focus,input[type='search']:focus,input[type='tel']:focus,input[type='text']:focus,input[type='url']:focus,textarea:focus,select:focus{border-color:#0069d9;outline:0}select{background:url('data:image/svg+xml;utf8,') center right no-repeat;padding-right:3.0rem}select:focus{background-image:url('data:image/svg+xml;utf8,')}textarea{min-height:6.5rem}label,legend{display:block;font-size:1.6rem;font-weight:700;margin-bottom:.5rem}fieldset{border-width:0;padding:0}input[type='checkbox'],input[type='radio']{display:inline}.label-inline{display:inline-block;font-weight:normal;margin-left:.5rem}.row{display:flex;flex-direction:column;padding:0;width:100%}.row.row-no-padding{padding:0}.row.row-no-padding>.column{padding:0}.row.row-wrap{flex-wrap:wrap}.row.row-top{align-items:flex-start}.row.row-bottom{align-items:flex-end}.row.row-center{align-items:center}.row.row-stretch{align-items:stretch}.row.row-baseline{align-items:baseline}.row .column{display:block;flex:1 1 auto;margin-left:0;max-width:100%;width:100%}.row .column.column-offset-10{margin-left:10%}.row .column.column-offset-20{margin-left:20%}.row .column.column-offset-25{margin-left:25%}.row .column.column-offset-33,.row .column.column-offset-34{margin-left:33.3333%}.row .column.column-offset-50{margin-left:50%}.row .column.column-offset-66,.row .column.column-offset-67{margin-left:66.6666%}.row .column.column-offset-75{margin-left:75%}.row .column.column-offset-80{margin-left:80%}.row .column.column-offset-90{margin-left:90%}.row .column.column-10{flex:0 0 10%;max-width:10%}.row .column.column-20{flex:0 0 20%;max-width:20%}.row .column.column-25{flex:0 0 25%;max-width:25%}.row .column.column-33,.row .column.column-34{flex:0 0 33.3333%;max-width:33.3333%}.row .column.column-40{flex:0 0 40%;max-width:40%}.row .column.column-50{flex:0 0 50%;max-width:50%}.row .column.column-60{flex:0 0 60%;max-width:60%}.row .column.column-66,.row .column.column-67{flex:0 0 66.6666%;max-width:66.6666%}.row .column.column-75{flex:0 0 75%;max-width:75%}.row .column.column-80{flex:0 0 80%;max-width:80%}.row .column.column-90{flex:0 0 90%;max-width:90%}.row .column .column-top{align-self:flex-start}.row .column .column-bottom{align-self:flex-end}.row .column .column-center{-ms-grid-row-align:center;align-self:center}@media (min-width: 40rem){.row{flex-direction:row;margin-left:-1.0rem;width:calc(100% + 2.0rem)}.row .column{margin-bottom:inherit;padding:0 1.0rem}}a{color:#0069d9;text-decoration:none}a:focus,a:hover{color:#606c76}dl,ol,ul{list-style:none;margin-top:0;padding-left:0}dl dl,dl ol,dl ul,ol dl,ol ol,ol ul,ul dl,ul ol,ul ul{font-size:90%;margin:1.5rem 0 1.5rem 3.0rem}ol{list-style:decimal inside}ul{list-style:circle inside}.button,button,dd,dt,li{margin-bottom:1.0rem}fieldset,input,select,textarea{margin-bottom:1.5rem}blockquote,dl,figure,form,ol,p,pre,table,ul{margin-bottom:2.5rem}table{border-spacing:0;width:100%}td,th{border-bottom:0.1rem solid #e1e1e1;padding:1.2rem 1.5rem;text-align:left}td:first-child,th:first-child{padding-left:0}td:last-child,th:last-child{padding-right:0}b,strong{font-weight:bold}p{margin-top:0}h1,h2,h3,h4,h5,h6{font-weight:300;letter-spacing:-.1rem;margin-bottom:2.0rem;margin-top:0}h1{font-size:4.6rem;line-height:1.2}h2{font-size:3.6rem;line-height:1.25}h3{font-size:2.8rem;line-height:1.3}h4{font-size:2.2rem;letter-spacing:-.08rem;line-height:1.35}h5{font-size:1.8rem;letter-spacing:-.05rem;line-height:1.5}h6{font-size:1.6rem;letter-spacing:0;line-height:1.4}img{max-width:100%}.clearfix:after{clear:both;content:' ';display:table}.float-left{float:left}.float-right{float:right} 10 | 11 | /* General style */ 12 | h1{font-size: 3.6rem; line-height: 1.25} 13 | h2{font-size: 2.8rem; line-height: 1.3} 14 | h3{font-size: 2.2rem; letter-spacing: -.08rem; line-height: 1.35} 15 | h4{font-size: 1.8rem; letter-spacing: -.05rem; line-height: 1.5} 16 | h5{font-size: 1.6rem; letter-spacing: 0; line-height: 1.4} 17 | h6{font-size: 1.4rem; letter-spacing: 0; line-height: 1.2} 18 | 19 | .container{ 20 | margin: 0 auto; 21 | max-width: 80.0rem; 22 | padding: 0 2.0rem; 23 | position: relative; 24 | width: 100% 25 | } 26 | select { 27 | width: auto; 28 | } 29 | 30 | /* Alerts and form errors */ 31 | .alert { 32 | padding: 15px; 33 | margin-bottom: 20px; 34 | border: 1px solid transparent; 35 | border-radius: 4px; 36 | } 37 | .alert-info { 38 | color: #31708f; 39 | background-color: #d9edf7; 40 | border-color: #bce8f1; 41 | } 42 | .alert-warning { 43 | color: #8a6d3b; 44 | background-color: #fcf8e3; 45 | border-color: #faebcc; 46 | } 47 | .alert-danger { 48 | color: #a94442; 49 | background-color: #f2dede; 50 | border-color: #ebccd1; 51 | } 52 | .alert p { 53 | margin-bottom: 0; 54 | } 55 | .alert:empty { 56 | display: none; 57 | } 58 | .help-block { 59 | color: #a94442; 60 | display: block; 61 | margin: -1rem 0 2rem; 62 | } 63 | 64 | /* Phoenix promo and logo */ 65 | .phx-hero { 66 | text-align: center; 67 | border-bottom: 1px solid #e3e3e3; 68 | background: #eee; 69 | border-radius: 6px; 70 | padding: 3em; 71 | margin-bottom: 3rem; 72 | font-weight: 200; 73 | font-size: 120%; 74 | } 75 | .phx-hero p { 76 | margin: 0; 77 | } 78 | .phx-logo { 79 | min-width: 300px; 80 | margin: 1rem; 81 | display: block; 82 | } 83 | .phx-logo img { 84 | width: auto; 85 | display: block; 86 | } 87 | 88 | /* Headers */ 89 | header { 90 | width: 100%; 91 | background: #fdfdfd; 92 | border-bottom: 1px solid #eaeaea; 93 | margin-bottom: 2rem; 94 | } 95 | header section { 96 | align-items: center; 97 | display: flex; 98 | flex-direction: column; 99 | justify-content: space-between; 100 | } 101 | header section :first-child { 102 | order: 2; 103 | } 104 | header section :last-child { 105 | order: 1; 106 | } 107 | header nav ul, 108 | header nav li { 109 | margin: 0; 110 | padding: 0; 111 | display: block; 112 | text-align: right; 113 | white-space: nowrap; 114 | } 115 | header nav ul { 116 | margin: 1rem; 117 | margin-top: 0; 118 | } 119 | header nav a { 120 | display: block; 121 | } 122 | 123 | @media (min-width: 40.0rem) { /* Small devices (landscape phones, 576px and up) */ 124 | header section { 125 | flex-direction: row; 126 | } 127 | header nav ul { 128 | margin: 1rem; 129 | } 130 | .phx-logo { 131 | flex-basis: 527px; 132 | margin: 2rem 1rem; 133 | } 134 | } 135 | --------------------------------------------------------------------------------