Compare commits

...

3 Commits
main ... vue3ts

Author SHA1 Message Date
8ebafd8bae nginx config
Some checks failed
parsley/pipeline/head There was a failure building this commit
2021-12-31 13:14:05 -06:00
df2ccf86b2 intial ts conversion 2021-11-28 23:19:56 -06:00
d82fce08d9 Prepare for new frontend 2021-11-27 17:10:08 -06:00
52 changed files with 5939 additions and 2168 deletions

View File

@ -1 +0,0 @@
> 1%, not ie <= 12

5
.editorconfig Normal file
View File

@ -0,0 +1,5 @@
[*.{js,jsx,ts,tsx,vue}]
indent_style = space
indent_size = 2
trim_trailing_whitespace = true
insert_final_newline = true

1
.foreman Normal file
View File

@ -0,0 +1 @@
port: 3000

1
.gitignore vendored
View File

@ -20,6 +20,7 @@
.byebug_history
/public/assets
/dist
.DS_Store
/public/packs

View File

@ -1 +1 @@
3.0.1
3.0.3

View File

@ -1,4 +1,4 @@
FROM ruby:3.0.1-buster
FROM ruby:3.0.3-bullseye
RUN curl -sL https://deb.nodesource.com/setup_lts.x | bash - && \
curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - && \
@ -8,11 +8,10 @@ RUN curl -sL https://deb.nodesource.com/setup_lts.x | bash - && \
nodejs \
yarn \
nginx && \
yarn global add @vue/cli && \
gem update --system && gem install bundler && \
apt-get autoremove -y && apt-get clean && rm -rf /var/lib/apt/lists/*
RUN gem update --system && gem install bundler
# Install nginx config files
RUN rm /etc/nginx/sites-enabled/default
ADD docker/nginx_server.conf /etc/nginx/sites-enabled/parsley.conf
@ -32,12 +31,12 @@ COPY Gemfile* ./
RUN bundle install
COPY package.json yarn.lock ./
RUN yarn install --production=true --frozen-lockfile
RUN yarn install --frozen-lockfile
COPY . .
# Compile assets
RUN env RAILS_ENV=production bundle exec rails webpacker:clobber webpacker:compile
RUN yarn build
ENV PORT=80
EXPOSE 80

14
Gemfile
View File

@ -1,28 +1,26 @@
source 'https://rubygems.org'
gem 'rails', '6.1.3.2'
gem 'rails', '6.1.4.1'
gem 'pg', '~> 1.2.3'
gem 'webpacker', '5.3.0'
gem 'bootsnap', '>= 1.1.0', require: false
gem 'oj', '~> 3.11.5'
gem 'oj', '~> 3.13.9'
gem 'kaminari', '~> 1.2.1'
gem 'unitwise', '~> 2.2.0'
gem 'redcarpet', '~> 3.5.1'
gem 'dalli', '~> 2.7.11'
gem 'puma', '~> 5.3'
gem 'dalli', '~> 3.0.4'
gem 'puma', '~> 5.5'
gem 'bcrypt', '~> 3.1.16'
gem 'tzinfo-data'
group :development, :test do
gem 'sqlite3', '~> 1.4.2'
gem 'guard', '~> 2.16.2'
gem 'guard-rspec', require: false
gem 'rspec-rails', '~> 5.0.1'
gem 'listen', '~> 3.3'
gem 'rspec-rails', '~> 5.0.2'
gem 'rails-controller-testing'
gem 'factory_bot_rails', '~> 6.2.0'
gem 'database_cleaner', '~> 2.0.1'

View File

@ -1,73 +1,72 @@
GEM
remote: https://rubygems.org/
specs:
actioncable (6.1.3.2)
actionpack (= 6.1.3.2)
activesupport (= 6.1.3.2)
actioncable (6.1.4.1)
actionpack (= 6.1.4.1)
activesupport (= 6.1.4.1)
nio4r (~> 2.0)
websocket-driver (>= 0.6.1)
actionmailbox (6.1.3.2)
actionpack (= 6.1.3.2)
activejob (= 6.1.3.2)
activerecord (= 6.1.3.2)
activestorage (= 6.1.3.2)
activesupport (= 6.1.3.2)
actionmailbox (6.1.4.1)
actionpack (= 6.1.4.1)
activejob (= 6.1.4.1)
activerecord (= 6.1.4.1)
activestorage (= 6.1.4.1)
activesupport (= 6.1.4.1)
mail (>= 2.7.1)
actionmailer (6.1.3.2)
actionpack (= 6.1.3.2)
actionview (= 6.1.3.2)
activejob (= 6.1.3.2)
activesupport (= 6.1.3.2)
actionmailer (6.1.4.1)
actionpack (= 6.1.4.1)
actionview (= 6.1.4.1)
activejob (= 6.1.4.1)
activesupport (= 6.1.4.1)
mail (~> 2.5, >= 2.5.4)
rails-dom-testing (~> 2.0)
actionpack (6.1.3.2)
actionview (= 6.1.3.2)
activesupport (= 6.1.3.2)
actionpack (6.1.4.1)
actionview (= 6.1.4.1)
activesupport (= 6.1.4.1)
rack (~> 2.0, >= 2.0.9)
rack-test (>= 0.6.3)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.2.0)
actiontext (6.1.3.2)
actionpack (= 6.1.3.2)
activerecord (= 6.1.3.2)
activestorage (= 6.1.3.2)
activesupport (= 6.1.3.2)
actiontext (6.1.4.1)
actionpack (= 6.1.4.1)
activerecord (= 6.1.4.1)
activestorage (= 6.1.4.1)
activesupport (= 6.1.4.1)
nokogiri (>= 1.8.5)
actionview (6.1.3.2)
activesupport (= 6.1.3.2)
actionview (6.1.4.1)
activesupport (= 6.1.4.1)
builder (~> 3.1)
erubi (~> 1.4)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.1, >= 1.2.0)
activejob (6.1.3.2)
activesupport (= 6.1.3.2)
activejob (6.1.4.1)
activesupport (= 6.1.4.1)
globalid (>= 0.3.6)
activemodel (6.1.3.2)
activesupport (= 6.1.3.2)
activerecord (6.1.3.2)
activemodel (= 6.1.3.2)
activesupport (= 6.1.3.2)
activestorage (6.1.3.2)
actionpack (= 6.1.3.2)
activejob (= 6.1.3.2)
activerecord (= 6.1.3.2)
activesupport (= 6.1.3.2)
activemodel (6.1.4.1)
activesupport (= 6.1.4.1)
activerecord (6.1.4.1)
activemodel (= 6.1.4.1)
activesupport (= 6.1.4.1)
activestorage (6.1.4.1)
actionpack (= 6.1.4.1)
activejob (= 6.1.4.1)
activerecord (= 6.1.4.1)
activesupport (= 6.1.4.1)
marcel (~> 1.0.0)
mini_mime (~> 1.0.2)
activesupport (6.1.3.2)
mini_mime (>= 1.1.0)
activesupport (6.1.4.1)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 1.6, < 2)
minitest (>= 5.1)
tzinfo (~> 2.0)
zeitwerk (~> 2.3)
bcrypt (3.1.16)
bootsnap (1.7.5)
bootsnap (1.9.3)
msgpack (~> 1.0)
builder (3.2.4)
coderay (1.1.3)
concurrent-ruby (1.1.8)
concurrent-ruby (1.1.9)
crass (1.0.6)
dalli (2.7.11)
dalli (3.0.4)
database_cleaner (2.0.1)
database_cleaner-active_record (~> 2.0.0)
database_cleaner-active_record (2.0.1)
@ -81,25 +80,10 @@ GEM
factory_bot_rails (6.2.0)
factory_bot (~> 6.2.0)
railties (>= 5.0.0)
ffi (1.15.0)
formatador (0.2.5)
globalid (0.4.2)
activesupport (>= 4.2.0)
guard (2.16.2)
formatador (>= 0.2.4)
listen (>= 2.7, < 4.0)
lumberjack (>= 1.0.12, < 2.0)
nenv (~> 0.1)
notiffany (~> 0.0)
pry (>= 0.9.12)
shellany (~> 0.0)
thor (>= 0.18.1)
guard-compat (1.2.1)
guard-rspec (4.7.3)
guard (~> 2.1)
guard-compat (~> 1.1)
rspec (>= 2.99.0, < 4.0)
i18n (1.8.10)
ffi (1.15.4)
globalid (1.0.0)
activesupport (>= 5.0)
i18n (1.8.11)
concurrent-ruby (~> 1.0)
kaminari (1.2.1)
activesupport (>= 4.1.0)
@ -114,59 +98,49 @@ GEM
kaminari-core (= 1.2.1)
kaminari-core (1.2.1)
liner (0.2.4)
listen (3.5.1)
listen (3.7.0)
rb-fsevent (~> 0.10, >= 0.10.3)
rb-inotify (~> 0.9, >= 0.9.10)
loofah (2.9.1)
loofah (2.12.0)
crass (~> 1.0.2)
nokogiri (>= 1.5.9)
lumberjack (1.2.8)
mail (2.7.1)
mini_mime (>= 0.1.1)
marcel (1.0.1)
marcel (1.0.2)
memoizable (0.4.2)
thread_safe (~> 0.3, >= 0.3.1)
method_source (1.0.0)
mini_mime (1.0.3)
mini_portile2 (2.5.1)
mini_mime (1.1.2)
mini_portile2 (2.6.1)
minitest (5.14.4)
msgpack (1.4.2)
nenv (0.3.0)
nio4r (2.5.7)
nokogiri (1.11.3)
mini_portile2 (~> 2.5.0)
nio4r (2.5.8)
nokogiri (1.12.5)
mini_portile2 (~> 2.6.1)
racc (~> 1.4)
notiffany (0.1.3)
nenv (~> 0.1)
shellany (~> 0.0)
oj (3.11.5)
oj (3.13.9)
parslet (1.8.2)
pg (1.2.3)
pry (0.14.1)
coderay (~> 1.1)
method_source (~> 1.0)
puma (5.3.0)
puma (5.5.2)
nio4r (~> 2.0)
racc (1.5.2)
racc (1.6.0)
rack (2.2.3)
rack-proxy (0.6.5)
rack
rack-test (1.1.0)
rack (>= 1.0, < 3)
rails (6.1.3.2)
actioncable (= 6.1.3.2)
actionmailbox (= 6.1.3.2)
actionmailer (= 6.1.3.2)
actionpack (= 6.1.3.2)
actiontext (= 6.1.3.2)
actionview (= 6.1.3.2)
activejob (= 6.1.3.2)
activemodel (= 6.1.3.2)
activerecord (= 6.1.3.2)
activestorage (= 6.1.3.2)
activesupport (= 6.1.3.2)
rails (6.1.4.1)
actioncable (= 6.1.4.1)
actionmailbox (= 6.1.4.1)
actionmailer (= 6.1.4.1)
actionpack (= 6.1.4.1)
actiontext (= 6.1.4.1)
actionview (= 6.1.4.1)
activejob (= 6.1.4.1)
activemodel (= 6.1.4.1)
activerecord (= 6.1.4.1)
activestorage (= 6.1.4.1)
activesupport (= 6.1.4.1)
bundler (>= 1.15.0)
railties (= 6.1.3.2)
railties (= 6.1.4.1)
sprockets-rails (>= 2.0.0)
rails-controller-testing (1.0.5)
actionpack (>= 5.0.1.rc1)
@ -175,23 +149,19 @@ GEM
rails-dom-testing (2.0.3)
activesupport (>= 4.2.0)
nokogiri (>= 1.6)
rails-html-sanitizer (1.3.0)
rails-html-sanitizer (1.4.2)
loofah (~> 2.3)
railties (6.1.3.2)
actionpack (= 6.1.3.2)
activesupport (= 6.1.3.2)
railties (6.1.4.1)
actionpack (= 6.1.4.1)
activesupport (= 6.1.4.1)
method_source
rake (>= 0.8.7)
rake (>= 0.13)
thor (~> 1.0)
rake (13.0.3)
rake (13.0.6)
rb-fsevent (0.11.0)
rb-inotify (0.10.1)
ffi (~> 1.0)
redcarpet (3.5.1)
rspec (3.10.0)
rspec-core (~> 3.10.0)
rspec-expectations (~> 3.10.0)
rspec-mocks (~> 3.10.0)
rspec-core (3.10.1)
rspec-support (~> 3.10.0)
rspec-expectations (3.10.1)
@ -200,7 +170,7 @@ GEM
rspec-mocks (3.10.2)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.10.0)
rspec-rails (5.0.1)
rspec-rails (5.0.2)
actionpack (>= 5.2)
activesupport (>= 5.2)
railties (>= 5.2)
@ -208,38 +178,31 @@ GEM
rspec-expectations (~> 3.10)
rspec-mocks (~> 3.10)
rspec-support (~> 3.10)
rspec-support (3.10.2)
semantic_range (3.0.0)
shellany (0.0.1)
rspec-support (3.10.3)
signed_multiset (0.2.1)
sprockets (4.0.2)
concurrent-ruby (~> 1.0)
rack (> 1, < 3)
sprockets-rails (3.2.2)
actionpack (>= 4.0)
activesupport (>= 4.0)
sprockets-rails (3.4.1)
actionpack (>= 5.2)
activesupport (>= 5.2)
sprockets (>= 3.0.0)
sqlite3 (1.4.2)
thor (1.1.0)
thread_safe (0.3.6)
tzinfo (2.0.4)
concurrent-ruby (~> 1.0)
tzinfo-data (1.2021.1)
tzinfo-data (1.2021.5)
tzinfo (>= 1.0.0)
unitwise (2.2.0)
liner (~> 0.2)
memoizable (~> 0.4)
parslet (~> 1.5)
signed_multiset (~> 0.2)
webpacker (5.3.0)
activesupport (>= 5.2)
rack-proxy (>= 0.6.1)
railties (>= 5.2)
semantic_range (>= 2.3.0)
websocket-driver (0.7.3)
websocket-driver (0.7.5)
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.5)
zeitwerk (2.4.2)
zeitwerk (2.5.1)
PLATFORMS
ruby
@ -247,23 +210,21 @@ PLATFORMS
DEPENDENCIES
bcrypt (~> 3.1.16)
bootsnap (>= 1.1.0)
dalli (~> 2.7.11)
dalli (~> 3.0.4)
database_cleaner (~> 2.0.1)
factory_bot_rails (~> 6.2.0)
guard (~> 2.16.2)
guard-rspec
kaminari (~> 1.2.1)
oj (~> 3.11.5)
listen (~> 3.3)
oj (~> 3.13.9)
pg (~> 1.2.3)
puma (~> 5.3)
rails (= 6.1.3.2)
puma (~> 5.5)
rails (= 6.1.4.1)
rails-controller-testing
redcarpet (~> 3.5.1)
rspec-rails (~> 5.0.1)
rspec-rails (~> 5.0.2)
sqlite3 (~> 1.4.2)
tzinfo-data
unitwise (~> 2.2.0)
webpacker (= 5.3.0)
BUNDLED WITH
2.2.17
2.2.32

View File

@ -1,2 +1,2 @@
frontend: yarn serve
rails: bundle exec rails s -b 0.0.0.0
webpacker: bin/webpack-dev-server

View File

@ -5,7 +5,7 @@ A self hosted cookbook
Parsley is released under the MIT License.
Copyright (C) 2020 Dan Elbert
Copyright (C) 2021 Dan Elbert
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View File

@ -5,7 +5,7 @@ class ApplicationSerializer
def initialize(items, serializer, opts = {})
super(items, opts)
@collection_name = opts[:collection_name]
@collection_name = 'list'
@serializer = serializer
end
@ -14,10 +14,10 @@ class ApplicationSerializer
if @collection_name && item.respond_to?(:total_pages)
{
total_count: item.total_count,
total_pages: item.total_pages,
current_page: item.current_page,
page_size: item.limit_value,
totalCount: item.total_count,
totalPages: item.total_pages,
currentPage: item.current_page,
pageSize: item.limit_value,
@collection_name.to_sym => list
}
else

View File

@ -1,78 +1,5 @@
module.exports = function(api) {
var validEnv = ['development', 'test', 'production']
var currentEnv = api.env()
var isDevelopmentEnv = api.env('development')
var isProductionEnv = api.env('production')
var isTestEnv = api.env('test')
if (!validEnv.includes(currentEnv)) {
throw new Error(
'Please specify a valid `NODE_ENV` or ' +
'`BABEL_ENV` environment variables. Valid values are "development", ' +
'"test", and "production". Instead, received: ' +
JSON.stringify(currentEnv) +
'.'
)
}
return {
presets: [
isTestEnv && [
'@babel/preset-env',
{
targets: {
node: 'current'
}
}
],
(isProductionEnv || isDevelopmentEnv) && [
'@babel/preset-env',
{
forceAllTransforms: true,
useBuiltIns: 'entry',
corejs: 3,
modules: false,
exclude: ['transform-typeof-symbol']
}
]
].filter(Boolean),
plugins: [
'babel-plugin-macros',
'@babel/plugin-syntax-dynamic-import',
isTestEnv && 'babel-plugin-dynamic-import-node',
'@babel/plugin-transform-destructuring',
[
'@babel/plugin-proposal-class-properties',
{
loose: true
}
],
[
'@babel/plugin-proposal-private-methods',
{
"loose": true
}
],
[
'@babel/plugin-proposal-object-rest-spread',
{
useBuiltIns: true
}
],
[
'@babel/plugin-transform-runtime',
{
helpers: false,
regenerator: true,
corejs: false
}
],
[
'@babel/plugin-transform-regenerator',
{
async: false
}
]
].filter(Boolean)
}
}
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
]
};

View File

@ -5,13 +5,14 @@ upstream parsley_app {
server {
listen 80;
root /parsley/public;
root /parsley/dist;
location / {
try_files $uri @rails;
index index.html index.htm;
try_files $uri $uri/ @rails;
}
location ~ ^/(assets|packs)/ {
location ~ ^/(css|img|js)/ {
try_files $uri @rails;
gzip_static on;
expires 1y;

View File

@ -1,26 +1,90 @@
{
"name": "parsley",
"version": "0.1.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"test:unit": "vue-cli-service test:unit",
"lint": "vue-cli-service lint"
},
"dependencies": {
"@rails/webpacker": "5.3.0",
"@tweenjs/tween.js": "^18.6.4",
"autosize": "^4.0.2",
"bulma": "^0.8.2",
"cheerio": "^1.0.0-rc.9",
"css-loader": "^5.2.4",
"lodash": "^4.17.21",
"svg-loader": "^0.0.2",
"url-loader": "4.1.1",
"vue": "^2.6.12",
"vue-loader": "^15.9.6",
"vue-progressbar": "^0.7.5",
"vue-resize": "^1.0.1",
"vue-router": "^3.5.1",
"vue-template-compiler": "^2.6.12",
"vuex": "^3.6.2",
"vuex-router-sync": "^5.0.0",
"webpack": "^4.46.0"
"core-js": "^3.6.5",
"register-service-worker": "^1.7.1",
"vue": "^3.0.0",
"vue-class-component": "^8.0.0-0",
"vue-router": "^4.0.0-0",
"vuex": "^4.0.0-0"
},
"devDependencies": {
"webpack-cli": "^3.3.12",
"webpack-dev-server": "^3.11.2"
}
"@types/chai": "^4.2.11",
"@types/mocha": "^5.2.4",
"@typescript-eslint/eslint-plugin": "^4.18.0",
"@typescript-eslint/parser": "^4.18.0",
"@vue/cli-plugin-babel": "~4.5.0",
"@vue/cli-plugin-eslint": "~4.5.0",
"@vue/cli-plugin-pwa": "~4.5.0",
"@vue/cli-plugin-router": "~4.5.0",
"@vue/cli-plugin-typescript": "~4.5.0",
"@vue/cli-plugin-unit-mocha": "~4.5.0",
"@vue/cli-plugin-vuex": "~4.5.0",
"@vue/cli-service": "~4.5.0",
"@vue/compiler-sfc": "^3.0.0",
"@vue/eslint-config-standard": "^5.1.2",
"@vue/eslint-config-typescript": "^7.0.0",
"@vue/test-utils": "^2.0.0-0",
"chai": "^4.1.2",
"eslint": "^6.7.2",
"eslint-plugin-import": "^2.20.2",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^4.2.1",
"eslint-plugin-standard": "^4.0.0",
"eslint-plugin-vue": "^7.0.0",
"sass": "^1.26.5",
"sass-loader": "^8.0.2",
"typescript": "~4.1.5"
},
"resolutions": {
"workbox-webpack-plugin": "~6.4.1"
},
"eslintConfig": {
"root": true,
"env": {
"node": true
},
"extends": [
"plugin:vue/vue3-essential",
"@vue/standard",
"@vue/typescript/recommended"
],
"parserOptions": {
"ecmaVersion": 2020
},
"rules": {
"space-before-function-paren": [
"error",
"never"
],
"semi": [
"error",
"always"
]
},
"overrides": [
{
"files": [
"**/__tests__/*.{j,t}s?(x)",
"**/tests/unit/**/*.spec.{j,t}s?(x)"
],
"env": {
"mocha": true
}
}
]
},
"browserslist": [
"> 1%",
"last 2 versions",
"not dead"
]
}

View File

@ -1,12 +0,0 @@
module.exports = {
plugins: [
require('postcss-import'),
require('postcss-flexbugs-fixes'),
require('postcss-preset-env')({
autoprefixer: {
flexbox: 'no-2009'
},
stage: 3
})
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 799 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.00251 14.9297L0 1.07422H6.14651L8.00251 4.27503L9.84583 1.07422H16L8.00251 14.9297Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 215 B

17
public/index.html Normal file
View File

@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
<noscript>
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>

30
src/App.vue Normal file
View File

@ -0,0 +1,30 @@
<template>
<div id="nav">
<router-link to="/">Home</router-link> |
<router-link to="/about">About</router-link>
</div>
<router-view/>
</template>
<style lang="scss">
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
}
#nav {
padding: 30px;
a {
font-weight: bold;
color: #2c3e50;
&.router-link-exact-active {
color: #42b983;
}
}
}
</style>

BIN
src/assets/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

View File

@ -0,0 +1,67 @@
<template>
<div class="hello">
<h1>{{ msg }}</h1>
<p>
For a guide and recipes on how to configure / customize this project,<br>
check out the
<a href="https://cli.vuejs.org" target="_blank" rel="noopener">vue-cli documentation</a>.
</p>
<h3>Installed CLI Plugins</h3>
<ul>
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-babel" target="_blank" rel="noopener">babel</a></li>
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-pwa" target="_blank" rel="noopener">pwa</a></li>
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-router" target="_blank" rel="noopener">router</a></li>
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-vuex" target="_blank" rel="noopener">vuex</a></li>
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-eslint" target="_blank" rel="noopener">eslint</a></li>
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-unit-mocha" target="_blank" rel="noopener">unit-mocha</a></li>
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-typescript" target="_blank" rel="noopener">typescript</a></li>
</ul>
<h3>Essential Links</h3>
<ul>
<li><a href="https://vuejs.org" target="_blank" rel="noopener">Core Docs</a></li>
<li><a href="https://forum.vuejs.org" target="_blank" rel="noopener">Forum</a></li>
<li><a href="https://chat.vuejs.org" target="_blank" rel="noopener">Community Chat</a></li>
<li><a href="https://twitter.com/vuejs" target="_blank" rel="noopener">Twitter</a></li>
<li><a href="https://news.vuejs.org" target="_blank" rel="noopener">News</a></li>
</ul>
<h3>Ecosystem</h3>
<ul>
<li><a href="https://router.vuejs.org" target="_blank" rel="noopener">vue-router</a></li>
<li><a href="https://vuex.vuejs.org" target="_blank" rel="noopener">vuex</a></li>
<li><a href="https://github.com/vuejs/vue-devtools#vue-devtools" target="_blank" rel="noopener">vue-devtools</a></li>
<li><a href="https://vue-loader.vuejs.org" target="_blank" rel="noopener">vue-loader</a></li>
<li><a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">awesome-vue</a></li>
</ul>
</div>
</template>
<script lang="ts">
import { Options, Vue } from 'vue-class-component';
@Options({
props: {
msg: String
}
})
export default class HelloWorld extends Vue {
msg!: string
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped lang="scss">
h3 {
margin: 40px 0 0;
}
ul {
list-style-type: none;
padding: 0;
}
li {
display: inline-block;
margin: 0 10px;
}
a {
color: #42b983;
}
</style>

147
src/lib/api.ts Normal file
View File

@ -0,0 +1,147 @@
import * as Errors from './errors';
import { PagedCollection, Recipe } from '@/lib/models';
type UpdateHandler = {
url: string,
component: unknown,
handler: (res: Response) => void
}
class Api {
cacheFirstUpdateHandlers: UpdateHandler[] = [];
constructor() {
navigator.serviceWorker.addEventListener('message', async(event) => {
if (event.data.type === 'CACHE_UPDATED' && event.data.meta === 'workbox-broadcast-update') {
const { cacheName, updatedURL } = event.data.payload;
const interestedHandlers = this.cacheFirstUpdateHandlers.filter(h => h.url === updatedURL);
if (interestedHandlers.length > 0) {
const cache = await caches.open(cacheName);
const updatedResponse = await cache.match(updatedURL);
if (updatedResponse !== undefined) {
interestedHandlers.forEach(h => h.handler(updatedResponse));
}
}
}
});
}
async checkStatus(response: Response): Promise<Response> {
if (response.ok) {
return response;
} else if (response.status === 404) {
throw new Errors.ApiNotFoundError(response.statusText, response);
} else if (response.status === 422) {
let json = null;
try {
json = await response.json();
} catch (e) {
// no-op
}
throw new Errors.ApiValidationError('Validation Error', response, json);
} else {
throw new Errors.ApiServerError(response.statusText || 'Unknown Server Error', response);
}
}
performRequest<TParams, TReturns>(url: string, method: string, params: TParams, headers: {[index: string]: string} = {}): Promise<TReturns> {
const hasBody = (params !== null && params !== undefined) && Object.keys(params).length !== 0;
let body: string | null = null;
const reqHeaders = new Headers();
reqHeaders.append('Accept', 'application/json');
reqHeaders.append('Content-Type', 'application/json');
for (const key in headers) {
reqHeaders.append(key, headers[key]);
}
if (hasBody) {
body = JSON.stringify(params);
}
return fetch(url, {
headers: reqHeaders,
method: method,
credentials: 'same-origin',
body: body
}).then(this.checkStatus)
.then(res => res.json());
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
objectToUrlParams(obj: any, queryParams: string[] = [], prefixes: string[] = []): string[] {
for (const key in obj) {
const val = obj[key];
const paramName = prefixes.join('[') + '['.repeat(Math.min(prefixes.length, 1)) + encodeURIComponent(key) + ']'.repeat(prefixes.length);
if (Array.isArray(val)) {
for (const x of val) {
queryParams.push(paramName + '[]=' + (x === null ? '' : encodeURIComponent(x.toString())));
}
} else if (val !== null && typeof (val) === 'object') {
this.objectToUrlParams(val, queryParams, prefixes.concat([key]));
} else {
queryParams.push(paramName + '=' + (val === null ? '' : encodeURIComponent(val)));
}
}
return queryParams;
}
buildGetUrl<TParams>(url: string, params: TParams): string {
const queryParams = this.objectToUrlParams(params);
if (queryParams.length) {
url = url + '?' + queryParams.join('&');
}
return url;
}
get<TParams, TReturn>(url: string, params: TParams): Promise<TReturn> {
return this.performRequest<null, TReturn>(this.buildGetUrl(url, params), 'GET', null);
}
setupCacheFirstGet<TParams, TReturn>(url: string, params: TParams, component: unknown, dataHandler: (data: TReturn) => void): () => void {
const fullUrl = this.buildGetUrl(url, params);
this.cacheFirstUpdateHandlers.push({
url: fullUrl,
component: component,
handler: res => res.json().then(data => dataHandler(data))
});
this.performRequest<null, TReturn>(fullUrl, 'GET', null, { 'Stale-While-Revalidate': 'true' })
.then((data: TReturn) => dataHandler(data));
return () => {
this.disableCacheFirstGet(url, params, component);
};
}
disableCacheFirstGet<TParams>(url: string | null, params: TParams, component: unknown) {
let fullUrl: string | null = null;
if (url !== null) {
fullUrl = this.buildGetUrl(url, params);
}
this.cacheFirstUpdateHandlers = this.cacheFirstUpdateHandlers.filter(h => !(h.component === component && (fullUrl === null || fullUrl === h.url)));
}
getRecipeList(component: unknown, page: number, per: number, sortColumn: string | null, sortDirection: string | null, name: string | null, tags: string[] | null, dataHandler: (json: PagedCollection<Recipe>) => void): () => void {
const params = {
criteria: {
page: page,
per: per,
sort_column: sortColumn,
sort_direction: sortDirection,
name: name,
tags: tags
}
};
return this.setupCacheFirstGet('/recipes', params, component, dataHandler);
}
}
export default new Api();

57
src/lib/errors.ts Normal file
View File

@ -0,0 +1,57 @@
type ValidationErrors = {
[key: string]: string[]
}
export class ApiError extends Error {
public response: Response;
constructor(message: string, response: Response) {
super(message);
this.name = 'ApiError';
this.response = response;
}
public get responseCode(): number {
if (this.response) {
return this.response.status;
} else {
return 0;
}
}
}
export class ApiServerError extends ApiError {
constructor(message: string, response: Response) {
super(message, response);
this.name = 'ApiServerError';
}
}
export class ApiNotFoundError extends ApiError {
constructor(message: string, response: Response) {
super(message, response);
this.name = 'ApiNotFoundError';
}
}
export class ApiValidationError extends ApiError {
json?: { [index: string]: string[] };
constructor(message: string, response: Response, json?: { [index: string]: string[] }) {
super(message, response);
this.name = 'ApiValidationError';
this.json = json;
}
validationErrors(): ValidationErrors {
const errors = {} as ValidationErrors;
if (this.json !== null) {
for (const key in this.json) {
errors[key] = this.json[key] as string[];
}
} else {
errors.base = ['unknown error'];
}
return errors;
}
}

12
src/lib/models.ts Normal file
View File

@ -0,0 +1,12 @@
export interface PagedCollection<T> {
totalCount: number
totalPages: number
currentPage: number
pageSize: number
list: T[]
}
export interface Recipe {
id: number
name: string
}

8
src/main.ts Normal file
View File

@ -0,0 +1,8 @@
import { createApp } from 'vue';
import App from './App.vue';
import { registerServiceWorker } from './registerServiceWorker';
import router from './router';
import store from './store';
registerServiceWorker(store);
createApp(App).use(store).use(router).mount('#app');

View File

@ -0,0 +1,38 @@
/* eslint-disable no-console */
import { register } from 'register-service-worker';
import { Store } from 'vuex';
import { State } from '@/store/state';
export function registerServiceWorker(store: Store<State>): void {
if (process.env.NODE_ENV === 'production') {
register(`${process.env.BASE_URL}service-worker.js`, {
ready() {
store.commit('setUpdateAvailable', false);
console.log(
'App is being served from cache by a service worker.\n' +
'For more details, visit https://goo.gl/AFskqB'
);
},
registered() {
console.log('Service worker has been registered.');
},
cached() {
console.log('Content has been cached for offline use.');
},
updatefound() {
console.log('New content is downloading.');
},
updated() {
store.commit('setUpdateAvailable', true);
console.log('New content is available; please refresh.');
},
offline() {
console.log('No internet connection found. App is running in offline mode.');
},
error(error) {
console.error('Error during service worker registration:', error);
}
});
}
}

25
src/router/index.ts Normal file
View File

@ -0,0 +1,25 @@
import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router';
import Recipes from '../views/Recipes.vue';
const routes: Array<RouteRecordRaw> = [
{
path: '/',
name: 'Recipes',
component: Recipes
},
{
path: '/about',
name: 'About',
// route level code-splitting
// this generates a separate chunk (about.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
}
];
const router = createRouter({
history: createWebHashHistory(),
routes
});
export default router;

32
src/service-worker.js Normal file
View File

@ -0,0 +1,32 @@
import { NetworkFirst, NetworkOnly, StaleWhileRevalidate } from 'workbox-strategies';
import { BroadcastUpdatePlugin } from 'workbox-broadcast-update/BroadcastUpdatePlugin';
import { cleanupOutdatedCaches, precacheAndRoute } from 'workbox-precaching';
import { setDefaultHandler, registerRoute } from 'workbox-routing';
import { setCacheNameDetails } from 'workbox-core';
setCacheNameDetails({
prefix: 'parsley'
});
cleanupOutdatedCaches();
precacheAndRoute(self.__WB_MANIFEST);
const isStaleWhileRevalidate = ({ request }) => {
return request.headers.get('Cache-Then-Network') === 'true';
};
registerRoute(
isStaleWhileRevalidate,
new StaleWhileRevalidate({
plugins: [
new BroadcastUpdatePlugin()
]
})
);
registerRoute(
new RegExp('^(?!http).+'),
new NetworkOnly()
);
setDefaultHandler(new NetworkFirst());

6
src/shims-vue.d.ts vendored Normal file
View File

@ -0,0 +1,6 @@
/* eslint-disable */
declare module '*.vue' {
import type { DefineComponent } from 'vue'
const component: DefineComponent<{}, {}, any>
export default component
}

17
src/store/index.ts Normal file
View File

@ -0,0 +1,17 @@
import { createStore } from 'vuex';
import { State } from './state';
export default createStore<State>({
state: {
updateAvailable: false
},
mutations: {
setUpdateAvailable(state, payload) {
state.updateAvailable = payload;
}
},
actions: {
},
modules: {
}
});

3
src/store/state.ts Normal file
View File

@ -0,0 +1,3 @@
export interface State {
updateAvailable: boolean
}

5
src/views/About.vue Normal file
View File

@ -0,0 +1,5 @@
<template>
<div class="about">
<h1>This is an about page</h1>
</div>
</template>

18
src/views/Home.vue Normal file
View File

@ -0,0 +1,18 @@
<template>
<div class="home">
<img alt="Vue logo" src="../assets/logo.png">
<HelloWorld msg="Welcome to Your Vue.js + TypeScript App"/>
</div>
</template>
<script lang="ts">
import { Options, Vue } from 'vue-class-component';
import HelloWorld from '@/components/HelloWorld.vue'; // @ is an alias to /src
@Options({
components: {
HelloWorld
}
})
export default class Home extends Vue {}
</script>

41
src/views/Recipes.vue Normal file
View File

@ -0,0 +1,41 @@
<template>
<div class="recipes">
<ul v-if="recipes !== null">
<li v-for="r in recipes.list" :key="r.id">
{{ r.name }}
</li>
</ul>
</div>
</template>
<script lang="ts">
import api from '@/lib/api';
import { PagedCollection, Recipe } from '@/lib/models';
import { Options, Vue } from 'vue-class-component';
@Options({
components: {
}
})
export default class Recipes extends Vue {
recipes: (PagedCollection<Recipe> | null) = null
cancelCacheUpdate: (() => void) | null = null
updateRecipes(): void {
if (this.cancelCacheUpdate !== null) {
this.cancelCacheUpdate();
}
this.cancelCacheUpdate = api.getRecipeList(this, 1, 20, null, null, null, null, data => { this.recipes = data; });
}
mounted(): void {
this.updateRecipes();
}
unmounted(): void {
if (this.cancelCacheUpdate !== null) {
this.cancelCacheUpdate();
}
}
}
</script>

9
src/vuex.d.ts vendored Normal file
View File

@ -0,0 +1,9 @@
import { Store } from 'vuex';
import { State } from '@/store/state';
declare module '@vue/runtime-core' {
// provide typings for `this.$store`
interface ComponentCustomProperties {
$store: Store<State>
}
}

View File

@ -0,0 +1,13 @@
import { expect } from 'chai';
import { shallowMount } from '@vue/test-utils';
import HelloWorld from '@/components/HelloWorld.vue';
describe('HelloWorld.vue', () => {
it('renders props.msg when passed', () => {
const msg = 'new message';
const wrapper = shallowMount(HelloWorld, {
props: { msg }
});
expect(wrapper.text()).to.include(msg);
});
});

42
tsconfig.json Normal file
View File

@ -0,0 +1,42 @@
{
"compilerOptions": {
"target": "esnext",
"module": "esnext",
"strict": true,
"jsx": "preserve",
"importHelpers": true,
"moduleResolution": "node",
"experimentalDecorators": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"sourceMap": true,
"baseUrl": ".",
"types": [
"webpack-env",
"mocha",
"chai"
],
"paths": {
"@/*": [
"src/*"
]
},
"lib": [
"esnext",
"dom",
"dom.iterable",
"scripthost"
]
},
"include": [
"src/**/*.ts",
"src/**/*.tsx",
"src/**/*.vue",
"tests/**/*.ts",
"tests/**/*.tsx"
],
"exclude": [
"node_modules"
]
}

19
vue.config.js Normal file
View File

@ -0,0 +1,19 @@
// eslint-disable-next-line @typescript-eslint/no-var-requires
const path = require('path');
// In development, the Rails app will run on a port 100 higher than this one (default way foreman assigns ports)
const devRailsPort = (parseInt(process.env.PORT || '3000') + 100).toString();
module.exports = {
pwa: {
workboxPluginMode: 'InjectManifest',
workboxOptions: {
swSrc: path.join(__dirname, 'src/service-worker.js'),
compileSrc: true
}
},
devServer: {
proxy: `http://localhost:${devRailsPort}`
}
};

7020
yarn.lock

File diff suppressed because it is too large Load Diff