├── backend ├── hasura │ ├── metadata │ │ ├── network.yaml │ │ ├── allow_list.yaml │ │ ├── api_limits.yaml │ │ ├── cron_triggers.yaml │ │ ├── inherited_roles.yaml │ │ ├── query_collections.yaml │ │ ├── remote_schemas.yaml │ │ ├── rest_endpoints.yaml │ │ ├── version.yaml │ │ ├── graphql_schema_introspection.yaml │ │ ├── databases │ │ │ ├── default │ │ │ │ └── tables │ │ │ │ │ ├── tables.yaml │ │ │ │ │ ├── public_profile.yaml │ │ │ │ │ ├── public_likes.yaml │ │ │ │ │ ├── public_follows.yaml │ │ │ │ │ ├── public_users.yaml │ │ │ │ │ ├── public_tags.yaml │ │ │ │ │ ├── public_comments.yaml │ │ │ │ │ └── public_articles.yaml │ │ │ └── databases.yaml │ │ ├── actions.graphql │ │ └── actions.yaml │ ├── migrations │ │ └── default │ │ │ ├── 1590750212586_create_table_public_tags │ │ │ ├── down.sql │ │ │ └── up.sql │ │ │ ├── 1590346274456_create_table_public_users │ │ │ ├── down.sql │ │ │ └── up.sql │ │ │ ├── 1590605196105_create_table_public_articles │ │ │ ├── down.sql │ │ │ └── up.sql │ │ │ ├── 1591362644347_create_table_public_follows │ │ │ ├── down.sql │ │ │ └── up.sql │ │ │ ├── 1591363326405_drop_view_public_user_profile │ │ │ └── up.sql │ │ │ ├── 1592205504466_create_table_public_comments │ │ │ ├── down.sql │ │ │ └── up.sql │ │ │ ├── 1591123243959_create_table_public_favourites │ │ │ ├── down.sql │ │ │ └── up.sql │ │ │ ├── 1591123349453_rename_table_public_favourites │ │ │ ├── up.sql │ │ │ └── down.sql │ │ │ ├── 1591442159613_alter_table_public_users_add_column_bio │ │ │ ├── down.sql │ │ │ └── up.sql │ │ │ ├── 1591442170694_alter_table_public_users_add_column_profile_image │ │ │ ├── down.sql │ │ │ └── up.sql │ │ │ ├── 1590671257528_create_user_profile_view │ │ │ └── up.sql │ │ │ ├── 1591455792307_drop_check_constraint_public_follows_cannot_follow_self │ │ │ ├── up.sql │ │ │ └── down.sql │ │ │ ├── 1590583551228_alter_table_public_users_add_check_constraint_non_empty_email │ │ │ ├── down.sql │ │ │ └── up.sql │ │ │ ├── 1591362960012_alter_table_public_follows_alter_column_following │ │ │ ├── down.sql │ │ │ └── up.sql │ │ │ ├── 1590583471552_alter_table_public_users_add_check_constraint_non_empty_username │ │ │ ├── down.sql │ │ │ └── up.sql │ │ │ ├── 1591123476154_alter_table_public_likes_add_unique_user_id_article_id │ │ │ ├── down.sql │ │ │ └── up.sql │ │ │ ├── 1591453234774_alter_table_public_follows_add_check_constraint_cannot_follow_self │ │ │ ├── down.sql │ │ │ └── up.sql │ │ │ ├── 1590583532032_alter_table_public_users_add_check_constraint_non_empty_password_hash │ │ │ ├── down.sql │ │ │ └── up.sql │ │ │ ├── 1590917793693_tag_count │ │ │ └── up.sql │ │ │ ├── 1602056993085_create_profile_view │ │ │ └── up.sql │ │ │ └── 1591123797156_create_likes_for_article_function │ │ │ └── up.sql │ ├── config.yaml │ └── docker-compose.yaml └── actions │ ├── api │ └── index.js │ ├── .gitignore │ ├── src │ ├── Crypto │ │ ├── Jwt.js │ │ ├── Bcrypt.js │ │ ├── Jwt.purs │ │ └── Bcrypt.purs │ ├── Env │ │ ├── Env.purs │ │ └── Env.js │ ├── Token.purs │ ├── Articles.purs │ ├── Action.purs │ ├── Hasura.purs │ ├── Users.purs │ └── Password.purs │ ├── packages.dhall │ ├── .env.local │ ├── test │ ├── Main.purs │ └── PasswordTest.purs │ ├── spago.dhall │ ├── README.md │ └── package.json ├── .envrc ├── .gitignore ├── api └── index.js ├── frontend ├── src │ ├── elm │ │ ├── Hasura │ │ │ ├── elm-graphql-metadata.json │ │ │ ├── Union.elm │ │ │ ├── Interface.elm │ │ │ ├── VerifyScalarCodecs.elm │ │ │ ├── ScalarCodecs.elm │ │ │ ├── Object │ │ │ │ ├── Articles_sum_fields.elm │ │ │ │ ├── Articles_avg_fields.elm │ │ │ │ ├── Articles_stddev_fields.elm │ │ │ │ ├── Articles_var_pop_fields.elm │ │ │ │ ├── Articles_var_samp_fields.elm │ │ │ │ ├── Articles_variance_fields.elm │ │ │ │ ├── Articles_stddev_pop_fields.elm │ │ │ │ ├── Articles_stddev_samp_fields.elm │ │ │ │ ├── Follows.elm │ │ │ │ ├── Likes_max_fields.elm │ │ │ │ ├── Likes_min_fields.elm │ │ │ │ ├── Likes_sum_fields.elm │ │ │ │ ├── Likes_avg_fields.elm │ │ │ │ ├── Likes_stddev_fields.elm │ │ │ │ ├── Likes_var_pop_fields.elm │ │ │ │ ├── Likes_var_samp_fields.elm │ │ │ │ ├── Likes_variance_fields.elm │ │ │ │ ├── Likes_stddev_pop_fields.elm │ │ │ │ ├── Likes_stddev_samp_fields.elm │ │ │ │ ├── UnlikeResponse.elm │ │ │ │ ├── Likes_aggregate.elm │ │ │ │ ├── Articles_aggregate.elm │ │ │ │ ├── Tags_mutation_response.elm │ │ │ │ ├── Likes_mutation_response.elm │ │ │ │ ├── Users_mutation_response.elm │ │ │ │ ├── Follows_mutation_response.elm │ │ │ │ ├── Articles_mutation_response.elm │ │ │ │ ├── Comments_mutation_response.elm │ │ │ │ ├── Tags.elm │ │ │ │ ├── Likes.elm │ │ │ │ ├── Comments.elm │ │ │ │ ├── Articles_max_fields.elm │ │ │ │ ├── Articles_min_fields.elm │ │ │ │ └── TokenResponse.elm │ │ │ ├── Enum │ │ │ │ ├── Tags_select_column.elm │ │ │ │ ├── Comments_update_column.elm │ │ │ │ ├── Follows_select_column.elm │ │ │ │ ├── Articles_constraint.elm │ │ │ │ ├── Comments_constraint.elm │ │ │ │ ├── Likes_select_column.elm │ │ │ │ ├── Users_constraint.elm │ │ │ │ ├── Articles_update_column.elm │ │ │ │ ├── Comments_select_column.elm │ │ │ │ ├── Users_select_column.elm │ │ │ │ ├── Users_update_column.elm │ │ │ │ ├── Articles_select_column.elm │ │ │ │ └── Profile_select_column.elm │ │ │ ├── Scalar.elm │ │ │ └── Object.elm │ │ ├── Utils │ │ │ ├── List.elm │ │ │ ├── SelectionSet.elm │ │ │ ├── Update.elm │ │ │ ├── String.elm │ │ │ └── Element.elm │ │ ├── Api │ │ │ ├── Token.elm │ │ │ ├── Date.elm │ │ │ └── Authors.elm │ │ ├── Element │ │ │ ├── Layout │ │ │ │ └── Block.elm │ │ │ ├── Divider.elm │ │ │ ├── Anchor.elm │ │ │ ├── Scale.elm │ │ │ ├── Icon │ │ │ │ ├── Plane.elm │ │ │ │ ├── Pencil.elm │ │ │ │ ├── Chevron.elm │ │ │ │ ├── Ellipsis.elm │ │ │ │ ├── Heart.elm │ │ │ │ ├── Plus.elm │ │ │ │ └── Bin.elm │ │ │ ├── Icon.elm │ │ │ ├── Avatar.elm │ │ │ ├── Transition │ │ │ │ └── Simple.elm │ │ │ ├── Tab.elm │ │ │ ├── Transition.elm │ │ │ ├── Loader │ │ │ │ └── Spinner.elm │ │ │ └── Palette.elm │ │ ├── Page │ │ │ └── NotFound.elm │ │ ├── Animation │ │ │ └── Named.elm │ │ ├── Article │ │ │ ├── Feed.elm │ │ │ ├── Author.elm │ │ │ ├── Comment.elm │ │ │ ├── Page.elm │ │ │ └── Author │ │ │ │ └── Follow.elm │ │ ├── User │ │ │ └── Element.elm │ │ ├── Form │ │ │ ├── Error.elm │ │ │ ├── Field.elm │ │ │ └── Button.elm │ │ ├── Main │ │ │ └── index.d.ts │ │ ├── Context.elm │ │ ├── Ports.elm │ │ ├── Route │ │ │ └── Effect.elm │ │ ├── Tag.elm │ │ └── Article.elm │ ├── ts │ │ ├── utils.ts │ │ └── user.ts │ ├── index.ts │ ├── index.html │ └── style.css ├── .gitignore ├── tsconfig.json └── README.md ├── elm-constants.json ├── vercel.json ├── .env.local ├── vite.config.js ├── default.nix ├── .github └── workflows │ ├── cd.yml │ ├── ci.yml │ └── infrastructure.yml ├── tests ├── Program │ ├── Selector.elm │ └── Expect.elm ├── TagTest.elm ├── Page │ ├── SignUpTest.elm │ ├── SignInTest.elm │ ├── SettingsTest.elm │ ├── AuthorTest.elm │ ├── HomeTest.elm │ └── NewPostTest.elm └── Helpers.elm ├── LICENSE ├── package.json ├── elm.json └── README.md /backend/hasura/metadata/network.yaml: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /backend/hasura/metadata/allow_list.yaml: -------------------------------------------------------------------------------- 1 | [] 2 | -------------------------------------------------------------------------------- /backend/hasura/metadata/api_limits.yaml: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /backend/hasura/metadata/cron_triggers.yaml: -------------------------------------------------------------------------------- 1 | [] 2 | -------------------------------------------------------------------------------- /backend/hasura/metadata/inherited_roles.yaml: -------------------------------------------------------------------------------- 1 | [] 2 | -------------------------------------------------------------------------------- /backend/hasura/metadata/query_collections.yaml: -------------------------------------------------------------------------------- 1 | [] 2 | -------------------------------------------------------------------------------- /backend/hasura/metadata/remote_schemas.yaml: -------------------------------------------------------------------------------- 1 | [] 2 | -------------------------------------------------------------------------------- /backend/hasura/metadata/rest_endpoints.yaml: -------------------------------------------------------------------------------- 1 | [] 2 | -------------------------------------------------------------------------------- /backend/hasura/metadata/version.yaml: -------------------------------------------------------------------------------- 1 | version: 3 2 | -------------------------------------------------------------------------------- /.envrc: -------------------------------------------------------------------------------- 1 | set -o allexport 2 | source .env.local 3 | use nix -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vercel 2 | .idea 3 | dist 4 | 5 | node_modules 6 | elm-stuff -------------------------------------------------------------------------------- /api/index.js: -------------------------------------------------------------------------------- 1 | module.exports = require("../backend/actions/server.min.js"); 2 | -------------------------------------------------------------------------------- /backend/actions/api/index.js: -------------------------------------------------------------------------------- 1 | module.exports = require("../server.min.js"); 2 | -------------------------------------------------------------------------------- /backend/hasura/metadata/graphql_schema_introspection.yaml: -------------------------------------------------------------------------------- 1 | disabled_for_roles: [] 2 | -------------------------------------------------------------------------------- /backend/hasura/migrations/default/1590750212586_create_table_public_tags/down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE "public"."tags"; 2 | -------------------------------------------------------------------------------- /backend/hasura/migrations/default/1590346274456_create_table_public_users/down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE "public"."users"; 2 | -------------------------------------------------------------------------------- /backend/hasura/migrations/default/1590605196105_create_table_public_articles/down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE "public"."articles"; 2 | -------------------------------------------------------------------------------- /backend/hasura/migrations/default/1591362644347_create_table_public_follows/down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE "public"."follows"; 2 | -------------------------------------------------------------------------------- /backend/hasura/migrations/default/1591363326405_drop_view_public_user_profile/up.sql: -------------------------------------------------------------------------------- 1 | DROP VIEW "public"."user_profile"; 2 | -------------------------------------------------------------------------------- /backend/hasura/migrations/default/1592205504466_create_table_public_comments/down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE "public"."comments"; 2 | -------------------------------------------------------------------------------- /backend/hasura/migrations/default/1591123243959_create_table_public_favourites/down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE "public"."favourites"; 2 | -------------------------------------------------------------------------------- /frontend/src/elm/Hasura/elm-graphql-metadata.json: -------------------------------------------------------------------------------- 1 | {"targetElmPackageVersion": "5.0.0", "generatedByNpmPackageVersion": "4.0.1"} -------------------------------------------------------------------------------- /elm-constants.json: -------------------------------------------------------------------------------- 1 | { 2 | "path": "./frontend/src/elm", 3 | "moduleName": "Constants", 4 | "values": ["HASURA_GRAPHQL_URL"] 5 | } 6 | -------------------------------------------------------------------------------- /backend/hasura/migrations/default/1591123349453_rename_table_public_favourites/up.sql: -------------------------------------------------------------------------------- 1 | alter table "public"."favourites" rename to "likes"; 2 | -------------------------------------------------------------------------------- /backend/hasura/migrations/default/1591123349453_rename_table_public_favourites/down.sql: -------------------------------------------------------------------------------- 1 | alter table "public"."likes" rename to "favourites"; 2 | -------------------------------------------------------------------------------- /backend/hasura/migrations/default/1591442159613_alter_table_public_users_add_column_bio/down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "public"."users" DROP COLUMN "bio"; 2 | -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | elm-stuff 2 | repl-temp-* 3 | 4 | node_modules 5 | 6 | .cache 7 | dist 8 | .idea 9 | 10 | src/elm/Constants.elm 11 | -------------------------------------------------------------------------------- /backend/hasura/migrations/default/1591442159613_alter_table_public_users_add_column_bio/up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "public"."users" ADD COLUMN "bio" text NULL; 2 | -------------------------------------------------------------------------------- /backend/hasura/migrations/default/1591442170694_alter_table_public_users_add_column_profile_image/down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "public"."users" DROP COLUMN "profile_image"; 2 | -------------------------------------------------------------------------------- /frontend/src/elm/Utils/List.elm: -------------------------------------------------------------------------------- 1 | module Utils.List exposing (remove) 2 | 3 | 4 | remove : a -> List a -> List a 5 | remove a = 6 | List.filter (\x -> x /= a) 7 | -------------------------------------------------------------------------------- /backend/hasura/migrations/default/1590671257528_create_user_profile_view/up.sql: -------------------------------------------------------------------------------- 1 | CREATE VIEW user_profile AS 2 | SELECT id AS user_id, email, username FROM users; 3 | -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "rewrites": [ 3 | { "source": "/api/:path", "destination": "/api" }, 4 | { "source": "/(.*)", "destination": "/index.html" } 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /backend/hasura/migrations/default/1591442170694_alter_table_public_users_add_column_profile_image/up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "public"."users" ADD COLUMN "profile_image" text NULL; 2 | -------------------------------------------------------------------------------- /backend/hasura/migrations/default/1591455792307_drop_check_constraint_public_follows_cannot_follow_self/up.sql: -------------------------------------------------------------------------------- 1 | alter table "public"."follows" drop constraint "cannot_follow_self"; 2 | -------------------------------------------------------------------------------- /backend/hasura/migrations/default/1590583551228_alter_table_public_users_add_check_constraint_non_empty_email/down.sql: -------------------------------------------------------------------------------- 1 | alter table "public"."users" drop constraint "non_empty_email"; 2 | -------------------------------------------------------------------------------- /backend/hasura/migrations/default/1591362960012_alter_table_public_follows_alter_column_following/down.sql: -------------------------------------------------------------------------------- 1 | alter table "public"."follows" rename column "following_id" to "following"; 2 | -------------------------------------------------------------------------------- /backend/hasura/migrations/default/1591362960012_alter_table_public_follows_alter_column_following/up.sql: -------------------------------------------------------------------------------- 1 | alter table "public"."follows" rename column "following" to "following_id"; 2 | -------------------------------------------------------------------------------- /backend/hasura/migrations/default/1590583471552_alter_table_public_users_add_check_constraint_non_empty_username/down.sql: -------------------------------------------------------------------------------- 1 | alter table "public"."users" drop constraint "non_empty_username"; 2 | -------------------------------------------------------------------------------- /backend/hasura/migrations/default/1591123476154_alter_table_public_likes_add_unique_user_id_article_id/down.sql: -------------------------------------------------------------------------------- 1 | alter table "public"."likes" drop constraint "likes_user_id_article_id_key"; 2 | -------------------------------------------------------------------------------- /backend/hasura/migrations/default/1591453234774_alter_table_public_follows_add_check_constraint_cannot_follow_self/down.sql: -------------------------------------------------------------------------------- 1 | alter table "public"."follows" drop constraint "cannot_follow_self"; 2 | -------------------------------------------------------------------------------- /backend/hasura/migrations/default/1590583532032_alter_table_public_users_add_check_constraint_non_empty_password_hash/down.sql: -------------------------------------------------------------------------------- 1 | alter table "public"."users" drop constraint "non_empty_password_hash"; 2 | -------------------------------------------------------------------------------- /backend/actions/.gitignore: -------------------------------------------------------------------------------- 1 | /bower_components/ 2 | /node_modules/ 3 | /.pulp-cache/ 4 | /output/ 5 | /generated-docs/ 6 | /.psc-package/ 7 | /.psc* 8 | /.purs* 9 | /.psa* 10 | /.spago 11 | 12 | server.js -------------------------------------------------------------------------------- /backend/hasura/migrations/default/1590583551228_alter_table_public_users_add_check_constraint_non_empty_email/up.sql: -------------------------------------------------------------------------------- 1 | alter table "public"."users" add constraint "non_empty_email" check ((email = ''::text) IS NOT TRUE); 2 | -------------------------------------------------------------------------------- /backend/hasura/migrations/default/1591123476154_alter_table_public_likes_add_unique_user_id_article_id/up.sql: -------------------------------------------------------------------------------- 1 | alter table "public"."likes" add constraint "likes_user_id_article_id_key" unique ("user_id", "article_id"); 2 | -------------------------------------------------------------------------------- /backend/actions/src/Crypto/Jwt.js: -------------------------------------------------------------------------------- 1 | const jwt = require("jsonwebtoken"); 2 | 3 | const secret = process.env.HASURA_GRAPHQL_JWT_SECRET; 4 | 5 | exports.sign_ = function (data) { 6 | return jwt.sign(data, secret); 7 | }; 8 | -------------------------------------------------------------------------------- /backend/hasura/migrations/default/1590583471552_alter_table_public_users_add_check_constraint_non_empty_username/up.sql: -------------------------------------------------------------------------------- 1 | alter table "public"."users" add constraint "non_empty_username" check ((username = '') IS NOT TRUE); 2 | -------------------------------------------------------------------------------- /backend/hasura/migrations/default/1590917793693_tag_count/up.sql: -------------------------------------------------------------------------------- 1 | CREATE FUNCTION tag_count(tag_row tags) 2 | RETURNS INT AS $$ 3 | SELECT COUNT(*)::INT FROM tags WHERE tags.tag = tag_row.tag 4 | $$ LANGUAGE sql STABLE; 5 | -------------------------------------------------------------------------------- /backend/hasura/migrations/default/1591453234774_alter_table_public_follows_add_check_constraint_cannot_follow_self/up.sql: -------------------------------------------------------------------------------- 1 | alter table "public"."follows" add constraint "cannot_follow_self" check (user_id != following_id); 2 | -------------------------------------------------------------------------------- /backend/hasura/migrations/default/1591455792307_drop_check_constraint_public_follows_cannot_follow_self/down.sql: -------------------------------------------------------------------------------- 1 | alter table "public"."follows" add constraint "cannot_follow_self" check (CHECK (user_id <> following_id)); 2 | -------------------------------------------------------------------------------- /.env.local: -------------------------------------------------------------------------------- 1 | HASURA_GRAPHQL_ADMIN_SECRET=ilovebread 2 | ACTIONS_SECRET=ilovebread 3 | HASURA_GRAPHQL_URL=http://localhost:8080/v1/graphql 4 | HASURA_GRAPHQL_JWT_SECRET=3EK6FD+o0+c7tzBNVfjpMkNDi2yARAAKzQlk8O2IKoxQu4nF7EdAh8s3TwpHwrdWT6R -------------------------------------------------------------------------------- /backend/actions/packages.dhall: -------------------------------------------------------------------------------- 1 | let upstream = 2 | https://github.com/purescript/package-sets/releases/download/psc-0.13.8-20210118/packages.dhall sha256:a59c5c93a68d5d066f3815a89f398bcf00e130a51cb185b2da29b20e2d8ae115 3 | 4 | in upstream -------------------------------------------------------------------------------- /backend/hasura/migrations/default/1590583532032_alter_table_public_users_add_check_constraint_non_empty_password_hash/up.sql: -------------------------------------------------------------------------------- 1 | alter table "public"."users" add constraint "non_empty_password_hash" check ((password_hash = ''::text) IS NOT TRUE); 2 | -------------------------------------------------------------------------------- /backend/actions/.env.local: -------------------------------------------------------------------------------- 1 | HASURA_GRAPHQL_ADMIN_SECRET=ilovebread 2 | HASURA_GRAPHQL_URL=http://localhost:8080/v1/graphql 3 | HASURA_GRAPHQL_JWT_SECRET=3EK6FD+o0+c7tzBNVfjpMkNDi2yARAAKzQlk8O2IKoxQu4nF7EdAh8s3TwpHwrdWT6R 4 | ACTIONS_SECRET=ilovebread 5 | -------------------------------------------------------------------------------- /backend/actions/test/Main.purs: -------------------------------------------------------------------------------- 1 | module Test.Main where 2 | 3 | import Prelude 4 | import Effect (Effect) 5 | import Test.Password as Password 6 | import Test.Unit.Main (runTest) 7 | 8 | main :: Effect Unit 9 | main = runTest do Password.suite 10 | -------------------------------------------------------------------------------- /frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es5", 5 | "lib": ["dom", "es6"], 6 | "strict": true, 7 | "sourceMap": false 8 | }, 9 | "exclude": ["node_modules", "dist"] 10 | } 11 | -------------------------------------------------------------------------------- /backend/hasura/migrations/default/1602056993085_create_profile_view/up.sql: -------------------------------------------------------------------------------- 1 | CREATE OR REPLACE VIEW "public"."profile" AS 2 | SELECT users.id AS user_id, 3 | users.email, 4 | users.username, 5 | users.bio, 6 | users.profile_image 7 | FROM users; 8 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import elmPlugin from "vite-plugin-elm"; 2 | 3 | export default { 4 | root: "./frontend/src", 5 | plugins: [elmPlugin()], 6 | server: { 7 | port: 1234, 8 | }, 9 | build: { 10 | outDir: "../../dist", 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /backend/actions/src/Env/Env.purs: -------------------------------------------------------------------------------- 1 | module Env 2 | ( adminSecret 3 | , graphqlUrl 4 | , actionsSecret 5 | ) where 6 | 7 | foreign import adminSecret :: String 8 | 9 | foreign import graphqlUrl :: String 10 | 11 | foreign import actionsSecret :: String 12 | -------------------------------------------------------------------------------- /backend/hasura/migrations/default/1590346274456_create_table_public_users/up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE "public"."users"("id" serial NOT NULL, "username" text NOT NULL, "email" text NOT NULL, "password_hash" text NOT NULL, PRIMARY KEY ("id") , UNIQUE ("id"), UNIQUE ("username")); 2 | -------------------------------------------------------------------------------- /backend/hasura/migrations/default/1591123797156_create_likes_for_article_function/up.sql: -------------------------------------------------------------------------------- 1 | CREATE FUNCTION likes_for_article(articles_row articles) 2 | RETURNS INT AS $$ 3 | SELECT COUNT(*)::INT FROM likes WHERE likes.article_id = articles_row.id 4 | $$ LANGUAGE sql STABLE; 5 | -------------------------------------------------------------------------------- /frontend/src/ts/utils.ts: -------------------------------------------------------------------------------- 1 | export function safeParse(val: string | null): T | null { 2 | if (val !== null) { 3 | try { 4 | return JSON.parse(val); 5 | } catch (err) { 6 | return null; 7 | } 8 | } else { 9 | return null; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /backend/actions/src/Crypto/Bcrypt.js: -------------------------------------------------------------------------------- 1 | const bcrypt = require("bcrypt"); 2 | 3 | exports.hash_ = function (password) { 4 | return bcrypt.hashSync(password, 10); 5 | }; 6 | 7 | exports.compare_ = function (password, hash) { 8 | return bcrypt.compareSync(password, hash); 9 | }; 10 | -------------------------------------------------------------------------------- /frontend/src/elm/Hasura/Union.elm: -------------------------------------------------------------------------------- 1 | -- Do not manually edit this file, it was auto-generated by dillonkearns/elm-graphql 2 | -- https://github.com/dillonkearns/elm-graphql 3 | 4 | 5 | module Hasura.Union exposing (..) 6 | 7 | 8 | placeholder : String 9 | placeholder = 10 | "" 11 | -------------------------------------------------------------------------------- /frontend/src/elm/Hasura/Interface.elm: -------------------------------------------------------------------------------- 1 | -- Do not manually edit this file, it was auto-generated by dillonkearns/elm-graphql 2 | -- https://github.com/dillonkearns/elm-graphql 3 | 4 | 5 | module Hasura.Interface exposing (..) 6 | 7 | 8 | placeholder : String 9 | placeholder = 10 | "" 11 | -------------------------------------------------------------------------------- /backend/hasura/metadata/databases/default/tables/tables.yaml: -------------------------------------------------------------------------------- 1 | - "!include public_articles.yaml" 2 | - "!include public_comments.yaml" 3 | - "!include public_follows.yaml" 4 | - "!include public_likes.yaml" 5 | - "!include public_profile.yaml" 6 | - "!include public_tags.yaml" 7 | - "!include public_users.yaml" 8 | -------------------------------------------------------------------------------- /frontend/src/elm/Hasura/VerifyScalarCodecs.elm: -------------------------------------------------------------------------------- 1 | -- Do not manually edit this file, it was auto-generated by dillonkearns/elm-graphql 2 | -- https://github.com/dillonkearns/elm-graphql 3 | 4 | 5 | module Hasura.VerifyScalarCodecs exposing (..) 6 | 7 | 8 | placeholder : String 9 | placeholder = 10 | "" 11 | -------------------------------------------------------------------------------- /frontend/src/elm/Api/Token.elm: -------------------------------------------------------------------------------- 1 | module Api.Token exposing 2 | ( Token 3 | , token 4 | , value 5 | ) 6 | 7 | 8 | type Token 9 | = Token String 10 | 11 | 12 | token : String -> Token 13 | token = 14 | Token 15 | 16 | 17 | value : Token -> String 18 | value (Token raw) = 19 | raw 20 | -------------------------------------------------------------------------------- /backend/hasura/migrations/default/1590750212586_create_table_public_tags/up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE "public"."tags"("id" serial NOT NULL, "article_id" integer NOT NULL, "tag" text NOT NULL, PRIMARY KEY ("id") , FOREIGN KEY ("article_id") REFERENCES "public"."articles"("id") ON UPDATE cascade ON DELETE cascade, UNIQUE ("article_id", "tag")); 2 | -------------------------------------------------------------------------------- /frontend/src/elm/Utils/SelectionSet.elm: -------------------------------------------------------------------------------- 1 | module Utils.SelectionSet exposing (failOnNothing) 2 | 3 | import Graphql.SelectionSet as SelectionSet 4 | 5 | 6 | failOnNothing : SelectionSet.SelectionSet (Maybe a) typeLock -> SelectionSet.SelectionSet a typeLock 7 | failOnNothing = 8 | SelectionSet.mapOrFail (Result.fromMaybe "required") 9 | -------------------------------------------------------------------------------- /frontend/src/elm/Element/Layout/Block.elm: -------------------------------------------------------------------------------- 1 | module Element.Layout.Block exposing (halfWidthPlus) 2 | 3 | import Element exposing (..) 4 | import Element.Layout as Layout 5 | 6 | 7 | halfWidthPlus : Int -> Element msg -> Element msg 8 | halfWidthPlus extra = 9 | el [ width (fill |> maximum ((Layout.maxWidth // 2) + extra)) ] 10 | -------------------------------------------------------------------------------- /backend/actions/spago.dhall: -------------------------------------------------------------------------------- 1 | { name = "realworld-actions" 2 | , dependencies = 3 | [ "console" 4 | , "effect" 5 | , "nullable" 6 | , "payload" 7 | , "prelude" 8 | , "psci-support" 9 | , "simple-ajax" 10 | , "strings" 11 | ] 12 | , packages = ./packages.dhall 13 | , sources = [ "src/**/*.purs", "test/**/*.purs" ] 14 | } 15 | -------------------------------------------------------------------------------- /frontend/src/elm/Page/NotFound.elm: -------------------------------------------------------------------------------- 1 | module Page.NotFound exposing (view) 2 | 3 | import Context exposing (Context) 4 | import Element exposing (Element) 5 | import Element.Layout as Layout 6 | import Element.Text as Text 7 | 8 | 9 | view : Context -> Element msg 10 | view context = 11 | Layout.layout |> Layout.toPage context (Text.title [] "Not Found") 12 | -------------------------------------------------------------------------------- /frontend/src/index.ts: -------------------------------------------------------------------------------- 1 | import { Elm } from "./elm/Main.elm"; 2 | import * as User from "./ts/user"; 3 | 4 | const app = Elm.Main.init({ 5 | flags: { user: User.load() }, 6 | node: document.getElementById("app"), 7 | }); 8 | 9 | app.ports.saveUser.subscribe((user) => { 10 | User.save(user); 11 | }); 12 | 13 | app.ports.logout_.subscribe(() => { 14 | User.clear(); 15 | }); 16 | -------------------------------------------------------------------------------- /frontend/src/elm/Animation/Named.elm: -------------------------------------------------------------------------------- 1 | module Animation.Named exposing 2 | ( fadeIn 3 | , yoyo 4 | ) 5 | 6 | import Animation exposing (Animation) 7 | 8 | 9 | fadeIn : Float -> List Animation.Option -> Animation 10 | fadeIn = 11 | Animation.named "fade-in" 12 | 13 | 14 | yoyo : Float -> List Animation.Option -> Animation 15 | yoyo = 16 | Animation.named "yoyo" 17 | -------------------------------------------------------------------------------- /frontend/src/elm/Element/Divider.elm: -------------------------------------------------------------------------------- 1 | module Element.Divider exposing (divider) 2 | 3 | import Element exposing (..) 4 | import Element.Border as Border 5 | import Element.Palette as Palette 6 | 7 | 8 | divider : Element msg 9 | divider = 10 | el 11 | [ Border.width 1 12 | , Border.color Palette.lightGrey 13 | , width fill 14 | ] 15 | none 16 | -------------------------------------------------------------------------------- /backend/actions/README.md: -------------------------------------------------------------------------------- 1 | # Realworld Actions server 2 | 3 | Http server for hasura actions built using Purescript + Payload 4 | 5 | ## Get up and running locally 6 | 7 | - `npm install` 8 | 9 | ## Run the server 10 | 11 | - `source .env.local` 12 | - `npm run dev` 13 | 14 | and a server will be available at `http://localhost:3000` 15 | 16 | ## Run the tests 17 | 18 | - `npm test` 19 | -------------------------------------------------------------------------------- /backend/actions/src/Env/Env.js: -------------------------------------------------------------------------------- 1 | exports.adminSecret = getEnv("HASURA_GRAPHQL_ADMIN_SECRET"); 2 | exports.graphqlUrl = getEnv("HASURA_GRAPHQL_URL"); 3 | exports.actionsSecret = getEnv("ACTIONS_SECRET"); 4 | 5 | function getEnv(env_var) { 6 | const val = process.env[env_var]; 7 | if (val) { 8 | return val; 9 | } else { 10 | throw new Error(`missing ${env_var}`); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /backend/hasura/migrations/default/1591123243959_create_table_public_favourites/up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE "public"."favourites"("id" serial NOT NULL, "user_id" integer NOT NULL, "article_id" integer NOT NULL, PRIMARY KEY ("id") , FOREIGN KEY ("article_id") REFERENCES "public"."articles"("id") ON UPDATE cascade ON DELETE cascade, FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON UPDATE cascade ON DELETE cascade); 2 | -------------------------------------------------------------------------------- /backend/actions/src/Crypto/Jwt.purs: -------------------------------------------------------------------------------- 1 | module Crypto.Jwt (sign, Jwt) where 2 | 3 | import Prelude 4 | import Simple.JSON (class WriteForeign, writeImpl) 5 | 6 | newtype Jwt 7 | = Jwt String 8 | 9 | instance writeForeignJwt :: WriteForeign Jwt where 10 | writeImpl (Jwt s) = writeImpl s 11 | 12 | sign :: forall a. a -> Jwt 13 | sign = sign_ >>> Jwt 14 | 15 | foreign import sign_ :: forall a. a -> String 16 | -------------------------------------------------------------------------------- /backend/hasura/migrations/default/1590605196105_create_table_public_articles/up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE "public"."articles"("id" serial NOT NULL, "author_id" integer NOT NULL, "title" text NOT NULL, "about" text NOT NULL, "content" text NOT NULL, "created_at" timestamptz NOT NULL DEFAULT now(), PRIMARY KEY ("id") , FOREIGN KEY ("author_id") REFERENCES "public"."users"("id") ON UPDATE cascade ON DELETE cascade, UNIQUE ("id")); 2 | -------------------------------------------------------------------------------- /backend/hasura/migrations/default/1591362644347_create_table_public_follows/up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE "public"."follows"("id" serial NOT NULL, "user_id" integer NOT NULL, "following" integer NOT NULL, PRIMARY KEY ("id") , FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON UPDATE cascade ON DELETE cascade, FOREIGN KEY ("following") REFERENCES "public"."users"("id") ON UPDATE cascade ON DELETE cascade, UNIQUE ("user_id", "following")); 2 | -------------------------------------------------------------------------------- /backend/hasura/metadata/databases/databases.yaml: -------------------------------------------------------------------------------- 1 | - name: default 2 | kind: postgres 3 | configuration: 4 | connection_info: 5 | database_url: 6 | from_env: DATABASE_URL 7 | isolation_level: read-committed 8 | pool_settings: 9 | connection_lifetime: 600 10 | idle_timeout: 180 11 | max_connections: 50 12 | retries: 1 13 | use_prepared_statements: true 14 | tables: "!include default/tables/tables.yaml" 15 | -------------------------------------------------------------------------------- /backend/hasura/migrations/default/1592205504466_create_table_public_comments/up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE "public"."comments"("id" serial NOT NULL, "article_id" integer NOT NULL, "user_id" integer NOT NULL, "comment" text NOT NULL, "created_at" timestamptz NOT NULL DEFAULT now(), PRIMARY KEY ("id") , FOREIGN KEY ("article_id") REFERENCES "public"."articles"("id") ON UPDATE cascade ON DELETE cascade, FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON UPDATE cascade ON DELETE cascade); 2 | -------------------------------------------------------------------------------- /backend/hasura/config.yaml: -------------------------------------------------------------------------------- 1 | version: 3 2 | endpoint: http://localhost:8080 3 | admin_secret: ilovebread 4 | api_paths: 5 | v1_query: v1/query 6 | v2_query: v2/query 7 | v1_metadata: v1/metadata 8 | graphql: v1/graphql 9 | config: v1alpha1/config 10 | pg_dump: v1alpha1/pg_dump 11 | version: v1/version 12 | seeds_directory: seeds 13 | actions: 14 | kind: synchronous 15 | handler_webhook_baseurl: http://localhost:3000 16 | codegen: 17 | framework: "" 18 | output_dir: "" 19 | -------------------------------------------------------------------------------- /default.nix: -------------------------------------------------------------------------------- 1 | { pkgs ? import { } }: 2 | let 3 | easy-ps = import 4 | (pkgs.fetchFromGitHub { 5 | owner = "justinwoo"; 6 | repo = "easy-purescript-nix"; 7 | rev = "9a44ddfad868fe804e22973d32839c2f2167571c"; 8 | sha256 = "0dz6q8qyldbkzyma1fw7p44zm493hs4jxxzr7ll1h36z8i6k38xq"; 9 | }) { 10 | inherit pkgs; 11 | }; 12 | in 13 | pkgs.mkShell { 14 | buildInputs = [ 15 | easy-ps.purs-0_13_8 16 | easy-ps.spago 17 | easy-ps.purty 18 | pkgs.terraform 19 | ]; 20 | } -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | # Realworld Frontend 2 | 3 | UI for realworld app, built with Elm, Typescript + parcel 4 | 5 | # Get up and running 6 | 7 | Install dependencies 8 | 9 | ```sh 10 | > npm install 11 | ``` 12 | 13 | Run the dev server 14 | 15 | ```sh 16 | > npm run dev 17 | ``` 18 | 19 | Generate elm graphql modules (hasura must be running locally) 20 | 21 | ```sh 22 | > npm run schema 23 | ``` 24 | 25 | Build the project 26 | 27 | ```sh 28 | > npm run build 29 | ``` 30 | 31 | Run unit tests 32 | 33 | ```sh 34 | > npm run test 35 | ``` 36 | -------------------------------------------------------------------------------- /backend/actions/src/Token.purs: -------------------------------------------------------------------------------- 1 | module Token (generate) where 2 | 3 | import Prelude 4 | import Crypto.Jwt (Jwt) 5 | import Crypto.Jwt as Jwt 6 | 7 | type User a 8 | = { id :: Int 9 | , username :: String 10 | | a 11 | } 12 | 13 | generate :: forall a. User a -> Jwt 14 | generate user = 15 | Jwt.sign 16 | { name: user.username 17 | , "https://hasura.io/jwt/claims": 18 | { "x-hasura-allowed-roles": [ "user" ] 19 | , "x-hasura-default-role": "user" 20 | , "x-hasura-user-id": show user.id 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /frontend/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 9 | 10 | Conduit 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /frontend/src/ts/user.ts: -------------------------------------------------------------------------------- 1 | import * as Utils from "./utils"; 2 | 3 | interface User { 4 | id: number; 5 | username: string; 6 | email: string; 7 | token: string; 8 | bio: string | null; 9 | profileImage: string | null; 10 | following: number[]; 11 | } 12 | 13 | export function load(): User | null { 14 | return Utils.safeParse(localStorage.getItem("user")); 15 | } 16 | 17 | export function save(user: User) { 18 | localStorage.setItem("user", JSON.stringify(user)); 19 | } 20 | 21 | export function clear() { 22 | localStorage.removeItem("user"); 23 | } 24 | -------------------------------------------------------------------------------- /frontend/src/elm/Element/Anchor.elm: -------------------------------------------------------------------------------- 1 | module Element.Anchor exposing 2 | ( description 3 | , htmlDescription 4 | ) 5 | 6 | import Element 7 | import Html 8 | import Html.Attributes 9 | 10 | 11 | description : String -> Element.Attribute msg 12 | description = 13 | htmlDescription >> Element.htmlAttribute 14 | 15 | 16 | fromLabel : String -> String 17 | fromLabel = 18 | String.toLower >> String.split " " >> String.join "-" 19 | 20 | 21 | htmlDescription : String -> Html.Attribute msg 22 | htmlDescription = 23 | fromLabel >> Html.Attributes.attribute "data-label" 24 | -------------------------------------------------------------------------------- /backend/hasura/metadata/databases/default/tables/public_profile.yaml: -------------------------------------------------------------------------------- 1 | table: 2 | name: profile 3 | schema: public 4 | array_relationships: 5 | - name: follows 6 | using: 7 | manual_configuration: 8 | column_mapping: 9 | user_id: user_id 10 | insertion_order: null 11 | remote_table: 12 | name: follows 13 | schema: public 14 | select_permissions: 15 | - permission: 16 | columns: 17 | - user_id 18 | - email 19 | - username 20 | - bio 21 | - profile_image 22 | filter: 23 | user_id: 24 | _eq: X-Hasura-User-Id 25 | role: user 26 | -------------------------------------------------------------------------------- /.github/workflows/cd.yml: -------------------------------------------------------------------------------- 1 | name: CD 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | migrate-hasura: 10 | runs-on: ubuntu-latest 11 | env: 12 | HASURA_ENDPOINT: ${{ secrets.HASURA_ENDPOINT }} 13 | HASURA_ADMIN_SECRET: ${{ secrets.HASURA_GRAPHQL_ADMIN_SECRET }} 14 | HASURA_WORKDIR: ./backend/hasura 15 | steps: 16 | - uses: actions/checkout@master 17 | - uses: tibotiber/hasura-action@master 18 | with: 19 | args: migrate apply --database-name default 20 | - uses: tibotiber/hasura-action@master 21 | with: 22 | args: metadata apply 23 | -------------------------------------------------------------------------------- /backend/hasura/metadata/actions.graphql: -------------------------------------------------------------------------------- 1 | type Mutation { 2 | login( 3 | username: String! 4 | password: String! 5 | ): TokenResponse! 6 | } 7 | 8 | type Mutation { 9 | signup( 10 | email: String! 11 | username: String! 12 | password: String! 13 | ): TokenResponse! 14 | } 15 | 16 | type Mutation { 17 | unlike_article( 18 | article_id: Int! 19 | ): UnlikeResponse! 20 | } 21 | 22 | type TokenResponse { 23 | token: String! 24 | user_id: Int! 25 | username: String! 26 | email: String! 27 | bio: String 28 | profile_image: String 29 | } 30 | 31 | type UnlikeResponse { 32 | article_id: Int 33 | } 34 | 35 | -------------------------------------------------------------------------------- /frontend/src/elm/Api/Date.elm: -------------------------------------------------------------------------------- 1 | module Api.Date exposing (fromScalar) 2 | 3 | import Date exposing (Date) 4 | import Graphql.SelectionSet as SelectionSet 5 | import Hasura.Scalar exposing (Timestamptz(..)) 6 | import Iso8601 7 | import Time exposing (utc) 8 | 9 | 10 | fromScalar : SelectionSet.SelectionSet Timestamptz typeLock -> SelectionSet.SelectionSet Date typeLock 11 | fromScalar = 12 | SelectionSet.mapOrFail fromTimestamp_ 13 | 14 | 15 | fromTimestamp_ : Timestamptz -> Result String Date 16 | fromTimestamp_ (Timestamptz str) = 17 | Iso8601.toTime str 18 | |> Result.map (Date.fromPosix utc) 19 | |> Result.mapError (always "error decoding date") 20 | -------------------------------------------------------------------------------- /frontend/src/elm/Element/Scale.elm: -------------------------------------------------------------------------------- 1 | module Element.Scale exposing 2 | ( edges 3 | , extraLarge 4 | , extraSmall 5 | , large 6 | , medium 7 | , small 8 | ) 9 | 10 | 11 | extraSmall : number 12 | extraSmall = 13 | 8 14 | 15 | 16 | small : number 17 | small = 18 | 14 19 | 20 | 21 | medium : number 22 | medium = 23 | 24 24 | 25 | 26 | large : number 27 | large = 28 | 32 29 | 30 | 31 | extraLarge : number 32 | extraLarge = 33 | 64 34 | 35 | 36 | edges : { top : number, right : number, bottom : number, left : number } 37 | edges = 38 | { top = 0 39 | , right = 0 40 | , bottom = 0 41 | , left = 0 42 | } 43 | -------------------------------------------------------------------------------- /frontend/src/elm/Element/Icon/Plane.elm: -------------------------------------------------------------------------------- 1 | module Element.Icon.Plane exposing (icon) 2 | 3 | import Element exposing (Color, Element) 4 | import Element.Icon as Icon 5 | import Element.Palette as Palette 6 | import Svg 7 | import Svg.Attributes exposing (..) 8 | 9 | 10 | icon : Color -> Element msg 11 | icon color = 12 | Icon.small 13 | (Svg.svg [ viewBox "0 0 15 15" ] 14 | [ Svg.path 15 | [ d "M.4 0a.4.4 0 00-.4.6L1.5 6 8 7 1.5 8 0 13.5a.4.4 0 00.6.5L14 7.4a.4.4 0 000-.7L.6 0H.4z" 16 | , fill (Palette.toRgbString color) 17 | , Icon.hoverTarget 18 | ] 19 | [] 20 | ] 21 | ) 22 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | test-frontend: 13 | runs-on: ubuntu-latest 14 | env: 15 | HASURA_GRAPHQL_URL: "http://test/graphql" 16 | steps: 17 | - uses: actions/checkout@v2 18 | - uses: actions/setup-node@v1 19 | with: 20 | node-version: 16.x 21 | - run: npm install 22 | - run: npm test 23 | test-actions: 24 | defaults: 25 | run: 26 | working-directory: ./backend/actions 27 | runs-on: ubuntu-latest 28 | steps: 29 | - uses: actions/checkout@v2 30 | - run: npm install 31 | - run: npm test 32 | -------------------------------------------------------------------------------- /frontend/src/elm/Element/Icon/Pencil.elm: -------------------------------------------------------------------------------- 1 | module Element.Icon.Pencil exposing (icon) 2 | 3 | import Element exposing (Color, Element) 4 | import Element.Icon as Icon 5 | import Element.Palette as Palette 6 | import Svg 7 | import Svg.Attributes exposing (..) 8 | 9 | 10 | icon : Color -> Element msg 11 | icon color = 12 | Icon.small 13 | (Svg.svg [ viewBox "0 0 14 15" ] 14 | [ Svg.path 15 | [ d "M11.7.1l-1.3 1.4-.2.2-9 9.7-1 2.9-.2.7.7-.3 2.8-1.2 9-9.8.2-.2 1.2-1.4c.5-.8-1.2-2.6-2.2-2z" 16 | , fill (Palette.toRgbString color) 17 | , fillRule "nonzero" 18 | , Icon.hoverTarget 19 | ] 20 | [] 21 | ] 22 | ) 23 | -------------------------------------------------------------------------------- /backend/actions/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "actions-server", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "directories": { 6 | "test": "test" 7 | }, 8 | "scripts": { 9 | "vercel-build": "npm run build", 10 | "build": "spago bundle-app --to server.js && npm run compress", 11 | "compress": "uglifyjs --compress --mangle --output server.min.js -- server.js", 12 | "dev": "spago run", 13 | "test": "spago test" 14 | }, 15 | "author": "Andrew MacMurray", 16 | "license": "MIT", 17 | "dependencies": { 18 | "bcrypt": "^5.0.1", 19 | "jsonwebtoken": "^9.0.0", 20 | "xhr2": "^0.2.1" 21 | }, 22 | "devDependencies": { 23 | "purescript": "^0.13.8", 24 | "spago": "^0.19.2", 25 | "uglify-js": "^3.15.5" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /frontend/src/elm/Element/Icon/Chevron.elm: -------------------------------------------------------------------------------- 1 | module Element.Icon.Chevron exposing (down) 2 | 3 | import Element.Icon as Icon 4 | import Element.Palette as Palette 5 | import Svg 6 | import Svg.Attributes exposing (..) 7 | 8 | 9 | down color = 10 | Icon.small 11 | (Svg.svg [ viewBox "0 0 18 15", width "100%" ] 12 | [ Svg.g 13 | [ stroke (Palette.toRgbString color) 14 | , fill "none" 15 | , fillRule "evenodd" 16 | , Icon.hoverTarget 17 | ] 18 | [ Svg.path [ d "M2.5 4.5l7 8", strokeWidth "2.5", strokeLinecap "round" ] [] 19 | , Svg.path [ d "M16.5 4.5l-7 8", strokeWidth "2.5", strokeLinecap "round" ] [] 20 | ] 21 | ] 22 | ) 23 | -------------------------------------------------------------------------------- /frontend/src/elm/Article/Feed.elm: -------------------------------------------------------------------------------- 1 | module Article.Feed exposing 2 | ( Feed 3 | , ForAuthor 4 | , Home 5 | , forAuthor 6 | ) 7 | 8 | import Article exposing (Article) 9 | import Article.Author exposing (Author) 10 | import Tag 11 | 12 | 13 | 14 | -- Feed 15 | 16 | 17 | type alias Feed = 18 | { articles : List Article 19 | , count : Int 20 | } 21 | 22 | 23 | 24 | -- Home Feed 25 | 26 | 27 | type alias Home = 28 | { feed : Feed 29 | , popularTags : List Tag.Popular 30 | } 31 | 32 | 33 | 34 | -- Author Feed 35 | 36 | 37 | type alias ForAuthor = 38 | { feed : Feed 39 | , author : Author 40 | } 41 | 42 | 43 | forAuthor : Maybe Author -> Feed -> Maybe ForAuthor 44 | forAuthor author feed = 45 | Maybe.map (ForAuthor feed) author 46 | -------------------------------------------------------------------------------- /frontend/src/elm/Element/Icon/Ellipsis.elm: -------------------------------------------------------------------------------- 1 | module Element.Icon.Ellipsis exposing (icon) 2 | 3 | import Element exposing (Color, Element) 4 | import Element.Icon as Icon 5 | import Element.Palette as Palette 6 | import Svg 7 | import Svg.Attributes exposing (..) 8 | 9 | 10 | icon : Color -> Element msg 11 | icon color = 12 | Icon.small 13 | (Svg.svg [ viewBox "0 0 17 15" ] 14 | [ Svg.g 15 | [ fill (Palette.toRgbString color) 16 | , fillRule "evenodd" 17 | , Icon.hoverTarget 18 | ] 19 | [ Svg.circle [ cx "6", cy "2", r "2" ] [] 20 | , Svg.circle [ cx "6", cy "13", r "2" ] [] 21 | , Svg.circle [ cx "6", cy "7.5", r "2" ] [] 22 | ] 23 | ] 24 | ) 25 | -------------------------------------------------------------------------------- /backend/actions/src/Articles.purs: -------------------------------------------------------------------------------- 1 | module Articles where 2 | 3 | import Prelude 4 | import Hasura as Hasura 5 | import Users as Users 6 | 7 | type ArticleToUnlike 8 | = { user_id :: Users.Id 9 | , article_id :: ArticleId 10 | } 11 | 12 | type ArticleId 13 | = Int 14 | 15 | unlike :: ArticleToUnlike -> Hasura.Response ArticleId 16 | unlike toUnlike = do 17 | toSelection toUnlike <$> Hasura.request { query: unlikeMutation, variables: toUnlike } 18 | 19 | toSelection :: ArticleToUnlike -> {} -> ArticleId 20 | toSelection article _ = article.article_id 21 | 22 | unlikeMutation :: String 23 | unlikeMutation = 24 | """ 25 | mutation Unlike($user_id: Int, $article_id: Int) { 26 | delete_likes(where: {_and: {article_id: {_eq: $article_id}, user_id: {_eq: $user_id}}}) { 27 | affected_rows 28 | } 29 | } 30 | """ 31 | -------------------------------------------------------------------------------- /frontend/src/elm/User/Element.elm: -------------------------------------------------------------------------------- 1 | module User.Element exposing 2 | ( showIfLoggedIn 3 | , showIfMe 4 | ) 5 | 6 | import Article.Author exposing (Author) 7 | import Element exposing (Element) 8 | import User exposing (User) 9 | 10 | 11 | showIfLoggedIn : Element msg -> User -> Element msg 12 | showIfLoggedIn element user = 13 | case user of 14 | User.Guest -> 15 | Element.none 16 | 17 | User.Author _ -> 18 | element 19 | 20 | 21 | showIfMe : Element msg -> User -> Author -> Element msg 22 | showIfMe element user author = 23 | case user of 24 | User.Guest -> 25 | Element.none 26 | 27 | User.Author profile -> 28 | if User.equals profile author then 29 | element 30 | 31 | else 32 | Element.none 33 | -------------------------------------------------------------------------------- /frontend/src/elm/Form/Error.elm: -------------------------------------------------------------------------------- 1 | module Form.Error exposing (attributes) 2 | 3 | import Element exposing (Attribute) 4 | import Element.Anchor as Anchor 5 | import Element.Border as Border 6 | import Element.Palette as Palette 7 | import Form.Field as Field exposing (Field) 8 | import Form.Validation as Validation exposing (Validation) 9 | 10 | 11 | attributes : Field inputs -> inputs -> Validation inputs output -> List (Attribute msg) 12 | attributes field inputs validation = 13 | if hasError field inputs validation then 14 | [ Border.color Palette.red 15 | , Anchor.description (Field.label field ++ "-error") 16 | ] 17 | 18 | else 19 | [] 20 | 21 | 22 | hasError : Field inputs -> inputs -> Validation inputs output -> Bool 23 | hasError field inputs = 24 | Validation.run inputs >> Validation.hasErrorFor (Field.id field) 25 | -------------------------------------------------------------------------------- /tests/Program/Selector.elm: -------------------------------------------------------------------------------- 1 | module Program.Selector exposing 2 | ( el 3 | , fieldError 4 | , filledArea 5 | , filledField 6 | ) 7 | 8 | import Element.Anchor as Anchor 9 | import Html.Attributes 10 | import Test.Html.Selector exposing (Selector, all, attribute, tag) 11 | 12 | 13 | el : String -> Selector 14 | el label = 15 | Test.Html.Selector.attribute (Anchor.htmlDescription label) 16 | 17 | 18 | fieldError : String -> Selector 19 | fieldError label = 20 | el (label ++ "-error") 21 | 22 | 23 | filledField : String -> Selector 24 | filledField val = 25 | all 26 | [ tag "input" 27 | , attribute (Html.Attributes.value val) 28 | ] 29 | 30 | 31 | filledArea : String -> Selector 32 | filledArea val = 33 | all 34 | [ tag "textarea" 35 | , attribute (Html.Attributes.value val) 36 | ] 37 | -------------------------------------------------------------------------------- /backend/actions/src/Crypto/Bcrypt.purs: -------------------------------------------------------------------------------- 1 | module Crypto.Bcrypt 2 | ( hash 3 | , compare 4 | , Hash 5 | ) where 6 | 7 | import Prelude 8 | import Data.Function.Uncurried (Fn2, runFn2) 9 | import Data.Newtype (class Newtype, unwrap, wrap) 10 | import Simple.JSON (class ReadForeign, class WriteForeign, readImpl, writeImpl) 11 | 12 | newtype Hash 13 | = Hash String 14 | 15 | derive instance newtypeHash :: Newtype Hash _ 16 | 17 | instance readHash :: ReadForeign Hash where 18 | readImpl f = Hash <$> readImpl f 19 | 20 | instance writeHash :: WriteForeign Hash where 21 | writeImpl = unwrap >>> writeImpl 22 | 23 | hash :: String -> Hash 24 | hash = hash_ >>> wrap 25 | 26 | compare :: String -> Hash -> Boolean 27 | compare password h = runFn2 compare_ password (unwrap h) 28 | 29 | foreign import hash_ :: String -> String 30 | 31 | foreign import compare_ :: Fn2 String String Boolean 32 | -------------------------------------------------------------------------------- /frontend/src/elm/Main/index.d.ts: -------------------------------------------------------------------------------- 1 | // WARNING: Do not manually modify this file. It was generated using: 2 | // https://github.com/dillonkearns/elm-typescript-interop 3 | // Type definitions for Elm ports 4 | 5 | export namespace Elm { 6 | namespace Main { 7 | export interface App { 8 | ports: { 9 | saveUser: { 10 | subscribe(callback: (data: { id: number; username: string; email: string; token: string; bio: string | null; profileImage: string | null; following: number[] }) => void): void 11 | } 12 | logout_: { 13 | subscribe(callback: (data: null) => void): void 14 | } 15 | }; 16 | } 17 | export function init(options: { 18 | node?: HTMLElement | null; 19 | flags: { user: { id: number; username: string; email: string; token: string; bio: string | null; profileImage: string | null; following: number[] } | null }; 20 | }): Elm.Main.App; 21 | } 22 | } -------------------------------------------------------------------------------- /frontend/src/elm/Hasura/ScalarCodecs.elm: -------------------------------------------------------------------------------- 1 | -- Do not manually edit this file, it was auto-generated by dillonkearns/elm-graphql 2 | -- https://github.com/dillonkearns/elm-graphql 3 | 4 | 5 | module Hasura.ScalarCodecs exposing (..) 6 | 7 | import Hasura.Scalar exposing (defaultCodecs) 8 | import Json.Decode as Decode exposing (Decoder) 9 | 10 | 11 | type alias Id = 12 | Hasura.Scalar.Id 13 | 14 | 15 | type alias Json = 16 | Hasura.Scalar.Json 17 | 18 | 19 | type alias Timestamptz = 20 | Hasura.Scalar.Timestamptz 21 | 22 | 23 | type alias Uuid = 24 | Hasura.Scalar.Uuid 25 | 26 | 27 | codecs : Hasura.Scalar.Codecs Id Json Timestamptz Uuid 28 | codecs = 29 | Hasura.Scalar.defineCodecs 30 | { codecId = defaultCodecs.codecId 31 | , codecJson = defaultCodecs.codecJson 32 | , codecTimestamptz = defaultCodecs.codecTimestamptz 33 | , codecUuid = defaultCodecs.codecUuid 34 | } 35 | -------------------------------------------------------------------------------- /frontend/src/elm/Element/Icon.elm: -------------------------------------------------------------------------------- 1 | module Element.Icon exposing 2 | ( blackHover 3 | , hoverTarget 4 | , small 5 | , whiteHover 6 | , whiteHoverStroke 7 | ) 8 | 9 | import Element exposing (..) 10 | import Html.Attributes 11 | import Svg exposing (Svg) 12 | import Svg.Attributes 13 | 14 | 15 | small : Svg msg -> Element msg 16 | small = 17 | el [ width (px 17), height (px 15) ] << html 18 | 19 | 20 | whiteHover : Element.Attribute msg 21 | whiteHover = 22 | htmlAttribute (Html.Attributes.class "icon-white-hover") 23 | 24 | 25 | whiteHoverStroke : Element.Attribute msg 26 | whiteHoverStroke = 27 | htmlAttribute (Html.Attributes.class "icon-white-hover-stroke") 28 | 29 | 30 | blackHover : Attribute msg 31 | blackHover = 32 | htmlAttribute (Html.Attributes.class "icon-black-hover") 33 | 34 | 35 | hoverTarget : Svg.Attribute msg 36 | hoverTarget = 37 | Svg.Attributes.class "icon-hover-target" 38 | -------------------------------------------------------------------------------- /backend/hasura/metadata/databases/default/tables/public_likes.yaml: -------------------------------------------------------------------------------- 1 | table: 2 | name: likes 3 | schema: public 4 | configuration: 5 | column_config: {} 6 | custom_column_names: {} 7 | custom_root_fields: 8 | insert: like_articles 9 | insert_one: like_article 10 | object_relationships: 11 | - name: article 12 | using: 13 | foreign_key_constraint_on: article_id 14 | - name: user 15 | using: 16 | foreign_key_constraint_on: user_id 17 | insert_permissions: 18 | - permission: 19 | check: {} 20 | columns: 21 | - article_id 22 | set: 23 | user_id: x-hasura-User-Id 24 | role: user 25 | select_permissions: 26 | - permission: 27 | allow_aggregations: true 28 | columns: 29 | - article_id 30 | - user_id 31 | filter: {} 32 | role: anonymous 33 | - permission: 34 | allow_aggregations: true 35 | columns: 36 | - article_id 37 | - user_id 38 | filter: {} 39 | role: user 40 | -------------------------------------------------------------------------------- /frontend/src/elm/Element/Icon/Heart.elm: -------------------------------------------------------------------------------- 1 | module Element.Icon.Heart exposing (icon) 2 | 3 | import Element 4 | import Element.Icon as Icon 5 | import Element.Palette as Palette 6 | import Svg 7 | import Svg.Attributes exposing (..) 8 | 9 | 10 | icon : Element.Color -> Element.Element msg 11 | icon color = 12 | Icon.small 13 | (Svg.svg [ viewBox "0 0 101 88", width "100%" ] 14 | [ Svg.path 15 | [ d path_ 16 | , fill (Palette.toRgbString color) 17 | , fillRule "nonzero" 18 | , Icon.hoverTarget 19 | ] 20 | [] 21 | ] 22 | ) 23 | 24 | 25 | path_ : String 26 | path_ = 27 | "M13.441 50.606C4.727 42.054.942 32.679.942 24.29.942 9.982 10.483.441 24.953.441c13.484 0 18.096 6.252 25.989 15.297C58.836 6.693 63.44.441 76.925.441c14.477 0 24.018 9.541 24.018 23.849 0 8.389-3.784 17.765-12.498 26.316L50.942 87.942 13.441 50.606z" 28 | -------------------------------------------------------------------------------- /frontend/src/elm/Element/Avatar.elm: -------------------------------------------------------------------------------- 1 | module Element.Avatar exposing 2 | ( large 3 | , medium 4 | , small 5 | ) 6 | 7 | import Element exposing (..) 8 | import Html.Attributes exposing (class) 9 | 10 | 11 | small : Maybe String -> Element msg 12 | small = 13 | image_ 25 14 | 15 | 16 | medium : Maybe String -> Element msg 17 | medium = 18 | image_ 35 19 | 20 | 21 | large : Maybe String -> Element msg 22 | large = 23 | image_ 45 24 | 25 | 26 | image_ : Int -> Maybe String -> Element msg 27 | image_ size src = 28 | Element.image 29 | [ width (px size) 30 | , height (px size) 31 | , htmlAttribute (class "circular-image") 32 | ] 33 | { src = defaultIfNoProfileImage src 34 | , description = "avatar-image" 35 | } 36 | 37 | 38 | defaultIfNoProfileImage : Maybe String -> String 39 | defaultIfNoProfileImage = 40 | Maybe.withDefault "https://static.productionready.io/images/smiley-cyrus.jpg" 41 | -------------------------------------------------------------------------------- /backend/hasura/metadata/databases/default/tables/public_follows.yaml: -------------------------------------------------------------------------------- 1 | table: 2 | name: follows 3 | schema: public 4 | configuration: 5 | column_config: {} 6 | custom_column_names: {} 7 | custom_root_fields: 8 | delete: unfollow_authors 9 | insert: follow_authors 10 | insert_one: follow_author 11 | object_relationships: 12 | - name: user 13 | using: 14 | foreign_key_constraint_on: following_id 15 | insert_permissions: 16 | - permission: 17 | check: 18 | following_id: 19 | _ne: X-Hasura-User-Id 20 | columns: 21 | - following_id 22 | set: 23 | user_id: x-hasura-User-Id 24 | role: user 25 | select_permissions: 26 | - permission: 27 | columns: 28 | - following_id 29 | filter: {} 30 | role: anonymous 31 | - permission: 32 | columns: 33 | - following_id 34 | filter: {} 35 | role: user 36 | delete_permissions: 37 | - permission: 38 | filter: 39 | user_id: 40 | _eq: X-Hasura-User-Id 41 | role: user 42 | -------------------------------------------------------------------------------- /frontend/src/elm/Utils/Update.elm: -------------------------------------------------------------------------------- 1 | module Utils.Update exposing 2 | ( andThenWithEffect 3 | , updateWith 4 | , withCmd 5 | , withEffect 6 | ) 7 | 8 | import Effect exposing (Effect) 9 | 10 | 11 | updateWith : 12 | (subModel -> model) 13 | -> (subMsg -> msg) 14 | -> ( subModel, Effect subMsg ) 15 | -> ( model, Effect msg ) 16 | updateWith modelF msgF ( model, eff ) = 17 | ( modelF model 18 | , Effect.map msgF eff 19 | ) 20 | 21 | 22 | withCmd : Cmd msg -> ( model, Cmd msg ) -> ( model, Cmd msg ) 23 | withCmd cmd2 ( model, cmd1 ) = 24 | ( model, Cmd.batch [ cmd1, cmd2 ] ) 25 | 26 | 27 | withEffect : Effect msg -> ( model, Effect msg ) -> ( model, Effect msg ) 28 | withEffect eff2 ( model, eff1 ) = 29 | ( model, Effect.batch [ eff1, eff2 ] ) 30 | 31 | 32 | andThenWithEffect : (model -> Effect msg) -> ( model, Effect msg ) -> ( model, Effect msg ) 33 | andThenWithEffect toEffect ( model, eff ) = 34 | ( model, Effect.batch [ toEffect model, eff ] ) 35 | -------------------------------------------------------------------------------- /frontend/src/elm/Hasura/Object/Articles_sum_fields.elm: -------------------------------------------------------------------------------- 1 | -- Do not manually edit this file, it was auto-generated by dillonkearns/elm-graphql 2 | -- https://github.com/dillonkearns/elm-graphql 3 | 4 | 5 | module Hasura.Object.Articles_sum_fields exposing (..) 6 | 7 | import Graphql.Internal.Builder.Argument as Argument exposing (Argument) 8 | import Graphql.Internal.Builder.Object as Object 9 | import Graphql.Internal.Encode as Encode exposing (Value) 10 | import Graphql.Operation exposing (RootMutation, RootQuery, RootSubscription) 11 | import Graphql.OptionalArgument exposing (OptionalArgument(..)) 12 | import Graphql.SelectionSet exposing (SelectionSet) 13 | import Hasura.InputObject 14 | import Hasura.Interface 15 | import Hasura.Object 16 | import Hasura.Scalar 17 | import Hasura.ScalarCodecs 18 | import Hasura.Union 19 | import Json.Decode as Decode 20 | 21 | 22 | id : SelectionSet (Maybe Int) Hasura.Object.Articles_sum_fields 23 | id = 24 | Object.selectionForField "(Maybe Int)" "id" [] (Decode.int |> Decode.nullable) 25 | -------------------------------------------------------------------------------- /frontend/src/elm/Hasura/Object/Articles_avg_fields.elm: -------------------------------------------------------------------------------- 1 | -- Do not manually edit this file, it was auto-generated by dillonkearns/elm-graphql 2 | -- https://github.com/dillonkearns/elm-graphql 3 | 4 | 5 | module Hasura.Object.Articles_avg_fields exposing (..) 6 | 7 | import Graphql.Internal.Builder.Argument as Argument exposing (Argument) 8 | import Graphql.Internal.Builder.Object as Object 9 | import Graphql.Internal.Encode as Encode exposing (Value) 10 | import Graphql.Operation exposing (RootMutation, RootQuery, RootSubscription) 11 | import Graphql.OptionalArgument exposing (OptionalArgument(..)) 12 | import Graphql.SelectionSet exposing (SelectionSet) 13 | import Hasura.InputObject 14 | import Hasura.Interface 15 | import Hasura.Object 16 | import Hasura.Scalar 17 | import Hasura.ScalarCodecs 18 | import Hasura.Union 19 | import Json.Decode as Decode 20 | 21 | 22 | id : SelectionSet (Maybe Float) Hasura.Object.Articles_avg_fields 23 | id = 24 | Object.selectionForField "(Maybe Float)" "id" [] (Decode.float |> Decode.nullable) 25 | -------------------------------------------------------------------------------- /frontend/src/elm/Hasura/Object/Articles_stddev_fields.elm: -------------------------------------------------------------------------------- 1 | -- Do not manually edit this file, it was auto-generated by dillonkearns/elm-graphql 2 | -- https://github.com/dillonkearns/elm-graphql 3 | 4 | 5 | module Hasura.Object.Articles_stddev_fields exposing (..) 6 | 7 | import Graphql.Internal.Builder.Argument as Argument exposing (Argument) 8 | import Graphql.Internal.Builder.Object as Object 9 | import Graphql.Internal.Encode as Encode exposing (Value) 10 | import Graphql.Operation exposing (RootMutation, RootQuery, RootSubscription) 11 | import Graphql.OptionalArgument exposing (OptionalArgument(..)) 12 | import Graphql.SelectionSet exposing (SelectionSet) 13 | import Hasura.InputObject 14 | import Hasura.Interface 15 | import Hasura.Object 16 | import Hasura.Scalar 17 | import Hasura.ScalarCodecs 18 | import Hasura.Union 19 | import Json.Decode as Decode 20 | 21 | 22 | id : SelectionSet (Maybe Float) Hasura.Object.Articles_stddev_fields 23 | id = 24 | Object.selectionForField "(Maybe Float)" "id" [] (Decode.float |> Decode.nullable) 25 | -------------------------------------------------------------------------------- /frontend/src/elm/Hasura/Object/Articles_var_pop_fields.elm: -------------------------------------------------------------------------------- 1 | -- Do not manually edit this file, it was auto-generated by dillonkearns/elm-graphql 2 | -- https://github.com/dillonkearns/elm-graphql 3 | 4 | 5 | module Hasura.Object.Articles_var_pop_fields exposing (..) 6 | 7 | import Graphql.Internal.Builder.Argument as Argument exposing (Argument) 8 | import Graphql.Internal.Builder.Object as Object 9 | import Graphql.Internal.Encode as Encode exposing (Value) 10 | import Graphql.Operation exposing (RootMutation, RootQuery, RootSubscription) 11 | import Graphql.OptionalArgument exposing (OptionalArgument(..)) 12 | import Graphql.SelectionSet exposing (SelectionSet) 13 | import Hasura.InputObject 14 | import Hasura.Interface 15 | import Hasura.Object 16 | import Hasura.Scalar 17 | import Hasura.ScalarCodecs 18 | import Hasura.Union 19 | import Json.Decode as Decode 20 | 21 | 22 | id : SelectionSet (Maybe Float) Hasura.Object.Articles_var_pop_fields 23 | id = 24 | Object.selectionForField "(Maybe Float)" "id" [] (Decode.float |> Decode.nullable) 25 | -------------------------------------------------------------------------------- /frontend/src/elm/Hasura/Object/Articles_var_samp_fields.elm: -------------------------------------------------------------------------------- 1 | -- Do not manually edit this file, it was auto-generated by dillonkearns/elm-graphql 2 | -- https://github.com/dillonkearns/elm-graphql 3 | 4 | 5 | module Hasura.Object.Articles_var_samp_fields exposing (..) 6 | 7 | import Graphql.Internal.Builder.Argument as Argument exposing (Argument) 8 | import Graphql.Internal.Builder.Object as Object 9 | import Graphql.Internal.Encode as Encode exposing (Value) 10 | import Graphql.Operation exposing (RootMutation, RootQuery, RootSubscription) 11 | import Graphql.OptionalArgument exposing (OptionalArgument(..)) 12 | import Graphql.SelectionSet exposing (SelectionSet) 13 | import Hasura.InputObject 14 | import Hasura.Interface 15 | import Hasura.Object 16 | import Hasura.Scalar 17 | import Hasura.ScalarCodecs 18 | import Hasura.Union 19 | import Json.Decode as Decode 20 | 21 | 22 | id : SelectionSet (Maybe Float) Hasura.Object.Articles_var_samp_fields 23 | id = 24 | Object.selectionForField "(Maybe Float)" "id" [] (Decode.float |> Decode.nullable) 25 | -------------------------------------------------------------------------------- /frontend/src/elm/Hasura/Object/Articles_variance_fields.elm: -------------------------------------------------------------------------------- 1 | -- Do not manually edit this file, it was auto-generated by dillonkearns/elm-graphql 2 | -- https://github.com/dillonkearns/elm-graphql 3 | 4 | 5 | module Hasura.Object.Articles_variance_fields exposing (..) 6 | 7 | import Graphql.Internal.Builder.Argument as Argument exposing (Argument) 8 | import Graphql.Internal.Builder.Object as Object 9 | import Graphql.Internal.Encode as Encode exposing (Value) 10 | import Graphql.Operation exposing (RootMutation, RootQuery, RootSubscription) 11 | import Graphql.OptionalArgument exposing (OptionalArgument(..)) 12 | import Graphql.SelectionSet exposing (SelectionSet) 13 | import Hasura.InputObject 14 | import Hasura.Interface 15 | import Hasura.Object 16 | import Hasura.Scalar 17 | import Hasura.ScalarCodecs 18 | import Hasura.Union 19 | import Json.Decode as Decode 20 | 21 | 22 | id : SelectionSet (Maybe Float) Hasura.Object.Articles_variance_fields 23 | id = 24 | Object.selectionForField "(Maybe Float)" "id" [] (Decode.float |> Decode.nullable) 25 | -------------------------------------------------------------------------------- /frontend/src/elm/Hasura/Object/Articles_stddev_pop_fields.elm: -------------------------------------------------------------------------------- 1 | -- Do not manually edit this file, it was auto-generated by dillonkearns/elm-graphql 2 | -- https://github.com/dillonkearns/elm-graphql 3 | 4 | 5 | module Hasura.Object.Articles_stddev_pop_fields exposing (..) 6 | 7 | import Graphql.Internal.Builder.Argument as Argument exposing (Argument) 8 | import Graphql.Internal.Builder.Object as Object 9 | import Graphql.Internal.Encode as Encode exposing (Value) 10 | import Graphql.Operation exposing (RootMutation, RootQuery, RootSubscription) 11 | import Graphql.OptionalArgument exposing (OptionalArgument(..)) 12 | import Graphql.SelectionSet exposing (SelectionSet) 13 | import Hasura.InputObject 14 | import Hasura.Interface 15 | import Hasura.Object 16 | import Hasura.Scalar 17 | import Hasura.ScalarCodecs 18 | import Hasura.Union 19 | import Json.Decode as Decode 20 | 21 | 22 | id : SelectionSet (Maybe Float) Hasura.Object.Articles_stddev_pop_fields 23 | id = 24 | Object.selectionForField "(Maybe Float)" "id" [] (Decode.float |> Decode.nullable) 25 | -------------------------------------------------------------------------------- /frontend/src/elm/Hasura/Object/Articles_stddev_samp_fields.elm: -------------------------------------------------------------------------------- 1 | -- Do not manually edit this file, it was auto-generated by dillonkearns/elm-graphql 2 | -- https://github.com/dillonkearns/elm-graphql 3 | 4 | 5 | module Hasura.Object.Articles_stddev_samp_fields exposing (..) 6 | 7 | import Graphql.Internal.Builder.Argument as Argument exposing (Argument) 8 | import Graphql.Internal.Builder.Object as Object 9 | import Graphql.Internal.Encode as Encode exposing (Value) 10 | import Graphql.Operation exposing (RootMutation, RootQuery, RootSubscription) 11 | import Graphql.OptionalArgument exposing (OptionalArgument(..)) 12 | import Graphql.SelectionSet exposing (SelectionSet) 13 | import Hasura.InputObject 14 | import Hasura.Interface 15 | import Hasura.Object 16 | import Hasura.Scalar 17 | import Hasura.ScalarCodecs 18 | import Hasura.Union 19 | import Json.Decode as Decode 20 | 21 | 22 | id : SelectionSet (Maybe Float) Hasura.Object.Articles_stddev_samp_fields 23 | id = 24 | Object.selectionForField "(Maybe Float)" "id" [] (Decode.float |> Decode.nullable) 25 | -------------------------------------------------------------------------------- /backend/hasura/docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: "3.6" 2 | services: 3 | postgres: 4 | image: postgres 5 | restart: always 6 | ports: 7 | - "65432:5432" 8 | environment: 9 | POSTGRES_HOST_AUTH_METHOD: trust 10 | graphql-engine: 11 | image: hasura/graphql-engine:v2.16.0 12 | ports: 13 | - "8080:8080" 14 | depends_on: 15 | - "postgres" 16 | restart: always 17 | environment: 18 | DATABASE_URL: "postgres://postgres:@postgres:5432/postgres" 19 | HASURA_GRAPHQL_METADATA_DATABASE_URL: "postgres://postgres:@postgres:5432/postgres" 20 | HASURA_GRAPHQL_ENABLE_CONSOLE: "true" 21 | HASURA_GRAPHQL_ENABLED_LOG_TYPES: startup, http-log, webhook-log, websocket-log, query-log 22 | HASURA_GRAPHQL_ADMIN_SECRET: ilovebread 23 | HASURA_GRAPHQL_UNAUTHORIZED_ROLE: anonymous 24 | HASURA_GRAPHQL_JWT_SECRET: '{"type":"HS256","key":"3EK6FD+o0+c7tzBNVfjpMkNDi2yARAAKzQlk8O2IKoxQu4nF7EdAh8s3TwpHwrdWT6R"}' 25 | ACTIONS_BASE_URL: http://host.docker.internal:3000/api 26 | ACTIONS_SECRET: ilovebread 27 | -------------------------------------------------------------------------------- /frontend/src/elm/Element/Transition/Simple.elm: -------------------------------------------------------------------------------- 1 | module Element.Transition.Simple exposing 2 | ( delay 3 | , ease 4 | , easeOut 5 | , linear 6 | ) 7 | 8 | import Element exposing (htmlAttribute) 9 | import Html.Attributes 10 | 11 | 12 | 13 | -- Eases 14 | 15 | 16 | linear : Int -> Element.Attribute msg 17 | linear = 18 | all_ "linear" 19 | 20 | 21 | ease : Int -> Element.Attribute msg 22 | ease = 23 | all_ "ease" 24 | 25 | 26 | easeOut : Int -> Element.Attribute msg 27 | easeOut = 28 | all_ "ease-out" 29 | 30 | 31 | 32 | -- Delay 33 | 34 | 35 | delay : Int -> Element.Attribute msg 36 | delay ms = 37 | style "transition-delay" (millis ms) 38 | 39 | 40 | 41 | -- Helpers 42 | 43 | 44 | all_ : String -> Int -> Element.Attribute msg 45 | all_ ease_ ms = 46 | style "transition" (millis ms ++ " " ++ ease_) 47 | 48 | 49 | style : String -> String -> Element.Attribute msg 50 | style a b = 51 | htmlAttribute (Html.Attributes.style a b) 52 | 53 | 54 | millis : Int -> String 55 | millis n = 56 | String.fromInt n ++ "ms" 57 | -------------------------------------------------------------------------------- /tests/TagTest.elm: -------------------------------------------------------------------------------- 1 | module TagTest exposing (suite) 2 | 3 | import Expect 4 | import Tag exposing (Tag) 5 | import Test exposing (describe, test) 6 | 7 | 8 | suite : Test.Test 9 | suite = 10 | describe "Tag.parse" 11 | [ test "Separates tags by characters other than letters and numbers" <| 12 | \_ -> 13 | Tag.parse "tag1, tag2,tag3!//&tag4" 14 | |> expectTags 15 | [ "tag1" 16 | , "tag2" 17 | , "tag3" 18 | , "tag4" 19 | ] 20 | , test "Handles empty input" <| 21 | \_ -> 22 | Tag.parse "" |> expectTags [] 23 | , test "Removes duplicates" <| 24 | \_ -> 25 | Tag.parse "tag2, tag1, tag1, tag1" |> expectTags [ "tag2", "tag1" ] 26 | ] 27 | 28 | 29 | expectTags : List String -> List Tag -> Expect.Expectation 30 | expectTags = 31 | Expect.equal << tags 32 | 33 | 34 | tags : List String -> List Tag 35 | tags = 36 | List.map Tag.one 37 | -------------------------------------------------------------------------------- /tests/Page/SignUpTest.elm: -------------------------------------------------------------------------------- 1 | module Page.SignUpTest exposing (suite) 2 | 3 | import Expect 4 | import Program 5 | import Program.Expect as Expect 6 | import Program.Selector exposing (el, filledField) 7 | import ProgramTest exposing (clickButton, ensureViewHas, expectViewHas) 8 | import Route 9 | import Test exposing (..) 10 | 11 | 12 | suite : Test 13 | suite = 14 | test "User is redirected to home after signup" <| 15 | \_ -> 16 | Program.withPage Route.SignUp 17 | |> Program.start 18 | |> Program.fillField "email" "a@b.com" 19 | |> Program.fillField "username" "amacmurray" 20 | |> Program.fillField "password" "abc123" 21 | |> ensureViewHas 22 | [ filledField "a@b.com" 23 | , filledField "amacmurray" 24 | , filledField "abc123" 25 | ] 26 | |> clickButton "Sign Up" 27 | |> Expect.all 28 | [ Expect.redirectHome 29 | , expectViewHas [ el "new-post-link" ] 30 | ] 31 | -------------------------------------------------------------------------------- /tests/Program/Expect.elm: -------------------------------------------------------------------------------- 1 | module Program.Expect exposing 2 | ( contains 3 | , hasNoEls 4 | , redirect 5 | , redirectHome 6 | , redirectToPublishedArticle 7 | ) 8 | 9 | import Expect exposing (Expectation) 10 | import Program exposing (BlogProgramTest, baseUrl) 11 | import Program.Selector exposing (el) 12 | import ProgramTest 13 | import Route exposing (Route) 14 | import Test.Html.Query as Query 15 | 16 | 17 | hasNoEls : String -> Query.Single msg -> Expect.Expectation 18 | hasNoEls = 19 | contains 0 20 | 21 | 22 | contains : Int -> String -> Query.Single msg -> Expectation 23 | contains n label = 24 | Query.findAll [ el label ] >> Query.count (Expect.equal n) 25 | 26 | 27 | redirect : Route -> BlogProgramTest -> Expectation 28 | redirect route = 29 | ProgramTest.expectBrowserUrl (\s -> Expect.equal (baseUrl ++ Route.routeToString route) s) 30 | 31 | 32 | redirectToPublishedArticle : BlogProgramTest -> Expectation 33 | redirectToPublishedArticle = 34 | redirect (Route.Article 1) 35 | 36 | 37 | redirectHome : BlogProgramTest -> Expectation 38 | redirectHome = 39 | redirect Route.home 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Andrew MacMurray 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /frontend/src/elm/Element/Icon/Plus.elm: -------------------------------------------------------------------------------- 1 | module Element.Icon.Plus exposing (cross, icon) 2 | 3 | import Element exposing (Color, Element, el) 4 | import Element.Icon as Icon 5 | import Element.Palette as Palette 6 | import Element.Transition.Simple as Transition 7 | import Svg 8 | import Svg.Attributes exposing (..) 9 | 10 | 11 | cross : Color -> Element msg 12 | cross = 13 | icon_ 135 14 | 15 | 16 | icon : Color -> Element msg 17 | icon = 18 | icon_ 0 19 | 20 | 21 | icon_ : Float -> Color -> Element msg 22 | icon_ degrees_ color = 23 | el 24 | [ Element.rotate (degrees degrees_) 25 | , Transition.ease 300 26 | ] 27 | (Icon.small 28 | (Svg.svg [ viewBox "0 0 16 15", width "100%" ] 29 | [ Svg.g 30 | [ fill (Palette.toRgbString color) 31 | , Icon.hoverTarget 32 | ] 33 | [ Svg.rect [ x "7", width "2", height "15", rx "1" ] [] 34 | , Svg.path [ d "M15.5 7.5a1 1 0 01-1 1h-13a1 1 0 010-2h13a1 1 0 011 1z" ] [] 35 | ] 36 | ] 37 | ) 38 | ) 39 | -------------------------------------------------------------------------------- /backend/hasura/metadata/databases/default/tables/public_users.yaml: -------------------------------------------------------------------------------- 1 | table: 2 | name: users 3 | schema: public 4 | configuration: 5 | column_config: {} 6 | custom_column_names: {} 7 | custom_root_fields: 8 | insert_one: create_user 9 | select_by_pk: user 10 | update_by_pk: update_user 11 | array_relationships: 12 | - name: articles 13 | using: 14 | foreign_key_constraint_on: 15 | column: author_id 16 | table: 17 | name: articles 18 | schema: public 19 | - name: follows 20 | using: 21 | foreign_key_constraint_on: 22 | column: user_id 23 | table: 24 | name: follows 25 | schema: public 26 | select_permissions: 27 | - permission: 28 | columns: 29 | - id 30 | - profile_image 31 | - username 32 | filter: {} 33 | role: anonymous 34 | - permission: 35 | columns: 36 | - id 37 | - profile_image 38 | - username 39 | filter: {} 40 | role: user 41 | update_permissions: 42 | - permission: 43 | check: null 44 | columns: 45 | - bio 46 | - email 47 | - profile_image 48 | - username 49 | filter: 50 | id: 51 | _eq: X-Hasura-User-Id 52 | role: user 53 | -------------------------------------------------------------------------------- /backend/hasura/metadata/databases/default/tables/public_tags.yaml: -------------------------------------------------------------------------------- 1 | table: 2 | name: tags 3 | schema: public 4 | configuration: 5 | column_config: {} 6 | custom_column_names: {} 7 | custom_root_fields: 8 | insert_one: insert_tag 9 | select_aggregate: tags_summary 10 | select_by_pk: tag 11 | object_relationships: 12 | - name: article 13 | using: 14 | foreign_key_constraint_on: article_id 15 | computed_fields: 16 | - comment: "" 17 | definition: 18 | function: 19 | name: tag_count 20 | schema: public 21 | name: count 22 | insert_permissions: 23 | - permission: 24 | check: 25 | article: 26 | author_id: 27 | _eq: X-Hasura-User-Id 28 | columns: 29 | - article_id 30 | - tag 31 | role: user 32 | select_permissions: 33 | - permission: 34 | columns: 35 | - tag 36 | computed_fields: 37 | - count 38 | filter: {} 39 | role: anonymous 40 | - permission: 41 | columns: 42 | - tag 43 | computed_fields: 44 | - count 45 | filter: {} 46 | role: user 47 | delete_permissions: 48 | - permission: 49 | filter: 50 | article: 51 | author_id: 52 | _eq: X-Hasura-User-Id 53 | role: user 54 | -------------------------------------------------------------------------------- /frontend/src/elm/Hasura/Object/Follows.elm: -------------------------------------------------------------------------------- 1 | -- Do not manually edit this file, it was auto-generated by dillonkearns/elm-graphql 2 | -- https://github.com/dillonkearns/elm-graphql 3 | 4 | 5 | module Hasura.Object.Follows exposing (..) 6 | 7 | import Graphql.Internal.Builder.Argument as Argument exposing (Argument) 8 | import Graphql.Internal.Builder.Object as Object 9 | import Graphql.Internal.Encode as Encode exposing (Value) 10 | import Graphql.Operation exposing (RootMutation, RootQuery, RootSubscription) 11 | import Graphql.OptionalArgument exposing (OptionalArgument(..)) 12 | import Graphql.SelectionSet exposing (SelectionSet) 13 | import Hasura.InputObject 14 | import Hasura.Interface 15 | import Hasura.Object 16 | import Hasura.Scalar 17 | import Hasura.ScalarCodecs 18 | import Hasura.Union 19 | import Json.Decode as Decode 20 | 21 | 22 | following_id : SelectionSet Int Hasura.Object.Follows 23 | following_id = 24 | Object.selectionForField "Int" "following_id" [] Decode.int 25 | 26 | 27 | {-| An object relationship 28 | -} 29 | user : SelectionSet decodesTo Hasura.Object.Users -> SelectionSet decodesTo Hasura.Object.Follows 30 | user object_ = 31 | Object.selectionForCompositeField "user" [] object_ identity 32 | -------------------------------------------------------------------------------- /frontend/src/elm/Hasura/Object/Likes_max_fields.elm: -------------------------------------------------------------------------------- 1 | -- Do not manually edit this file, it was auto-generated by dillonkearns/elm-graphql 2 | -- https://github.com/dillonkearns/elm-graphql 3 | 4 | 5 | module Hasura.Object.Likes_max_fields exposing (..) 6 | 7 | import Graphql.Internal.Builder.Argument as Argument exposing (Argument) 8 | import Graphql.Internal.Builder.Object as Object 9 | import Graphql.Internal.Encode as Encode exposing (Value) 10 | import Graphql.Operation exposing (RootMutation, RootQuery, RootSubscription) 11 | import Graphql.OptionalArgument exposing (OptionalArgument(..)) 12 | import Graphql.SelectionSet exposing (SelectionSet) 13 | import Hasura.InputObject 14 | import Hasura.Interface 15 | import Hasura.Object 16 | import Hasura.Scalar 17 | import Hasura.ScalarCodecs 18 | import Hasura.Union 19 | import Json.Decode as Decode 20 | 21 | 22 | article_id : SelectionSet (Maybe Int) Hasura.Object.Likes_max_fields 23 | article_id = 24 | Object.selectionForField "(Maybe Int)" "article_id" [] (Decode.int |> Decode.nullable) 25 | 26 | 27 | user_id : SelectionSet (Maybe Int) Hasura.Object.Likes_max_fields 28 | user_id = 29 | Object.selectionForField "(Maybe Int)" "user_id" [] (Decode.int |> Decode.nullable) 30 | -------------------------------------------------------------------------------- /frontend/src/elm/Hasura/Object/Likes_min_fields.elm: -------------------------------------------------------------------------------- 1 | -- Do not manually edit this file, it was auto-generated by dillonkearns/elm-graphql 2 | -- https://github.com/dillonkearns/elm-graphql 3 | 4 | 5 | module Hasura.Object.Likes_min_fields exposing (..) 6 | 7 | import Graphql.Internal.Builder.Argument as Argument exposing (Argument) 8 | import Graphql.Internal.Builder.Object as Object 9 | import Graphql.Internal.Encode as Encode exposing (Value) 10 | import Graphql.Operation exposing (RootMutation, RootQuery, RootSubscription) 11 | import Graphql.OptionalArgument exposing (OptionalArgument(..)) 12 | import Graphql.SelectionSet exposing (SelectionSet) 13 | import Hasura.InputObject 14 | import Hasura.Interface 15 | import Hasura.Object 16 | import Hasura.Scalar 17 | import Hasura.ScalarCodecs 18 | import Hasura.Union 19 | import Json.Decode as Decode 20 | 21 | 22 | article_id : SelectionSet (Maybe Int) Hasura.Object.Likes_min_fields 23 | article_id = 24 | Object.selectionForField "(Maybe Int)" "article_id" [] (Decode.int |> Decode.nullable) 25 | 26 | 27 | user_id : SelectionSet (Maybe Int) Hasura.Object.Likes_min_fields 28 | user_id = 29 | Object.selectionForField "(Maybe Int)" "user_id" [] (Decode.int |> Decode.nullable) 30 | -------------------------------------------------------------------------------- /frontend/src/elm/Hasura/Object/Likes_sum_fields.elm: -------------------------------------------------------------------------------- 1 | -- Do not manually edit this file, it was auto-generated by dillonkearns/elm-graphql 2 | -- https://github.com/dillonkearns/elm-graphql 3 | 4 | 5 | module Hasura.Object.Likes_sum_fields exposing (..) 6 | 7 | import Graphql.Internal.Builder.Argument as Argument exposing (Argument) 8 | import Graphql.Internal.Builder.Object as Object 9 | import Graphql.Internal.Encode as Encode exposing (Value) 10 | import Graphql.Operation exposing (RootMutation, RootQuery, RootSubscription) 11 | import Graphql.OptionalArgument exposing (OptionalArgument(..)) 12 | import Graphql.SelectionSet exposing (SelectionSet) 13 | import Hasura.InputObject 14 | import Hasura.Interface 15 | import Hasura.Object 16 | import Hasura.Scalar 17 | import Hasura.ScalarCodecs 18 | import Hasura.Union 19 | import Json.Decode as Decode 20 | 21 | 22 | article_id : SelectionSet (Maybe Int) Hasura.Object.Likes_sum_fields 23 | article_id = 24 | Object.selectionForField "(Maybe Int)" "article_id" [] (Decode.int |> Decode.nullable) 25 | 26 | 27 | user_id : SelectionSet (Maybe Int) Hasura.Object.Likes_sum_fields 28 | user_id = 29 | Object.selectionForField "(Maybe Int)" "user_id" [] (Decode.int |> Decode.nullable) 30 | -------------------------------------------------------------------------------- /frontend/src/elm/Context.elm: -------------------------------------------------------------------------------- 1 | module Context exposing 2 | ( Context 3 | , closeMenu 4 | , init 5 | , openMenu 6 | , setUser 7 | , updateUser 8 | ) 9 | 10 | import Element.Layout.Menu as Menu exposing (Menu) 11 | import Ports 12 | import User exposing (User) 13 | 14 | 15 | 16 | -- Context 17 | 18 | 19 | type alias Context = 20 | { user : User 21 | , menu : Menu 22 | } 23 | 24 | 25 | 26 | -- Init 27 | 28 | 29 | init : Maybe Ports.User -> Context 30 | init user = 31 | { menu = Menu.Closed 32 | , user = userFromFlags user 33 | } 34 | 35 | 36 | userFromFlags : Maybe Ports.User -> User 37 | userFromFlags = 38 | Maybe.map Ports.toLoggedIn >> Maybe.withDefault User.Guest 39 | 40 | 41 | 42 | -- Update 43 | 44 | 45 | setUser : User -> Context -> Context 46 | setUser user = 47 | updateUser (always user) 48 | 49 | 50 | updateUser : (User -> User) -> Context -> Context 51 | updateUser toUser context = 52 | { context | user = toUser context.user } 53 | 54 | 55 | openMenu : Context -> Context 56 | openMenu context = 57 | { context | menu = Menu.Open } 58 | 59 | 60 | closeMenu : Context -> Context 61 | closeMenu context = 62 | { context | menu = Menu.Closed } 63 | -------------------------------------------------------------------------------- /backend/actions/test/PasswordTest.purs: -------------------------------------------------------------------------------- 1 | module Test.Password where 2 | 3 | import Prelude 4 | import Data.Either (Either(..)) 5 | import Password as Password 6 | import Test.Unit (TestSuite, describe, test) 7 | import Test.Unit.Assert as Assert 8 | 9 | suite :: TestSuite 10 | suite = 11 | describe "Password" do 12 | test "Checks password meets criteria" do 13 | Assert.equal 14 | (Right unit) 15 | (Password.checkCriteria "Abc123456") 16 | test "Short passwords" do 17 | Assert.equal 18 | (Left "Password does not meet criteria (at least 8 characters)") 19 | (Password.checkCriteria "Abc1234") 20 | test "Lower and uppercase" do 21 | Assert.equal 22 | (Left "Password does not meet criteria (contain uppercase, contain lowercase)") 23 | (Password.checkCriteria "12345678") 24 | test "Numbers" do 25 | Assert.equal 26 | (Left "Password does not meet criteria (contain numbers)") 27 | (Password.checkCriteria "ABCabcabc") 28 | test "Combines All errors" do 29 | Assert.equal 30 | (Left "Password does not meet criteria (at least 8 characters, contain uppercase, contain lowercase, contain numbers)") 31 | (Password.checkCriteria "?") 32 | -------------------------------------------------------------------------------- /frontend/src/elm/Article/Author.elm: -------------------------------------------------------------------------------- 1 | module Article.Author exposing 2 | ( Author 3 | , Id 4 | , build 5 | , id 6 | , profileImage 7 | , username 8 | ) 9 | 10 | -- Author 11 | 12 | 13 | type Author 14 | = Author Author_ 15 | 16 | 17 | type alias Author_ = 18 | { id : Id 19 | , username : String 20 | , profileImage : Maybe String 21 | } 22 | 23 | 24 | type alias Id = 25 | Int 26 | 27 | 28 | 29 | -- Construct 30 | 31 | 32 | build : Id -> String -> Maybe String -> Author 33 | build id_ username_ profileImage_ = 34 | Author 35 | { id = id_ 36 | , username = username_ 37 | , profileImage = profileImage_ 38 | } 39 | 40 | 41 | 42 | -- Query 43 | 44 | 45 | id : Author -> Id 46 | id = 47 | author_ >> .id 48 | 49 | 50 | username : Author -> String 51 | username = 52 | author_ >> .username >> trimLegacyLongUsernames 53 | 54 | 55 | profileImage : Author -> Maybe String 56 | profileImage = 57 | author_ >> .profileImage 58 | 59 | 60 | trimLegacyLongUsernames : String -> String 61 | trimLegacyLongUsernames = 62 | String.left 16 63 | 64 | 65 | 66 | -- Helpers 67 | 68 | 69 | author_ : Author -> Author_ 70 | author_ (Author a) = 71 | a 72 | -------------------------------------------------------------------------------- /frontend/src/elm/Hasura/Object/Likes_avg_fields.elm: -------------------------------------------------------------------------------- 1 | -- Do not manually edit this file, it was auto-generated by dillonkearns/elm-graphql 2 | -- https://github.com/dillonkearns/elm-graphql 3 | 4 | 5 | module Hasura.Object.Likes_avg_fields exposing (..) 6 | 7 | import Graphql.Internal.Builder.Argument as Argument exposing (Argument) 8 | import Graphql.Internal.Builder.Object as Object 9 | import Graphql.Internal.Encode as Encode exposing (Value) 10 | import Graphql.Operation exposing (RootMutation, RootQuery, RootSubscription) 11 | import Graphql.OptionalArgument exposing (OptionalArgument(..)) 12 | import Graphql.SelectionSet exposing (SelectionSet) 13 | import Hasura.InputObject 14 | import Hasura.Interface 15 | import Hasura.Object 16 | import Hasura.Scalar 17 | import Hasura.ScalarCodecs 18 | import Hasura.Union 19 | import Json.Decode as Decode 20 | 21 | 22 | article_id : SelectionSet (Maybe Float) Hasura.Object.Likes_avg_fields 23 | article_id = 24 | Object.selectionForField "(Maybe Float)" "article_id" [] (Decode.float |> Decode.nullable) 25 | 26 | 27 | user_id : SelectionSet (Maybe Float) Hasura.Object.Likes_avg_fields 28 | user_id = 29 | Object.selectionForField "(Maybe Float)" "user_id" [] (Decode.float |> Decode.nullable) 30 | -------------------------------------------------------------------------------- /frontend/src/elm/Hasura/Object/Likes_stddev_fields.elm: -------------------------------------------------------------------------------- 1 | -- Do not manually edit this file, it was auto-generated by dillonkearns/elm-graphql 2 | -- https://github.com/dillonkearns/elm-graphql 3 | 4 | 5 | module Hasura.Object.Likes_stddev_fields exposing (..) 6 | 7 | import Graphql.Internal.Builder.Argument as Argument exposing (Argument) 8 | import Graphql.Internal.Builder.Object as Object 9 | import Graphql.Internal.Encode as Encode exposing (Value) 10 | import Graphql.Operation exposing (RootMutation, RootQuery, RootSubscription) 11 | import Graphql.OptionalArgument exposing (OptionalArgument(..)) 12 | import Graphql.SelectionSet exposing (SelectionSet) 13 | import Hasura.InputObject 14 | import Hasura.Interface 15 | import Hasura.Object 16 | import Hasura.Scalar 17 | import Hasura.ScalarCodecs 18 | import Hasura.Union 19 | import Json.Decode as Decode 20 | 21 | 22 | article_id : SelectionSet (Maybe Float) Hasura.Object.Likes_stddev_fields 23 | article_id = 24 | Object.selectionForField "(Maybe Float)" "article_id" [] (Decode.float |> Decode.nullable) 25 | 26 | 27 | user_id : SelectionSet (Maybe Float) Hasura.Object.Likes_stddev_fields 28 | user_id = 29 | Object.selectionForField "(Maybe Float)" "user_id" [] (Decode.float |> Decode.nullable) 30 | -------------------------------------------------------------------------------- /frontend/src/elm/Hasura/Object/Likes_var_pop_fields.elm: -------------------------------------------------------------------------------- 1 | -- Do not manually edit this file, it was auto-generated by dillonkearns/elm-graphql 2 | -- https://github.com/dillonkearns/elm-graphql 3 | 4 | 5 | module Hasura.Object.Likes_var_pop_fields exposing (..) 6 | 7 | import Graphql.Internal.Builder.Argument as Argument exposing (Argument) 8 | import Graphql.Internal.Builder.Object as Object 9 | import Graphql.Internal.Encode as Encode exposing (Value) 10 | import Graphql.Operation exposing (RootMutation, RootQuery, RootSubscription) 11 | import Graphql.OptionalArgument exposing (OptionalArgument(..)) 12 | import Graphql.SelectionSet exposing (SelectionSet) 13 | import Hasura.InputObject 14 | import Hasura.Interface 15 | import Hasura.Object 16 | import Hasura.Scalar 17 | import Hasura.ScalarCodecs 18 | import Hasura.Union 19 | import Json.Decode as Decode 20 | 21 | 22 | article_id : SelectionSet (Maybe Float) Hasura.Object.Likes_var_pop_fields 23 | article_id = 24 | Object.selectionForField "(Maybe Float)" "article_id" [] (Decode.float |> Decode.nullable) 25 | 26 | 27 | user_id : SelectionSet (Maybe Float) Hasura.Object.Likes_var_pop_fields 28 | user_id = 29 | Object.selectionForField "(Maybe Float)" "user_id" [] (Decode.float |> Decode.nullable) 30 | -------------------------------------------------------------------------------- /frontend/src/elm/Hasura/Object/Likes_var_samp_fields.elm: -------------------------------------------------------------------------------- 1 | -- Do not manually edit this file, it was auto-generated by dillonkearns/elm-graphql 2 | -- https://github.com/dillonkearns/elm-graphql 3 | 4 | 5 | module Hasura.Object.Likes_var_samp_fields exposing (..) 6 | 7 | import Graphql.Internal.Builder.Argument as Argument exposing (Argument) 8 | import Graphql.Internal.Builder.Object as Object 9 | import Graphql.Internal.Encode as Encode exposing (Value) 10 | import Graphql.Operation exposing (RootMutation, RootQuery, RootSubscription) 11 | import Graphql.OptionalArgument exposing (OptionalArgument(..)) 12 | import Graphql.SelectionSet exposing (SelectionSet) 13 | import Hasura.InputObject 14 | import Hasura.Interface 15 | import Hasura.Object 16 | import Hasura.Scalar 17 | import Hasura.ScalarCodecs 18 | import Hasura.Union 19 | import Json.Decode as Decode 20 | 21 | 22 | article_id : SelectionSet (Maybe Float) Hasura.Object.Likes_var_samp_fields 23 | article_id = 24 | Object.selectionForField "(Maybe Float)" "article_id" [] (Decode.float |> Decode.nullable) 25 | 26 | 27 | user_id : SelectionSet (Maybe Float) Hasura.Object.Likes_var_samp_fields 28 | user_id = 29 | Object.selectionForField "(Maybe Float)" "user_id" [] (Decode.float |> Decode.nullable) 30 | -------------------------------------------------------------------------------- /frontend/src/elm/Hasura/Object/Likes_variance_fields.elm: -------------------------------------------------------------------------------- 1 | -- Do not manually edit this file, it was auto-generated by dillonkearns/elm-graphql 2 | -- https://github.com/dillonkearns/elm-graphql 3 | 4 | 5 | module Hasura.Object.Likes_variance_fields exposing (..) 6 | 7 | import Graphql.Internal.Builder.Argument as Argument exposing (Argument) 8 | import Graphql.Internal.Builder.Object as Object 9 | import Graphql.Internal.Encode as Encode exposing (Value) 10 | import Graphql.Operation exposing (RootMutation, RootQuery, RootSubscription) 11 | import Graphql.OptionalArgument exposing (OptionalArgument(..)) 12 | import Graphql.SelectionSet exposing (SelectionSet) 13 | import Hasura.InputObject 14 | import Hasura.Interface 15 | import Hasura.Object 16 | import Hasura.Scalar 17 | import Hasura.ScalarCodecs 18 | import Hasura.Union 19 | import Json.Decode as Decode 20 | 21 | 22 | article_id : SelectionSet (Maybe Float) Hasura.Object.Likes_variance_fields 23 | article_id = 24 | Object.selectionForField "(Maybe Float)" "article_id" [] (Decode.float |> Decode.nullable) 25 | 26 | 27 | user_id : SelectionSet (Maybe Float) Hasura.Object.Likes_variance_fields 28 | user_id = 29 | Object.selectionForField "(Maybe Float)" "user_id" [] (Decode.float |> Decode.nullable) 30 | -------------------------------------------------------------------------------- /frontend/src/elm/Hasura/Object/Likes_stddev_pop_fields.elm: -------------------------------------------------------------------------------- 1 | -- Do not manually edit this file, it was auto-generated by dillonkearns/elm-graphql 2 | -- https://github.com/dillonkearns/elm-graphql 3 | 4 | 5 | module Hasura.Object.Likes_stddev_pop_fields exposing (..) 6 | 7 | import Graphql.Internal.Builder.Argument as Argument exposing (Argument) 8 | import Graphql.Internal.Builder.Object as Object 9 | import Graphql.Internal.Encode as Encode exposing (Value) 10 | import Graphql.Operation exposing (RootMutation, RootQuery, RootSubscription) 11 | import Graphql.OptionalArgument exposing (OptionalArgument(..)) 12 | import Graphql.SelectionSet exposing (SelectionSet) 13 | import Hasura.InputObject 14 | import Hasura.Interface 15 | import Hasura.Object 16 | import Hasura.Scalar 17 | import Hasura.ScalarCodecs 18 | import Hasura.Union 19 | import Json.Decode as Decode 20 | 21 | 22 | article_id : SelectionSet (Maybe Float) Hasura.Object.Likes_stddev_pop_fields 23 | article_id = 24 | Object.selectionForField "(Maybe Float)" "article_id" [] (Decode.float |> Decode.nullable) 25 | 26 | 27 | user_id : SelectionSet (Maybe Float) Hasura.Object.Likes_stddev_pop_fields 28 | user_id = 29 | Object.selectionForField "(Maybe Float)" "user_id" [] (Decode.float |> Decode.nullable) 30 | -------------------------------------------------------------------------------- /frontend/src/elm/Element/Icon/Bin.elm: -------------------------------------------------------------------------------- 1 | module Element.Icon.Bin exposing (icon) 2 | 3 | import Element exposing (Color, Element) 4 | import Element.Icon as Icon 5 | import Element.Palette as Palette 6 | import Svg 7 | import Svg.Attributes exposing (..) 8 | 9 | 10 | icon : Color -> Element msg 11 | icon color = 12 | Icon.small 13 | (Svg.svg [ viewBox "0 0 13 15" ] 14 | [ Svg.g 15 | [ fill (Palette.toRgbString color) 16 | , fillRule "nonzero" 17 | , Icon.hoverTarget 18 | ] 19 | [ Svg.path [ d binPath ] [] 20 | , Svg.path [ d lidPath ] [] 21 | ] 22 | ] 23 | ) 24 | 25 | 26 | binPath : String 27 | binPath = 28 | "M11.2 4.7H1v8.2c0 1 .6 1.8 1.5 2l.4.1h6.5c1-.2 1.8-1 1.8-2.1v-8-.2zM3.8 9v3.7c-.1.3-.3.5-.6.4-.2 0-.4-.2-.4-.5V12v-2V7c0-.3.2-.6.5-.5.3 0 .5.2.5.5v2zm2.8.9V12.7c0 .3-.3.4-.5.4-.3 0-.5-.2-.5-.5v-1.2-4.2V7c0-.3.3-.5.5-.4.3 0 .5.2.5.5v2.8zm2.8 0V12.6c0 .3-.3.5-.5.4-.3 0-.5-.2-.5-.5v-2.2-3.2V7c0-.3.3-.4.5-.4.3 0 .5.2.5.4v2.8z" 29 | 30 | 31 | lidPath : String 32 | lidPath = 33 | "M12.2 2.8c0-.6-.3-1-.9-1H8.8V.8C8.8.3 8.5 0 8 0H4.1c-.4 0-.7.3-.7.7V2H.8c-.4 0-.7.2-.8.7v.3c0 .5.3.8.9.8h10.5c.4 0 .7-.2.8-.7v-.2z" 34 | -------------------------------------------------------------------------------- /frontend/src/elm/Hasura/Object/Likes_stddev_samp_fields.elm: -------------------------------------------------------------------------------- 1 | -- Do not manually edit this file, it was auto-generated by dillonkearns/elm-graphql 2 | -- https://github.com/dillonkearns/elm-graphql 3 | 4 | 5 | module Hasura.Object.Likes_stddev_samp_fields exposing (..) 6 | 7 | import Graphql.Internal.Builder.Argument as Argument exposing (Argument) 8 | import Graphql.Internal.Builder.Object as Object 9 | import Graphql.Internal.Encode as Encode exposing (Value) 10 | import Graphql.Operation exposing (RootMutation, RootQuery, RootSubscription) 11 | import Graphql.OptionalArgument exposing (OptionalArgument(..)) 12 | import Graphql.SelectionSet exposing (SelectionSet) 13 | import Hasura.InputObject 14 | import Hasura.Interface 15 | import Hasura.Object 16 | import Hasura.Scalar 17 | import Hasura.ScalarCodecs 18 | import Hasura.Union 19 | import Json.Decode as Decode 20 | 21 | 22 | article_id : SelectionSet (Maybe Float) Hasura.Object.Likes_stddev_samp_fields 23 | article_id = 24 | Object.selectionForField "(Maybe Float)" "article_id" [] (Decode.float |> Decode.nullable) 25 | 26 | 27 | user_id : SelectionSet (Maybe Float) Hasura.Object.Likes_stddev_samp_fields 28 | user_id = 29 | Object.selectionForField "(Maybe Float)" "user_id" [] (Decode.float |> Decode.nullable) 30 | -------------------------------------------------------------------------------- /frontend/src/elm/Hasura/Object/UnlikeResponse.elm: -------------------------------------------------------------------------------- 1 | -- Do not manually edit this file, it was auto-generated by dillonkearns/elm-graphql 2 | -- https://github.com/dillonkearns/elm-graphql 3 | 4 | 5 | module Hasura.Object.UnlikeResponse exposing (..) 6 | 7 | import Graphql.Internal.Builder.Argument as Argument exposing (Argument) 8 | import Graphql.Internal.Builder.Object as Object 9 | import Graphql.Internal.Encode as Encode exposing (Value) 10 | import Graphql.Operation exposing (RootMutation, RootQuery, RootSubscription) 11 | import Graphql.OptionalArgument exposing (OptionalArgument(..)) 12 | import Graphql.SelectionSet exposing (SelectionSet) 13 | import Hasura.InputObject 14 | import Hasura.Interface 15 | import Hasura.Object 16 | import Hasura.Scalar 17 | import Hasura.ScalarCodecs 18 | import Hasura.Union 19 | import Json.Decode as Decode 20 | 21 | 22 | article : SelectionSet decodesTo Hasura.Object.Articles -> SelectionSet (Maybe decodesTo) Hasura.Object.UnlikeResponse 23 | article object_ = 24 | Object.selectionForCompositeField "article" [] object_ (identity >> Decode.nullable) 25 | 26 | 27 | article_id : SelectionSet (Maybe Int) Hasura.Object.UnlikeResponse 28 | article_id = 29 | Object.selectionForField "(Maybe Int)" "article_id" [] (Decode.int |> Decode.nullable) 30 | -------------------------------------------------------------------------------- /frontend/src/elm/Utils/String.elm: -------------------------------------------------------------------------------- 1 | module Utils.String exposing 2 | ( NonEmpty 3 | , Optional 4 | , fromNonEmpty 5 | , fromOptional 6 | , pluralize 7 | , toNonEmpty 8 | , toOptional 9 | ) 10 | 11 | -- Utils 12 | 13 | 14 | pluralize : String -> Int -> String 15 | pluralize s n = 16 | if n == 1 then 17 | "1 " ++ s 18 | 19 | else 20 | String.fromInt n ++ " " ++ s ++ "s" 21 | 22 | 23 | 24 | -- Non Empty 25 | 26 | 27 | type NonEmpty 28 | = NonEmpty String 29 | 30 | 31 | toNonEmpty : String -> Maybe NonEmpty 32 | toNonEmpty str = 33 | if String.isEmpty str then 34 | Nothing 35 | 36 | else 37 | Just (NonEmpty str) 38 | 39 | 40 | fromNonEmpty : NonEmpty -> String 41 | fromNonEmpty (NonEmpty str) = 42 | str 43 | 44 | 45 | 46 | -- Optional 47 | 48 | 49 | type Optional 50 | = Entered String 51 | | Empty 52 | 53 | 54 | toOptional : String -> Optional 55 | toOptional str = 56 | if String.isEmpty str then 57 | Empty 58 | 59 | else 60 | Entered str 61 | 62 | 63 | fromOptional : Optional -> Maybe String 64 | fromOptional optional = 65 | case optional of 66 | Empty -> 67 | Nothing 68 | 69 | Entered str -> 70 | Just str 71 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "realworld-hasura", 3 | "version": "1.0.0", 4 | "scripts": { 5 | "dev": "elm-constants && vite", 6 | "build": "elm-constants && vite build", 7 | "schema": "elm-graphql http://localhost:8080/v1/graphql --base Hasura --header 'x-hasura-role: user' --header 'x-hasura-admin-secret: ilovebread' --output frontend/src/elm", 8 | "test": "elm-constants && elm-test", 9 | "seeds": "cd hasura && hasura seeds apply", 10 | "migrate": "npm run migrate:db && npm run migrate:metadata", 11 | "migrate:db": "cd backend/hasura && hasura migrate apply", 12 | "migrate:metadata": "cd backend/hasura && hasura metadata apply", 13 | "console": "cd backend/hasura && hasura console", 14 | "hasura": "cd backend/hasura && docker-compose up", 15 | "actions": "cd backend/actions && npm run dev" 16 | }, 17 | "license": "MIT", 18 | "dependencies": { 19 | "bcrypt": "^5.0.1", 20 | "jsonwebtoken": "^9.0.0", 21 | "xhr2": "^0.2.1" 22 | }, 23 | "devDependencies": { 24 | "@dillonkearns/elm-graphql": "^4.0.2", 25 | "elm": "^0.19.1-5", 26 | "elm-constants": "^1.0.0", 27 | "elm-test": "^0.19.1-revision7", 28 | "hasura-cli": "^2.4.0", 29 | "typescript": "^4.6.4", 30 | "vite": "^2.9.9", 31 | "vite-plugin-elm": "^2.6.1" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /frontend/src/elm/Form/Field.elm: -------------------------------------------------------------------------------- 1 | module Form.Field exposing 2 | ( Config 3 | , Field 4 | , field 5 | , id 6 | , identity 7 | , label 8 | , update 9 | , value 10 | ) 11 | 12 | -- Field Config 13 | 14 | 15 | type Field inputs 16 | = Field (Config inputs) 17 | 18 | 19 | type alias Config inputs = 20 | { value : inputs -> String 21 | , update : inputs -> String -> inputs 22 | , label : String 23 | } 24 | 25 | 26 | 27 | -- Construct 28 | 29 | 30 | field : Config inputs -> Field inputs 31 | field = 32 | Field 33 | 34 | 35 | identity : String -> Field String 36 | identity label_ = 37 | Field 38 | { label = label_ 39 | , update = always Basics.identity 40 | , value = Basics.identity 41 | } 42 | 43 | 44 | 45 | -- Query 46 | 47 | 48 | id : Field inputs -> String 49 | id = 50 | config >> .label 51 | 52 | 53 | label : Field inputs -> String 54 | label = 55 | config >> .label 56 | 57 | 58 | value : Field inputs -> inputs -> String 59 | value = 60 | config >> .value 61 | 62 | 63 | update : Field inputs -> inputs -> String -> inputs 64 | update = 65 | config >> .update 66 | 67 | 68 | 69 | -- Helpers 70 | 71 | 72 | config : Field inputs -> Config inputs 73 | config (Field config_) = 74 | config_ 75 | -------------------------------------------------------------------------------- /frontend/src/elm/Hasura/Object/Likes_aggregate.elm: -------------------------------------------------------------------------------- 1 | -- Do not manually edit this file, it was auto-generated by dillonkearns/elm-graphql 2 | -- https://github.com/dillonkearns/elm-graphql 3 | 4 | 5 | module Hasura.Object.Likes_aggregate exposing (..) 6 | 7 | import Graphql.Internal.Builder.Argument as Argument exposing (Argument) 8 | import Graphql.Internal.Builder.Object as Object 9 | import Graphql.Internal.Encode as Encode exposing (Value) 10 | import Graphql.Operation exposing (RootMutation, RootQuery, RootSubscription) 11 | import Graphql.OptionalArgument exposing (OptionalArgument(..)) 12 | import Graphql.SelectionSet exposing (SelectionSet) 13 | import Hasura.InputObject 14 | import Hasura.Interface 15 | import Hasura.Object 16 | import Hasura.Scalar 17 | import Hasura.ScalarCodecs 18 | import Hasura.Union 19 | import Json.Decode as Decode 20 | 21 | 22 | aggregate : SelectionSet decodesTo Hasura.Object.Likes_aggregate_fields -> SelectionSet (Maybe decodesTo) Hasura.Object.Likes_aggregate 23 | aggregate object_ = 24 | Object.selectionForCompositeField "aggregate" [] object_ (identity >> Decode.nullable) 25 | 26 | 27 | nodes : SelectionSet decodesTo Hasura.Object.Likes -> SelectionSet (List decodesTo) Hasura.Object.Likes_aggregate 28 | nodes object_ = 29 | Object.selectionForCompositeField "nodes" [] object_ (identity >> Decode.list) 30 | -------------------------------------------------------------------------------- /backend/hasura/metadata/databases/default/tables/public_comments.yaml: -------------------------------------------------------------------------------- 1 | table: 2 | name: comments 3 | schema: public 4 | configuration: 5 | column_config: {} 6 | custom_column_names: {} 7 | custom_root_fields: 8 | delete_by_pk: delete_comment 9 | insert_one: post_comment 10 | update_by_pk: update_comment 11 | object_relationships: 12 | - name: article 13 | using: 14 | foreign_key_constraint_on: article_id 15 | - name: user 16 | using: 17 | foreign_key_constraint_on: user_id 18 | insert_permissions: 19 | - permission: 20 | check: 21 | comment: 22 | _ne: "" 23 | columns: 24 | - article_id 25 | - comment 26 | set: 27 | user_id: x-hasura-User-Id 28 | role: user 29 | select_permissions: 30 | - permission: 31 | columns: 32 | - comment 33 | - created_at 34 | - id 35 | filter: {} 36 | role: anonymous 37 | - permission: 38 | columns: 39 | - comment 40 | - created_at 41 | - id 42 | filter: {} 43 | role: user 44 | update_permissions: 45 | - permission: 46 | check: 47 | comment: 48 | _ne: "" 49 | columns: 50 | - comment 51 | filter: 52 | user_id: 53 | _eq: X-Hasura-User-Id 54 | role: user 55 | delete_permissions: 56 | - permission: 57 | filter: 58 | user_id: 59 | _eq: X-Hasura-User-Id 60 | role: user 61 | -------------------------------------------------------------------------------- /frontend/src/elm/Hasura/Object/Articles_aggregate.elm: -------------------------------------------------------------------------------- 1 | -- Do not manually edit this file, it was auto-generated by dillonkearns/elm-graphql 2 | -- https://github.com/dillonkearns/elm-graphql 3 | 4 | 5 | module Hasura.Object.Articles_aggregate exposing (..) 6 | 7 | import Graphql.Internal.Builder.Argument as Argument exposing (Argument) 8 | import Graphql.Internal.Builder.Object as Object 9 | import Graphql.Internal.Encode as Encode exposing (Value) 10 | import Graphql.Operation exposing (RootMutation, RootQuery, RootSubscription) 11 | import Graphql.OptionalArgument exposing (OptionalArgument(..)) 12 | import Graphql.SelectionSet exposing (SelectionSet) 13 | import Hasura.InputObject 14 | import Hasura.Interface 15 | import Hasura.Object 16 | import Hasura.Scalar 17 | import Hasura.ScalarCodecs 18 | import Hasura.Union 19 | import Json.Decode as Decode 20 | 21 | 22 | aggregate : SelectionSet decodesTo Hasura.Object.Articles_aggregate_fields -> SelectionSet (Maybe decodesTo) Hasura.Object.Articles_aggregate 23 | aggregate object_ = 24 | Object.selectionForCompositeField "aggregate" [] object_ (identity >> Decode.nullable) 25 | 26 | 27 | nodes : SelectionSet decodesTo Hasura.Object.Articles -> SelectionSet (List decodesTo) Hasura.Object.Articles_aggregate 28 | nodes object_ = 29 | Object.selectionForCompositeField "nodes" [] object_ (identity >> Decode.list) 30 | -------------------------------------------------------------------------------- /frontend/src/elm/Hasura/Object/Tags_mutation_response.elm: -------------------------------------------------------------------------------- 1 | -- Do not manually edit this file, it was auto-generated by dillonkearns/elm-graphql 2 | -- https://github.com/dillonkearns/elm-graphql 3 | 4 | 5 | module Hasura.Object.Tags_mutation_response exposing (..) 6 | 7 | import Graphql.Internal.Builder.Argument as Argument exposing (Argument) 8 | import Graphql.Internal.Builder.Object as Object 9 | import Graphql.Internal.Encode as Encode exposing (Value) 10 | import Graphql.Operation exposing (RootMutation, RootQuery, RootSubscription) 11 | import Graphql.OptionalArgument exposing (OptionalArgument(..)) 12 | import Graphql.SelectionSet exposing (SelectionSet) 13 | import Hasura.InputObject 14 | import Hasura.Interface 15 | import Hasura.Object 16 | import Hasura.Scalar 17 | import Hasura.ScalarCodecs 18 | import Hasura.Union 19 | import Json.Decode as Decode 20 | 21 | 22 | {-| number of affected rows by the mutation 23 | -} 24 | affected_rows : SelectionSet Int Hasura.Object.Tags_mutation_response 25 | affected_rows = 26 | Object.selectionForField "Int" "affected_rows" [] Decode.int 27 | 28 | 29 | {-| data of the affected rows by the mutation 30 | -} 31 | returning : SelectionSet decodesTo Hasura.Object.Tags -> SelectionSet (List decodesTo) Hasura.Object.Tags_mutation_response 32 | returning object_ = 33 | Object.selectionForCompositeField "returning" [] object_ (identity >> Decode.list) 34 | -------------------------------------------------------------------------------- /frontend/src/elm/Hasura/Object/Likes_mutation_response.elm: -------------------------------------------------------------------------------- 1 | -- Do not manually edit this file, it was auto-generated by dillonkearns/elm-graphql 2 | -- https://github.com/dillonkearns/elm-graphql 3 | 4 | 5 | module Hasura.Object.Likes_mutation_response exposing (..) 6 | 7 | import Graphql.Internal.Builder.Argument as Argument exposing (Argument) 8 | import Graphql.Internal.Builder.Object as Object 9 | import Graphql.Internal.Encode as Encode exposing (Value) 10 | import Graphql.Operation exposing (RootMutation, RootQuery, RootSubscription) 11 | import Graphql.OptionalArgument exposing (OptionalArgument(..)) 12 | import Graphql.SelectionSet exposing (SelectionSet) 13 | import Hasura.InputObject 14 | import Hasura.Interface 15 | import Hasura.Object 16 | import Hasura.Scalar 17 | import Hasura.ScalarCodecs 18 | import Hasura.Union 19 | import Json.Decode as Decode 20 | 21 | 22 | {-| number of affected rows by the mutation 23 | -} 24 | affected_rows : SelectionSet Int Hasura.Object.Likes_mutation_response 25 | affected_rows = 26 | Object.selectionForField "Int" "affected_rows" [] Decode.int 27 | 28 | 29 | {-| data of the affected rows by the mutation 30 | -} 31 | returning : SelectionSet decodesTo Hasura.Object.Likes -> SelectionSet (List decodesTo) Hasura.Object.Likes_mutation_response 32 | returning object_ = 33 | Object.selectionForCompositeField "returning" [] object_ (identity >> Decode.list) 34 | -------------------------------------------------------------------------------- /frontend/src/elm/Hasura/Object/Users_mutation_response.elm: -------------------------------------------------------------------------------- 1 | -- Do not manually edit this file, it was auto-generated by dillonkearns/elm-graphql 2 | -- https://github.com/dillonkearns/elm-graphql 3 | 4 | 5 | module Hasura.Object.Users_mutation_response exposing (..) 6 | 7 | import Graphql.Internal.Builder.Argument as Argument exposing (Argument) 8 | import Graphql.Internal.Builder.Object as Object 9 | import Graphql.Internal.Encode as Encode exposing (Value) 10 | import Graphql.Operation exposing (RootMutation, RootQuery, RootSubscription) 11 | import Graphql.OptionalArgument exposing (OptionalArgument(..)) 12 | import Graphql.SelectionSet exposing (SelectionSet) 13 | import Hasura.InputObject 14 | import Hasura.Interface 15 | import Hasura.Object 16 | import Hasura.Scalar 17 | import Hasura.ScalarCodecs 18 | import Hasura.Union 19 | import Json.Decode as Decode 20 | 21 | 22 | {-| number of affected rows by the mutation 23 | -} 24 | affected_rows : SelectionSet Int Hasura.Object.Users_mutation_response 25 | affected_rows = 26 | Object.selectionForField "Int" "affected_rows" [] Decode.int 27 | 28 | 29 | {-| data of the affected rows by the mutation 30 | -} 31 | returning : SelectionSet decodesTo Hasura.Object.Users -> SelectionSet (List decodesTo) Hasura.Object.Users_mutation_response 32 | returning object_ = 33 | Object.selectionForCompositeField "returning" [] object_ (identity >> Decode.list) 34 | -------------------------------------------------------------------------------- /frontend/src/elm/Hasura/Object/Follows_mutation_response.elm: -------------------------------------------------------------------------------- 1 | -- Do not manually edit this file, it was auto-generated by dillonkearns/elm-graphql 2 | -- https://github.com/dillonkearns/elm-graphql 3 | 4 | 5 | module Hasura.Object.Follows_mutation_response exposing (..) 6 | 7 | import Graphql.Internal.Builder.Argument as Argument exposing (Argument) 8 | import Graphql.Internal.Builder.Object as Object 9 | import Graphql.Internal.Encode as Encode exposing (Value) 10 | import Graphql.Operation exposing (RootMutation, RootQuery, RootSubscription) 11 | import Graphql.OptionalArgument exposing (OptionalArgument(..)) 12 | import Graphql.SelectionSet exposing (SelectionSet) 13 | import Hasura.InputObject 14 | import Hasura.Interface 15 | import Hasura.Object 16 | import Hasura.Scalar 17 | import Hasura.ScalarCodecs 18 | import Hasura.Union 19 | import Json.Decode as Decode 20 | 21 | 22 | {-| number of affected rows by the mutation 23 | -} 24 | affected_rows : SelectionSet Int Hasura.Object.Follows_mutation_response 25 | affected_rows = 26 | Object.selectionForField "Int" "affected_rows" [] Decode.int 27 | 28 | 29 | {-| data of the affected rows by the mutation 30 | -} 31 | returning : SelectionSet decodesTo Hasura.Object.Follows -> SelectionSet (List decodesTo) Hasura.Object.Follows_mutation_response 32 | returning object_ = 33 | Object.selectionForCompositeField "returning" [] object_ (identity >> Decode.list) 34 | -------------------------------------------------------------------------------- /frontend/src/elm/Hasura/Object/Articles_mutation_response.elm: -------------------------------------------------------------------------------- 1 | -- Do not manually edit this file, it was auto-generated by dillonkearns/elm-graphql 2 | -- https://github.com/dillonkearns/elm-graphql 3 | 4 | 5 | module Hasura.Object.Articles_mutation_response exposing (..) 6 | 7 | import Graphql.Internal.Builder.Argument as Argument exposing (Argument) 8 | import Graphql.Internal.Builder.Object as Object 9 | import Graphql.Internal.Encode as Encode exposing (Value) 10 | import Graphql.Operation exposing (RootMutation, RootQuery, RootSubscription) 11 | import Graphql.OptionalArgument exposing (OptionalArgument(..)) 12 | import Graphql.SelectionSet exposing (SelectionSet) 13 | import Hasura.InputObject 14 | import Hasura.Interface 15 | import Hasura.Object 16 | import Hasura.Scalar 17 | import Hasura.ScalarCodecs 18 | import Hasura.Union 19 | import Json.Decode as Decode 20 | 21 | 22 | {-| number of affected rows by the mutation 23 | -} 24 | affected_rows : SelectionSet Int Hasura.Object.Articles_mutation_response 25 | affected_rows = 26 | Object.selectionForField "Int" "affected_rows" [] Decode.int 27 | 28 | 29 | {-| data of the affected rows by the mutation 30 | -} 31 | returning : SelectionSet decodesTo Hasura.Object.Articles -> SelectionSet (List decodesTo) Hasura.Object.Articles_mutation_response 32 | returning object_ = 33 | Object.selectionForCompositeField "returning" [] object_ (identity >> Decode.list) 34 | -------------------------------------------------------------------------------- /frontend/src/elm/Hasura/Object/Comments_mutation_response.elm: -------------------------------------------------------------------------------- 1 | -- Do not manually edit this file, it was auto-generated by dillonkearns/elm-graphql 2 | -- https://github.com/dillonkearns/elm-graphql 3 | 4 | 5 | module Hasura.Object.Comments_mutation_response exposing (..) 6 | 7 | import Graphql.Internal.Builder.Argument as Argument exposing (Argument) 8 | import Graphql.Internal.Builder.Object as Object 9 | import Graphql.Internal.Encode as Encode exposing (Value) 10 | import Graphql.Operation exposing (RootMutation, RootQuery, RootSubscription) 11 | import Graphql.OptionalArgument exposing (OptionalArgument(..)) 12 | import Graphql.SelectionSet exposing (SelectionSet) 13 | import Hasura.InputObject 14 | import Hasura.Interface 15 | import Hasura.Object 16 | import Hasura.Scalar 17 | import Hasura.ScalarCodecs 18 | import Hasura.Union 19 | import Json.Decode as Decode 20 | 21 | 22 | {-| number of affected rows by the mutation 23 | -} 24 | affected_rows : SelectionSet Int Hasura.Object.Comments_mutation_response 25 | affected_rows = 26 | Object.selectionForField "Int" "affected_rows" [] Decode.int 27 | 28 | 29 | {-| data of the affected rows by the mutation 30 | -} 31 | returning : SelectionSet decodesTo Hasura.Object.Comments -> SelectionSet (List decodesTo) Hasura.Object.Comments_mutation_response 32 | returning object_ = 33 | Object.selectionForCompositeField "returning" [] object_ (identity >> Decode.list) 34 | -------------------------------------------------------------------------------- /frontend/src/elm/Hasura/Object/Tags.elm: -------------------------------------------------------------------------------- 1 | -- Do not manually edit this file, it was auto-generated by dillonkearns/elm-graphql 2 | -- https://github.com/dillonkearns/elm-graphql 3 | 4 | 5 | module Hasura.Object.Tags exposing (..) 6 | 7 | import Graphql.Internal.Builder.Argument as Argument exposing (Argument) 8 | import Graphql.Internal.Builder.Object as Object 9 | import Graphql.Internal.Encode as Encode exposing (Value) 10 | import Graphql.Operation exposing (RootMutation, RootQuery, RootSubscription) 11 | import Graphql.OptionalArgument exposing (OptionalArgument(..)) 12 | import Graphql.SelectionSet exposing (SelectionSet) 13 | import Hasura.InputObject 14 | import Hasura.Interface 15 | import Hasura.Object 16 | import Hasura.Scalar 17 | import Hasura.ScalarCodecs 18 | import Hasura.Union 19 | import Json.Decode as Decode 20 | 21 | 22 | {-| An object relationship 23 | -} 24 | article : SelectionSet decodesTo Hasura.Object.Articles -> SelectionSet decodesTo Hasura.Object.Tags 25 | article object_ = 26 | Object.selectionForCompositeField "article" [] object_ identity 27 | 28 | 29 | {-| A computed field, executes function "tag\_count" 30 | -} 31 | count : SelectionSet (Maybe Int) Hasura.Object.Tags 32 | count = 33 | Object.selectionForField "(Maybe Int)" "count" [] (Decode.int |> Decode.nullable) 34 | 35 | 36 | tag : SelectionSet String Hasura.Object.Tags 37 | tag = 38 | Object.selectionForField "String" "tag" [] Decode.string 39 | -------------------------------------------------------------------------------- /backend/hasura/metadata/actions.yaml: -------------------------------------------------------------------------------- 1 | actions: 2 | - name: login 3 | definition: 4 | kind: synchronous 5 | handler: '{{ACTIONS_BASE_URL}}/login' 6 | headers: 7 | - name: actions-secret 8 | value_from_env: ACTIONS_SECRET 9 | permissions: 10 | - role: anonymous 11 | - role: user 12 | - name: signup 13 | definition: 14 | kind: synchronous 15 | handler: '{{ACTIONS_BASE_URL}}/signup' 16 | headers: 17 | - name: actions-secret 18 | value_from_env: ACTIONS_SECRET 19 | permissions: 20 | - role: anonymous 21 | - role: user 22 | - name: unlike_article 23 | definition: 24 | kind: synchronous 25 | handler: '{{ACTIONS_BASE_URL}}/unlike' 26 | forward_client_headers: true 27 | headers: 28 | - name: actions-secret 29 | value_from_env: ACTIONS_SECRET 30 | permissions: 31 | - role: user 32 | custom_types: 33 | enums: [] 34 | input_objects: [] 35 | objects: 36 | - name: TokenResponse 37 | relationships: 38 | - field_mapping: 39 | user_id: user_id 40 | name: follows 41 | remote_table: 42 | name: follows 43 | schema: public 44 | source: default 45 | type: array 46 | - name: UnlikeResponse 47 | relationships: 48 | - field_mapping: 49 | article_id: id 50 | name: article 51 | remote_table: 52 | name: articles 53 | schema: public 54 | source: default 55 | type: object 56 | scalars: [] 57 | -------------------------------------------------------------------------------- /frontend/src/elm/Ports.elm: -------------------------------------------------------------------------------- 1 | port module Ports exposing 2 | ( User 3 | , logout 4 | , saveUser 5 | , toLoggedIn 6 | , toProfile 7 | , toUser 8 | ) 9 | 10 | import Api.Token as Token 11 | import User 12 | 13 | 14 | 15 | -- User 16 | 17 | 18 | type alias User = 19 | { id : User.Id 20 | , username : String 21 | , email : String 22 | , token : String 23 | , bio : Maybe String 24 | , profileImage : Maybe String 25 | , following : List Int 26 | } 27 | 28 | 29 | port saveUser : User -> Cmd msg 30 | 31 | 32 | toUser : User.Profile -> User 33 | toUser user = 34 | { id = User.id user 35 | , username = User.username user 36 | , email = User.email user 37 | , token = Token.value (User.token user) 38 | , bio = User.bio user 39 | , profileImage = User.profileImage user 40 | , following = User.following user 41 | } 42 | 43 | 44 | toLoggedIn : User -> User.User 45 | toLoggedIn = 46 | User.Author << toProfile 47 | 48 | 49 | toProfile : User -> User.Profile 50 | toProfile u = 51 | User.profile (Token.token u.token) 52 | { id = u.id 53 | , username = u.username 54 | , email = u.email 55 | , bio = u.bio 56 | , profileImage = u.profileImage 57 | , following = u.following 58 | } 59 | 60 | 61 | 62 | -- Logout 63 | 64 | 65 | logout : Cmd msg 66 | logout = 67 | logout_ () 68 | 69 | 70 | port logout_ : () -> Cmd msg 71 | -------------------------------------------------------------------------------- /tests/Page/SignInTest.elm: -------------------------------------------------------------------------------- 1 | module Page.SignInTest exposing (suite) 2 | 3 | import Program 4 | import Program.Expect as Expect 5 | import Program.Selector exposing (el, filledField) 6 | import ProgramTest exposing (clickButton, ensureViewHas, expectViewHas) 7 | import Route 8 | import Test exposing (..) 9 | 10 | 11 | suite : Test 12 | suite = 13 | describe "Sign In Page" 14 | [ test "User is redirected to home when signed in" <| 15 | \_ -> 16 | Program.withPage Route.SignIn 17 | |> Program.start 18 | |> Program.fillField "username" "amacmurray" 19 | |> Program.fillField "password" "abc123" 20 | |> ensureViewHas 21 | [ filledField "amacmurray" 22 | , filledField "abc123" 23 | ] 24 | |> clickButton "Sign In" 25 | |> Expect.redirectHome 26 | , test "User sees authenticated navbar items when signed in" <| 27 | \_ -> 28 | Program.withPage Route.SignIn 29 | |> Program.start 30 | |> ensureViewHas [ el "sign-in-link" ] 31 | |> Program.fillField "username" "amacmurray" 32 | |> Program.fillField "password" "abc123" 33 | |> clickButton "Sign In" 34 | |> expectViewHas [ el "new-post-link" ] 35 | ] 36 | -------------------------------------------------------------------------------- /frontend/src/elm/Route/Effect.elm: -------------------------------------------------------------------------------- 1 | module Route.Effect exposing 2 | ( Route(..) 3 | , el 4 | , fromUrl 5 | ) 6 | 7 | import Element exposing (Element) 8 | import Url exposing (Url) 9 | import Url.Builder exposing (absolute) 10 | import Url.Parser as Parser exposing ((), oneOf, s) 11 | 12 | 13 | 14 | {-- 15 | Effect Route 16 | Use to trigger a global update onClick from an Element with no Msg 17 | --} 18 | 19 | 20 | type Route 21 | = OpenMenu 22 | | CloseMenu 23 | 24 | 25 | 26 | -- Parse 27 | 28 | 29 | fromUrl : Url -> Maybe Route 30 | fromUrl = 31 | Parser.parse parser 32 | 33 | 34 | parser : Parser.Parser (Route -> c) c 35 | parser = 36 | oneOf 37 | [ Parser.map OpenMenu (s effect s open) 38 | , Parser.map CloseMenu (s effect s close) 39 | ] 40 | 41 | 42 | effect : String 43 | effect = 44 | "route-effect" 45 | 46 | 47 | open : String 48 | open = 49 | "open-menu" 50 | 51 | 52 | close : String 53 | close = 54 | "close-menu" 55 | 56 | 57 | 58 | -- Render 59 | 60 | 61 | el : Route -> Element msg -> Element msg 62 | el route el_ = 63 | Element.link [] 64 | { url = routeToString route 65 | , label = el_ 66 | } 67 | 68 | 69 | routeToString : Route -> String 70 | routeToString route = 71 | case route of 72 | OpenMenu -> 73 | absolute [ effect, open ] [] 74 | 75 | CloseMenu -> 76 | absolute [ effect, close ] [] 77 | -------------------------------------------------------------------------------- /elm.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "application", 3 | "source-directories": [ 4 | "./frontend/src/elm" 5 | ], 6 | "elm-version": "0.19.1", 7 | "dependencies": { 8 | "direct": { 9 | "dillonkearns/elm-graphql": "5.0.2", 10 | "dillonkearns/elm-markdown": "5.1.0", 11 | "elm/browser": "1.0.2", 12 | "elm/core": "1.0.5", 13 | "elm/html": "1.0.0", 14 | "elm/http": "2.0.0", 15 | "elm/json": "1.1.3", 16 | "elm/regex": "1.0.0", 17 | "elm/svg": "1.0.1", 18 | "elm/time": "1.0.0", 19 | "elm/url": "1.0.0", 20 | "justinmimbs/date": "3.2.0", 21 | "mdgriffith/elm-ui": "1.1.5", 22 | "rtfeldman/elm-iso8601-date-strings": "1.1.3" 23 | }, 24 | "indirect": { 25 | "elm/bytes": "1.0.8", 26 | "elm/file": "1.0.5", 27 | "elm/parser": "1.1.0", 28 | "elm/virtual-dom": "1.0.2", 29 | "elm-community/list-extra": "8.2.4", 30 | "lukewestby/elm-string-interpolate": "1.0.4", 31 | "rtfeldman/elm-hex": "1.0.0" 32 | } 33 | }, 34 | "test-dependencies": { 35 | "direct": { 36 | "avh4/elm-program-test": "3.2.0", 37 | "elm-explorations/test": "1.2.2" 38 | }, 39 | "indirect": { 40 | "avh4/elm-fifo": "1.0.4", 41 | "elm/random": "1.0.0" 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /frontend/src/elm/Element/Tab.elm: -------------------------------------------------------------------------------- 1 | module Element.Tab exposing 2 | ( active 3 | , link 4 | , tabs 5 | ) 6 | 7 | import Element exposing (..) 8 | import Element.Anchor as Anchor 9 | import Element.Background as Background 10 | import Element.Events exposing (onClick) 11 | import Element.Palette as Palette 12 | import Element.Scale as Scale exposing (edges) 13 | import Element.Text as Text 14 | import Utils.Element exposing (wrappedRow_) 15 | 16 | 17 | tabs : List (Element msg) -> Element msg 18 | tabs els = 19 | wrappedRow_ 20 | [ width fill 21 | , spacing Scale.large 22 | , paddingEach { edges | bottom = Scale.large } 23 | ] 24 | els 25 | 26 | 27 | active : String -> Element msg 28 | active text = 29 | el 30 | [ Anchor.description ("active-tab-" ++ text) 31 | , below 32 | (el 33 | [ width fill 34 | , height (px 2) 35 | , Background.color Palette.darkGreen 36 | , moveDown Scale.extraSmall 37 | ] 38 | none 39 | ) 40 | ] 41 | (greenSubtitle text) 42 | 43 | 44 | link : msg -> String -> Element msg 45 | link msg text = 46 | el [ onClick msg ] (subtitleLink text) 47 | 48 | 49 | subtitleLink : String -> Element msg 50 | subtitleLink = 51 | Text.subtitle [ Text.pointer ] 52 | 53 | 54 | greenSubtitle : String -> Element msg 55 | greenSubtitle = 56 | Text.subtitle [ Text.darkGreen, Text.pointer ] 57 | -------------------------------------------------------------------------------- /frontend/src/style.css: -------------------------------------------------------------------------------- 1 | .force-wrapped-row-fill > div { 2 | width: 100% !important; 3 | } 4 | 5 | .underline-hover:hover { 6 | text-decoration: underline; 7 | } 8 | 9 | .circular-image img { 10 | border-radius: 100%; 11 | } 12 | 13 | .icon-hover-target { 14 | transition: 0.2s ease; 15 | transition-property: fill, stroke; 16 | } 17 | 18 | .icon-white-hover:hover .icon-hover-target { 19 | fill: white; 20 | } 21 | 22 | .icon-white-hover-stroke:hover .icon-hover-target { 23 | stroke: white; 24 | } 25 | 26 | .icon-black-hover:hover .icon-hover-target { 27 | fill: black; 28 | } 29 | 30 | /* responsive */ 31 | 32 | .show-on-desktop { 33 | display: none !important; 34 | } 35 | 36 | @media (min-width: 1000px) { 37 | .show-on-desktop { 38 | display: initial !important; 39 | } 40 | } 41 | 42 | @media (min-width: 999px) { 43 | .hide-on-desktop { 44 | display: none !important; 45 | } 46 | } 47 | 48 | /* animations */ 49 | 50 | @keyframes yoyo { 51 | 0% { 52 | transform: translateX(0px) 53 | } 54 | 50% { 55 | transform: translateX(90px) 56 | } 57 | 100% { 58 | transform: translateX(0px) 59 | } 60 | } 61 | 62 | @keyframes fade-in { 63 | from { 64 | opacity: 0; 65 | } 66 | to { 67 | opacity: 1; 68 | } 69 | } 70 | 71 | @keyframes spin { 72 | 0% { 73 | transform: rotate(0deg); 74 | } 75 | 100% { 76 | transform: rotate(360deg); 77 | } 78 | } 79 | 80 | .bold-override div div { 81 | font-weight: bold; 82 | color: rgb(56, 58, 60); 83 | } -------------------------------------------------------------------------------- /frontend/src/elm/Article/Comment.elm: -------------------------------------------------------------------------------- 1 | module Article.Comment exposing 2 | ( Comment 3 | , Comment_ 4 | , build 5 | , by 6 | , date 7 | , equals 8 | , id 9 | , isBy 10 | , update 11 | , value 12 | ) 13 | 14 | import Article.Author exposing (Author) 15 | import Date exposing (Date) 16 | import User exposing (User) 17 | 18 | 19 | 20 | -- Comment 21 | 22 | 23 | type Comment 24 | = Comment Comment_ 25 | 26 | 27 | type alias Comment_ = 28 | { id : Int 29 | , value : String 30 | , date : Date 31 | , by : Author 32 | } 33 | 34 | 35 | 36 | -- Construct 37 | 38 | 39 | build : Comment_ -> Comment 40 | build = 41 | Comment 42 | 43 | 44 | 45 | -- Query 46 | 47 | 48 | id : Comment -> Int 49 | id = 50 | comment_ >> .id 51 | 52 | 53 | by : Comment -> Author 54 | by = 55 | comment_ >> .by 56 | 57 | 58 | date : Comment -> Date 59 | date = 60 | comment_ >> .date 61 | 62 | 63 | value : Comment -> String 64 | value = 65 | comment_ >> .value 66 | 67 | 68 | isBy : User -> Comment -> Bool 69 | isBy user comment = 70 | case user of 71 | User.Guest -> 72 | False 73 | 74 | User.Author profile -> 75 | User.equals profile (by comment) 76 | 77 | 78 | equals : Comment -> Comment -> Bool 79 | equals c1 c2 = 80 | id c1 == id c2 81 | 82 | 83 | comment_ : Comment -> Comment_ 84 | comment_ (Comment c) = 85 | c 86 | 87 | 88 | 89 | -- Update 90 | 91 | 92 | update : Comment -> String -> Comment 93 | update (Comment c) v = 94 | Comment { c | value = v } 95 | -------------------------------------------------------------------------------- /frontend/src/elm/Element/Transition.elm: -------------------------------------------------------------------------------- 1 | module Element.Transition exposing 2 | ( all 3 | , background 4 | , border 5 | , color 6 | ) 7 | 8 | import Element exposing (htmlAttribute) 9 | import Html.Attributes exposing (style) 10 | 11 | 12 | 13 | -- Transition Property 14 | 15 | 16 | type Property 17 | = Background Float 18 | | Border Float 19 | | Color Float 20 | 21 | 22 | 23 | -- Build 24 | 25 | 26 | background : Float -> Property 27 | background = 28 | Background 29 | 30 | 31 | border : Float -> Property 32 | border = 33 | Border 34 | 35 | 36 | color : Float -> Property 37 | color = 38 | Color 39 | 40 | 41 | 42 | -- Attribute 43 | 44 | 45 | all : Float -> List (Float -> Property) -> Element.Attribute msg 46 | all duration = 47 | List.map (toProp duration) 48 | >> propertiesToString 49 | >> style "transition" 50 | >> htmlAttribute 51 | 52 | 53 | 54 | -- Helpers 55 | 56 | 57 | toProp : Float -> (Float -> Property) -> Property 58 | toProp n p = 59 | p n 60 | 61 | 62 | propertiesToString : List Property -> String 63 | propertiesToString = 64 | List.map propToString >> String.join ", " 65 | 66 | 67 | propToString : Property -> String 68 | propToString property = 69 | case property of 70 | Border n -> 71 | "border-color " ++ s n ++ " ease" 72 | 73 | Background n -> 74 | "background " ++ s n ++ " ease" 75 | 76 | Color n -> 77 | "color " ++ s n ++ " ease" 78 | 79 | 80 | s : Float -> String 81 | s n = 82 | String.fromFloat n ++ "s" 83 | -------------------------------------------------------------------------------- /frontend/src/elm/Hasura/Object/Likes.elm: -------------------------------------------------------------------------------- 1 | -- Do not manually edit this file, it was auto-generated by dillonkearns/elm-graphql 2 | -- https://github.com/dillonkearns/elm-graphql 3 | 4 | 5 | module Hasura.Object.Likes exposing (..) 6 | 7 | import Graphql.Internal.Builder.Argument as Argument exposing (Argument) 8 | import Graphql.Internal.Builder.Object as Object 9 | import Graphql.Internal.Encode as Encode exposing (Value) 10 | import Graphql.Operation exposing (RootMutation, RootQuery, RootSubscription) 11 | import Graphql.OptionalArgument exposing (OptionalArgument(..)) 12 | import Graphql.SelectionSet exposing (SelectionSet) 13 | import Hasura.InputObject 14 | import Hasura.Interface 15 | import Hasura.Object 16 | import Hasura.Scalar 17 | import Hasura.ScalarCodecs 18 | import Hasura.Union 19 | import Json.Decode as Decode 20 | 21 | 22 | {-| An object relationship 23 | -} 24 | article : SelectionSet decodesTo Hasura.Object.Articles -> SelectionSet decodesTo Hasura.Object.Likes 25 | article object_ = 26 | Object.selectionForCompositeField "article" [] object_ identity 27 | 28 | 29 | article_id : SelectionSet Int Hasura.Object.Likes 30 | article_id = 31 | Object.selectionForField "Int" "article_id" [] Decode.int 32 | 33 | 34 | {-| An object relationship 35 | -} 36 | user : SelectionSet decodesTo Hasura.Object.Users -> SelectionSet decodesTo Hasura.Object.Likes 37 | user object_ = 38 | Object.selectionForCompositeField "user" [] object_ identity 39 | 40 | 41 | user_id : SelectionSet Int Hasura.Object.Likes 42 | user_id = 43 | Object.selectionForField "Int" "user_id" [] Decode.int 44 | -------------------------------------------------------------------------------- /frontend/src/elm/Element/Loader/Spinner.elm: -------------------------------------------------------------------------------- 1 | module Element.Loader.Spinner exposing (spinner) 2 | 3 | import Animation 4 | import Animation.Named as Animation 5 | import Element exposing (Element, html) 6 | import Element.Palette as Palette 7 | import Html 8 | import Html.Attributes 9 | 10 | 11 | spinner : Element.Color -> Element msg 12 | spinner color = 13 | let 14 | border = 15 | borderSize ++ " solid " 16 | 17 | borderSolid = 18 | border ++ Palette.toRgbString color 19 | 20 | gap = 21 | border ++ "transparent" 22 | in 23 | fadeIn [] 24 | (html 25 | (Html.div 26 | [ Html.Attributes.style "animation" "spin 0.7s linear infinite" 27 | , Html.Attributes.style "border-radius" "50%" 28 | , Html.Attributes.style "border-top" borderSolid 29 | , Html.Attributes.style "border-right" borderSolid 30 | , Html.Attributes.style "border-bottom" borderSolid 31 | , Html.Attributes.style "border-left" gap 32 | , Html.Attributes.style "width" toSize 33 | , Html.Attributes.style "height" toSize 34 | ] 35 | [] 36 | ) 37 | ) 38 | 39 | 40 | fadeIn : List (Element.Attribute msg) -> Element msg -> Element msg 41 | fadeIn = 42 | Animation.el (Animation.fadeIn 300 [ Animation.delay 200 ]) 43 | 44 | 45 | toSize : String 46 | toSize = 47 | px_ 8 48 | 49 | 50 | borderSize : String 51 | borderSize = 52 | px_ 2 53 | 54 | 55 | px_ : Float -> String 56 | px_ n = 57 | String.fromFloat n ++ "px" 58 | -------------------------------------------------------------------------------- /frontend/src/elm/Tag.elm: -------------------------------------------------------------------------------- 1 | module Tag exposing 2 | ( Popular 3 | , Tag 4 | , one 5 | , parse 6 | , value 7 | ) 8 | 9 | import Regex 10 | 11 | 12 | 13 | -- Tag 14 | 15 | 16 | type Tag 17 | = Tag String 18 | 19 | 20 | type alias Popular = 21 | { tag : Tag 22 | , count : Int 23 | } 24 | 25 | 26 | 27 | -- Single 28 | 29 | 30 | one : String -> Tag 31 | one = 32 | Tag 33 | 34 | 35 | 36 | -- Parse 37 | 38 | 39 | parse : String -> List Tag 40 | parse = 41 | Regex.replace specialCharacters (always " ") 42 | >> String.words 43 | >> removeEmpties 44 | >> format 45 | >> removeDuplicates 46 | >> List.map Tag 47 | 48 | 49 | removeEmpties : List String -> List String 50 | removeEmpties = 51 | List.filter (not << String.isEmpty) 52 | 53 | 54 | format : List String -> List String 55 | format = 56 | List.map String.toLower 57 | 58 | 59 | removeDuplicates : List a -> List a 60 | removeDuplicates = 61 | List.foldr addTag [] 62 | 63 | 64 | addTag : a -> List a -> List a 65 | addTag tag tags = 66 | if List.member tag tags then 67 | tags 68 | 69 | else 70 | tag :: tags 71 | 72 | 73 | 74 | -- Query 75 | 76 | 77 | value : Tag -> String 78 | value (Tag t) = 79 | t 80 | 81 | 82 | 83 | -- Helpers 84 | 85 | 86 | specialCharacters : Regex.Regex 87 | specialCharacters = 88 | Maybe.withDefault Regex.never (Regex.fromString specialCharacters_) 89 | 90 | 91 | specialCharacters_ : String 92 | specialCharacters_ = 93 | "\\`|\\~|\\!|\\@|\\#|\\$|\\%|\\^|\\&|\\*|\\(|\\)|\\+|\\=|\\[|\\{|\\]|\\}|\\||\\|'|\\<|\\,|\\.|\\>|\\?|\\/|\\|\\;|\\:|\\s" 94 | -------------------------------------------------------------------------------- /.github/workflows/infrastructure.yml: -------------------------------------------------------------------------------- 1 | name: Infrastructure 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | terraform: 13 | name: "Terraform" 14 | runs-on: ubuntu-latest 15 | defaults: 16 | run: 17 | working-directory: ./infrastructure/terraform 18 | steps: 19 | - uses: actions/checkout@v2 20 | - uses: hashicorp/setup-terraform@v1 21 | with: 22 | cli_config_credentials_token: ${{ secrets.TF_API_TOKEN }} 23 | 24 | - id: init 25 | run: terraform init 26 | 27 | - id: plan 28 | if: github.event_name == 'pull_request' 29 | run: terraform plan -no-color 30 | continue-on-error: true 31 | 32 | - uses: actions/github-script@0.9.0 33 | if: github.event_name == 'pull_request' 34 | env: 35 | PLAN: "terraform\n${{ steps.plan.outputs.stdout }}" 36 | with: 37 | github-token: ${{ secrets.GITHUB_TOKEN }} 38 | script: | 39 | const output = 40 | `#### Terraform Initialization ⚙️ \`${{ steps.init.outcome }}\` 41 | #### Terraform Plan 📖 \`${{ steps.plan.outcome }}\` 42 |
Show Plan 43 | 44 | \`\`\`${process.env.PLAN}\`\`\` 45 | 46 |
`; 47 | 48 | github.issues.createComment({ 49 | issue_number: context.issue.number, 50 | owner: context.repo.owner, 51 | repo: context.repo.repo, 52 | body: output 53 | }) 54 | 55 | - name: Terraform Plan Status 56 | if: steps.plan.outcome == 'failure' 57 | run: exit 1 58 | -------------------------------------------------------------------------------- /frontend/src/elm/Utils/Element.elm: -------------------------------------------------------------------------------- 1 | module Utils.Element exposing 2 | ( desktopOnly 3 | , maybe 4 | , mobileOnly 5 | , showOnDesktop 6 | , showOnMobile 7 | , wrappedRow_ 8 | ) 9 | 10 | import Element exposing (Element, el, fill, htmlAttribute, width) 11 | import Html.Attributes exposing (class) 12 | 13 | 14 | 15 | -- Utils 16 | 17 | 18 | maybe : (a -> Element msg) -> Maybe a -> Element msg 19 | maybe toElement = 20 | Maybe.map toElement >> Maybe.withDefault Element.none 21 | 22 | 23 | wrappedRow_ : List (Element.Attribute msg) -> List (Element msg) -> Element msg 24 | wrappedRow_ attrs = 25 | -- Hack to stop element from overflowing page's width 26 | Element.wrappedRow (forceRowFill :: attrs) 27 | 28 | 29 | forceRowFill : Element.Attribute msg 30 | forceRowFill = 31 | htmlAttribute (Html.Attributes.class "force-wrapped-row-fill") 32 | 33 | 34 | 35 | -- Responsive 36 | 37 | 38 | mobileOnly : (List (Element.Attribute msg) -> a) -> List (Element.Attribute msg) -> a 39 | mobileOnly node attrs = 40 | node (attrs ++ [ hideOnDesktop_ ]) 41 | 42 | 43 | desktopOnly : (List (Element.Attribute msg) -> c -> a) -> List (Element.Attribute msg) -> c -> a 44 | desktopOnly node attrs = 45 | node (attrs ++ [ showOnDesktop_ ]) 46 | 47 | 48 | showOnDesktop : Element msg -> Element msg 49 | showOnDesktop = 50 | el [ width fill, showOnDesktop_ ] 51 | 52 | 53 | showOnMobile : Element msg -> Element msg 54 | showOnMobile = 55 | el [ width fill, hideOnDesktop_ ] 56 | 57 | 58 | showOnDesktop_ : Element.Attribute msg 59 | showOnDesktop_ = 60 | htmlAttribute (class "show-on-desktop") 61 | 62 | 63 | hideOnDesktop_ : Element.Attribute msg 64 | hideOnDesktop_ = 65 | htmlAttribute (class "hide-on-desktop") 66 | -------------------------------------------------------------------------------- /backend/actions/src/Action.purs: -------------------------------------------------------------------------------- 1 | module Action 2 | ( error 3 | , userId 4 | , Request 5 | , UserRequest 6 | , Response 7 | , Error 8 | , UserId 9 | ) where 10 | 11 | import Prelude 12 | import Control.Monad.Except (except) 13 | import Data.Either (Either) 14 | import Data.Either as Either 15 | import Data.Int as Int 16 | import Data.List.Types (NonEmptyList) 17 | import Data.Newtype (class Newtype, unwrap) 18 | import Effect.Aff (Aff) 19 | import Foreign (F, ForeignError(..)) 20 | import Payload.Headers as Headers 21 | import Payload.ResponseTypes as Payload 22 | import Simple.JSON (class ReadForeign, readImpl) 23 | 24 | -- Request 25 | type Request a 26 | = { input :: a } 27 | 28 | type UserRequest a 29 | = { input :: a 30 | , session_variables :: { "x-hasura-user-id" :: UserId } 31 | } 32 | 33 | newtype UserId 34 | = UserId Int 35 | 36 | type Response a 37 | = Aff (Either Error a) 38 | 39 | type Error 40 | = Payload.Response 41 | { message :: String 42 | , code :: String 43 | } 44 | 45 | -- User Id 46 | userId :: forall a. UserRequest a -> Int 47 | userId request = unwrap $ request.session_variables."x-hasura-user-id" 48 | 49 | derive instance newtypeUserId :: Newtype UserId _ 50 | 51 | instance readForeignUserId :: ReadForeign UserId where 52 | readImpl f = do 53 | id <- readImpl f :: F String 54 | except (UserId <$> readUserId_ id) 55 | 56 | readUserId_ :: String -> Either (NonEmptyList ForeignError) Int 57 | readUserId_ = Int.fromString >>> Either.note errorMessage 58 | where 59 | errorMessage = pure (ForeignError "invalid user id") 60 | 61 | -- Error 62 | error :: Int -> String -> Error 63 | error code reason = 64 | Payload.Response 65 | { body: { message: reason, code: show code } 66 | , headers: Headers.empty 67 | , status: { code, reason } 68 | } 69 | -------------------------------------------------------------------------------- /frontend/src/elm/Hasura/Object/Comments.elm: -------------------------------------------------------------------------------- 1 | -- Do not manually edit this file, it was auto-generated by dillonkearns/elm-graphql 2 | -- https://github.com/dillonkearns/elm-graphql 3 | 4 | 5 | module Hasura.Object.Comments exposing (..) 6 | 7 | import Graphql.Internal.Builder.Argument as Argument exposing (Argument) 8 | import Graphql.Internal.Builder.Object as Object 9 | import Graphql.Internal.Encode as Encode exposing (Value) 10 | import Graphql.Operation exposing (RootMutation, RootQuery, RootSubscription) 11 | import Graphql.OptionalArgument exposing (OptionalArgument(..)) 12 | import Graphql.SelectionSet exposing (SelectionSet) 13 | import Hasura.InputObject 14 | import Hasura.Interface 15 | import Hasura.Object 16 | import Hasura.Scalar 17 | import Hasura.ScalarCodecs 18 | import Hasura.Union 19 | import Json.Decode as Decode 20 | 21 | 22 | {-| An object relationship 23 | -} 24 | article : SelectionSet decodesTo Hasura.Object.Articles -> SelectionSet decodesTo Hasura.Object.Comments 25 | article object_ = 26 | Object.selectionForCompositeField "article" [] object_ identity 27 | 28 | 29 | comment : SelectionSet String Hasura.Object.Comments 30 | comment = 31 | Object.selectionForField "String" "comment" [] Decode.string 32 | 33 | 34 | created_at : SelectionSet Hasura.ScalarCodecs.Timestamptz Hasura.Object.Comments 35 | created_at = 36 | Object.selectionForField "ScalarCodecs.Timestamptz" "created_at" [] (Hasura.ScalarCodecs.codecs |> Hasura.Scalar.unwrapCodecs |> .codecTimestamptz |> .decoder) 37 | 38 | 39 | id : SelectionSet Int Hasura.Object.Comments 40 | id = 41 | Object.selectionForField "Int" "id" [] Decode.int 42 | 43 | 44 | {-| An object relationship 45 | -} 46 | user : SelectionSet decodesTo Hasura.Object.Users -> SelectionSet decodesTo Hasura.Object.Comments 47 | user object_ = 48 | Object.selectionForCompositeField "user" [] object_ identity 49 | -------------------------------------------------------------------------------- /frontend/src/elm/Hasura/Enum/Tags_select_column.elm: -------------------------------------------------------------------------------- 1 | -- Do not manually edit this file, it was auto-generated by dillonkearns/elm-graphql 2 | -- https://github.com/dillonkearns/elm-graphql 3 | 4 | 5 | module Hasura.Enum.Tags_select_column exposing (..) 6 | 7 | import Json.Decode as Decode exposing (Decoder) 8 | 9 | 10 | {-| select columns of table "tags" 11 | 12 | - Tag - column name 13 | 14 | -} 15 | type Tags_select_column 16 | = Tag 17 | 18 | 19 | list : List Tags_select_column 20 | list = 21 | [ Tag ] 22 | 23 | 24 | decoder : Decoder Tags_select_column 25 | decoder = 26 | Decode.string 27 | |> Decode.andThen 28 | (\string -> 29 | case string of 30 | "tag" -> 31 | Decode.succeed Tag 32 | 33 | _ -> 34 | Decode.fail ("Invalid Tags_select_column type, " ++ string ++ " try re-running the @dillonkearns/elm-graphql CLI ") 35 | ) 36 | 37 | 38 | {-| Convert from the union type representating the Enum to a string that the GraphQL server will recognize. 39 | -} 40 | toString : Tags_select_column -> String 41 | toString enum = 42 | case enum of 43 | Tag -> 44 | "tag" 45 | 46 | 47 | {-| Convert from a String representation to an elm representation enum. 48 | This is the inverse of the Enum `toString` function. So you can call `toString` and then convert back `fromString` safely. 49 | 50 | Swapi.Enum.Episode.NewHope 51 | |> Swapi.Enum.Episode.toString 52 | |> Swapi.Enum.Episode.fromString 53 | == Just NewHope 54 | 55 | This can be useful for generating Strings to use for menus to check which item was selected. 56 | 57 | -} 58 | fromString : String -> Maybe Comments_update_column 59 | fromString enumString = 60 | case enumString of 61 | "comment" -> 62 | Just Comment 63 | 64 | _ -> 65 | Nothing 66 | -------------------------------------------------------------------------------- /frontend/src/elm/Hasura/Enum/Follows_select_column.elm: -------------------------------------------------------------------------------- 1 | -- Do not manually edit this file, it was auto-generated by dillonkearns/elm-graphql 2 | -- https://github.com/dillonkearns/elm-graphql 3 | 4 | 5 | module Hasura.Enum.Follows_select_column exposing (..) 6 | 7 | import Json.Decode as Decode exposing (Decoder) 8 | 9 | 10 | {-| select columns of table "follows" 11 | 12 | - Following\_id - column name 13 | 14 | -} 15 | type Follows_select_column 16 | = Following_id 17 | 18 | 19 | list : List Follows_select_column 20 | list = 21 | [ Following_id ] 22 | 23 | 24 | decoder : Decoder Follows_select_column 25 | decoder = 26 | Decode.string 27 | |> Decode.andThen 28 | (\string -> 29 | case string of 30 | "following_id" -> 31 | Decode.succeed Following_id 32 | 33 | _ -> 34 | Decode.fail ("Invalid Follows_select_column type, " ++ string ++ " try re-running the @dillonkearns/elm-graphql CLI ") 35 | ) 36 | 37 | 38 | {-| Convert from the union type representating the Enum to a string that the GraphQL server will recognize. 39 | -} 40 | toString : Follows_select_column -> String 41 | toString enum = 42 | case enum of 43 | Following_id -> 44 | "following_id" 45 | 46 | 47 | {-| Convert from a String representation to an elm representation enum. 48 | This is the inverse of the Enum `toString` function. So you can call `toString` and then convert back `fromString` safely. 49 | 50 | Swapi.Enum.Episode.NewHope 51 | |> Swapi.Enum.Episode.toString 52 | |> Swapi.Enum.Episode.fromString 53 | == Just NewHope 54 | 55 | This can be useful for generating Strings to use for menus to check which item was selected. 56 | 57 | -} 58 | fromString : String -> Maybe Articles_constraint 59 | fromString enumString = 60 | case enumString of 61 | "articles_pkey" -> 62 | Just Articles_pkey 63 | 64 | _ -> 65 | Nothing 66 | -------------------------------------------------------------------------------- /frontend/src/elm/Hasura/Enum/Comments_constraint.elm: -------------------------------------------------------------------------------- 1 | -- Do not manually edit this file, it was auto-generated by dillonkearns/elm-graphql 2 | -- https://github.com/dillonkearns/elm-graphql 3 | 4 | 5 | module Hasura.Enum.Comments_constraint exposing (..) 6 | 7 | import Json.Decode as Decode exposing (Decoder) 8 | 9 | 10 | {-| unique or primary key constraints on table "comments" 11 | 12 | - Comments\_pkey - unique or primary key constraint 13 | 14 | -} 15 | type Comments_constraint 16 | = Comments_pkey 17 | 18 | 19 | list : List Comments_constraint 20 | list = 21 | [ Comments_pkey ] 22 | 23 | 24 | decoder : Decoder Comments_constraint 25 | decoder = 26 | Decode.string 27 | |> Decode.andThen 28 | (\string -> 29 | case string of 30 | "comments_pkey" -> 31 | Decode.succeed Comments_pkey 32 | 33 | _ -> 34 | Decode.fail ("Invalid Comments_constraint type, " ++ string ++ " try re-running the @dillonkearns/elm-graphql CLI ") 35 | ) 36 | 37 | 38 | {-| Convert from the union type representating the Enum to a string that the GraphQL server will recognize. 39 | -} 40 | toString : Comments_constraint -> String 41 | toString enum = 42 | case enum of 43 | Comments_pkey -> 44 | "comments_pkey" 45 | 46 | 47 | {-| Convert from a String representation to an elm representation enum. 48 | This is the inverse of the Enum `toString` function. So you can call `toString` and then convert back `fromString` safely. 49 | 50 | Swapi.Enum.Episode.NewHope 51 | |> Swapi.Enum.Episode.toString 52 | |> Swapi.Enum.Episode.fromString 53 | == Just NewHope 54 | 55 | This can be useful for generating Strings to use for menus to check which item was selected. 64 | 65 | -} 66 | fromString : String -> Maybe Likes_select_column 67 | fromString enumString = 68 | case enumString of 69 | "article_id" -> 70 | Just Article_id 71 | 72 | "user_id" -> 73 | Just User_id 74 | 75 | _ -> 76 | Nothing 77 | -------------------------------------------------------------------------------- /frontend/src/elm/Form/Button.elm: -------------------------------------------------------------------------------- 1 | module Form.Button exposing 2 | ( validateOnInput 3 | , validateOnSubmit 4 | ) 5 | 6 | import Element exposing (Element) 7 | import Element.Button as Button exposing (Button) 8 | import Form.Validation as Validation exposing (Validation) 9 | 10 | 11 | 12 | -- Validate On Input 13 | 14 | 15 | type alias ValidateOnInputOptions inputs output msg = 16 | { validation : Validation inputs output 17 | , label : String 18 | , style : Button msg -> Button msg 19 | , onSubmit : output -> msg 20 | , inputs : inputs 21 | } 22 | 23 | 24 | validateOnInput : ValidateOnInputOptions inputs output msg -> Element msg 25 | validateOnInput { validation, label, style, onSubmit, inputs } = 26 | case Validation.run inputs validation of 27 | Validation.Success output -> 28 | Button.button (onSubmit output) label 29 | |> Button.description label 30 | |> style 31 | |> Button.toElement 32 | 33 | Validation.Failure _ -> 34 | Button.disabled label 35 | |> style 36 | |> Button.toElement 37 | 38 | 39 | 40 | -- Validate On Submit 41 | 42 | 43 | type alias ValidateOnSubmitOptions inputs output msg = 44 | { label : String 45 | , validation : Validation inputs output 46 | , style : Button msg -> Button msg 47 | , inputs : inputs 48 | , onSubmit : output -> msg 49 | , onError : inputs -> msg 50 | } 51 | 52 | 53 | validateOnSubmit : ValidateOnSubmitOptions { inputs | errorsVisible : Bool } output msg -> Element msg 54 | validateOnSubmit { inputs, style, validation, onSubmit, label, onError } = 55 | case Validation.run inputs validation of 56 | Validation.Success output -> 57 | Button.button (onSubmit output) label 58 | |> Button.description label 59 | |> style 60 | |> Button.toElement 61 | 62 | Validation.Failure _ -> 63 | if inputs.errorsVisible then 64 | Button.disabled label 65 | |> style 66 | |> Button.toElement 67 | 68 | else 69 | Button.button (onError { inputs | errorsVisible = True }) label 70 | |> Button.description label 71 | |> style 72 | |> Button.toElement 73 | -------------------------------------------------------------------------------- /frontend/src/elm/Hasura/Enum/Users_constraint.elm: -------------------------------------------------------------------------------- 1 | -- Do not manually edit this file, it was auto-generated by dillonkearns/elm-graphql 2 | -- https://github.com/dillonkearns/elm-graphql 3 | 4 | 5 | module Hasura.Enum.Users_constraint exposing (..) 6 | 7 | import Json.Decode as Decode exposing (Decoder) 8 | 9 | 10 | {-| unique or primary key constraints on table "users" 11 | 12 | - Users\_pkey - unique or primary key constraint 13 | - Users\_username\_key - unique or primary key constraint 14 | 15 | -} 16 | type Users_constraint 17 | = Users_pkey 18 | | Users_username_key 19 | 20 | 21 | list : List Users_constraint 22 | list = 23 | [ Users_pkey, Users_username_key ] 24 | 25 | 26 | decoder : Decoder Users_constraint 27 | decoder = 28 | Decode.string 29 | |> Decode.andThen 30 | (\string -> 31 | case string of 32 | "users_pkey" -> 33 | Decode.succeed Users_pkey 34 | 35 | "users_username_key" -> 36 | Decode.succeed Users_username_key 37 | 38 | _ -> 39 | Decode.fail ("Invalid Users_constraint type, " ++ string ++ " try re-running the @dillonkearns/elm-graphql CLI ") 40 | ) 41 | 42 | 43 | {-| Convert from the union type representating the Enum to a string that the GraphQL server will recognize. 44 | -} 45 | toString : Users_constraint -> String 46 | toString enum = 47 | case enum of 48 | Users_pkey -> 49 | "users_pkey" 50 | 51 | Users_username_key -> 52 | "users_username_key" 53 | 54 | 55 | {-| Convert from a String representation to an elm representation enum. 56 | This is the inverse of the Enum `toString` function. So you can call `toString` and then convert back `fromString` safely. 57 | 58 | Swapi.Enum.Episode.NewHope 59 | |> Swapi.Enum.Episode.toString 60 | |> Swapi.Enum.Episode.fromString 61 | == Just NewHope 62 | 63 | This can be useful for generating Strings to use for menus to check which item was selected. 72 | 73 | -} 74 | fromString : String -> Maybe Articles_update_column 75 | fromString enumString = 76 | case enumString of 77 | "about" -> 78 | Just About 79 | 80 | "content" -> 81 | Just Content 82 | 83 | "title" -> 84 | Just Title 85 | 86 | _ -> 87 | Nothing 88 | -------------------------------------------------------------------------------- /frontend/src/elm/Hasura/Enum/Comments_select_column.elm: -------------------------------------------------------------------------------- 1 | -- Do not manually edit this file, it was auto-generated by dillonkearns/elm-graphql 2 | -- https://github.com/dillonkearns/elm-graphql 3 | 4 | 5 | module Hasura.Enum.Comments_select_column exposing (..) 6 | 7 | import Json.Decode as Decode exposing (Decoder) 8 | 9 | 10 | {-| select columns of table "comments" 11 | 12 | - Comment - column name 13 | - Created\_at - column name 14 | - Id - column name 15 | 16 | -} 17 | type Comments_select_column 18 | = Comment 19 | | Created_at 20 | | Id 21 | 22 | 23 | list : List Comments_select_column 24 | list = 25 | [ Comment, Created_at, Id ] 26 | 27 | 28 | decoder : Decoder Comments_select_column 29 | decoder = 30 | Decode.string 31 | |> Decode.andThen 32 | (\string -> 33 | case string of 34 | "comment" -> 35 | Decode.succeed Comment 36 | 37 | "created_at" -> 38 | Decode.succeed Created_at 39 | 40 | "id" -> 41 | Decode.succeed Id 42 | 43 | _ -> 44 | Decode.fail ("Invalid Comments_select_column type, " ++ string ++ " try re-running the @dillonkearns/elm-graphql CLI ") 45 | ) 46 | 47 | 48 | {-| Convert from the union type representating the Enum to a string that the GraphQL server will recognize. 49 | -} 50 | toString : Comments_select_column -> String 51 | toString enum = 52 | case enum of 53 | Comment -> 54 | "comment" 55 | 56 | Created_at -> 57 | "created_at" 58 | 59 | Id -> 60 | "id" 61 | 62 | 63 | {-| Convert from a String representation to an elm representation enum. 64 | This is the inverse of the Enum `toString` function. So you can call `toString` and then convert back `fromString` safely. 65 | 66 | Swapi.Enum.Episode.NewHope 67 | |> Swapi.Enum.Episode.toString 68 | |> Swapi.Enum.Episode.fromString 69 | == Just NewHope 70 | 71 | This can be useful for generating Strings to use for menus to check which item was selected. 72 | 73 | -} 74 | fromString : String -> Maybe Users_select_column 75 | fromString enumString = 76 | case enumString of 77 | "id" -> 78 | Just Id 79 | 80 | "profile_image" -> 81 | Just Profile_image 82 | 83 | "username" -> 84 | Just Username 85 | 86 | _ -> 87 | Nothing 88 | -------------------------------------------------------------------------------- /frontend/src/elm/Article/Author/Follow.elm: -------------------------------------------------------------------------------- 1 | module Article.Author.Follow exposing 2 | ( Msg 3 | , button 4 | , effect 5 | ) 6 | 7 | import Api 8 | import Api.Users 9 | import Article.Author as Author exposing (Author) 10 | import Effect exposing (Effect) 11 | import Element exposing (..) 12 | import Element.Button as Button 13 | import User exposing (User) 14 | 15 | 16 | 17 | -- Types 18 | 19 | 20 | type Msg 21 | = FollowClicked Author 22 | | UnfollowClicked Author 23 | | FollowResponseReceived (Api.Response Author.Id) 24 | | UnfollowResponseReceived (Api.Response Author.Id) 25 | 26 | 27 | 28 | -- Effect 29 | 30 | 31 | effect : Msg -> Effect Msg 32 | effect msg = 33 | case msg of 34 | FollowClicked author -> 35 | followAuthor author 36 | 37 | UnfollowClicked author -> 38 | unfollowAuthor author 39 | 40 | FollowResponseReceived (Ok id) -> 41 | Effect.addToUserFollows id 42 | 43 | FollowResponseReceived (Err _) -> 44 | Effect.none 45 | 46 | UnfollowResponseReceived (Ok id) -> 47 | Effect.removeFromUserFollows id 48 | 49 | UnfollowResponseReceived (Err _) -> 50 | Effect.none 51 | 52 | 53 | followAuthor : Author -> Effect Msg 54 | followAuthor author_ = 55 | Api.Users.follow author_ FollowResponseReceived 56 | 57 | 58 | unfollowAuthor : Author -> Effect Msg 59 | unfollowAuthor author_ = 60 | Api.Users.unfollow author_ UnfollowResponseReceived 61 | 62 | 63 | 64 | -- Follow Button 65 | 66 | 67 | type alias Options msg = 68 | { user : User 69 | , author : Author 70 | , msg : Msg -> msg 71 | } 72 | 73 | 74 | button : Options msg -> Element msg 75 | button { author, user, msg } = 76 | case user of 77 | User.Guest -> 78 | none 79 | 80 | User.Author profile -> 81 | if User.equals profile author then 82 | none 83 | 84 | else if User.follows (Author.id author) profile then 85 | Button.button (msg <| UnfollowClicked author) "Unfollow" 86 | |> Button.description ("unfollow-" ++ Author.username author) 87 | |> Button.unfollow 88 | |> Button.toElement 89 | 90 | else 91 | Button.button (msg <| FollowClicked author) "Follow" 92 | |> Button.description ("follow-" ++ Author.username author) 93 | |> Button.follow 94 | |> Button.toElement 95 | -------------------------------------------------------------------------------- /frontend/src/elm/Hasura/Scalar.elm: -------------------------------------------------------------------------------- 1 | -- Do not manually edit this file, it was auto-generated by dillonkearns/elm-graphql 2 | -- https://github.com/dillonkearns/elm-graphql 3 | 4 | 5 | module Hasura.Scalar exposing (Codecs, Id(..), Json(..), Timestamptz(..), Uuid(..), defaultCodecs, defineCodecs, unwrapCodecs, unwrapEncoder) 6 | 7 | import Graphql.Codec exposing (Codec) 8 | import Graphql.Internal.Builder.Object as Object 9 | import Graphql.Internal.Encode 10 | import Json.Decode as Decode exposing (Decoder) 11 | import Json.Encode as Encode 12 | 13 | 14 | type Id 15 | = Id String 16 | 17 | 18 | type Json 19 | = Json String 20 | 21 | 22 | type Timestamptz 23 | = Timestamptz String 24 | 25 | 26 | type Uuid 27 | = Uuid String 28 | 29 | 30 | defineCodecs : 31 | { codecId : Codec valueId 32 | , codecJson : Codec valueJson 33 | , codecTimestamptz : Codec valueTimestamptz 34 | , codecUuid : Codec valueUuid 35 | } 36 | -> Codecs valueId valueJson valueTimestamptz valueUuid 37 | defineCodecs definitions = 38 | Codecs definitions 39 | 40 | 41 | unwrapCodecs : 42 | Codecs valueId valueJson valueTimestamptz valueUuid 43 | -> 44 | { codecId : Codec valueId 45 | , codecJson : Codec valueJson 46 | , codecTimestamptz : Codec valueTimestamptz 47 | , codecUuid : Codec valueUuid 48 | } 49 | unwrapCodecs (Codecs unwrappedCodecs) = 50 | unwrappedCodecs 51 | 52 | 53 | unwrapEncoder getter (Codecs unwrappedCodecs) = 54 | (unwrappedCodecs |> getter |> .encoder) >> Graphql.Internal.Encode.fromJson 55 | 56 | 57 | type Codecs valueId valueJson valueTimestamptz valueUuid 58 | = Codecs (RawCodecs valueId valueJson valueTimestamptz valueUuid) 59 | 60 | 61 | type alias RawCodecs valueId valueJson valueTimestamptz valueUuid = 62 | { codecId : Codec valueId 63 | , codecJson : Codec valueJson 64 | , codecTimestamptz : Codec valueTimestamptz 65 | , codecUuid : Codec valueUuid 66 | } 67 | 68 | 69 | defaultCodecs : RawCodecs Id Json Timestamptz Uuid 70 | defaultCodecs = 71 | { codecId = 72 | { encoder = \(Id raw) -> Encode.string raw 73 | , decoder = Object.scalarDecoder |> Decode.map Id 74 | } 75 | , codecJson = 76 | { encoder = \(Json raw) -> Encode.string raw 77 | , decoder = Object.scalarDecoder |> Decode.map Json 78 | } 79 | , codecTimestamptz = 80 | { encoder = \(Timestamptz raw) -> Encode.string raw 81 | , decoder = Object.scalarDecoder |> Decode.map Timestamptz 82 | } 83 | , codecUuid = 84 | { encoder = \(Uuid raw) -> Encode.string raw 85 | , decoder = Object.scalarDecoder |> Decode.map Uuid 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /frontend/src/elm/Api/Authors.elm: -------------------------------------------------------------------------------- 1 | module Api.Authors exposing 2 | ( FeedSelection 3 | , authoredArticles 4 | , likedArticles 5 | , loadFeed 6 | ) 7 | 8 | import Api 9 | import Api.Argument as Argument exposing (..) 10 | import Api.Articles as Articles 11 | import Article.Author as Author exposing (Author) 12 | import Article.Feed as Feed exposing (Feed) 13 | import Article.Page as Page 14 | import Effect exposing (Effect) 15 | import Graphql.Operation exposing (RootQuery) 16 | import Graphql.SelectionSet as SelectionSet exposing (SelectionSet, with) 17 | import Hasura.InputObject as Input 18 | import Hasura.Object exposing (Users) 19 | import Hasura.Object.Users as Users 20 | import Hasura.Query exposing (ArticlesOptionalArguments) 21 | 22 | 23 | type alias FeedSelection = 24 | Author.Id -> Page.Number -> SelectionSet Feed RootQuery 25 | 26 | 27 | authoredArticles : FeedSelection 28 | authoredArticles id_ page_ = 29 | Articles.feedSelection page_ (Articles.newestFirst >> authoredBy id_) 30 | 31 | 32 | likedArticles : FeedSelection 33 | likedArticles id_ page_ = 34 | Articles.feedSelection page_ (Articles.newestFirst >> likedBy id_) 35 | 36 | 37 | 38 | -- load 39 | 40 | 41 | loadFeed : FeedSelection -> Author.Id -> Page.Number -> (Api.Response (Maybe Feed.ForAuthor) -> msg) -> Effect msg 42 | loadFeed articles id_ page_ msg = 43 | authorFeedSelection id_ page_ articles 44 | |> Api.query msg 45 | |> Effect.loadAuthorFeed 46 | 47 | 48 | authorFeedSelection : Author.Id -> Page.Number -> FeedSelection -> SelectionSet (Maybe Feed.ForAuthor) RootQuery 49 | authorFeedSelection id_ page_ selection = 50 | SelectionSet.succeed Feed.forAuthor 51 | |> with (authorById id_) 52 | |> with (selection id_ page_) 53 | 54 | 55 | authoredBy : Author.Id -> ArticlesOptionalArguments -> ArticlesOptionalArguments 56 | authoredBy id_ = 57 | Argument.combine4 58 | (where_ Input.buildArticles_bool_exp) 59 | (author Input.buildUsers_bool_exp) 60 | (id Input.buildInt_comparison_exp) 61 | (eq_ id_) 62 | 63 | 64 | likedBy : Author.Id -> ArticlesOptionalArguments -> ArticlesOptionalArguments 65 | likedBy id_ = 66 | Argument.combine4 67 | (where_ Input.buildArticles_bool_exp) 68 | (likes Input.buildLikes_bool_exp) 69 | (user_id Input.buildInt_comparison_exp) 70 | (eq_ id_) 71 | 72 | 73 | authorById : Author.Id -> SelectionSet (Maybe Author) RootQuery 74 | authorById id_ = 75 | Hasura.Query.user { id = id_ } authorSelection 76 | 77 | 78 | authorSelection : SelectionSet Author Users 79 | authorSelection = 80 | SelectionSet.succeed Author.build 81 | |> with Users.id 82 | |> with Users.username 83 | |> with Users.profile_image 84 | -------------------------------------------------------------------------------- /frontend/src/elm/Hasura/Enum/Users_update_column.elm: -------------------------------------------------------------------------------- 1 | -- Do not manually edit this file, it was auto-generated by dillonkearns/elm-graphql 2 | -- https://github.com/dillonkearns/elm-graphql 3 | 4 | 5 | module Hasura.Enum.Users_update_column exposing (..) 6 | 7 | import Json.Decode as Decode exposing (Decoder) 8 | 9 | 10 | {-| update columns of table "users" 11 | 12 | - Bio - column name 13 | - Email - column name 14 | - Profile\_image - column name 15 | - Username - column name 16 | 17 | -} 18 | type Users_update_column 19 | = Bio 20 | | Email 21 | | Profile_image 22 | | Username 23 | 24 | 25 | list : List Users_update_column 26 | list = 27 | [ Bio, Email, Profile_image, Username ] 28 | 29 | 30 | decoder : Decoder Users_update_column 31 | decoder = 32 | Decode.string 33 | |> Decode.andThen 34 | (\string -> 35 | case string of 36 | "bio" -> 37 | Decode.succeed Bio 38 | 39 | "email" -> 40 | Decode.succeed Email 41 | 42 | "profile_image" -> 43 | Decode.succeed Profile_image 44 | 45 | "username" -> 46 | Decode.succeed Username 47 | 48 | _ -> 49 | Decode.fail ("Invalid Users_update_column type, " ++ string ++ " try re-running the @dillonkearns/elm-graphql CLI ") 50 | ) 51 | 52 | 53 | {-| Convert from the union type representating the Enum to a string that the GraphQL server will recognize. 54 | -} 55 | toString : Users_update_column -> String 56 | toString enum = 57 | case enum of 58 | Bio -> 59 | "bio" 60 | 61 | Email -> 62 | "email" 63 | 64 | Profile_image -> 65 | "profile_image" 66 | 67 | Username -> 68 | "username" 69 | 70 | 71 | {-| Convert from a String representation to an elm representation enum. 72 | This is the inverse of the Enum `toString` function. So you can call `toString` and then convert back `fromString` safely. 73 | 74 | Swapi.Enum.Episode.NewHope 75 | |> Swapi.Enum.Episode.toString 76 | |> Swapi.Enum.Episode.fromString 77 | == Just NewHope 78 | 79 | This can be useful for generating Strings to use for menus to check which item was selected. 88 | 89 | -} 90 | fromString : String -> Maybe Articles_select_column 91 | fromString enumString = 92 | case enumString of 93 | "about" -> 94 | Just About 95 | 96 | "content" -> 97 | Just Content 98 | 99 | "created_at" -> 100 | Just Created_at 101 | 102 | "id" -> 103 | Just Id 104 | 105 | "title" -> 106 | Just Title 107 | 108 | _ -> 109 | Nothing 110 | -------------------------------------------------------------------------------- /frontend/src/elm/Hasura/Enum/Profile_select_column.elm: -------------------------------------------------------------------------------- 1 | -- Do not manually edit this file, it was auto-generated by dillonkearns/elm-graphql 2 | -- https://github.com/dillonkearns/elm-graphql 3 | 4 | 5 | module Hasura.Enum.Profile_select_column exposing (..) 6 | 7 | import Json.Decode as Decode exposing (Decoder) 8 | 9 | 10 | {-| select columns of table "profile" 11 | 12 | - Bio - column name 13 | - Email - column name 14 | - Profile\_image - column name 15 | - User\_id - column name 16 | - Username - column name 17 | 18 | -} 19 | type Profile_select_column 20 | = Bio 21 | | Email 22 | | Profile_image 23 | | User_id 24 | | Username 25 | 26 | 27 | list : List Profile_select_column 28 | list = 29 | [ Bio, Email, Profile_image, User_id, Username ] 30 | 31 | 32 | decoder : Decoder Profile_select_column 33 | decoder = 34 | Decode.string 35 | |> Decode.andThen 36 | (\string -> 37 | case string of 38 | "bio" -> 39 | Decode.succeed Bio 40 | 41 | "email" -> 42 | Decode.succeed Email 43 | 44 | "profile_image" -> 45 | Decode.succeed Profile_image 46 | 47 | "user_id" -> 48 | Decode.succeed User_id 49 | 50 | "username" -> 51 | Decode.succeed Username 52 | 53 | _ -> 54 | Decode.fail ("Invalid Profile_select_column type, " ++ string ++ " try re-running the @dillonkearns/elm-graphql CLI ") 55 | ) 56 | 57 | 58 | {-| Convert from the union type representating the Enum to a string that the GraphQL server will recognize. 59 | -} 60 | toString : Profile_select_column -> String 61 | toString enum = 62 | case enum of 63 | Bio -> 64 | "bio" 65 | 66 | Email -> 67 | "email" 68 | 69 | Profile_image -> 70 | "profile_image" 71 | 72 | User_id -> 73 | "user_id" 74 | 75 | Username -> 76 | "username" 77 | 78 | 79 | {-| Convert from a String representation to an elm representation enum. 80 | This is the inverse of the Enum `toString` function. So you can call `toString` and then convert back `fromString` safely. 81 | 82 | Swapi.Enum.Episode.NewHope 83 | |> Swapi.Enum.Episode.toString 84 | |> Swapi.Enum.Episode.fromString 85 | == Just NewHope 86 | 87 | This can be useful for generating Strings to use for