├── .babelrc ├── .editorconfig ├── .eslintrc.js ├── .github └── workflows │ └── staging.yml ├── .gitignore ├── .husky ├── common.sh ├── pre-commit └── pre-push ├── .prettierignore ├── .prettierrc ├── README.md ├── components ├── button.vue ├── dropdown-select │ ├── dropdown-icon.vue │ ├── dropdown-item.vue │ └── dropdown-select.vue ├── explanation │ ├── explanation-container.vue │ ├── process-flowchart.vue │ ├── status-explanation.vue │ └── status-legend.vue ├── explore │ ├── filter-panel │ │ └── filter-panel.vue │ ├── promise-overview │ │ ├── active-filters.vue │ │ ├── chart-item.vue │ │ ├── filter-chip.vue │ │ ├── promise-overview.vue │ │ ├── promises-aggregator.ts │ │ ├── tab-body.vue │ │ └── tab-navigation.vue │ └── topic-group │ │ ├── icon-right.vue │ │ ├── topic-group.vue │ │ └── topic-utils.ts ├── external-link.vue ├── form-link.vue ├── guide │ ├── guide-arrow-breakline.vue │ └── guide-arrow.vue ├── landing.vue ├── link-banner.vue ├── party │ ├── party-card.vue │ └── party-promise.vue ├── promise-card │ ├── box-container.vue │ ├── expanded-card.vue │ ├── icon-up.vue │ ├── link.vue │ ├── ncpo.vue │ ├── promise-card.vue │ ├── single-card.vue │ ├── timeline-arrow.vue │ └── timeline.vue ├── scroll-icon.vue ├── status-animation.vue └── toggle │ ├── toggle-item.vue │ └── toggle-list.vue ├── data ├── metadata.json ├── parties.json ├── promises-example.json └── promises.json ├── jest-setup.ts ├── jest.config.js ├── layouts └── default.vue ├── models ├── filter.ts ├── party.ts └── promise.ts ├── nuxt.config.js ├── package.json ├── pages ├── about.vue ├── explore.vue ├── guide.vue ├── index.vue └── promises │ └── _id.vue ├── scripts └── data-fetcher │ ├── extracts │ ├── helpers.ts │ ├── party.ts │ ├── promise.ts │ └── timeline.ts │ ├── index.ts │ ├── tests │ ├── extracts │ │ ├── helper.test.ts │ │ ├── party.test.ts │ │ ├── promise.test.ts │ │ └── timeline.test.ts │ └── transforms │ │ ├── party.test.ts │ │ └── promise.test.ts │ └── transforms │ ├── party.ts │ └── promise.ts ├── shims-vue.d.ts ├── static ├── android-chrome-192x192.png ├── android-chrome-512x512.png ├── apple-touch-icon.png ├── favicon-16x16.png ├── favicon-32x32.png ├── favicon.ico ├── fonts │ ├── Anuphan-Regular.otf │ ├── Anuphan-SemiBold.otf │ ├── BaiJamjuree-Bold.ttf │ ├── BaiJamjuree-Regular.ttf │ ├── KondolarThai-Black.woff │ ├── KondolarThai-Regular.woff │ └── typography.css ├── images │ ├── article │ │ ├── article.png │ │ └── explore.png │ ├── background │ │ ├── blog_bg.png │ │ └── landing_bg.png │ ├── guide │ │ ├── censure-motion-01.png │ │ ├── censure-motion-02.png │ │ ├── censure-motion-03.png │ │ ├── long-collage.png │ │ ├── prayut.gif │ │ ├── section-01.png │ │ └── section-03.png │ ├── logo-addon.png │ ├── other-group.png │ ├── party │ │ ├── dummy.jpg │ │ ├── กรีน.jpg │ │ ├── กลาง.jpg │ │ ├── กล้า.jpg │ │ ├── กสิกรไทย.jpg │ │ ├── ก้าวไกล.jpg │ │ ├── คนงานไทย.jpg │ │ ├── คนธรรมดาแห่งประเทศไทย.jpg │ │ ├── ครูไทยเพื่อประชาชน.jpg │ │ ├── คลองไทย.jpg │ │ ├── ความหวังใหม่.jpg │ │ ├── ชาติพัฒนา.jpg │ │ ├── ชาติพันธุ์ไทย.jpg │ │ ├── ชาติไทยพัฒนา.jpg │ │ ├── ฐานรากไทย.jpg │ │ ├── ถิ่นกาขาวชาววิไล.jpg │ │ ├── ทางเลือกใหม่.jpg │ │ ├── ประชากรไทย.jpg │ │ ├── ประชาชนปฏิรูป.jpg │ │ ├── ประชาชาติ.jpg │ │ ├── ประชาธรรมไทย.jpg │ │ ├── ประชาธิปัตย์.jpg │ │ ├── ประชาธิปไตยเพื่อประชาชน.jpg │ │ ├── ประชาธิปไตยใหม่.jpg │ │ ├── ประชานิยม.jpg │ │ ├── ประชาภิวัฒน์.jpg │ │ ├── ประชาไทย.jpg │ │ ├── ผึ้งหลวง.jpg │ │ ├── พลังคนกีฬา.jpg │ │ ├── พลังครูไทย.jpg │ │ ├── พลังชล.jpg │ │ ├── พลังชาติไทย.jpg │ │ ├── พลังท้องถิ่นไท.jpg │ │ ├── พลังธรรมใหม่.jpg │ │ ├── พลังประชาธิปไตย.jpg │ │ ├── พลังประชารัฐ.jpg │ │ ├── พลังปวงชนไทย.jpg │ │ ├── พลังรัก.jpg │ │ ├── พลังศรัทธา.jpg │ │ ├── พลังสหกรณ์.jpg │ │ ├── พลังสังคม.jpg │ │ ├── พลังแผ่นดินทอง.jpg │ │ ├── พลังแรงงานไทย.jpg │ │ ├── พลังไทยดี.jpg │ │ ├── พลังไทยรักชาติ.jpg │ │ ├── พลังไทยรักไทย.jpg │ │ ├── พลังไทสร้างชาติ.jpg │ │ ├── พลเมืองไทย.jpg │ │ ├── พัฒนาประเทศไทย.jpg │ │ ├── ภราดรภาพ.jpg │ │ ├── ภาคีเครือข่ายไทย.jpg │ │ ├── ภูมิพลังเกษตรกรไทย.jpg │ │ ├── ภูมิใจไทย.jpg │ │ ├── มติประชา.jpg │ │ ├── มหาชน.jpg │ │ ├── ยางพาราไทย.jpg │ │ ├── รวมพลังประชาชาติไทย.jpg │ │ ├── รวมใจไทย.jpg │ │ ├── รักท้องถิ่นไทย.jpg │ │ ├── รักษ์ธรรม.jpg │ │ ├── รักษ์ผืนป่าประเทศไทย.jpg │ │ ├── สยามพัฒนา.jpg │ │ ├── สังคมประชาธิปไตยไทย.jpg │ │ ├── สามัญชน.jpg │ │ ├── อนาคตไทย.jpg │ │ ├── เครือข่ายชาวนาแห่งประเทศไทย.jpg │ │ ├── เพื่อคนไทย.jpg │ │ ├── เพื่อชาติ.jpg │ │ ├── เพื่อชีวิตใหม่.jpg │ │ ├── เพื่อธรรม.jpg │ │ ├── เพื่อนไทย.jpg │ │ ├── เพื่อสหกรณ์ไทย.jpg │ │ ├── เพื่อแผ่นดิน.jpg │ │ ├── เพื่อไทย.jpg │ │ ├── เพื่อไทยพัฒนา.jpg │ │ ├── เศรษฐกิจใหม่.jpg │ │ ├── เสรีรวมไทย.jpg │ │ ├── แทนคุณแผ่นดิน.jpg │ │ ├── แผ่นดินธรรม.jpg │ │ ├── ไทยธรรม.jpg │ │ ├── ไทยรักษาชาติ.jpg │ │ ├── ไทยรุ่งเรือง.jpg │ │ ├── ไทยศรีวิไลย์.jpg │ │ └── ไทรักธรรม.jpg │ ├── status │ │ ├── default.png │ │ ├── done.png │ │ ├── done_small.png │ │ ├── nodata.png │ │ ├── nodata_small.png │ │ ├── notfound.png │ │ ├── paused.png │ │ ├── paused_small.png │ │ ├── proposed.png │ │ ├── proposed_small.png │ │ ├── working.png │ │ └── working_small.png │ └── topic │ │ ├── administration.png │ │ ├── administration_small.png │ │ ├── culture.png │ │ ├── culture_small.png │ │ ├── economics.png │ │ ├── economics_small.png │ │ ├── environmental.png │ │ ├── environmental_small.png │ │ ├── equality.png │ │ ├── equality_small.png │ │ ├── foreign.png │ │ ├── foreign_small.png │ │ ├── proposed.png │ │ ├── security.png │ │ └── security_small.png ├── og │ ├── article.jpg │ ├── default.jpg │ ├── done.jpg │ ├── nodata.jpg │ ├── paused.jpg │ ├── proposed.jpg │ └── working.jpg └── site.webmanifest ├── tailwind.config.js ├── tailwind.safelist.txt ├── tests ├── components │ ├── button.test.ts │ ├── dropdown-select │ │ ├── dropdown-item.test.ts │ │ └── dropdown-select.test.ts │ ├── explanation │ │ ├── explanation-container.test.ts │ │ ├── status-explanation.test.ts │ │ └── status-legend.test.ts │ ├── explore │ │ ├── promise-overview │ │ │ ├── active-filters.test.ts │ │ │ ├── chart-item.test.ts │ │ │ ├── filter-chip.test.ts │ │ │ ├── promise-aggregator.test.ts │ │ │ ├── promise-overview.test.ts │ │ │ ├── tab-body.test.ts │ │ │ └── tab-navigation.test.ts │ │ └── topic-group │ │ │ ├── topic-group.test.ts │ │ │ └── topic-utils.test.ts │ ├── external-link.test.ts │ ├── form-link.test.ts │ ├── link-banner.test.ts │ ├── party-card.test.ts │ ├── promise-card │ │ ├── box-container.test.ts │ │ ├── expanded-card.test.ts │ │ ├── link.test.ts │ │ ├── promise-card.test.ts │ │ ├── single-card.test.ts │ │ └── timeline.test.ts │ ├── promises │ │ ├── _id.test.ts │ │ └── meta-utils.test.ts │ └── scroll-icon.test.ts └── fileTransformer.js ├── tsconfig.json ├── utils ├── metadata.ts └── promises-meta.ts └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "test": { 4 | "presets": [ 5 | [ 6 | "@babel/preset-env", 7 | { 8 | "targets": { 9 | "node": "current" 10 | } 11 | } 12 | ] 13 | ] 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | browser: true, 5 | node: true, 6 | }, 7 | extends: [ 8 | '@nuxtjs/eslint-config-typescript', 9 | 'plugin:nuxt/recommended', 10 | 'prettier', 11 | ], 12 | plugins: [], 13 | rules: { 14 | 'vue/multi-word-component-names': 'off', 15 | }, 16 | }; 17 | -------------------------------------------------------------------------------- /.github/workflows/staging.yml: -------------------------------------------------------------------------------- 1 | name: Deploy to staging 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths-ignore: 8 | - 'README.md' 9 | workflow_dispatch: 10 | 11 | concurrency: 12 | group: staging_environment 13 | cancel-in-progress: true 14 | 15 | jobs: 16 | build: 17 | runs-on: ubuntu-latest 18 | 19 | steps: 20 | - name: Checkout 🛎 21 | uses: actions/checkout@master 22 | 23 | - name: Setup node env 🏗 24 | uses: actions/setup-node@v3 25 | with: 26 | node-version: 16 27 | cache: yarn 28 | 29 | - name: Install dependencies 👨🏻‍💻 30 | run: yarn 31 | 32 | - name: Run tests 🧪 33 | run: yarn test 34 | 35 | - name: Fetch data 🏃🏻‍♂️ 36 | run: | 37 | touch .env 38 | echo NOCODB_API_PATH=${{ secrets.NOCODB_API_PATH }} >> .env 39 | echo NOCODB_API_TOKEN=${{ secrets.NOCODB_API_TOKEN }} >> .env 40 | yarn fetch-data 41 | 42 | - name: Build 🍳 43 | run: yarn build 44 | env: 45 | BASE_PATH: /promise-tracker 46 | 47 | - name: Deploy ☁️ 48 | uses: peaceiris/actions-gh-pages@v3 49 | with: 50 | github_token: ${{ secrets.GITHUB_TOKEN }} 51 | publish_dir: ./dist 52 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Node template 3 | # Logs 4 | /logs 5 | *.log 6 | npm-debug.log* 7 | yarn-debug.log* 8 | yarn-error.log* 9 | 10 | # Runtime data 11 | pids 12 | *.pid 13 | *.seed 14 | *.pid.lock 15 | 16 | # Directory for instrumented libs generated by jscoverage/JSCover 17 | lib-cov 18 | 19 | # Coverage directory used by tools like istanbul 20 | coverage 21 | 22 | # nyc test coverage 23 | .nyc_output 24 | 25 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 26 | .grunt 27 | 28 | # Bower dependency directory (https://bower.io/) 29 | bower_components 30 | 31 | # node-waf configuration 32 | .lock-wscript 33 | 34 | # Compiled binary addons (https://nodejs.org/api/addons.html) 35 | build/Release 36 | 37 | # Dependency directories 38 | node_modules/ 39 | jspm_packages/ 40 | 41 | # TypeScript v1 declaration files 42 | typings/ 43 | 44 | # Optional npm cache directory 45 | .npm 46 | 47 | # Optional eslint cache 48 | .eslintcache 49 | 50 | # Optional REPL history 51 | .node_repl_history 52 | 53 | # Output of 'npm pack' 54 | *.tgz 55 | 56 | # Yarn Integrity file 57 | .yarn-integrity 58 | 59 | # dotenv environment variables file 60 | .env 61 | 62 | # parcel-bundler cache (https://parceljs.org/) 63 | .cache 64 | 65 | # next.js build output 66 | .next 67 | 68 | # nuxt.js build output 69 | .nuxt 70 | 71 | # Nuxt generate 72 | dist 73 | 74 | # vuepress build output 75 | .vuepress/dist 76 | 77 | # Serverless directories 78 | .serverless 79 | 80 | # IDE / Editor 81 | .idea 82 | 83 | # Service worker 84 | sw.* 85 | 86 | # macOS 87 | .DS_Store 88 | 89 | # Vim swap files 90 | *.swp 91 | -------------------------------------------------------------------------------- /.husky/common.sh: -------------------------------------------------------------------------------- 1 | command_exists () { 2 | command -v "$1" >/dev/null 2>&1 3 | } 4 | 5 | # Workaround for Windows 10, Git Bash and Yarn 6 | if command_exists winpty && test -t 1; then 7 | exec < /dev/tty 8 | fi 9 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | . "$(dirname "$0")/common.sh" 4 | 5 | npm run lint-staged 6 | -------------------------------------------------------------------------------- /.husky/pre-push: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | . "$(dirname "$0")/common.sh" 4 | 5 | npm run test --silent 6 | npm run build 7 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | ### 2 | # Place your Prettier ignore content here 3 | 4 | ### 5 | # .gitignore content is duplicated here due to https://github.com/prettier/prettier/issues/8506 6 | 7 | # Created by .ignore support plugin (hsz.mobi) 8 | ### Node template 9 | # Logs 10 | /logs 11 | *.log 12 | npm-debug.log* 13 | yarn-debug.log* 14 | yarn-error.log* 15 | 16 | # Runtime data 17 | pids 18 | *.pid 19 | *.seed 20 | *.pid.lock 21 | 22 | # Directory for instrumented libs generated by jscoverage/JSCover 23 | lib-cov 24 | 25 | # Coverage directory used by tools like istanbul 26 | coverage 27 | 28 | # nyc test coverage 29 | .nyc_output 30 | 31 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 32 | .grunt 33 | 34 | # Bower dependency directory (https://bower.io/) 35 | bower_components 36 | 37 | # node-waf configuration 38 | .lock-wscript 39 | 40 | # Compiled binary addons (https://nodejs.org/api/addons.html) 41 | build/Release 42 | 43 | # Dependency directories 44 | node_modules/ 45 | jspm_packages/ 46 | 47 | # TypeScript v1 declaration files 48 | typings/ 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Optional REPL history 57 | .node_repl_history 58 | 59 | # Output of 'npm pack' 60 | *.tgz 61 | 62 | # Yarn Integrity file 63 | .yarn-integrity 64 | 65 | # dotenv environment variables file 66 | .env 67 | 68 | # parcel-bundler cache (https://parceljs.org/) 69 | .cache 70 | 71 | # next.js build output 72 | .next 73 | 74 | # nuxt.js build output 75 | .nuxt 76 | 77 | # Nuxt generate 78 | dist 79 | 80 | # vuepress build output 81 | .vuepress/dist 82 | 83 | # Serverless directories 84 | .serverless 85 | 86 | # IDE / Editor 87 | .idea 88 | 89 | # Service worker 90 | sw.* 91 | 92 | # macOS 93 | .DS_Store 94 | 95 | # Vim swap files 96 | *.swp 97 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "semi": true, 4 | "singleQuote": true 5 | } 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🤞 WeVis Promise Tracker 2 | 3 | Thai politicians and parties promise tracker 4 | 5 | ## 🌎 Environment 6 | 7 | - Production: https://promisetracker.wevis.info 8 | - Staging: https://wevisdemo.github.io/promise-tracker 9 | 10 | ## 💻 Tech stack 11 | 12 | - [NuxtJS](https://nuxtjs.org) with [Vue 2 and TypeScript](https://v2.vuejs.org/v2/guide/typescript.html#Basic-Usage) 13 | - [TailwindCSS](https://tailwindcss.com) 14 | - [Jest](https://jestjs.io) and [Vue Test Utils](https://v1.test-utils.vuejs.org/guides/#getting-started) 15 | - [NocoDB](https://nocodb.com) as a database 16 | 17 | ## 📐 Architecture 18 | 19 | The project use static site generator (SSG) strategy. In the pipeline, API is called once before the build time, which mean no API will be called after the static site is generated. To update the site after the data is updated, site needed to be built and deployed again. 20 | 21 | ```mermaid 22 | flowchart TD 23 | A[NocoDB] -->|yarn fetch-data| B[JSON files in /data] 24 | B -->|yarn dev| C[Nuxt dev environment] 25 | B -->|yarn build| D[Static site generated by Nuxt] 26 | ``` 27 | 28 | - Promises and related data is store on WeVis's NocoDB which allow non-dev to maintain the data 29 | - Use `yarn fetch-data` to download data from NocoDB, transform into JSON files in /data 30 | - JSON files then will be loaded by Nuxt from `yarn dev` and `yarn build` command 31 | 32 | ## 📂 Directory structure 33 | 34 | Use `pascal-case` for every file name since Linux (eg. Github Action runner) is not case-incensitive, in contrary to Mac and Windows. 35 | 36 | - `/components` Vue components 37 | - Put on the root if it's shared between pages 38 | - Put in the `/components//` if it's only used in that page 39 | - If components is too big or getting duplicated, you should split into several component which can also be group in sub-folder 40 | - `/data` JSON promises and related data, including example one before fetching script is implemented. 41 | - `/models` Data types (interface, enum, etc.) sharing across the project 42 | - `/pages` Represent [Nuxt routing](https://nuxtjs.org/docs/directory-structure/pages) 43 | - `/static` Static assets such as images 44 | - Before adding new assets, check first if it's already exist here. 45 | - `$config.path.base` can be used to reference `static` path (or base path) 46 | - `$config.path.images` can be used to reference `static/images` path 47 | ```vue 48 | 2 | 9 | 10 | 11 | 81 | -------------------------------------------------------------------------------- /components/dropdown-select/dropdown-icon.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 31 | -------------------------------------------------------------------------------- /components/dropdown-select/dropdown-item.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 43 | 44 | 52 | -------------------------------------------------------------------------------- /components/dropdown-select/dropdown-select.vue: -------------------------------------------------------------------------------- 1 | 47 | 48 | 108 | -------------------------------------------------------------------------------- /components/explanation/explanation-container.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 29 | -------------------------------------------------------------------------------- /components/explanation/status-explanation.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 18 | -------------------------------------------------------------------------------- /components/explore/promise-overview/active-filters.vue: -------------------------------------------------------------------------------- 1 | 49 | 50 | 103 | -------------------------------------------------------------------------------- /components/explore/promise-overview/chart-item.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 75 | -------------------------------------------------------------------------------- /components/explore/promise-overview/filter-chip.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 85 | -------------------------------------------------------------------------------- /components/explore/promise-overview/promise-overview.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 54 | -------------------------------------------------------------------------------- /components/explore/promise-overview/promises-aggregator.ts: -------------------------------------------------------------------------------- 1 | import { FilterType } from '~/models/filter'; 2 | import { 3 | TrackingPromise, 4 | PromiseStatus, 5 | promiseTopicTextMap, 6 | promiseStatusTextMap, 7 | promiseStatusOrder, 8 | promiseTopicOrder, 9 | } from '~/models/promise'; 10 | 11 | export interface ChartData { 12 | status: PromiseStatus; 13 | count: number; 14 | } 15 | 16 | export interface Chart { 17 | label: string; 18 | icon?: string; 19 | isNCPO?: boolean; 20 | data: ChartData[]; 21 | } 22 | 23 | interface StatusPair { 24 | [status: string]: number; 25 | } 26 | 27 | const parseChartDataFromStatusPair = (statuses: StatusPair): ChartData[] => 28 | statuses 29 | ? promiseStatusOrder.reduce( 30 | (list, status) => 31 | status in statuses 32 | ? [...list, { status, count: statuses[status] }] 33 | : list, 34 | [] 35 | ) 36 | : []; 37 | 38 | export const groupPromisesBy = ( 39 | groupBy: FilterType.Party | FilterType.Status | FilterType.Topic, 40 | promises: TrackingPromise[], 41 | maxGroup = 7 42 | ): { 43 | max: number; 44 | total: number; 45 | charts: Chart[]; 46 | } => { 47 | const groupedPromiseObject = promises.reduce<{ 48 | [key: string]: { isNCPO: boolean; count: number; statuses: StatusPair }; 49 | }>((obj, promise) => { 50 | const group = promise[groupBy]; 51 | 52 | if (!(group in obj)) { 53 | obj[group] = { 54 | isNCPO: false, 55 | count: 1, 56 | statuses: {}, 57 | }; 58 | } else { 59 | obj[group].count++; 60 | } 61 | 62 | if (!(promise.status in obj[group].statuses)) { 63 | obj[group].statuses[promise.status] = 1; 64 | } else { 65 | obj[group].statuses[promise.status]++; 66 | } 67 | 68 | if (promise.isNCPO === true) { 69 | obj[group].isNCPO = true; 70 | } 71 | 72 | return obj; 73 | }, {}); 74 | 75 | let charts: Chart[] = 76 | groupBy === FilterType.Party 77 | ? Object.entries(groupedPromiseObject) 78 | .map(([label, group]) => ({ 79 | label, 80 | icon: `party/${label.split('/')[0]}.jpg`, 81 | isNCPO: group.isNCPO, 82 | data: parseChartDataFromStatusPair(group.statuses), 83 | })) 84 | .sort( 85 | (a, z) => 86 | groupedPromiseObject[z.label].count - 87 | groupedPromiseObject[a.label].count 88 | ) 89 | : groupBy === FilterType.Topic 90 | ? promiseTopicOrder.map((key) => ({ 91 | label: promiseTopicTextMap.get(key)?.short as string, 92 | icon: `topic/${key}_small.png`, 93 | data: parseChartDataFromStatusPair( 94 | groupedPromiseObject[key]?.statuses 95 | ), 96 | })) 97 | : promiseStatusOrder.map((key) => ({ 98 | label: promiseStatusTextMap.get(key) as string, 99 | icon: `status/${key}_small.png`, 100 | data: parseChartDataFromStatusPair( 101 | groupedPromiseObject[key]?.statuses 102 | ), 103 | })); 104 | 105 | if (charts.length > maxGroup) { 106 | const otherParties = charts.slice(maxGroup); 107 | 108 | charts = [ 109 | ...charts.slice(0, maxGroup), 110 | { 111 | label: 'อื่นๆ', 112 | icon: `other-group.png`, 113 | data: promiseStatusOrder 114 | .map((status) => ({ 115 | status, 116 | count: otherParties.reduce( 117 | (sum, { data }) => 118 | sum + (data.find((d) => d.status === status)?.count || 0), 119 | 0 120 | ), 121 | })) 122 | .filter(({ count }) => count > 0), 123 | }, 124 | ]; 125 | } 126 | 127 | const chartsCount = charts.map(({ data }) => 128 | data.reduce((sum, { count }) => sum + count, 0) 129 | ); 130 | 131 | return { 132 | max: Math.max(...chartsCount), 133 | total: chartsCount.reduce((sum, count) => sum + count, 0), 134 | charts, 135 | }; 136 | }; 137 | -------------------------------------------------------------------------------- /components/explore/promise-overview/tab-body.vue: -------------------------------------------------------------------------------- 1 | 64 | 65 | 101 | 102 | 112 | -------------------------------------------------------------------------------- /components/explore/promise-overview/tab-navigation.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 61 | -------------------------------------------------------------------------------- /components/explore/topic-group/icon-right.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 23 | -------------------------------------------------------------------------------- /components/explore/topic-group/topic-utils.ts: -------------------------------------------------------------------------------- 1 | import { 2 | TrackingPromise, 3 | PromiseTopic, 4 | promiseTopicTextMap, 5 | promiseStatusTextMap, 6 | PromiseStatus, 7 | } from '@/models/promise'; 8 | 9 | export interface Group { 10 | by: string; 11 | where: PromiseTopic | PromiseStatus | string; 12 | } 13 | 14 | export const groupBy = (topic: String, status: String) => { 15 | if (topic !== '' && status === '') { 16 | return { by: 'topic', where: topic as PromiseTopic }; 17 | } else if (topic === '' && status !== '') { 18 | return { by: 'status', where: status as PromiseStatus }; 19 | } else { 20 | return { by: '', where: '' }; 21 | } 22 | }; 23 | 24 | export const filteredPromise = ( 25 | promises: TrackingPromise[], 26 | by: String, 27 | where: PromiseTopic | PromiseStatus | String 28 | ): TrackingPromise[] => { 29 | if (by === 'topic') { 30 | return promises.filter( 31 | (promise: TrackingPromise) => promise.topic === (where as PromiseTopic) 32 | ); 33 | } else if (by === 'status') { 34 | return promises.filter( 35 | (promise: TrackingPromise) => promise.status === (where as PromiseStatus) 36 | ); 37 | } else { 38 | return promises; 39 | } 40 | }; 41 | 42 | export const getPromisesLength = (promises: TrackingPromise[]): number => { 43 | return promises.length; 44 | }; 45 | 46 | const getTopicTitle = (topic: PromiseTopic): string | undefined => { 47 | return promiseTopicTextMap.get(topic)?.long; 48 | }; 49 | 50 | const getStatusTitle = (status: PromiseStatus): string | undefined => { 51 | return promiseStatusTextMap.get(status); 52 | }; 53 | 54 | export const getGroupTitle = ( 55 | by: string, 56 | where: string 57 | ): string | undefined => { 58 | if (by === 'topic') { 59 | const title = getTopicTitle(where as PromiseTopic); 60 | if (title) { 61 | return 'ประเด็น' + getTopicTitle(where as PromiseTopic); 62 | } else { 63 | return ''; 64 | } 65 | } else if (by === 'status') { 66 | const title = getStatusTitle(where as PromiseStatus); 67 | if (title) { 68 | return 'สถานะ: ' + getStatusTitle(where as PromiseStatus); 69 | } else { 70 | return ''; 71 | } 72 | } else { 73 | return ''; 74 | } 75 | }; 76 | 77 | export const computedPromisePerPage = ( 78 | promisePerPage: number, 79 | promiseLength: number 80 | ) => { 81 | if (promisePerPage > 0) { 82 | return promisePerPage; 83 | } else { 84 | return promiseLength; 85 | } 86 | }; 87 | 88 | export const pageLength = (promiseLength: number, promisePerPage: number) => { 89 | if (promisePerPage <= 0) return 0; 90 | return Math.ceil(promiseLength / promisePerPage); 91 | }; 92 | 93 | export const pageNumberArray = (pageLength: number, currentPage: number) => { 94 | if (currentPage > pageLength || pageLength < 0 || currentPage < 0) return []; 95 | const fullArray = Array.from({ length: pageLength }, (_, index) => index + 1); 96 | let returnedArray = []; 97 | 98 | if (fullArray.length <= 4) { 99 | return fullArray; 100 | } 101 | 102 | if (currentPage <= 2) { 103 | returnedArray = [1, 2, '...', fullArray.length]; 104 | } else if (currentPage >= 3 && currentPage < fullArray.length - 2) { 105 | returnedArray = [ 106 | 1, 107 | '...', 108 | currentPage, 109 | currentPage + 1, 110 | '...', 111 | fullArray.length, 112 | ]; 113 | } else if (currentPage >= fullArray.length - 2) { 114 | returnedArray = [ 115 | 1, 116 | '...', 117 | fullArray.length - 2, 118 | fullArray.length - 1, 119 | fullArray.length, 120 | ]; 121 | } else { 122 | return []; 123 | } 124 | return returnedArray; 125 | }; 126 | 127 | export const currentPagePromises = ( 128 | promises: TrackingPromise[], 129 | currentPage: number, 130 | promisePerPage: number 131 | ) => { 132 | if (currentPage < 1 || promisePerPage < 0) return []; 133 | const lastItemIndex: number = currentPage * promisePerPage; 134 | const firstItemIndex: number = lastItemIndex - promisePerPage; 135 | return promises.slice(firstItemIndex, lastItemIndex); 136 | }; 137 | -------------------------------------------------------------------------------- /components/external-link.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 20 | -------------------------------------------------------------------------------- /components/form-link.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 35 | -------------------------------------------------------------------------------- /components/guide/guide-arrow-breakline.vue: -------------------------------------------------------------------------------- 1 | 51 | 52 | 59 | -------------------------------------------------------------------------------- /components/guide/guide-arrow.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 27 | -------------------------------------------------------------------------------- /components/landing.vue: -------------------------------------------------------------------------------- 1 | 57 | 58 | 78 | 79 | 102 | -------------------------------------------------------------------------------- /components/link-banner.vue: -------------------------------------------------------------------------------- 1 | 44 | 45 | 96 | -------------------------------------------------------------------------------- /components/party/party-card.vue: -------------------------------------------------------------------------------- 1 | 63 | 64 | 124 | 125 | 126 | -------------------------------------------------------------------------------- /components/party/party-promise.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 72 | -------------------------------------------------------------------------------- /components/promise-card/box-container.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 14 | -------------------------------------------------------------------------------- /components/promise-card/expanded-card.vue: -------------------------------------------------------------------------------- 1 | 48 | 49 | 72 | -------------------------------------------------------------------------------- /components/promise-card/icon-up.vue: -------------------------------------------------------------------------------- 1 | 68 | 69 | 75 | -------------------------------------------------------------------------------- /components/promise-card/link.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 34 | -------------------------------------------------------------------------------- /components/promise-card/ncpo.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 22 | -------------------------------------------------------------------------------- /components/promise-card/promise-card.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 43 | -------------------------------------------------------------------------------- /components/promise-card/timeline-arrow.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 27 | -------------------------------------------------------------------------------- /components/promise-card/timeline.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 61 | -------------------------------------------------------------------------------- /components/scroll-icon.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 28 | 29 | 46 | -------------------------------------------------------------------------------- /components/status-animation.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 12 | 13 | 42 | -------------------------------------------------------------------------------- /components/toggle/toggle-item.vue: -------------------------------------------------------------------------------- 1 | 45 | 46 | 69 | -------------------------------------------------------------------------------- /components/toggle/toggle-list.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 52 | -------------------------------------------------------------------------------- /data/metadata.json: -------------------------------------------------------------------------------- 1 | { "lastUpdated": "2022-11-01T23:13:16.103Z" } 2 | -------------------------------------------------------------------------------- /data/parties.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "ครูไทยเพื่อประชาชน", 4 | "side": "government" 5 | }, 6 | { 7 | "name": "ชาติไทยพัฒนา", 8 | "side": "government" 9 | }, 10 | { 11 | "name": "ชาติพัฒนา", 12 | "side": "government" 13 | }, 14 | { 15 | "name": "ไทรักธรรม", 16 | "side": "government" 17 | }, 18 | { 19 | "name": "ประชาชนปฏิรูป", 20 | "side": "government" 21 | }, 22 | { 23 | "name": "ประชาธรรมไทย", 24 | "side": "government" 25 | }, 26 | { 27 | "name": "ประชาธิปไตยใหม่", 28 | "side": "government" 29 | }, 30 | { 31 | "name": "ประชาธิปัตย์", 32 | "side": "government" 33 | }, 34 | { 35 | "name": "ประชานิยม", 36 | "side": "government" 37 | }, 38 | { 39 | "name": "ประชาภิวัฒน์", 40 | "side": "government" 41 | }, 42 | { 43 | "name": "พลเมืองไทย", 44 | "side": "government" 45 | }, 46 | { 47 | "name": "พลังชาติไทย", 48 | "side": "government" 49 | }, 50 | { 51 | "name": "พลังท้องถิ่นไท", 52 | "side": "government" 53 | }, 54 | { 55 | "name": "พลังไทยรักไทย", 56 | "side": "government" 57 | }, 58 | { 59 | "name": "พลังธรรมใหม่", 60 | "side": "government" 61 | }, 62 | { 63 | "name": "พลังประชารัฐ", 64 | "side": "government" 65 | }, 66 | { 67 | "name": "ภูมิใจไทย", 68 | "side": "government" 69 | }, 70 | { 71 | "name": "รวมพลังประชาชาติไทย", 72 | "side": "government" 73 | }, 74 | { 75 | "name": "รักษ์ผืนป่าประเทศไทย", 76 | "side": "government" 77 | }, 78 | { 79 | "name": "เศรษฐกิจใหม่", 80 | "side": "government" 81 | }, 82 | { 83 | "name": "ก้าวไกล/อนาคตใหม่", 84 | "side": "opposition" 85 | }, 86 | { 87 | "name": "ไทยศรีวิไลย์", 88 | "side": "opposition" 89 | }, 90 | { 91 | "name": "ประชาชาติ", 92 | "side": "opposition" 93 | }, 94 | { 95 | "name": "พลังปวงชนไทย", 96 | "side": "opposition" 97 | }, 98 | { 99 | "name": "เพื่อชาติ", 100 | "side": "opposition" 101 | }, 102 | { 103 | "name": "เพื่อไทย", 104 | "side": "opposition" 105 | }, 106 | { 107 | "name": "เสรีรวมไทย", 108 | "side": "opposition" 109 | } 110 | ] 111 | -------------------------------------------------------------------------------- /data/promises-example.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": 1, 4 | "party": "พลังประชารัฐ", 5 | "title": "ต่อยอดบัตรสวัสดิการแห่งรัฐ", 6 | "category": "equality", 7 | "status": "done", 8 | "description": "เป็นโครงการที่ต่อยอดจากที่ทำไว้หนึ่งในโครงการสำคัญของรัฐบาลพลเอกประยุทธ์ จันทร์โอชา คือ โครงการ National e-payment มีโครงการย่อย “โครงการลงทะเบียนเพื่อสวัสดิการแห่งรัฐ” เพื่อฐานข้อมูลสำหรับการจัดสวัสดิการของหน่วยงานภาครัฐให้กับผู้ที่มีรายได้น้อยที่ต้องการความช่วยเหลือจากภาครัฐ ให้เงินคนทำงานรายได้ต่ำกว่า 30,000 บาทต่อปี (เส้นยากจน)\nเนื่องจากมองว่าที่ผ่านมาการจัดสวัสดิการสังคมและการให้เงินช่วยเหลือของภาครัฐยังมีข้อจำกัด โดยการจัดสรรเงินไม่ตรงกลุ่มเป้าหมาย เพราะข้อมูลยังกระจัดกระจาย และไม่พบข้อมูลเชิงลึก ดังนั้นโครงการขึ้นมาเพื่อเป็นฐานข้อมูลและให้ความช่วยเหลือได้\nนโยบายนี้เป็นหนึ่งในมาตรการช่วยเหลือในมาตรการสวัสดิการแห่งรัฐ ซึ่งอยู่ภายใต้โครงการ National e-Payment ของรัฐบาล โดยจะมีการลงทะเบียนเพื่อรับบัตรสวัสดิการแห่งรัฐ หรือบัตรคนจน โดย 1 ในคุณสมบัติของผู้ถือบัตร คือจะต้องมีรายได้ต่ำกว่า 100,000 บาท/ปี และแบ่งย่อยออกเป็น ผู้มีสิทธิที่มีรายได้ไม่เกินกว่า 30,000 บาท/ปี จะได้รับวงเงินค่าซื้อสินค้าอุปโภคบริโภคที่จำเป็น 300 บาท/คน/เดือน ซึ่งที่ผ่านมาทางรัฐบาลมีการโอนเงินผ่านบัตรนี้แล้ว รวมถึงมีเงินเยียวยาผู้ได้รับผลกระทบจากโควิด-19เพิ่มเติมอีก", 9 | "isNCPO": false, 10 | "imageUrl": "http://path.local/พลังประชารัฐ_7.jpg", 11 | "links": [ 12 | { 13 | "name": "ตรวจสอบสิทธิ์สวัสดิการแห่งรัฐ", 14 | "url": "https://govwelfare.cgd.go.th/welfare/check" 15 | }, 16 | { 17 | "name": "บทความข้อมูลเกี่ยวกับบัตรสวัสดิการแห่งรัฐ", 18 | "url": "https://spm.thaigov.go.th/FILEROOM/spm-thaigov/DRAWER004/GENERAL/DATA0000/00000438.PDF" 19 | }, 20 | { 21 | "name": "มาตรการช่วยเหลือผู้มีรายได้น้อย ในรัฐบาลประยุทธ์ - กรุงเทพธุรกิจ", 22 | "url": "https://www.bangkokbiznews.com/news/887110" 23 | } 24 | ], 25 | "timelines": [ 26 | { 27 | "label": "เปิดโอกาสให้ผู้มีรายได้น้อยลงทะเบียน", 28 | "from": "2016-04-03", 29 | "to": "2016-05-15" 30 | }, 31 | { "label": "เริ่มแจกบัตรสวัสดิการแห่งรัฐ", "from": "2017-09-21" } 32 | ] 33 | } 34 | ] 35 | -------------------------------------------------------------------------------- /jest-setup.ts: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom'; 2 | import { config } from '@vue/test-utils'; 3 | 4 | config.mocks.$config = { 5 | path: { 6 | base: '', 7 | images: '/images', 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | moduleNameMapper: { 3 | '^@/(.*)$': '/$1', 4 | '^~/(.*)$': '/$1', 5 | '^vue$': 'vue/dist/vue.common.js', 6 | }, 7 | moduleFileExtensions: ['ts', 'js', 'vue', 'json'], 8 | transform: { 9 | '^.+\\.ts$': 'ts-jest', 10 | '^.+\\.js$': 'babel-jest', 11 | '.*\\.(vue)$': 'vue-jest', 12 | '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': 13 | '/tests/fileTransformer.js', 14 | }, 15 | transformIgnorePatterns: ['/node_modules/(?!(@wevisdemo)/)'], 16 | collectCoverage: true, 17 | collectCoverageFrom: [ 18 | '/components/**/*.vue', 19 | '/pages/**/*.vue', 20 | '/scripts/**/*.ts', 21 | '!/scripts/data-fetcher/index.ts', 22 | ], 23 | testEnvironment: 'jsdom', 24 | setupFilesAfterEnv: ['/jest-setup.ts'], 25 | }; 26 | -------------------------------------------------------------------------------- /layouts/default.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 42 | -------------------------------------------------------------------------------- /models/filter.ts: -------------------------------------------------------------------------------- 1 | import { PromiseStatus, PromiseTopic } from './promise'; 2 | 3 | export enum FilterType { 4 | Party = 'party', 5 | Status = 'status', 6 | Keyword = 'keyword', 7 | Topic = 'topic', 8 | } 9 | 10 | export type Filter = 11 | | { 12 | type: FilterType.Party | FilterType.Keyword; 13 | value: string; 14 | } 15 | | { 16 | type: FilterType.Status; 17 | value: PromiseStatus; 18 | } 19 | | { 20 | type: FilterType.Topic; 21 | value: PromiseTopic; 22 | }; 23 | -------------------------------------------------------------------------------- /models/party.ts: -------------------------------------------------------------------------------- 1 | export enum PartySide { 2 | Government = 'government', 3 | Opposition = 'opposition', 4 | } 5 | 6 | export interface Party { 7 | name: string; 8 | side: PartySide; 9 | } 10 | -------------------------------------------------------------------------------- /models/promise.ts: -------------------------------------------------------------------------------- 1 | export enum PromiseTopic { 2 | Equality = 'equality', 3 | Security = 'security', 4 | Foreign = 'foreign', 5 | Administration = 'administration', 6 | Culture = 'culture', 7 | Economics = 'economics', 8 | Environmental = 'environmental', 9 | } 10 | 11 | export enum PromiseStatus { 12 | NoData = 'nodata', 13 | Proposed = 'proposed', 14 | Paused = 'paused', 15 | Working = 'working', 16 | Done = 'done', 17 | } 18 | 19 | export interface PromiseLink { 20 | name: string; 21 | url: string; 22 | } 23 | 24 | export interface PromiseTimeline { 25 | label: string; 26 | from: string; 27 | to?: string; 28 | } 29 | 30 | export interface TrackingPromise { 31 | id: number; 32 | party: string; 33 | title: string; 34 | topic: PromiseTopic; 35 | status: PromiseStatus; 36 | description: string; 37 | isNCPO: boolean; 38 | imageUrl?: string; 39 | links: PromiseLink[]; 40 | timelines: PromiseTimeline[]; 41 | } 42 | 43 | export const promiseStatusOrder: PromiseStatus[] = [ 44 | PromiseStatus.NoData, 45 | PromiseStatus.Proposed, 46 | PromiseStatus.Paused, 47 | PromiseStatus.Working, 48 | PromiseStatus.Done, 49 | ]; 50 | 51 | export const promiseTopicOrder: PromiseTopic[] = [ 52 | PromiseTopic.Equality, 53 | PromiseTopic.Security, 54 | PromiseTopic.Foreign, 55 | PromiseTopic.Administration, 56 | PromiseTopic.Culture, 57 | PromiseTopic.Economics, 58 | PromiseTopic.Environmental, 59 | ]; 60 | 61 | export const promiseTopicTextMap = new Map< 62 | PromiseTopic, 63 | { long: string; short: string } 64 | >([ 65 | [ 66 | PromiseTopic.Equality, 67 | { long: 'ความเท่าเทียม/คุณภาพชีวิต', short: 'คุณภาพชีวิต' }, 68 | ], 69 | [ 70 | PromiseTopic.Security, 71 | { long: 'ความมั่นคง/ปกป้องสถาบันกษัตริย์', short: 'ความมั่นคง' }, 72 | ], 73 | [PromiseTopic.Foreign, { long: 'ต่างประเทศ', short: 'ต่างประเทศ' }], 74 | [ 75 | PromiseTopic.Administration, 76 | { long: 'บริหารจัดการ(ราชการ)', short: 'บริหารจัดการ' }, 77 | ], 78 | [ 79 | PromiseTopic.Culture, 80 | { long: 'ศาสนาและวัฒนธรรม', short: 'ศาสนาและวัฒนธรรม' }, 81 | ], 82 | [PromiseTopic.Economics, { long: 'เศรษฐกิจ', short: 'เศรษฐกิจ' }], 83 | [PromiseTopic.Environmental, { long: 'สิ่งแวดล้อม', short: 'สิ่งแวดล้อม' }], 84 | ]); 85 | 86 | export const promiseStatusTextMap = new Map([ 87 | [PromiseStatus.NoData, 'ไม่พบความเคลื่อนไหว'], 88 | [PromiseStatus.Proposed, 'ถูกเสนอต่อสภา'], 89 | [PromiseStatus.Paused, 'ถูกระงับ'], 90 | [PromiseStatus.Working, 'กำลังดำเนินการ'], 91 | [PromiseStatus.Done, 'สำเร็จ'], 92 | ]); 93 | -------------------------------------------------------------------------------- /nuxt.config.js: -------------------------------------------------------------------------------- 1 | import promises from './data/promises.json'; 2 | import { createMetadata } from './utils/metadata'; 3 | 4 | const BASE_PATH = process.env.BASE_PATH || ''; 5 | const { title, meta } = createMetadata(); 6 | 7 | export default { 8 | target: 'static', 9 | 10 | head: { 11 | title, 12 | htmlAttrs: { 13 | lang: 'th', 14 | }, 15 | meta: [ 16 | { charset: 'utf-8' }, 17 | { name: 'viewport', content: 'width=device-width, initial-scale=1' }, 18 | { hid: 'description', name: 'description', content: '' }, 19 | { name: 'format-detection', content: 'telephone=no' }, 20 | ...meta, 21 | ], 22 | link: [ 23 | { rel: 'icon', type: 'image/x-icon', href: `${BASE_PATH}/favicon.ico` }, 24 | { 25 | rel: 'manifest', 26 | href: `${BASE_PATH}/site.webmanifest`, 27 | }, 28 | { rel: 'stylesheet', href: `${BASE_PATH}/fonts/typography.css` }, 29 | ], 30 | }, 31 | 32 | css: [ 33 | '@wevisdemo/ui/styles/typography.css', 34 | '@wevisdemo/ui/styles/components.css', 35 | ], 36 | 37 | plugins: [], 38 | 39 | components: true, 40 | 41 | buildModules: ['@nuxt/typescript-build', '@nuxtjs/tailwindcss'], 42 | 43 | modules: ['vue-plausible'], 44 | 45 | build: {}, 46 | 47 | publicRuntimeConfig: { 48 | path: { 49 | base: BASE_PATH, 50 | images: `${BASE_PATH}/images`, 51 | }, 52 | }, 53 | 54 | router: { 55 | base: BASE_PATH || '/', 56 | }, 57 | 58 | generate: { 59 | async routes() { 60 | const promisesRoute = await Promise.all( 61 | promises.map((promise) => ({ 62 | route: `/promises/${promise.id}`, 63 | payload: { 64 | promise, 65 | }, 66 | })) 67 | ); 68 | return [...promisesRoute]; 69 | }, 70 | }, 71 | plausible: process.env.ENABLE_PLAUSIBLE 72 | ? { 73 | domain: 'promisetracker.wevis.info', 74 | apiHost: 'https://analytics.punchup.world', 75 | } 76 | : {}, 77 | }; 78 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "promise-tracker", 3 | "version": "1.0.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "nuxt", 7 | "build": "nuxt build && nuxt generate", 8 | "lint:js": "eslint --ext \".js,.ts,.vue\" --ignore-path .gitignore .", 9 | "lint:prettier": "prettier --write .", 10 | "lint": "yarn lint:js && yarn lint:prettier", 11 | "lintfix": "prettier --write --list-different . && yarn lint:js --fix", 12 | "prepare": "husky install", 13 | "test": "jest --passWithNoTests", 14 | "test:watch": "jest --watch", 15 | "lint-staged": "lint-staged", 16 | "fetch-data": "ts-node -r dotenv/config scripts/data-fetcher" 17 | }, 18 | "lint-staged": { 19 | "*.{js,ts,vue}": "eslint --cache", 20 | "*.**": "prettier --write --ignore-unknown" 21 | }, 22 | "dependencies": { 23 | "@wevisdemo/ui": "^4.0.0", 24 | "core-js": "^3.19.3", 25 | "nuxt": "^2.15.8", 26 | "vue": "^2.6.14", 27 | "vue-plausible": "^1.3.1", 28 | "vue-server-renderer": "^2.6.14", 29 | "vue-template-compiler": "^2.6.14", 30 | "webpack": "^4.46.0" 31 | }, 32 | "devDependencies": { 33 | "@babel/eslint-parser": "^7.16.3", 34 | "@nuxt/types": "^2.15.8", 35 | "@nuxt/typescript-build": "^2.1.0", 36 | "@nuxtjs/eslint-config-typescript": "^8.0.0", 37 | "@nuxtjs/eslint-module": "^3.0.2", 38 | "@nuxtjs/tailwindcss": "^4.2.1", 39 | "@testing-library/jest-dom": "^5.16.2", 40 | "@types/jest": "^27.4.1", 41 | "@types/node-fetch": "^2.6.1", 42 | "@types/papaparse": "^5.3.2", 43 | "@vue/test-utils": "^1.3.0", 44 | "babel-core": "7.0.0-bridge.0", 45 | "babel-jest": "^27.4.4", 46 | "eslint": "^8.10.0", 47 | "eslint-config-prettier": "^8.3.0", 48 | "eslint-plugin-nuxt": "^3.1.0", 49 | "eslint-plugin-vue": "^8.5.0", 50 | "husky": "^7.0.4", 51 | "jest": "^27.4.4", 52 | "lint-staged": "^12.1.2", 53 | "node-fetch": "^2.6.7", 54 | "postcss": "^8.4.7", 55 | "prettier": "^2.5.1", 56 | "ts-jest": "^27.1.1", 57 | "ts-node": "^10.6.0", 58 | "tsconfig-paths": "^3.13.0", 59 | "vue-jest": "^3.0.4" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /pages/index.vue: -------------------------------------------------------------------------------- 1 | 60 | 61 | 82 | 83 | 89 | -------------------------------------------------------------------------------- /pages/promises/_id.vue: -------------------------------------------------------------------------------- 1 | 48 | 49 | 98 | 99 | 104 | -------------------------------------------------------------------------------- /scripts/data-fetcher/extracts/helpers.ts: -------------------------------------------------------------------------------- 1 | import fetch from 'node-fetch'; 2 | 3 | export const LIMIT = 1000; 4 | 5 | interface PageInfo { 6 | totalRows: number; 7 | page: number; 8 | pageSize: number; 9 | isFirstPage: boolean; 10 | isLastPage: boolean; 11 | } 12 | 13 | type Row = { [key: string]: any }; 14 | 15 | interface PublicViewResponse { 16 | list: Row[]; 17 | pageInfo: PageInfo; 18 | } 19 | 20 | export async function fetchNocoDB(resourcePath: string): Promise { 21 | const apiPath = process.env.NOCODB_API_PATH; 22 | const apiToken = process.env.NOCODB_API_TOKEN || ''; 23 | 24 | let currentPageInfo: PageInfo | null = null; 25 | const rows: Row[] = []; 26 | 27 | do { 28 | const res = await fetch( 29 | `${apiPath}${resourcePath}?limit=${LIMIT}&offset=${ 30 | currentPageInfo ? currentPageInfo.page * currentPageInfo.pageSize : 0 31 | }`, 32 | { 33 | headers: { 34 | 'xc-token': apiToken, 35 | }, 36 | } 37 | ); 38 | 39 | const { list, pageInfo } = (await res.json()) as PublicViewResponse; 40 | 41 | rows.push(...list); 42 | currentPageInfo = pageInfo; 43 | } while (!currentPageInfo?.isLastPage); 44 | 45 | return rows; 46 | } 47 | -------------------------------------------------------------------------------- /scripts/data-fetcher/extracts/party.ts: -------------------------------------------------------------------------------- 1 | import { fetchNocoDB } from './helpers'; 2 | 3 | export interface RawParty { 4 | name: string; 5 | side: string; 6 | } 7 | 8 | export async function getRawParties(): Promise { 9 | return (await fetchNocoDB('/parties')) as RawParty[]; 10 | } 11 | -------------------------------------------------------------------------------- /scripts/data-fetcher/extracts/promise.ts: -------------------------------------------------------------------------------- 1 | import { fetchNocoDB } from './helpers'; 2 | 3 | export interface RawLink { 4 | name: string; 5 | url: string; 6 | } 7 | 8 | export interface RawImage { 9 | url?: string; 10 | path?: string; 11 | title: string; 12 | mimetype: string; 13 | size: number; 14 | } 15 | 16 | export interface RawPromise { 17 | promiseId: number; 18 | party: string; 19 | topic: string; 20 | promiseTitle: string; 21 | status: string; 22 | explain: string; 23 | isNCPO: boolean; 24 | images: RawImage[]; 25 | vdo: string | null; 26 | links: RawLink[]; 27 | } 28 | 29 | const NAME_LINK_PREFIX = 'nameLink'; 30 | 31 | export async function getRawPromises(): Promise { 32 | const parsed = await fetchNocoDB('/promises'); 33 | 34 | const mapped = parsed.map((e): RawPromise => { 35 | const linkKeys = Object.keys(e).filter( 36 | (key: string) => key.startsWith(NAME_LINK_PREFIX) && e[key] !== '' 37 | ); 38 | const links: RawLink[] = linkKeys.map(createRawLink(e)); 39 | 40 | return { 41 | promiseId: Number(e.id), 42 | party: e.party, 43 | topic: e.topic, 44 | promiseTitle: e.promiseTitle, 45 | status: e.status, 46 | explain: e.explain, 47 | isNCPO: e.isNCPO, 48 | images: e.images, 49 | vdo: guardEmptiness(e.vdo), 50 | links, 51 | }; 52 | }); 53 | 54 | return mapped; 55 | } 56 | 57 | function createRawLink(data: { 58 | [key: string]: string; 59 | }): (key: string) => RawLink { 60 | return (key: string): RawLink => ({ 61 | name: data[key], 62 | url: data[`urlLink${key.replace(NAME_LINK_PREFIX, '')}`], 63 | }); 64 | } 65 | 66 | export function guardEmptiness(value: string): string | null { 67 | if (value === '-' || value === '') return null; 68 | return value; 69 | } 70 | -------------------------------------------------------------------------------- /scripts/data-fetcher/extracts/timeline.ts: -------------------------------------------------------------------------------- 1 | import { fetchNocoDB } from './helpers'; 2 | 3 | export interface RawTimeline { 4 | name: string; 5 | range: string; 6 | } 7 | 8 | export interface RawPromiseTimeline { 9 | promiseId: number; 10 | timelines: RawTimeline[]; 11 | } 12 | 13 | const NAME_PREFIX = 'name'; 14 | 15 | export async function getRawPromiseTimelines(): Promise { 16 | const parsed = await fetchNocoDB('/timelines'); 17 | 18 | const mapped = parsed.map((e): RawPromiseTimeline => { 19 | const linkKeys = Object.keys(e).filter( 20 | (key: string) => key.startsWith(NAME_PREFIX) && e[key] !== '' 21 | ); 22 | const timelines: RawTimeline[] = linkKeys.map(createRawTimeline(e)); 23 | 24 | return { 25 | promiseId: Number(e.promiseId), 26 | timelines, 27 | }; 28 | }); 29 | 30 | return mapped; 31 | } 32 | 33 | function createRawTimeline(data: { 34 | [key: string]: string; 35 | }): (key: string) => RawTimeline { 36 | return (key: string): RawTimeline => ({ 37 | name: data[key], 38 | range: data[`timeline${key.replace(NAME_PREFIX, '')}`], 39 | }); 40 | } 41 | -------------------------------------------------------------------------------- /scripts/data-fetcher/index.ts: -------------------------------------------------------------------------------- 1 | import { writeFile } from 'fs/promises'; 2 | import { getRawPromises } from './extracts/promise'; 3 | import { getRawPromiseTimelines } from './extracts/timeline'; 4 | import { getRawParties } from './extracts/party'; 5 | import { transformToTrackingPromises } from './transforms/promise'; 6 | import { transformToParties } from './transforms/party'; 7 | 8 | async function fetchData() { 9 | const promises = await transformToTrackingPromises( 10 | await getRawPromises(), 11 | await getRawPromiseTimelines() 12 | ); 13 | const parties = transformToParties(await getRawParties()); 14 | await writeFile('./data/promises.json', JSON.stringify(promises, null, 2)); 15 | await writeFile('./data/parties.json', JSON.stringify(parties, null, 2)); 16 | await writeFile( 17 | './data/metadata.json', 18 | JSON.stringify({ lastUpdated: new Date() }) 19 | ); 20 | } 21 | 22 | try { 23 | fetchData(); 24 | } catch (e) { 25 | console.error(e); 26 | process.exit(1); 27 | } 28 | -------------------------------------------------------------------------------- /scripts/data-fetcher/tests/extracts/helper.test.ts: -------------------------------------------------------------------------------- 1 | import fetch from 'node-fetch'; 2 | import { fetchNocoDB, LIMIT } from '../../extracts/helpers'; 3 | jest.mock('node-fetch', () => jest.fn()); 4 | 5 | const JSON = { list: [{ some: 'value' }], pageInfo: { isLastPage: true } }; 6 | 7 | describe('fetchNocoDB', () => { 8 | const MOCK_API_PATH = 'http://nocodb-instance/api'; 9 | const MOCK_TOKEN = 'mock_token'; 10 | let mockFetch: { 11 | json: jest.Mock; 12 | }; 13 | 14 | const originalEnv = { 15 | NOCODB_API_PATH: process.env.NOCODB_API_PATH, 16 | NOCODB_API_TOKEN: process.env.NOCODB_API_TOKEN, 17 | }; 18 | 19 | beforeAll(() => { 20 | process.env.NOCODB_API_PATH = MOCK_API_PATH; 21 | process.env.NOCODB_API_TOKEN = MOCK_TOKEN; 22 | }); 23 | 24 | afterAll(() => { 25 | process.env.NOCODB_API_PATH = originalEnv.NOCODB_API_PATH; 26 | process.env.NOCODB_API_TOKEN = originalEnv.NOCODB_API_TOKEN; 27 | }); 28 | 29 | beforeEach(() => { 30 | jest.clearAllMocks(); 31 | mockFetch = { 32 | json: jest.fn().mockResolvedValue(JSON), 33 | }; 34 | (fetch as unknown as any).mockResolvedValue(mockFetch); 35 | }); 36 | 37 | test('should fetch remote json from given resource', async () => { 38 | const RESOURCE = '/parties'; 39 | await fetchNocoDB(RESOURCE); 40 | expect(fetch).toBeCalledWith( 41 | `${MOCK_API_PATH}${RESOURCE}?limit=${LIMIT}&offset=0`, 42 | expect.anything() 43 | ); 44 | }); 45 | 46 | test('should add xc-token as header for auth purpose', async () => { 47 | await fetchNocoDB('/parties'); 48 | 49 | expect(fetch).toBeCalledWith(expect.anything(), { 50 | headers: { 51 | 'xc-token': MOCK_TOKEN, 52 | }, 53 | }); 54 | }); 55 | 56 | test('should return json response to caller', async () => { 57 | const actual = await fetchNocoDB('/parties'); 58 | 59 | expect(actual).toEqual(JSON.list); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /scripts/data-fetcher/tests/extracts/party.test.ts: -------------------------------------------------------------------------------- 1 | import { getRawParties, RawParty } from '../../extracts/party'; 2 | import { fetchNocoDB } from '../../extracts/helpers'; 3 | 4 | jest.mock('../../extracts/helpers'); 5 | 6 | describe('getRawParties', () => { 7 | beforeEach(() => { 8 | jest.resetAllMocks(); 9 | }); 10 | 11 | test('should fetch parties from NocoDB API', async () => { 12 | const RESOURCE_PATH = '/parties'; 13 | await getRawParties(); 14 | expect(fetchNocoDB).toBeCalledWith(RESOURCE_PATH); 15 | }); 16 | 17 | test('should extract name and side for raw party', async () => { 18 | mockResolvedFetchNocoDB([{ name: 'ก้าวไกล', side: 'opposition' }]); 19 | 20 | const parties = await getRawParties(); 21 | 22 | expect(parties[0]).toEqual({ 23 | name: 'ก้าวไกล', 24 | side: 'opposition', 25 | }); 26 | }); 27 | 28 | function mockResolvedFetchNocoDB(raw: RawParty[]): void { 29 | (fetchNocoDB as unknown as any).mockResolvedValue(raw); 30 | } 31 | }); 32 | -------------------------------------------------------------------------------- /scripts/data-fetcher/tests/extracts/timeline.test.ts: -------------------------------------------------------------------------------- 1 | import { getRawPromiseTimelines } from '../../extracts/timeline'; 2 | import { fetchNocoDB } from '../../extracts/helpers'; 3 | 4 | jest.mock('../../extracts/helpers'); 5 | describe('getRawPromiseTimelines', () => { 6 | beforeEach(() => { 7 | jest.resetAllMocks(); 8 | }); 9 | 10 | test('should fetch timelines from NocoDB API', async () => { 11 | const RESOURCE_PATH = '/timelines'; 12 | mockResolvedFetchNocoDB([]); 13 | 14 | await getRawPromiseTimelines(); 15 | 16 | expect(fetchNocoDB).toBeCalledWith(RESOURCE_PATH); 17 | }); 18 | 19 | test('should extract promiseId as number', async () => { 20 | mockResolvedFetchNocoDB([ 21 | { 22 | ...getStubJSONTimeline(), 23 | promiseId: 10, 24 | }, 25 | ]); 26 | 27 | const timelines = await getRawPromiseTimelines(); 28 | 29 | expect(timelines[0].promiseId).toBe(10); 30 | }); 31 | 32 | describe('extract timelines', () => { 33 | test('single timeline', async () => { 34 | mockResolvedFetchNocoDB([ 35 | { 36 | ...getStubJSONTimeline(), 37 | name1: 'name1', 38 | timeline1: 'range1', 39 | }, 40 | ]); 41 | 42 | const timelines = await getRawPromiseTimelines(); 43 | 44 | expect(timelines[0].timelines).toEqual([ 45 | { 46 | name: 'name1', 47 | range: 'range1', 48 | }, 49 | ]); 50 | }); 51 | 52 | test('2 timelines', async () => { 53 | mockResolvedFetchNocoDB([ 54 | { 55 | ...getStubJSONTimeline(), 56 | name1: 'name1', 57 | timeline1: 'range1', 58 | name2: 'name2', 59 | timeline2: 'range2', 60 | }, 61 | ]); 62 | 63 | const timelines = await getRawPromiseTimelines(); 64 | 65 | expect(timelines[0].timelines).toEqual([ 66 | { 67 | name: 'name1', 68 | range: 'range1', 69 | }, 70 | { 71 | name: 'name2', 72 | range: 'range2', 73 | }, 74 | ]); 75 | }); 76 | 77 | test('be able to handle dynamic number of timelines according to headers', async () => { 78 | mockResolvedFetchNocoDB([ 79 | { 80 | ...getStubJSONTimeline(), 81 | name1: 'name1', 82 | timeline1: 'range1', 83 | name2: 'name2', 84 | timeline2: 'range2', 85 | name3: 'name3', 86 | timeline3: 'range3', 87 | name4: 'name4', 88 | timeline4: 'range4', 89 | }, 90 | ]); 91 | 92 | const timelines = await getRawPromiseTimelines(); 93 | 94 | expect(timelines[0].timelines).toEqual([ 95 | { 96 | name: 'name1', 97 | range: 'range1', 98 | }, 99 | { 100 | name: 'name2', 101 | range: 'range2', 102 | }, 103 | { 104 | name: 'name3', 105 | range: 'range3', 106 | }, 107 | { 108 | name: 'name4', 109 | range: 'range4', 110 | }, 111 | ]); 112 | }); 113 | }); 114 | 115 | function getStubJSONTimeline() { 116 | return { 117 | promiseId: 0, 118 | }; 119 | } 120 | 121 | function mockResolvedFetchNocoDB( 122 | raw: { [key: string]: string | number }[] 123 | ): void { 124 | (fetchNocoDB as unknown as any).mockResolvedValue(raw); 125 | } 126 | }); 127 | -------------------------------------------------------------------------------- /scripts/data-fetcher/tests/transforms/party.test.ts: -------------------------------------------------------------------------------- 1 | import { transformToParties } from '../../transforms/party'; 2 | import { PartySide } from '~/models/party'; 3 | 4 | describe('transformToParties', () => { 5 | test('should map name directly', () => { 6 | const raw = [ 7 | { name: 'พลังประชารัฐ', side: 'government' }, 8 | { name: 'เพื่อไทย', side: 'opposition' }, 9 | ]; 10 | 11 | const parties = transformToParties(raw); 12 | 13 | expect(parties[0].name).toBe('พลังประชารัฐ'); 14 | expect(parties[1].name).toBe('เพื่อไทย'); 15 | }); 16 | 17 | describe('transform side', () => { 18 | test('should transform side to PartySide', () => { 19 | const raw = [ 20 | { side: 'government', name: 'พลังประชารัฐ' }, 21 | { side: 'opposition', name: 'เพื่อไทย' }, 22 | ]; 23 | 24 | const parties = transformToParties(raw); 25 | 26 | expect(parties[0].side).toBe(PartySide.Government); 27 | expect(parties[1].side).toBe(PartySide.Opposition); 28 | }); 29 | 30 | test('should throw error when cannot map to any side', () => { 31 | const CANNOT_BE_MAPPED_SIDE = 'center'; 32 | const PARTY_NAME = 'กลางใจเธอ'; 33 | const raw = [{ side: CANNOT_BE_MAPPED_SIDE, name: PARTY_NAME }]; 34 | 35 | expect(() => transformToParties(raw)).toThrowError( 36 | `Cannot find side to map "${CANNOT_BE_MAPPED_SIDE}" on party = "${PARTY_NAME}"` 37 | ); 38 | }); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /scripts/data-fetcher/transforms/party.ts: -------------------------------------------------------------------------------- 1 | import { RawParty } from '../extracts/party'; 2 | import { Party, PartySide } from '~/models/party'; 3 | 4 | export function transformToParties(rawParties: RawParty[]): Party[] { 5 | return rawParties.map((p) => { 6 | try { 7 | return { 8 | name: p.name, 9 | side: mapSide(p.side), 10 | }; 11 | } catch (e) { 12 | (e as Error).message += ` on party = "${p.name}"`; 13 | throw e; 14 | } 15 | }); 16 | } 17 | 18 | const sideMap = new Map([ 19 | ['government', PartySide.Government], 20 | ['opposition', PartySide.Opposition], 21 | ]); 22 | 23 | function mapSide(side: string): PartySide { 24 | const mapped = sideMap.get(side); 25 | if (mapped) { 26 | return mapped; 27 | } 28 | throw new Error(`Cannot find side to map "${side}"`); 29 | } 30 | -------------------------------------------------------------------------------- /scripts/data-fetcher/transforms/promise.ts: -------------------------------------------------------------------------------- 1 | import { RawPromise } from '../extracts/promise'; 2 | import { RawPromiseTimeline } from '../extracts/timeline'; 3 | import { 4 | PromiseTopic, 5 | PromiseStatus, 6 | TrackingPromise, 7 | PromiseTimeline, 8 | } from '~/models/promise'; 9 | 10 | export function transformToTrackingPromises( 11 | rawPromises: RawPromise[], 12 | rawTimelines: RawPromiseTimeline[] 13 | ): TrackingPromise[] { 14 | return rawPromises.map((r) => { 15 | try { 16 | const topic = mapTopic(r.topic); 17 | const status = mapStatus(r.status); 18 | 19 | const timelines = ( 20 | rawTimelines.find((tl) => tl.promiseId === r.promiseId)?.timelines || [] 21 | ) 22 | .filter(({ name, range }) => name && range) 23 | .map((tl): PromiseTimeline => { 24 | const { from, to } = convertRangeToFromTo(tl.range); 25 | return { 26 | label: tl.name, 27 | from, 28 | to, 29 | }; 30 | }); 31 | 32 | const [image] = r.images; 33 | 34 | const imageUrl = 35 | image?.url || 36 | (image?.path 37 | ? `${new URL(process.env.NOCODB_API_PATH as string).origin}/${ 38 | image?.path 39 | }` 40 | : undefined); 41 | 42 | return { 43 | id: r.promiseId, 44 | party: r.party, 45 | title: r.promiseTitle, 46 | topic, 47 | status, 48 | description: r.explain, 49 | isNCPO: r.isNCPO, 50 | imageUrl, 51 | links: r.links, 52 | timelines, 53 | }; 54 | } catch (e) { 55 | (e as Error).message += ` on promiseId = ${r.promiseId}`; 56 | throw e; 57 | } 58 | }); 59 | } 60 | 61 | function mapTopic(value: string): PromiseTopic { 62 | if (Object.values(PromiseTopic).includes(value as PromiseTopic)) { 63 | return value as PromiseTopic; 64 | } 65 | throw new Error(`Cannot find topic to map "${value}"`); 66 | } 67 | 68 | function mapStatus(value: string): PromiseStatus { 69 | if (Object.values(PromiseStatus).includes(value as PromiseStatus)) { 70 | return value as PromiseStatus; 71 | } 72 | throw new Error(`Cannot find status to map "${value}"`); 73 | } 74 | 75 | function convertRangeToFromTo(value: string): { 76 | from: string; 77 | to: string | undefined; 78 | } { 79 | const [from, to] = value.split('-').map((e) => e.trim()); 80 | if (from === '' || to === '') { 81 | throw new Error(`Incorrect timeline "${value}"`); 82 | } 83 | return { 84 | from: convertDateToISOFormat(from), 85 | to: to ? convertDateToISOFormat(to) : undefined, 86 | }; 87 | } 88 | 89 | function convertDateToISOFormat(date: string): string { 90 | const splited = date.split('/'); 91 | if (splited.length === 3) { 92 | const [day, month, year] = splited; 93 | return `${year}-${month}-${day}`; 94 | } else if (splited.length === 2) { 95 | const [month, year] = splited; 96 | return `${year}-${month}`; 97 | } 98 | throw new Error(`Incorrect timeline "${date}"`); 99 | } 100 | -------------------------------------------------------------------------------- /shims-vue.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.vue' { 2 | import Vue from 'vue'; 3 | export default Vue; 4 | } 5 | -------------------------------------------------------------------------------- /static/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wevisdemo/promise-tracker/5b91853702f02ab2c3dc98902af5a0aabc21d6ee/static/android-chrome-192x192.png -------------------------------------------------------------------------------- /static/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wevisdemo/promise-tracker/5b91853702f02ab2c3dc98902af5a0aabc21d6ee/static/android-chrome-512x512.png -------------------------------------------------------------------------------- /static/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wevisdemo/promise-tracker/5b91853702f02ab2c3dc98902af5a0aabc21d6ee/static/apple-touch-icon.png -------------------------------------------------------------------------------- /static/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wevisdemo/promise-tracker/5b91853702f02ab2c3dc98902af5a0aabc21d6ee/static/favicon-16x16.png -------------------------------------------------------------------------------- /static/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wevisdemo/promise-tracker/5b91853702f02ab2c3dc98902af5a0aabc21d6ee/static/favicon-32x32.png -------------------------------------------------------------------------------- /static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wevisdemo/promise-tracker/5b91853702f02ab2c3dc98902af5a0aabc21d6ee/static/favicon.ico -------------------------------------------------------------------------------- /static/fonts/Anuphan-Regular.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wevisdemo/promise-tracker/5b91853702f02ab2c3dc98902af5a0aabc21d6ee/static/fonts/Anuphan-Regular.otf -------------------------------------------------------------------------------- /static/fonts/Anuphan-SemiBold.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wevisdemo/promise-tracker/5b91853702f02ab2c3dc98902af5a0aabc21d6ee/static/fonts/Anuphan-SemiBold.otf -------------------------------------------------------------------------------- /static/fonts/BaiJamjuree-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wevisdemo/promise-tracker/5b91853702f02ab2c3dc98902af5a0aabc21d6ee/static/fonts/BaiJamjuree-Bold.ttf -------------------------------------------------------------------------------- /static/fonts/BaiJamjuree-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wevisdemo/promise-tracker/5b91853702f02ab2c3dc98902af5a0aabc21d6ee/static/fonts/BaiJamjuree-Regular.ttf -------------------------------------------------------------------------------- /static/fonts/KondolarThai-Black.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wevisdemo/promise-tracker/5b91853702f02ab2c3dc98902af5a0aabc21d6ee/static/fonts/KondolarThai-Black.woff -------------------------------------------------------------------------------- /static/fonts/KondolarThai-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wevisdemo/promise-tracker/5b91853702f02ab2c3dc98902af5a0aabc21d6ee/static/fonts/KondolarThai-Regular.woff -------------------------------------------------------------------------------- /static/fonts/typography.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Kondolar Thai'; 3 | src: url(KondolarThai-Regular.woff) format('woff'); 4 | } 5 | @font-face { 6 | font-family: 'Kondolar Thai'; 7 | font-weight: 900; 8 | src: url(KondolarThai-Black.woff) format('woff'); 9 | } 10 | @font-face { 11 | font-family: 'Anuphan'; 12 | src: url(Anuphan-Regular.otf) format('opentype'); 13 | } 14 | @font-face { 15 | font-family: 'Anuphan'; 16 | font-weight: 600; 17 | src: url(Anuphan-SemiBold.otf) format('opentype'); 18 | } 19 | @font-face { 20 | font-family: 'Bai Jamjuri'; 21 | src: url(BaiJamjuree-Regular.ttf) format('truetype'); 22 | } 23 | @font-face { 24 | font-family: 'Bai Jamjuri'; 25 | font-weight: 700; 26 | src: url(BaiJamjuree-Bold.ttf) format('truetype'); 27 | } 28 | -------------------------------------------------------------------------------- /static/images/article/article.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wevisdemo/promise-tracker/5b91853702f02ab2c3dc98902af5a0aabc21d6ee/static/images/article/article.png -------------------------------------------------------------------------------- /static/images/article/explore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wevisdemo/promise-tracker/5b91853702f02ab2c3dc98902af5a0aabc21d6ee/static/images/article/explore.png -------------------------------------------------------------------------------- /static/images/background/blog_bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wevisdemo/promise-tracker/5b91853702f02ab2c3dc98902af5a0aabc21d6ee/static/images/background/blog_bg.png -------------------------------------------------------------------------------- /static/images/background/landing_bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wevisdemo/promise-tracker/5b91853702f02ab2c3dc98902af5a0aabc21d6ee/static/images/background/landing_bg.png -------------------------------------------------------------------------------- /static/images/guide/censure-motion-01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wevisdemo/promise-tracker/5b91853702f02ab2c3dc98902af5a0aabc21d6ee/static/images/guide/censure-motion-01.png -------------------------------------------------------------------------------- /static/images/guide/censure-motion-02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wevisdemo/promise-tracker/5b91853702f02ab2c3dc98902af5a0aabc21d6ee/static/images/guide/censure-motion-02.png -------------------------------------------------------------------------------- /static/images/guide/censure-motion-03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wevisdemo/promise-tracker/5b91853702f02ab2c3dc98902af5a0aabc21d6ee/static/images/guide/censure-motion-03.png -------------------------------------------------------------------------------- /static/images/guide/long-collage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wevisdemo/promise-tracker/5b91853702f02ab2c3dc98902af5a0aabc21d6ee/static/images/guide/long-collage.png -------------------------------------------------------------------------------- /static/images/guide/prayut.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wevisdemo/promise-tracker/5b91853702f02ab2c3dc98902af5a0aabc21d6ee/static/images/guide/prayut.gif -------------------------------------------------------------------------------- /static/images/guide/section-01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wevisdemo/promise-tracker/5b91853702f02ab2c3dc98902af5a0aabc21d6ee/static/images/guide/section-01.png -------------------------------------------------------------------------------- /static/images/guide/section-03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wevisdemo/promise-tracker/5b91853702f02ab2c3dc98902af5a0aabc21d6ee/static/images/guide/section-03.png -------------------------------------------------------------------------------- /static/images/logo-addon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wevisdemo/promise-tracker/5b91853702f02ab2c3dc98902af5a0aabc21d6ee/static/images/logo-addon.png -------------------------------------------------------------------------------- /static/images/other-group.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wevisdemo/promise-tracker/5b91853702f02ab2c3dc98902af5a0aabc21d6ee/static/images/other-group.png -------------------------------------------------------------------------------- /static/images/party/dummy.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wevisdemo/promise-tracker/5b91853702f02ab2c3dc98902af5a0aabc21d6ee/static/images/party/dummy.jpg -------------------------------------------------------------------------------- /static/images/party/กรีน.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wevisdemo/promise-tracker/5b91853702f02ab2c3dc98902af5a0aabc21d6ee/static/images/party/กรีน.jpg -------------------------------------------------------------------------------- /static/images/party/กลาง.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wevisdemo/promise-tracker/5b91853702f02ab2c3dc98902af5a0aabc21d6ee/static/images/party/กลาง.jpg -------------------------------------------------------------------------------- /static/images/party/กล้า.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wevisdemo/promise-tracker/5b91853702f02ab2c3dc98902af5a0aabc21d6ee/static/images/party/กล้า.jpg -------------------------------------------------------------------------------- /static/images/party/กสิกรไทย.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wevisdemo/promise-tracker/5b91853702f02ab2c3dc98902af5a0aabc21d6ee/static/images/party/กสิกรไทย.jpg -------------------------------------------------------------------------------- /static/images/party/ก้าวไกล.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wevisdemo/promise-tracker/5b91853702f02ab2c3dc98902af5a0aabc21d6ee/static/images/party/ก้าวไกล.jpg -------------------------------------------------------------------------------- /static/images/party/คนงานไทย.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wevisdemo/promise-tracker/5b91853702f02ab2c3dc98902af5a0aabc21d6ee/static/images/party/คนงานไทย.jpg -------------------------------------------------------------------------------- /static/images/party/คนธรรมดาแห่งประเทศไทย.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wevisdemo/promise-tracker/5b91853702f02ab2c3dc98902af5a0aabc21d6ee/static/images/party/คนธรรมดาแห่งประเทศไทย.jpg -------------------------------------------------------------------------------- /static/images/party/ครูไทยเพื่อประชาชน.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wevisdemo/promise-tracker/5b91853702f02ab2c3dc98902af5a0aabc21d6ee/static/images/party/ครูไทยเพื่อประชาชน.jpg -------------------------------------------------------------------------------- /static/images/party/คลองไทย.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wevisdemo/promise-tracker/5b91853702f02ab2c3dc98902af5a0aabc21d6ee/static/images/party/คลองไทย.jpg -------------------------------------------------------------------------------- /static/images/party/ความหวังใหม่.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wevisdemo/promise-tracker/5b91853702f02ab2c3dc98902af5a0aabc21d6ee/static/images/party/ความหวังใหม่.jpg -------------------------------------------------------------------------------- /static/images/party/ชาติพัฒนา.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wevisdemo/promise-tracker/5b91853702f02ab2c3dc98902af5a0aabc21d6ee/static/images/party/ชาติพัฒนา.jpg -------------------------------------------------------------------------------- /static/images/party/ชาติพันธุ์ไทย.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wevisdemo/promise-tracker/5b91853702f02ab2c3dc98902af5a0aabc21d6ee/static/images/party/ชาติพันธุ์ไทย.jpg -------------------------------------------------------------------------------- /static/images/party/ชาติไทยพัฒนา.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wevisdemo/promise-tracker/5b91853702f02ab2c3dc98902af5a0aabc21d6ee/static/images/party/ชาติไทยพัฒนา.jpg -------------------------------------------------------------------------------- /static/images/party/ฐานรากไทย.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wevisdemo/promise-tracker/5b91853702f02ab2c3dc98902af5a0aabc21d6ee/static/images/party/ฐานรากไทย.jpg -------------------------------------------------------------------------------- /static/images/party/ถิ่นกาขาวชาววิไล.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wevisdemo/promise-tracker/5b91853702f02ab2c3dc98902af5a0aabc21d6ee/static/images/party/ถิ่นกาขาวชาววิไล.jpg -------------------------------------------------------------------------------- /static/images/party/ทางเลือกใหม่.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wevisdemo/promise-tracker/5b91853702f02ab2c3dc98902af5a0aabc21d6ee/static/images/party/ทางเลือกใหม่.jpg -------------------------------------------------------------------------------- /static/images/party/ประชากรไทย.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wevisdemo/promise-tracker/5b91853702f02ab2c3dc98902af5a0aabc21d6ee/static/images/party/ประชากรไทย.jpg -------------------------------------------------------------------------------- /static/images/party/ประชาชนปฏิรูป.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wevisdemo/promise-tracker/5b91853702f02ab2c3dc98902af5a0aabc21d6ee/static/images/party/ประชาชนปฏิรูป.jpg -------------------------------------------------------------------------------- /static/images/party/ประชาชาติ.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wevisdemo/promise-tracker/5b91853702f02ab2c3dc98902af5a0aabc21d6ee/static/images/party/ประชาชาติ.jpg -------------------------------------------------------------------------------- /static/images/party/ประชาธรรมไทย.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wevisdemo/promise-tracker/5b91853702f02ab2c3dc98902af5a0aabc21d6ee/static/images/party/ประชาธรรมไทย.jpg -------------------------------------------------------------------------------- /static/images/party/ประชาธิปัตย์.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wevisdemo/promise-tracker/5b91853702f02ab2c3dc98902af5a0aabc21d6ee/static/images/party/ประชาธิปัตย์.jpg -------------------------------------------------------------------------------- /static/images/party/ประชาธิปไตยเพื่อประชาชน.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wevisdemo/promise-tracker/5b91853702f02ab2c3dc98902af5a0aabc21d6ee/static/images/party/ประชาธิปไตยเพื่อประชาชน.jpg -------------------------------------------------------------------------------- /static/images/party/ประชาธิปไตยใหม่.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wevisdemo/promise-tracker/5b91853702f02ab2c3dc98902af5a0aabc21d6ee/static/images/party/ประชาธิปไตยใหม่.jpg -------------------------------------------------------------------------------- /static/images/party/ประชานิยม.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wevisdemo/promise-tracker/5b91853702f02ab2c3dc98902af5a0aabc21d6ee/static/images/party/ประชานิยม.jpg -------------------------------------------------------------------------------- /static/images/party/ประชาภิวัฒน์.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wevisdemo/promise-tracker/5b91853702f02ab2c3dc98902af5a0aabc21d6ee/static/images/party/ประชาภิวัฒน์.jpg -------------------------------------------------------------------------------- /static/images/party/ประชาไทย.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wevisdemo/promise-tracker/5b91853702f02ab2c3dc98902af5a0aabc21d6ee/static/images/party/ประชาไทย.jpg -------------------------------------------------------------------------------- /static/images/party/ผึ้งหลวง.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wevisdemo/promise-tracker/5b91853702f02ab2c3dc98902af5a0aabc21d6ee/static/images/party/ผึ้งหลวง.jpg -------------------------------------------------------------------------------- /static/images/party/พลังคนกีฬา.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wevisdemo/promise-tracker/5b91853702f02ab2c3dc98902af5a0aabc21d6ee/static/images/party/พลังคนกีฬา.jpg -------------------------------------------------------------------------------- /static/images/party/พลังครูไทย.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wevisdemo/promise-tracker/5b91853702f02ab2c3dc98902af5a0aabc21d6ee/static/images/party/พลังครูไทย.jpg -------------------------------------------------------------------------------- /static/images/party/พลังชล.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wevisdemo/promise-tracker/5b91853702f02ab2c3dc98902af5a0aabc21d6ee/static/images/party/พลังชล.jpg -------------------------------------------------------------------------------- /static/images/party/พลังชาติไทย.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wevisdemo/promise-tracker/5b91853702f02ab2c3dc98902af5a0aabc21d6ee/static/images/party/พลังชาติไทย.jpg -------------------------------------------------------------------------------- /static/images/party/พลังท้องถิ่นไท.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wevisdemo/promise-tracker/5b91853702f02ab2c3dc98902af5a0aabc21d6ee/static/images/party/พลังท้องถิ่นไท.jpg -------------------------------------------------------------------------------- /static/images/party/พลังธรรมใหม่.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wevisdemo/promise-tracker/5b91853702f02ab2c3dc98902af5a0aabc21d6ee/static/images/party/พลังธรรมใหม่.jpg -------------------------------------------------------------------------------- /static/images/party/พลังประชาธิปไตย.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wevisdemo/promise-tracker/5b91853702f02ab2c3dc98902af5a0aabc21d6ee/static/images/party/พลังประชาธิปไตย.jpg -------------------------------------------------------------------------------- /static/images/party/พลังประชารัฐ.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wevisdemo/promise-tracker/5b91853702f02ab2c3dc98902af5a0aabc21d6ee/static/images/party/พลังประชารัฐ.jpg -------------------------------------------------------------------------------- /static/images/party/พลังปวงชนไทย.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wevisdemo/promise-tracker/5b91853702f02ab2c3dc98902af5a0aabc21d6ee/static/images/party/พลังปวงชนไทย.jpg -------------------------------------------------------------------------------- /static/images/party/พลังรัก.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wevisdemo/promise-tracker/5b91853702f02ab2c3dc98902af5a0aabc21d6ee/static/images/party/พลังรัก.jpg -------------------------------------------------------------------------------- /static/images/party/พลังศรัทธา.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wevisdemo/promise-tracker/5b91853702f02ab2c3dc98902af5a0aabc21d6ee/static/images/party/พลังศรัทธา.jpg -------------------------------------------------------------------------------- /static/images/party/พลังสหกรณ์.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wevisdemo/promise-tracker/5b91853702f02ab2c3dc98902af5a0aabc21d6ee/static/images/party/พลังสหกรณ์.jpg -------------------------------------------------------------------------------- /static/images/party/พลังสังคม.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wevisdemo/promise-tracker/5b91853702f02ab2c3dc98902af5a0aabc21d6ee/static/images/party/พลังสังคม.jpg -------------------------------------------------------------------------------- /static/images/party/พลังแผ่นดินทอง.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wevisdemo/promise-tracker/5b91853702f02ab2c3dc98902af5a0aabc21d6ee/static/images/party/พลังแผ่นดินทอง.jpg -------------------------------------------------------------------------------- /static/images/party/พลังแรงงานไทย.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wevisdemo/promise-tracker/5b91853702f02ab2c3dc98902af5a0aabc21d6ee/static/images/party/พลังแรงงานไทย.jpg -------------------------------------------------------------------------------- /static/images/party/พลังไทยดี.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wevisdemo/promise-tracker/5b91853702f02ab2c3dc98902af5a0aabc21d6ee/static/images/party/พลังไทยดี.jpg -------------------------------------------------------------------------------- /static/images/party/พลังไทยรักชาติ.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wevisdemo/promise-tracker/5b91853702f02ab2c3dc98902af5a0aabc21d6ee/static/images/party/พลังไทยรักชาติ.jpg -------------------------------------------------------------------------------- /static/images/party/พลังไทยรักไทย.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wevisdemo/promise-tracker/5b91853702f02ab2c3dc98902af5a0aabc21d6ee/static/images/party/พลังไทยรักไทย.jpg -------------------------------------------------------------------------------- /static/images/party/พลังไทสร้างชาติ.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wevisdemo/promise-tracker/5b91853702f02ab2c3dc98902af5a0aabc21d6ee/static/images/party/พลังไทสร้างชาติ.jpg -------------------------------------------------------------------------------- /static/images/party/พลเมืองไทย.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wevisdemo/promise-tracker/5b91853702f02ab2c3dc98902af5a0aabc21d6ee/static/images/party/พลเมืองไทย.jpg -------------------------------------------------------------------------------- /static/images/party/พัฒนาประเทศไทย.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wevisdemo/promise-tracker/5b91853702f02ab2c3dc98902af5a0aabc21d6ee/static/images/party/พัฒนาประเทศไทย.jpg -------------------------------------------------------------------------------- /static/images/party/ภราดรภาพ.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wevisdemo/promise-tracker/5b91853702f02ab2c3dc98902af5a0aabc21d6ee/static/images/party/ภราดรภาพ.jpg -------------------------------------------------------------------------------- /static/images/party/ภาคีเครือข่ายไทย.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wevisdemo/promise-tracker/5b91853702f02ab2c3dc98902af5a0aabc21d6ee/static/images/party/ภาคีเครือข่ายไทย.jpg -------------------------------------------------------------------------------- /static/images/party/ภูมิพลังเกษตรกรไทย.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wevisdemo/promise-tracker/5b91853702f02ab2c3dc98902af5a0aabc21d6ee/static/images/party/ภูมิพลังเกษตรกรไทย.jpg -------------------------------------------------------------------------------- /static/images/party/ภูมิใจไทย.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wevisdemo/promise-tracker/5b91853702f02ab2c3dc98902af5a0aabc21d6ee/static/images/party/ภูมิใจไทย.jpg -------------------------------------------------------------------------------- /static/images/party/มติประชา.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wevisdemo/promise-tracker/5b91853702f02ab2c3dc98902af5a0aabc21d6ee/static/images/party/มติประชา.jpg -------------------------------------------------------------------------------- /static/images/party/มหาชน.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wevisdemo/promise-tracker/5b91853702f02ab2c3dc98902af5a0aabc21d6ee/static/images/party/มหาชน.jpg -------------------------------------------------------------------------------- /static/images/party/ยางพาราไทย.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wevisdemo/promise-tracker/5b91853702f02ab2c3dc98902af5a0aabc21d6ee/static/images/party/ยางพาราไทย.jpg -------------------------------------------------------------------------------- /static/images/party/รวมพลังประชาชาติไทย.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wevisdemo/promise-tracker/5b91853702f02ab2c3dc98902af5a0aabc21d6ee/static/images/party/รวมพลังประชาชาติไทย.jpg -------------------------------------------------------------------------------- /static/images/party/รวมใจไทย.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wevisdemo/promise-tracker/5b91853702f02ab2c3dc98902af5a0aabc21d6ee/static/images/party/รวมใจไทย.jpg -------------------------------------------------------------------------------- /static/images/party/รักท้องถิ่นไทย.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wevisdemo/promise-tracker/5b91853702f02ab2c3dc98902af5a0aabc21d6ee/static/images/party/รักท้องถิ่นไทย.jpg -------------------------------------------------------------------------------- /static/images/party/รักษ์ธรรม.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wevisdemo/promise-tracker/5b91853702f02ab2c3dc98902af5a0aabc21d6ee/static/images/party/รักษ์ธรรม.jpg -------------------------------------------------------------------------------- /static/images/party/รักษ์ผืนป่าประเทศไทย.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wevisdemo/promise-tracker/5b91853702f02ab2c3dc98902af5a0aabc21d6ee/static/images/party/รักษ์ผืนป่าประเทศไทย.jpg -------------------------------------------------------------------------------- /static/images/party/สยามพัฒนา.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wevisdemo/promise-tracker/5b91853702f02ab2c3dc98902af5a0aabc21d6ee/static/images/party/สยามพัฒนา.jpg -------------------------------------------------------------------------------- /static/images/party/สังคมประชาธิปไตยไทย.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wevisdemo/promise-tracker/5b91853702f02ab2c3dc98902af5a0aabc21d6ee/static/images/party/สังคมประชาธิปไตยไทย.jpg -------------------------------------------------------------------------------- /static/images/party/สามัญชน.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wevisdemo/promise-tracker/5b91853702f02ab2c3dc98902af5a0aabc21d6ee/static/images/party/สามัญชน.jpg -------------------------------------------------------------------------------- /static/images/party/อนาคตไทย.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wevisdemo/promise-tracker/5b91853702f02ab2c3dc98902af5a0aabc21d6ee/static/images/party/อนาคตไทย.jpg -------------------------------------------------------------------------------- /static/images/party/เครือข่ายชาวนาแห่งประเทศไทย.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wevisdemo/promise-tracker/5b91853702f02ab2c3dc98902af5a0aabc21d6ee/static/images/party/เครือข่ายชาวนาแห่งประเทศไทย.jpg -------------------------------------------------------------------------------- /static/images/party/เพื่อคนไทย.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wevisdemo/promise-tracker/5b91853702f02ab2c3dc98902af5a0aabc21d6ee/static/images/party/เพื่อคนไทย.jpg -------------------------------------------------------------------------------- /static/images/party/เพื่อชาติ.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wevisdemo/promise-tracker/5b91853702f02ab2c3dc98902af5a0aabc21d6ee/static/images/party/เพื่อชาติ.jpg -------------------------------------------------------------------------------- /static/images/party/เพื่อชีวิตใหม่.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wevisdemo/promise-tracker/5b91853702f02ab2c3dc98902af5a0aabc21d6ee/static/images/party/เพื่อชีวิตใหม่.jpg -------------------------------------------------------------------------------- /static/images/party/เพื่อธรรม.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wevisdemo/promise-tracker/5b91853702f02ab2c3dc98902af5a0aabc21d6ee/static/images/party/เพื่อธรรม.jpg -------------------------------------------------------------------------------- /static/images/party/เพื่อนไทย.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wevisdemo/promise-tracker/5b91853702f02ab2c3dc98902af5a0aabc21d6ee/static/images/party/เพื่อนไทย.jpg -------------------------------------------------------------------------------- /static/images/party/เพื่อสหกรณ์ไทย.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wevisdemo/promise-tracker/5b91853702f02ab2c3dc98902af5a0aabc21d6ee/static/images/party/เพื่อสหกรณ์ไทย.jpg -------------------------------------------------------------------------------- /static/images/party/เพื่อแผ่นดิน.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wevisdemo/promise-tracker/5b91853702f02ab2c3dc98902af5a0aabc21d6ee/static/images/party/เพื่อแผ่นดิน.jpg -------------------------------------------------------------------------------- /static/images/party/เพื่อไทย.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wevisdemo/promise-tracker/5b91853702f02ab2c3dc98902af5a0aabc21d6ee/static/images/party/เพื่อไทย.jpg -------------------------------------------------------------------------------- /static/images/party/เพื่อไทยพัฒนา.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wevisdemo/promise-tracker/5b91853702f02ab2c3dc98902af5a0aabc21d6ee/static/images/party/เพื่อไทยพัฒนา.jpg -------------------------------------------------------------------------------- /static/images/party/เศรษฐกิจใหม่.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wevisdemo/promise-tracker/5b91853702f02ab2c3dc98902af5a0aabc21d6ee/static/images/party/เศรษฐกิจใหม่.jpg -------------------------------------------------------------------------------- /static/images/party/เสรีรวมไทย.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wevisdemo/promise-tracker/5b91853702f02ab2c3dc98902af5a0aabc21d6ee/static/images/party/เสรีรวมไทย.jpg -------------------------------------------------------------------------------- /static/images/party/แทนคุณแผ่นดิน.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wevisdemo/promise-tracker/5b91853702f02ab2c3dc98902af5a0aabc21d6ee/static/images/party/แทนคุณแผ่นดิน.jpg -------------------------------------------------------------------------------- /static/images/party/แผ่นดินธรรม.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wevisdemo/promise-tracker/5b91853702f02ab2c3dc98902af5a0aabc21d6ee/static/images/party/แผ่นดินธรรม.jpg -------------------------------------------------------------------------------- /static/images/party/ไทยธรรม.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wevisdemo/promise-tracker/5b91853702f02ab2c3dc98902af5a0aabc21d6ee/static/images/party/ไทยธรรม.jpg -------------------------------------------------------------------------------- /static/images/party/ไทยรักษาชาติ.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wevisdemo/promise-tracker/5b91853702f02ab2c3dc98902af5a0aabc21d6ee/static/images/party/ไทยรักษาชาติ.jpg -------------------------------------------------------------------------------- /static/images/party/ไทยรุ่งเรือง.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wevisdemo/promise-tracker/5b91853702f02ab2c3dc98902af5a0aabc21d6ee/static/images/party/ไทยรุ่งเรือง.jpg -------------------------------------------------------------------------------- /static/images/party/ไทยศรีวิไลย์.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wevisdemo/promise-tracker/5b91853702f02ab2c3dc98902af5a0aabc21d6ee/static/images/party/ไทยศรีวิไลย์.jpg -------------------------------------------------------------------------------- /static/images/party/ไทรักธรรม.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wevisdemo/promise-tracker/5b91853702f02ab2c3dc98902af5a0aabc21d6ee/static/images/party/ไทรักธรรม.jpg -------------------------------------------------------------------------------- /static/images/status/default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wevisdemo/promise-tracker/5b91853702f02ab2c3dc98902af5a0aabc21d6ee/static/images/status/default.png -------------------------------------------------------------------------------- /static/images/status/done.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wevisdemo/promise-tracker/5b91853702f02ab2c3dc98902af5a0aabc21d6ee/static/images/status/done.png -------------------------------------------------------------------------------- /static/images/status/done_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wevisdemo/promise-tracker/5b91853702f02ab2c3dc98902af5a0aabc21d6ee/static/images/status/done_small.png -------------------------------------------------------------------------------- /static/images/status/nodata.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wevisdemo/promise-tracker/5b91853702f02ab2c3dc98902af5a0aabc21d6ee/static/images/status/nodata.png -------------------------------------------------------------------------------- /static/images/status/nodata_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wevisdemo/promise-tracker/5b91853702f02ab2c3dc98902af5a0aabc21d6ee/static/images/status/nodata_small.png -------------------------------------------------------------------------------- /static/images/status/notfound.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wevisdemo/promise-tracker/5b91853702f02ab2c3dc98902af5a0aabc21d6ee/static/images/status/notfound.png -------------------------------------------------------------------------------- /static/images/status/paused.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wevisdemo/promise-tracker/5b91853702f02ab2c3dc98902af5a0aabc21d6ee/static/images/status/paused.png -------------------------------------------------------------------------------- /static/images/status/paused_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wevisdemo/promise-tracker/5b91853702f02ab2c3dc98902af5a0aabc21d6ee/static/images/status/paused_small.png -------------------------------------------------------------------------------- /static/images/status/proposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wevisdemo/promise-tracker/5b91853702f02ab2c3dc98902af5a0aabc21d6ee/static/images/status/proposed.png -------------------------------------------------------------------------------- /static/images/status/proposed_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wevisdemo/promise-tracker/5b91853702f02ab2c3dc98902af5a0aabc21d6ee/static/images/status/proposed_small.png -------------------------------------------------------------------------------- /static/images/status/working.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wevisdemo/promise-tracker/5b91853702f02ab2c3dc98902af5a0aabc21d6ee/static/images/status/working.png -------------------------------------------------------------------------------- /static/images/status/working_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wevisdemo/promise-tracker/5b91853702f02ab2c3dc98902af5a0aabc21d6ee/static/images/status/working_small.png -------------------------------------------------------------------------------- /static/images/topic/administration.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wevisdemo/promise-tracker/5b91853702f02ab2c3dc98902af5a0aabc21d6ee/static/images/topic/administration.png -------------------------------------------------------------------------------- /static/images/topic/administration_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wevisdemo/promise-tracker/5b91853702f02ab2c3dc98902af5a0aabc21d6ee/static/images/topic/administration_small.png -------------------------------------------------------------------------------- /static/images/topic/culture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wevisdemo/promise-tracker/5b91853702f02ab2c3dc98902af5a0aabc21d6ee/static/images/topic/culture.png -------------------------------------------------------------------------------- /static/images/topic/culture_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wevisdemo/promise-tracker/5b91853702f02ab2c3dc98902af5a0aabc21d6ee/static/images/topic/culture_small.png -------------------------------------------------------------------------------- /static/images/topic/economics.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wevisdemo/promise-tracker/5b91853702f02ab2c3dc98902af5a0aabc21d6ee/static/images/topic/economics.png -------------------------------------------------------------------------------- /static/images/topic/economics_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wevisdemo/promise-tracker/5b91853702f02ab2c3dc98902af5a0aabc21d6ee/static/images/topic/economics_small.png -------------------------------------------------------------------------------- /static/images/topic/environmental.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wevisdemo/promise-tracker/5b91853702f02ab2c3dc98902af5a0aabc21d6ee/static/images/topic/environmental.png -------------------------------------------------------------------------------- /static/images/topic/environmental_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wevisdemo/promise-tracker/5b91853702f02ab2c3dc98902af5a0aabc21d6ee/static/images/topic/environmental_small.png -------------------------------------------------------------------------------- /static/images/topic/equality.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wevisdemo/promise-tracker/5b91853702f02ab2c3dc98902af5a0aabc21d6ee/static/images/topic/equality.png -------------------------------------------------------------------------------- /static/images/topic/equality_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wevisdemo/promise-tracker/5b91853702f02ab2c3dc98902af5a0aabc21d6ee/static/images/topic/equality_small.png -------------------------------------------------------------------------------- /static/images/topic/foreign.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wevisdemo/promise-tracker/5b91853702f02ab2c3dc98902af5a0aabc21d6ee/static/images/topic/foreign.png -------------------------------------------------------------------------------- /static/images/topic/foreign_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wevisdemo/promise-tracker/5b91853702f02ab2c3dc98902af5a0aabc21d6ee/static/images/topic/foreign_small.png -------------------------------------------------------------------------------- /static/images/topic/proposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wevisdemo/promise-tracker/5b91853702f02ab2c3dc98902af5a0aabc21d6ee/static/images/topic/proposed.png -------------------------------------------------------------------------------- /static/images/topic/security.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wevisdemo/promise-tracker/5b91853702f02ab2c3dc98902af5a0aabc21d6ee/static/images/topic/security.png -------------------------------------------------------------------------------- /static/images/topic/security_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wevisdemo/promise-tracker/5b91853702f02ab2c3dc98902af5a0aabc21d6ee/static/images/topic/security_small.png -------------------------------------------------------------------------------- /static/og/article.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wevisdemo/promise-tracker/5b91853702f02ab2c3dc98902af5a0aabc21d6ee/static/og/article.jpg -------------------------------------------------------------------------------- /static/og/default.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wevisdemo/promise-tracker/5b91853702f02ab2c3dc98902af5a0aabc21d6ee/static/og/default.jpg -------------------------------------------------------------------------------- /static/og/done.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wevisdemo/promise-tracker/5b91853702f02ab2c3dc98902af5a0aabc21d6ee/static/og/done.jpg -------------------------------------------------------------------------------- /static/og/nodata.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wevisdemo/promise-tracker/5b91853702f02ab2c3dc98902af5a0aabc21d6ee/static/og/nodata.jpg -------------------------------------------------------------------------------- /static/og/paused.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wevisdemo/promise-tracker/5b91853702f02ab2c3dc98902af5a0aabc21d6ee/static/og/paused.jpg -------------------------------------------------------------------------------- /static/og/proposed.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wevisdemo/promise-tracker/5b91853702f02ab2c3dc98902af5a0aabc21d6ee/static/og/proposed.jpg -------------------------------------------------------------------------------- /static/og/working.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wevisdemo/promise-tracker/5b91853702f02ab2c3dc98902af5a0aabc21d6ee/static/og/working.jpg -------------------------------------------------------------------------------- /static/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Promise Tracker", 3 | "short_name": "Promise Tracker", 4 | "icons": [ 5 | { 6 | "src": "/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/android-chrome-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "theme_color": "#ffffff", 17 | "background_color": "#ffffff", 18 | "display": "standalone" 19 | } 20 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | purge: { 3 | content: ['./tailwind.safelist.txt'], 4 | }, 5 | darkMode: false, // or 'media' or 'class' 6 | theme: { 7 | extend: {}, 8 | colors: { 9 | transparent: 'transparent', 10 | black: '#000000', 11 | white: '#FFFFFF', 12 | gray: '#BBBBBB', 13 | ultramarine: '#3904E9', 14 | status: { 15 | nodata: '#8F8F8F', 16 | proposed: '#FD9154', 17 | paused: '#E91E63', 18 | working: '#F4C51F', 19 | done: '#48DBDB', 20 | }, 21 | }, 22 | fontFamily: {}, 23 | fontSize: {}, 24 | fontWeight: {}, 25 | lineHeight: {}, 26 | letterSpacing: {}, 27 | }, 28 | variants: { 29 | extend: {}, 30 | }, 31 | plugins: [], 32 | }; 33 | -------------------------------------------------------------------------------- /tailwind.safelist.txt: -------------------------------------------------------------------------------- 1 | bg-status-nodata 2 | bg-status-proposed 3 | bg-status-paused 4 | bg-status-working 5 | bg-status-done -------------------------------------------------------------------------------- /tests/components/button.test.ts: -------------------------------------------------------------------------------- 1 | import { mount } from '@vue/test-utils'; 2 | import Button from '@/components/button.vue'; 3 | 4 | test('should render slot', () => { 5 | const SLOT_TEXT = 'Test Button'; 6 | const wrapper = mount(Button, { slots: { default: SLOT_TEXT } }); 7 | 8 | const button = wrapper.getComponent({ name: 'button' }); 9 | 10 | expect(button.text()).toBe(SLOT_TEXT); 11 | }); 12 | 13 | test('should emit event on click', () => { 14 | const wrapper = mount(Button); 15 | const button = wrapper.getComponent({ name: 'button' }); 16 | button.trigger('click'); 17 | expect(wrapper.emitted('click')).toBeTruthy(); 18 | }); 19 | 20 | describe('should render correct theme from given prop', () => { 21 | test('primary-blue', () => { 22 | const EXPECTED_CLASS = [ 23 | 'border-ultramarine', 24 | 'text-ultramarine', 25 | 'bg-white', 26 | 'hover:border-white', 27 | 'hover:text-white', 28 | 'hover:bg-ultramarine', 29 | ]; 30 | 31 | const wrapper = mount(Button, { propsData: { theme: 'primary-blue' } }); 32 | 33 | const button = wrapper.getComponent({ name: 'button' }); 34 | 35 | expect(button.classes()).toEqual(expect.arrayContaining(EXPECTED_CLASS)); 36 | }); 37 | 38 | test('primary-white', () => { 39 | const EXPECTED_CLASS = [ 40 | 'border-white', 41 | 'text-white', 42 | 'bg-ultramarine', 43 | 'hover:border-ultramarine', 44 | 'hover:text-ultramarine', 45 | 'hover:bg-white', 46 | ]; 47 | 48 | const wrapper = mount(Button, { propsData: { theme: 'primary-white' } }); 49 | 50 | const button = wrapper.getComponent({ name: 'button' }); 51 | 52 | expect(button.classes()).toEqual(expect.arrayContaining(EXPECTED_CLASS)); 53 | }); 54 | 55 | test('secondary-white', () => { 56 | const EXPECTED_CLASS = [ 57 | 'border-white', 58 | 'text-white', 59 | 'bg-transparent', 60 | 'hover:border-ultramarine', 61 | 'hover:text-ultramarine', 62 | 'hover:bg-white', 63 | ]; 64 | 65 | const wrapper = mount(Button, { propsData: { theme: 'secondary-white' } }); 66 | 67 | const button = wrapper.getComponent({ name: 'button' }); 68 | 69 | expect(button.classes()).toEqual(expect.arrayContaining(EXPECTED_CLASS)); 70 | }); 71 | 72 | test('secondary-blue', () => { 73 | const EXPECTED_CLASS = [ 74 | 'border-ultramarine', 75 | 'text-ultramarine', 76 | 'bg-transparent', 77 | 'hover:border-white', 78 | 'hover:text-white', 79 | 'hover:bg-ultramarine', 80 | ]; 81 | 82 | const wrapper = mount(Button, { propsData: { theme: 'secondary-blue' } }); 83 | 84 | const button = wrapper.getComponent({ name: 'button' }); 85 | 86 | expect(button.classes()).toEqual(expect.arrayContaining(EXPECTED_CLASS)); 87 | }); 88 | }); 89 | -------------------------------------------------------------------------------- /tests/components/dropdown-select/dropdown-item.test.ts: -------------------------------------------------------------------------------- 1 | import { mount } from '@vue/test-utils'; 2 | import DropdownItem from '@/components/dropdown-select/dropdown-item.vue'; 3 | 4 | describe('icon image', () => { 5 | test('should have icon image when url is present', () => { 6 | const ICON_URL = '/party/พลังประชารัฐ.jpg'; 7 | const wrapper = mount(DropdownItem, { 8 | propsData: { 9 | option: { 10 | label: 'พลังประชารัฐ', 11 | iconUrl: ICON_URL, 12 | }, 13 | }, 14 | }); 15 | 16 | const img = wrapper.find('img'); 17 | 18 | expect(img.exists()).toBe(true); 19 | expect(img.attributes('src')).toContain(ICON_URL); 20 | }); 21 | 22 | test('should NOT have icon image when url is NOT present', () => { 23 | const wrapper = mount(DropdownItem, { 24 | propsData: { 25 | option: { 26 | label: 'พลังประชารัฐ', 27 | }, 28 | }, 29 | }); 30 | 31 | expect(wrapper.find('img').exists()).toBe(false); 32 | }); 33 | }); 34 | 35 | describe('header item', () => { 36 | const HEADER_OPTION = { 37 | isHeader: true, 38 | label: 'พรรคร่วมรัฐบาล', 39 | }; 40 | 41 | test('should render div instead of button', () => { 42 | const wrapper = mount(DropdownItem, { 43 | propsData: { 44 | option: HEADER_OPTION, 45 | }, 46 | }); 47 | 48 | expect(wrapper.find('button').exists()).toBeFalsy(); 49 | expect(wrapper.find('div').exists()).toBeTruthy(); 50 | }); 51 | }); 52 | 53 | describe('non-header item', () => { 54 | const OPTION = { 55 | isHeader: false, 56 | label: 'ประชาธิปัตย์', 57 | value: 'ประชาธิปัตย์', 58 | }; 59 | 60 | test('should emit click event when click on element', () => { 61 | const wrapper = mount(DropdownItem, { 62 | propsData: { 63 | option: OPTION, 64 | }, 65 | }); 66 | 67 | wrapper.find('button').trigger('click'); 68 | 69 | expect(wrapper.emitted().click![0]).toEqual([OPTION.value]); 70 | }); 71 | }); 72 | -------------------------------------------------------------------------------- /tests/components/dropdown-select/dropdown-select.test.ts: -------------------------------------------------------------------------------- 1 | import { mount } from '@vue/test-utils'; 2 | import DropdownSelect from '@/components/dropdown-select/dropdown-select.vue'; 3 | import DropdownItem from '@/components/dropdown-select/dropdown-item.vue'; 4 | 5 | describe('non-selecting state', () => { 6 | test('should have white background with black text', () => { 7 | const wrapper = mount(DropdownSelect, { 8 | propsData: { 9 | options: [{ label: 'party1' }], 10 | }, 11 | }); 12 | 13 | expect(wrapper.find('#select-box').classes()).toContain('bg-white'); 14 | }); 15 | 16 | test('should NOT show items', () => { 17 | const wrapper = mount(DropdownSelect, { 18 | propsData: { 19 | options: [{ label: 'party1' }], 20 | }, 21 | }); 22 | 23 | const item = wrapper.findComponent(DropdownItem); 24 | 25 | expect(item.exists()).toBe(false); 26 | }); 27 | 28 | test('should show placeholder', () => { 29 | const PLACEHOLDER = 'select party'; 30 | const wrapper = mount(DropdownSelect, { 31 | propsData: { 32 | options: [{ label: 'party1' }], 33 | placeholder: PLACEHOLDER, 34 | }, 35 | }); 36 | 37 | expect(wrapper.html()).toContain(PLACEHOLDER); 38 | }); 39 | }); 40 | 41 | describe('selecting state', () => { 42 | test('should have transparent background with white text', async () => { 43 | const wrapper = mount(DropdownSelect, { 44 | propsData: { 45 | options: [{ label: 'party1' }], 46 | }, 47 | }); 48 | 49 | const selectBox = wrapper.find('#select-box'); 50 | await selectBox.trigger('click'); 51 | 52 | expect(selectBox.classes()).toContain('bg-transparent'); 53 | }); 54 | 55 | test('should show items with default option', async () => { 56 | const OPTIONS = [{ label: 'party1' }, { label: 'party2' }]; 57 | const wrapper = mount(DropdownSelect, { 58 | propsData: { 59 | options: OPTIONS, 60 | }, 61 | }); 62 | 63 | await wrapper.find('#select-box').trigger('click'); 64 | 65 | const items = wrapper.findAllComponents(DropdownItem); 66 | expect(items.length).toBe(OPTIONS.length + 1); 67 | }); 68 | 69 | test('should show selecting placeholder instead of normal placeholder', async () => { 70 | const PLACEHOLDER = 'select party'; 71 | const SELECTING_PLACEHOLDER = 'select one from the list'; 72 | const wrapper = mount(DropdownSelect, { 73 | propsData: { 74 | options: [{ label: 'party1' }], 75 | placeholder: PLACEHOLDER, 76 | placeholderSelecting: SELECTING_PLACEHOLDER, 77 | }, 78 | }); 79 | 80 | await wrapper.find('#select-box').trigger('click'); 81 | 82 | expect(wrapper.html()).toContain(SELECTING_PLACEHOLDER); 83 | }); 84 | }); 85 | 86 | describe('selected state', () => { 87 | test('should show selected item', async () => { 88 | const SELECTED_OPTION = { 89 | label: 'party1', 90 | iconUrl: '/party/party1.jpg', 91 | }; 92 | const wrapper = mount(DropdownSelect, { 93 | propsData: { 94 | options: [SELECTED_OPTION], 95 | selected: SELECTED_OPTION.label, 96 | }, 97 | }); 98 | 99 | await wrapper.find('#select-box').trigger('click'); 100 | 101 | expect(wrapper.html()).toContain(SELECTED_OPTION.label); 102 | expect(wrapper.find('img').attributes('src')).toContain( 103 | SELECTED_OPTION.iconUrl 104 | ); 105 | }); 106 | }); 107 | 108 | test('should emit input event when select on selectable item', async () => { 109 | const OPTION = { label: 'party1', value: 'p1' }; 110 | const wrapper = mount(DropdownSelect, { 111 | propsData: { 112 | options: [OPTION], 113 | }, 114 | }); 115 | 116 | await wrapper.find('#select-box').trigger('click'); 117 | await wrapper.findAllComponents(DropdownItem).at(1).trigger('click'); 118 | 119 | expect(wrapper.emitted().input![0]).toEqual([OPTION.value]); 120 | }); 121 | 122 | test('should NOT display items when select on item', async () => { 123 | const OPTION = { label: 'party1' }; 124 | const wrapper = mount(DropdownSelect, { 125 | propsData: { 126 | options: [OPTION], 127 | }, 128 | }); 129 | 130 | await wrapper.find('#select-box').trigger('click'); 131 | await wrapper.findComponent(DropdownItem).trigger('click'); 132 | 133 | expect(wrapper.findComponent(DropdownItem).exists()).toBe(false); 134 | }); 135 | -------------------------------------------------------------------------------- /tests/components/explanation/explanation-container.test.ts: -------------------------------------------------------------------------------- 1 | import { mount } from '@vue/test-utils'; 2 | import ExplanationContainer from '@/components/explanation/explanation-container.vue'; 3 | 4 | test('renders title', () => { 5 | const titleText = 'test title text'; 6 | const wrapper = mount(ExplanationContainer, { 7 | propsData: { title: titleText }, 8 | }); 9 | const title = wrapper.find('h1'); 10 | expect(title.text()).toBe(titleText); 11 | }); 12 | 13 | test('renders slot content', () => { 14 | const slotContent = '

test slot test

'; 15 | const wrapper = mount(ExplanationContainer, { 16 | propsData: { title: 'test title text' }, 17 | slots: { default: slotContent }, 18 | }); 19 | expect(wrapper.html()).toContain(slotContent); 20 | }); 21 | -------------------------------------------------------------------------------- /tests/components/explanation/status-explanation.test.ts: -------------------------------------------------------------------------------- 1 | import { mount } from '@vue/test-utils'; 2 | import ExplanationContainer from '@/components/explanation/explanation-container.vue'; 3 | import ProcessFlowchart from '@/components/explanation/process-flowchart.vue'; 4 | import StatusLegend from '@/components/explanation/status-legend.vue'; 5 | import StatusExplanation from '@/components/explanation/status-explanation.vue'; 6 | 7 | test('renders container component', () => { 8 | const wrapper = mount(StatusExplanation); 9 | const container = wrapper.getComponent(ExplanationContainer); 10 | expect(container).toBeTruthy(); 11 | }); 12 | 13 | test('renders flowchart component', () => { 14 | const wrapper = mount(StatusExplanation); 15 | const flowchart = wrapper.getComponent(ProcessFlowchart); 16 | expect(flowchart).toBeTruthy(); 17 | }); 18 | 19 | test('renders status legend component', () => { 20 | const wrapper = mount(StatusExplanation); 21 | const statusLegend = wrapper.getComponent(StatusLegend); 22 | expect(statusLegend).toBeTruthy(); 23 | }); 24 | -------------------------------------------------------------------------------- /tests/components/explanation/status-legend.test.ts: -------------------------------------------------------------------------------- 1 | import { mount } from '@vue/test-utils'; 2 | import StatusLegend from '@/components/explanation/status-legend.vue'; 3 | 4 | describe('Inline handling', () => { 5 | test('renders column wise', () => { 6 | const wrapper = mount(StatusLegend, { 7 | propsData: { showInline: false }, 8 | }); 9 | const container = wrapper.find('[data-testid="status-legend-container"]'); 10 | expect(container.classes()).toEqual([]); 11 | }); 12 | test('renders inline', () => { 13 | const wrapper = mount(StatusLegend, { 14 | propsData: { showInline: true }, 15 | }); 16 | const container = wrapper.find('[data-testid="status-legend-container"]'); 17 | expect(container.classes()).toEqual(['flex']); 18 | }); 19 | }); 20 | 21 | describe('Details handling', () => { 22 | test('renders details', () => { 23 | const wrapper = mount(StatusLegend, { 24 | propsData: { showDetail: true }, 25 | }); 26 | const detail = wrapper.find('p'); 27 | expect(detail.exists()).toBe(true); 28 | }); 29 | test('does not render details', () => { 30 | const wrapper = mount(StatusLegend, { 31 | propsData: { showDetail: false }, 32 | }); 33 | const detail = wrapper.find('p'); 34 | expect(detail.exists()).toBe(false); 35 | }); 36 | }); 37 | 38 | describe('Style handling', () => { 39 | test('show only selected legend', () => { 40 | const expectedClasses: [] = []; 41 | const wrapper = mount(StatusLegend, { 42 | propsData: { showOnly: 'ไม่พบความเคลื่อนไหว' }, 43 | }); 44 | const text = wrapper.find('[data-testid="status-legend-text"]'); 45 | expect(text.classes()).toEqual(expect.arrayContaining(expectedClasses)); 46 | }); 47 | test('show detail', () => { 48 | const expectedClasses = ['mr-2', 'w-32', 'mb-2']; 49 | const wrapper = mount(StatusLegend, { 50 | propsData: { showDetail: true }, 51 | }); 52 | const text = wrapper.find('[data-testid="status-legend-text"]'); 53 | expect(text.classes()).toEqual(expect.arrayContaining(expectedClasses)); 54 | }); 55 | test('show inline without detail', () => { 56 | const expectedClasses = ['mr-2']; 57 | const wrapper = mount(StatusLegend, { 58 | propsData: { showDetail: false, showInline: true }, 59 | }); 60 | const text = wrapper.find('[data-testid="status-legend-text"]'); 61 | expect(text.classes()).toEqual(expect.arrayContaining(expectedClasses)); 62 | }); 63 | test('no inline, no detail', () => { 64 | const expectedClasses = ['w-32']; 65 | const wrapper = mount(StatusLegend, { 66 | propsData: { showInline: true }, 67 | }); 68 | const text = wrapper.find('[data-testid="status-legend-text"]'); 69 | expect(text.classes()).toEqual(expect.arrayContaining(expectedClasses)); 70 | }); 71 | }); 72 | 73 | describe('Selection handling', () => { 74 | const expectedText = [ 75 | { id: 0, text: 'ไม่พบความเคลื่อนไหว' }, 76 | { id: 1, text: 'ถูกเสนอต่อสภา' }, 77 | { id: 2, text: 'ถูกระงับ' }, 78 | { id: 3, text: 'กำลังดำเนินการ' }, 79 | { id: 4, text: 'สำเร็จ' }, 80 | ]; 81 | 82 | test.each(expectedText)('should render "$text"', ({ id, text }) => { 83 | const wrapper = mount(StatusLegend, { 84 | propsData: { showDetail: false }, 85 | }); 86 | 87 | const spans = wrapper.findAll('[data-testid="status-legend-text"]'); 88 | 89 | expect(spans.at(id).text()).toEqual(text); 90 | }); 91 | 92 | test('selects one', () => { 93 | const expectedText = ['ไม่พบความเคลื่อนไหว']; 94 | const wrapper = mount(StatusLegend, { 95 | propsData: { showDetail: false, showOnly: 'ไม่พบความเคลื่อนไหว' }, 96 | }); 97 | const spans = wrapper.findAll('[data-testid="status-legend-text"]'); 98 | expect(spans.length).toEqual(expectedText.length); 99 | expect(spans.at(0).text()).toBe(expectedText[0]); 100 | }); 101 | }); 102 | -------------------------------------------------------------------------------- /tests/components/explore/promise-overview/active-filters.test.ts: -------------------------------------------------------------------------------- 1 | import { mount } from '@vue/test-utils'; 2 | import ActiveFilters from '@/components/explore/promise-overview/active-filters.vue'; 3 | import FilterChip from '@/components/explore/promise-overview/filter-chip.vue'; 4 | import promises from '@/data/promises-example.json'; 5 | import { FilterType } from '@/models/filter'; 6 | import { PromiseStatus, PromiseTopic } from '@/models/promise'; 7 | 8 | test('should render ไม่พบคำสัญญาที่คุณค้นหา text if promises is empty', () => { 9 | const wrapper = mount(ActiveFilters, { propsData: { promises: [] } }); 10 | 11 | expect(wrapper.text()).toBe('ไม่พบคำสัญญาที่คุณค้นหา'); 12 | }); 13 | 14 | test('should render promises count', () => { 15 | const wrapper = mount(ActiveFilters, { propsData: { promises } }); 16 | 17 | const countLabel = wrapper.getComponent({ ref: 'countLabel' }); 18 | 19 | expect(countLabel.text()).toBe(`${promises.length} คำสัญญา`); 20 | }); 21 | 22 | test('should render FilterChip from given filters', () => { 23 | const filters = [ 24 | { 25 | type: FilterType.Party, 26 | value: 'พลังประชารัฐ', 27 | }, 28 | { 29 | type: FilterType.Keyword, 30 | value: 'รถเมล์', 31 | }, 32 | ]; 33 | 34 | const wrapper = mount(ActiveFilters, { propsData: { promises, filters } }); 35 | 36 | const filterChips = wrapper.findAllComponents(FilterChip); 37 | 38 | expect(filterChips.length).toBe(filters.length); 39 | expect(wrapper.text().includes('จากทุกพรรค ในทุกประเด็น')).toBeFalsy(); 40 | }); 41 | 42 | test('should render images from given filters, except keyword', () => { 43 | const filters = [ 44 | { 45 | type: FilterType.Party, 46 | value: 'พลังประชารัฐ', 47 | }, 48 | { 49 | type: FilterType.Status, 50 | value: PromiseStatus.NoData, 51 | }, 52 | { 53 | type: FilterType.Topic, 54 | value: PromiseTopic.Environmental, 55 | }, 56 | { 57 | type: FilterType.Keyword, 58 | value: 'รถเมล์', 59 | }, 60 | ]; 61 | 62 | const wrapper = mount(ActiveFilters, { propsData: { promises, filters } }); 63 | 64 | const images = wrapper.findAll('.active-image'); 65 | 66 | expect(images.length).toBe(filters.length - 1); 67 | }); 68 | 69 | test('should render จากทุกพรรค ในทุกประเด็น text if filters is empty', () => { 70 | const wrapper = mount(ActiveFilters, { 71 | propsData: { promises, filters: [] }, 72 | }); 73 | 74 | const filterChips = wrapper.findAllComponents(FilterChip); 75 | 76 | expect(filterChips.length).toBe(0); 77 | expect(wrapper.text().includes('จากทุกพรรค ในทุกประเด็น')).toBeTruthy(); 78 | }); 79 | 80 | test('should emit removefilter event with filter detail when filterchip emit remove', () => { 81 | const filters = [ 82 | { 83 | type: FilterType.Party, 84 | value: 'พลังประชารัฐ', 85 | }, 86 | { 87 | type: FilterType.Status, 88 | value: PromiseStatus.NoData, 89 | }, 90 | ]; 91 | 92 | const wrapper = mount(ActiveFilters, { propsData: { promises, filters } }); 93 | 94 | const firstFilterChip = wrapper.findComponent(FilterChip); 95 | 96 | firstFilterChip.get('button').trigger('click'); 97 | 98 | expect(wrapper.emitted().removefilter).toBeTruthy(); 99 | expect(wrapper.emitted().removefilter?.[0][0]).toEqual(filters[0]); 100 | }); 101 | -------------------------------------------------------------------------------- /tests/components/explore/promise-overview/chart-item.test.ts: -------------------------------------------------------------------------------- 1 | import { mount, config } from '@vue/test-utils'; 2 | import ChartItem from '@/components/explore/promise-overview/chart-item.vue'; 3 | import { PromiseStatus } from '@/models/promise'; 4 | 5 | const data = [ 6 | { status: PromiseStatus.NoData, count: 10 }, 7 | { status: PromiseStatus.Proposed, count: 5 }, 8 | ]; 9 | 10 | const max = 30; 11 | 12 | test('should render given label and data count as a text', () => { 13 | const label = 'ไม่พบความเคลื่อนไหว'; 14 | const wrapper = mount(ChartItem, { propsData: { label, data } }); 15 | 16 | expect(wrapper.find('p').text()).toBe(`${label} (15)`); 17 | }); 18 | 19 | test('should render given icon', () => { 20 | const icon = 'test.png'; 21 | const wrapper = mount(ChartItem, { propsData: { data, icon } }); 22 | 23 | expect(wrapper.find('img').attributes('src')).toBe( 24 | `${config.mocks.$config.path.images}/${icon}` 25 | ); 26 | }); 27 | 28 | test('should render corrected total bar width from given data and max', () => { 29 | const wrapper = mount(ChartItem, { propsData: { data, max } }); 30 | 31 | expect(wrapper.getComponent({ ref: 'barchart' }).attributes('style')).toBe( 32 | `width: 50%;` 33 | ); 34 | }); 35 | 36 | test('should render corrected subbar width from given data and max', () => { 37 | const wrapper = mount(ChartItem, { propsData: { data, max } }); 38 | 39 | const subbars = wrapper.getComponent({ ref: 'barchart' }).findAll('div'); 40 | 41 | expect(subbars.length).toBe(3); 42 | expect(subbars.at(1).attributes('style')).toBe(`width: 67%;`); 43 | expect(subbars.at(2).attributes('style')).toBe(`width: 33%;`); 44 | }); 45 | 46 | test('should render corrected subbar color from given data and max', () => { 47 | const wrapper = mount(ChartItem, { propsData: { data, max } }); 48 | 49 | const subbars = wrapper.getComponent({ ref: 'barchart' }).findAll('div'); 50 | 51 | expect(subbars.at(1).classes()).toContain('bg-status-nodata'); 52 | expect(subbars.at(2).classes()).toContain('bg-status-proposed'); 53 | }); 54 | 55 | test('should render overall percentage from given data as a text', () => { 56 | const total = 60; 57 | 58 | const wrapper = mount(ChartItem, { propsData: { data, total } }); 59 | 60 | expect(wrapper.find('span').text()).toBe('25%'); 61 | }); 62 | 63 | test('should render overall percentage with decimal point if exist from given data as a text', () => { 64 | const total = 45; 65 | 66 | const wrapper = mount(ChartItem, { propsData: { data, total } }); 67 | 68 | expect(wrapper.find('span').text()).toBe('33.3%'); 69 | }); 70 | -------------------------------------------------------------------------------- /tests/components/explore/promise-overview/filter-chip.test.ts: -------------------------------------------------------------------------------- 1 | import { mount } from '@vue/test-utils'; 2 | import FilterChip from '@/components/explore/promise-overview/filter-chip.vue'; 3 | import { Filter, FilterType } from '@/models/filter'; 4 | import { 5 | PromiseStatus, 6 | promiseStatusTextMap, 7 | PromiseTopic, 8 | promiseTopicTextMap, 9 | } from '@/models/promise'; 10 | 11 | const filterTestCases: [Filter, string][] = [ 12 | [ 13 | { 14 | type: FilterType.Party, 15 | value: 'พลังประชารัฐ', 16 | }, 17 | 'พลังประชารัฐ', 18 | ], 19 | [ 20 | { 21 | type: FilterType.Status, 22 | value: PromiseStatus.NoData, 23 | }, 24 | `สถานะ: ${promiseStatusTextMap.get(PromiseStatus.NoData)}`, 25 | ], 26 | [ 27 | { 28 | type: FilterType.Keyword, 29 | value: 'รถเมล์', 30 | }, 31 | 'คำค้นหา: รถเมล์', 32 | ], 33 | [ 34 | { 35 | type: FilterType.Topic, 36 | value: PromiseTopic.Environmental, 37 | }, 38 | `ประเด็น${promiseTopicTextMap.get(PromiseTopic.Environmental)?.short}`, 39 | ], 40 | ]; 41 | 42 | describe('should render correct text content for each filter type', () => { 43 | test.each(filterTestCases)( 44 | '%p -> %s', 45 | (filter: Filter, expectedText: string) => { 46 | const wrapper = mount(FilterChip, { propsData: { filter } }); 47 | 48 | expect(wrapper.text()).toBe(expectedText); 49 | } 50 | ); 51 | }); 52 | 53 | test('should emit remove event when remove button is click', () => { 54 | const wrapper = mount(FilterChip, { 55 | propsData: { filter: filterTestCases[0][0] }, 56 | }); 57 | 58 | const button = wrapper.get('button'); 59 | button.trigger('click'); 60 | 61 | expect(wrapper.emitted().remove).toBeTruthy(); 62 | }); 63 | -------------------------------------------------------------------------------- /tests/components/explore/promise-overview/promise-overview.test.ts: -------------------------------------------------------------------------------- 1 | import { mount } from '@vue/test-utils'; 2 | import PromiseOverview from '@/components/explore/promise-overview/promise-overview.vue'; 3 | import ChartItem from '@/components/explore/promise-overview/chart-item.vue'; 4 | import TabNavigation from '@/components/explore/promise-overview/tab-navigation.vue'; 5 | import ActiveFilters from '@/components/explore/promise-overview/active-filters.vue'; 6 | 7 | import { 8 | TrackingPromise, 9 | PromiseStatus, 10 | PromiseTopic, 11 | promiseStatusTextMap, 12 | promiseTopicTextMap, 13 | } from '@/models/promise'; 14 | import { FilterType } from '~/models/filter'; 15 | 16 | const promises: Partial[] = [ 17 | { 18 | party: 'พลังไทยรักไทย', 19 | topic: PromiseTopic.Economics, 20 | status: PromiseStatus.Done, 21 | }, 22 | { 23 | party: 'พลังไทยรักไทย', 24 | topic: PromiseTopic.Environmental, 25 | status: PromiseStatus.Done, 26 | }, 27 | { 28 | party: 'เพื่อไทย', 29 | topic: PromiseTopic.Culture, 30 | status: PromiseStatus.Done, 31 | }, 32 | { 33 | party: 'อนาคตใหม่', 34 | topic: PromiseTopic.Economics, 35 | status: PromiseStatus.NoData, 36 | }, 37 | { 38 | party: 'อนาคตใหม่', 39 | topic: PromiseTopic.Environmental, 40 | status: PromiseStatus.Paused, 41 | }, 42 | { 43 | party: 'ประชาธิปัติ', 44 | topic: PromiseTopic.Culture, 45 | status: PromiseStatus.Working, 46 | }, 47 | { 48 | party: 'พลังประชารัฐ', 49 | topic: PromiseTopic.Equality, 50 | status: PromiseStatus.Proposed, 51 | }, 52 | { 53 | party: 'ชาติไทยพัฒนา', 54 | topic: PromiseTopic.Security, 55 | status: PromiseStatus.NoData, 56 | }, 57 | ]; 58 | 59 | test('should open status tab by default', () => { 60 | const wrapper = mount(PromiseOverview, { propsData: { promises } }); 61 | 62 | const firstChart = wrapper.getComponent(ChartItem); 63 | 64 | expect(firstChart.text()).toContain( 65 | promiseStatusTextMap.get(PromiseStatus.NoData) as string 66 | ); 67 | }); 68 | 69 | test('should change tab when navigation is clicked', async () => { 70 | const wrapper = mount(PromiseOverview, { propsData: { promises } }); 71 | 72 | const tabButtons = wrapper.findComponent(TabNavigation).findAll('button'); 73 | 74 | await tabButtons.at(1).trigger('click'); 75 | 76 | expect(wrapper.getComponent(ChartItem).text()).toContain('พลังไทยรักไทย'); 77 | 78 | await tabButtons.at(2).trigger('click'); 79 | 80 | expect(wrapper.getComponent(ChartItem).text()).toContain( 81 | promiseTopicTextMap.get(PromiseTopic.Equality)?.short as string 82 | ); 83 | }); 84 | 85 | test('should emit removefilter event with filter detail when filterchip emit remove', () => { 86 | const filters = [ 87 | { 88 | type: FilterType.Party, 89 | value: 'พลังประชารัฐ', 90 | }, 91 | { 92 | type: FilterType.Status, 93 | value: PromiseStatus.NoData, 94 | }, 95 | ]; 96 | 97 | const wrapper = mount(PromiseOverview, { propsData: { promises, filters } }); 98 | 99 | wrapper.findComponent(ActiveFilters).vm.$emit('removefilter', filters[0]); 100 | 101 | expect(wrapper.emitted().removefilter).toBeTruthy(); 102 | expect(wrapper.emitted().removefilter?.[0][0]).toEqual(filters[0]); 103 | }); 104 | -------------------------------------------------------------------------------- /tests/components/explore/promise-overview/tab-body.test.ts: -------------------------------------------------------------------------------- 1 | import { mount } from '@vue/test-utils'; 2 | import TabBody from '@/components/explore/promise-overview/tab-body.vue'; 3 | import ChartItem from '@/components/explore/promise-overview/chart-item.vue'; 4 | import { TrackingPromise, PromiseStatus, PromiseTopic } from '@/models/promise'; 5 | import { FilterType } from '@/models/filter'; 6 | import { groupPromisesBy } from '~/components/explore/promise-overview/promises-aggregator'; 7 | 8 | const promises: Partial[] = [ 9 | { 10 | party: 'พลังไทยรักไทย', 11 | topic: PromiseTopic.Economics, 12 | status: PromiseStatus.Done, 13 | }, 14 | { 15 | party: 'พลังไทยรักไทย', 16 | topic: PromiseTopic.Environmental, 17 | status: PromiseStatus.Done, 18 | }, 19 | { 20 | party: 'เพื่อไทย', 21 | topic: PromiseTopic.Culture, 22 | status: PromiseStatus.Done, 23 | }, 24 | { 25 | party: 'อนาคตใหม่', 26 | topic: PromiseTopic.Economics, 27 | status: PromiseStatus.NoData, 28 | }, 29 | { 30 | party: 'อนาคตใหม่', 31 | topic: PromiseTopic.Environmental, 32 | status: PromiseStatus.Paused, 33 | }, 34 | { 35 | party: 'ประชาธิปัติ', 36 | topic: PromiseTopic.Culture, 37 | status: PromiseStatus.Working, 38 | }, 39 | { 40 | party: 'พลังประชารัฐ', 41 | topic: PromiseTopic.Equality, 42 | status: PromiseStatus.Proposed, 43 | }, 44 | { 45 | party: 'ชาติไทยพัฒนา', 46 | topic: PromiseTopic.Security, 47 | status: PromiseStatus.NoData, 48 | }, 49 | ]; 50 | 51 | test('should render chart item based on promise aggregator', () => { 52 | const wrapper = mount(TabBody, { 53 | propsData: { promises, groupBy: FilterType.Topic }, 54 | }); 55 | 56 | const { charts } = groupPromisesBy( 57 | FilterType.Topic, 58 | promises as TrackingPromise[] 59 | ); 60 | 61 | expect(wrapper.findAllComponents(ChartItem).length).toEqual(charts.length); 62 | }); 63 | -------------------------------------------------------------------------------- /tests/components/explore/promise-overview/tab-navigation.test.ts: -------------------------------------------------------------------------------- 1 | import { mount } from '@vue/test-utils'; 2 | import TabNavigation from '@/components/explore/promise-overview/tab-navigation.vue'; 3 | import { Filter, FilterType } from '@/models/filter'; 4 | import { PromiseStatus, PromiseTopic } from '@/models/promise'; 5 | 6 | const keywordFilter: Filter = { 7 | type: FilterType.Keyword, 8 | value: '', 9 | }; 10 | 11 | const partyFilter: Filter = { 12 | type: FilterType.Party, 13 | value: '', 14 | }; 15 | 16 | const statusFilter: Filter = { 17 | type: FilterType.Status, 18 | value: PromiseStatus.NoData, 19 | }; 20 | 21 | const topicFilter: Filter = { 22 | type: FilterType.Topic, 23 | value: PromiseTopic.Culture, 24 | }; 25 | 26 | const filterTestCases: [Filter[], string[]][] = [ 27 | [[], ['ดูตามสถานะ', 'ดูตามพรรค', 'ดูตามประเด็น']], 28 | [[keywordFilter], ['ดูตามสถานะ', 'ดูตามพรรค', 'ดูตามประเด็น']], 29 | [[partyFilter], ['ดูตามสถานะ', 'ดูตามประเด็น']], 30 | [[statusFilter], ['ดูตามพรรค', 'ดูตามประเด็น']], 31 | [[topicFilter], ['ดูตามสถานะ', 'ดูตามพรรค']], 32 | [[statusFilter, partyFilter], ['ดูตามประเด็น']], 33 | [[statusFilter, topicFilter], ['ดูตามพรรค']], 34 | [[partyFilter, topicFilter], ['ดูตามสถานะ']], 35 | [[partyFilter, topicFilter, statusFilter], []], 36 | ]; 37 | 38 | describe('should render buttons which are not in the filters', () => { 39 | test.each(filterTestCases)( 40 | '%p -> %p', 41 | (filters: Filter[], expectedButtonTexts: string[]) => { 42 | const wrapper = mount(TabNavigation, { 43 | propsData: { filters }, 44 | }); 45 | 46 | const buttons = wrapper.findAll('button'); 47 | 48 | expectedButtonTexts.forEach((text, index) => 49 | expect(buttons.at(index).text()).toBe(text) 50 | ); 51 | } 52 | ); 53 | }); 54 | 55 | test('active button should have ultramarine color, the rest are black', () => { 56 | const wrapper = mount(TabNavigation, { 57 | propsData: { filters: filterTestCases[0], activeTab: FilterType.Party }, 58 | }); 59 | 60 | const buttons = wrapper.findAll('button'); 61 | 62 | expect(buttons.at(0).classes()).toContain('bg-black'); 63 | expect(buttons.at(1).classes()).toContain('bg-ultramarine'); 64 | expect(buttons.at(2).classes()).toContain('bg-black'); 65 | }); 66 | 67 | test('should fire change event when button is clicked', () => { 68 | const wrapper = mount(TabNavigation, { 69 | propsData: { filters: filterTestCases[0] }, 70 | }); 71 | 72 | const button = wrapper.find('button'); 73 | 74 | button.trigger('click'); 75 | 76 | expect(wrapper.emitted().change).toBeTruthy(); 77 | expect(wrapper.emitted().change?.[0][0]).toBe(FilterType.Status); 78 | }); 79 | -------------------------------------------------------------------------------- /tests/components/external-link.test.ts: -------------------------------------------------------------------------------- 1 | import { mount } from '@vue/test-utils'; 2 | import ExternalLink from '@/components/external-link.vue'; 3 | 4 | describe('External Link', () => { 5 | test('should contain passed url', () => { 6 | const URL = 'www.google.com'; 7 | const wrapper = mount(ExternalLink, { 8 | propsData: { 9 | url: URL, 10 | }, 11 | }); 12 | 13 | const a = wrapper.find('a'); 14 | 15 | expect(a.attributes('href')).toEqual(URL); 16 | }); 17 | 18 | test('should render slot', () => { 19 | const SLOT_TEXT = 'Test Button'; 20 | const wrapper = mount(ExternalLink, { slots: { default: SLOT_TEXT } }); 21 | 22 | const a = wrapper.find('a'); 23 | 24 | expect(a.text()).toBe(SLOT_TEXT); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /tests/components/form-link.test.ts: -------------------------------------------------------------------------------- 1 | import { mount } from '@vue/test-utils'; 2 | import FormLink from '@/components/form-link.vue'; 3 | import Button from '@/components/button.vue'; 4 | 5 | describe('Form link', () => { 6 | test('should trigger a form click', async () => { 7 | const wrapper = mount(FormLink); 8 | 9 | const button = wrapper.findComponent(Button); 10 | await button.trigger('click'); 11 | 12 | expect(wrapper.emitted('tagClicked')).toBeTruthy(); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /tests/components/link-banner.test.ts: -------------------------------------------------------------------------------- 1 | import { mount } from '@vue/test-utils'; 2 | import LinkBanner from '@/components/link-banner.vue'; 3 | 4 | describe('Theme handling', () => { 5 | test('renders transparent-gray', () => { 6 | const expectedClasses = ['bg-gray', 'bg-opacity-10', 'text-white']; 7 | const wrapper = mount(LinkBanner, { 8 | propsData: { theme: 'transparent-gray' }, 9 | }); 10 | const theme = wrapper.get('[data-testid="theme"]'); 11 | expect(theme.classes()).toEqual(expect.arrayContaining(expectedClasses)); 12 | }); 13 | 14 | test('renders ultramarine', () => { 15 | const expectedClasses = ['bg-ultramarine', 'text-white']; 16 | const wrapper = mount(LinkBanner, { 17 | propsData: { theme: 'ultramarine' }, 18 | }); 19 | const theme = wrapper.get('[data-testid="theme"]'); 20 | expect(theme.classes()).toEqual(expect.arrayContaining(expectedClasses)); 21 | }); 22 | }); 23 | 24 | describe('Props handling', () => { 25 | test('renders icon on passed prop', () => { 26 | const wrapper = mount(LinkBanner, { 27 | propsData: { iconImage: 'topic/culture_small.png' }, 28 | }); 29 | const image = wrapper.find('img'); 30 | expect(image.exists()).toBe(true); 31 | }); 32 | 33 | test('do not render icon on empty prop', () => { 34 | const wrapper = mount(LinkBanner); 35 | const image = wrapper.find('img'); 36 | expect(image.exists()).toBe(false); 37 | }); 38 | 39 | test('renders title text', () => { 40 | const titleText = 'PROMISE TRACKER'; 41 | const wrapper = mount(LinkBanner, { 42 | propsData: { 43 | titleText, 44 | }, 45 | }); 46 | const heading = wrapper.find('h1'); 47 | expect(heading.text()).toContain(titleText); 48 | }); 49 | 50 | test('renders body text', () => { 51 | const bodyText = 52 | 'สำรวจคำสัญญาของพรรคการเมืองที่ผ่านมารักษาคำสัญญาได้แค่ไหน ?'; 53 | const wrapper = mount(LinkBanner, { 54 | propsData: { 55 | bodyText, 56 | }, 57 | }); 58 | const body = wrapper.find('p'); 59 | expect(body.text()).toContain(bodyText); 60 | }); 61 | 62 | test('renders button text', () => { 63 | const buttonText = 'อ่านต่อ'; 64 | const wrapper = mount(LinkBanner, { 65 | propsData: { 66 | buttonText, 67 | }, 68 | }); 69 | const button = wrapper.find('button'); 70 | expect(button.text()).toContain(buttonText); 71 | }); 72 | }); 73 | -------------------------------------------------------------------------------- /tests/components/party-card.test.ts: -------------------------------------------------------------------------------- 1 | import { mount, config } from '@vue/test-utils'; 2 | import PartyCard from '../../components/party/party-card.vue'; 3 | import { PromiseStatus } from '@/models/promise'; 4 | 5 | const data = [ 6 | { status: PromiseStatus.NoData, count: 10 }, 7 | { status: PromiseStatus.Proposed, count: 5 }, 8 | { status: PromiseStatus.Paused, count: 4 }, 9 | { status: PromiseStatus.Working, count: 1 }, 10 | { status: PromiseStatus.Done, count: 8 }, 11 | ]; 12 | 13 | describe('party card', () => { 14 | let wrapper = mount(PartyCard, { 15 | stubs: { 16 | NuxtLink: true, 17 | }, 18 | }); 19 | beforeEach(() => { 20 | wrapper = mount(PartyCard, { 21 | stubs: { 22 | NuxtLink: true, 23 | }, 24 | }); 25 | }); 26 | 27 | test('should render given party logo', async () => { 28 | const LOGO = 'test.png'; 29 | await wrapper.setProps({ partyLogo: LOGO }); 30 | 31 | expect(wrapper.find('img').attributes('src')).toBe( 32 | `${config.mocks.$config.path.images}/${LOGO}` 33 | ); 34 | }); 35 | 36 | test('should render dummy party logo when src is not given', async () => { 37 | const LOGO = ''; 38 | await wrapper.setProps({ partyLogo: LOGO }); 39 | 40 | expect(wrapper.find('img').attributes('src')).toBe( 41 | `${config.mocks.$config.path.images}/party/dummy.jpg` 42 | ); 43 | }); 44 | 45 | test('should render party name', async () => { 46 | const PARTY_NAME = 'รวมพลังประชาชาติไทย'; 47 | await wrapper.setProps({ partyName: PARTY_NAME }); 48 | 49 | const partyCard = wrapper.get('.h11'); 50 | 51 | expect(partyCard.text()).toBe(PARTY_NAME); 52 | }); 53 | 54 | test('should link to the given url', async () => { 55 | const URL_TEXT = '/explore?party-name=รวมพลังประชาชาติไทย'; 56 | await wrapper.setProps({ buttonUrl: URL_TEXT }); 57 | 58 | const routerLink = wrapper.get('nuxtlink-stub'); 59 | 60 | expect(routerLink.attributes().to).toBe(URL_TEXT); 61 | }); 62 | 63 | test('should render correct total sum of promises', async () => { 64 | const PROMISES_COUNT = '28'; 65 | await wrapper.setProps({ partyPromises: data }); 66 | 67 | const sumPromises = wrapper.get('.promise-sum'); 68 | 69 | expect(sumPromises.text()).toBe(PROMISES_COUNT); 70 | }); 71 | 72 | test('should render percentage for barchart width', async () => { 73 | const NODATA_PERCENTAGE = 'width: 35.71%;'; 74 | const PROPOSED_PERCENTAGE = 'width: 17.86%;'; 75 | await wrapper.setProps({ partyPromises: data }); 76 | 77 | const noData = wrapper.get('.bg-status-nodata'); 78 | const proposed = wrapper.get('.bg-status-proposed'); 79 | 80 | expect(noData.attributes().style).toBe(NODATA_PERCENTAGE); 81 | expect(proposed.attributes().style).toBe(PROPOSED_PERCENTAGE); 82 | }); 83 | }); 84 | -------------------------------------------------------------------------------- /tests/components/promise-card/box-container.test.ts: -------------------------------------------------------------------------------- 1 | import { mount } from '@vue/test-utils'; 2 | import BoxContainer from '@/components/promise-card/box-container.vue'; 3 | 4 | describe('BoxContainer', () => { 5 | test('should render slot', () => { 6 | const slotContent = '

test slot test

'; 7 | const wrapper = mount(BoxContainer, { slots: { default: slotContent } }); 8 | 9 | const container = wrapper.find('div'); 10 | 11 | expect(container.html()).toContain(slotContent); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /tests/components/promise-card/link.test.ts: -------------------------------------------------------------------------------- 1 | import { mount } from '@vue/test-utils'; 2 | import Link from '@/components/promise-card/link.vue'; 3 | import BoxContainer from '@/components/promise-card/box-container.vue'; 4 | import ExternalLink from '@/components/external-link.vue'; 5 | 6 | describe('Link Component', () => { 7 | const link = { 8 | name: 'ตรวจสอบสิทธิ์สวัสดิการแห่งรัฐ', 9 | url: 'https://govwelfare.cgd.go.th/welfare/check', 10 | }; 11 | 12 | test('should have border container', () => { 13 | const EXPECTED_CLASSES = ['border-black', 'border', 'wv-font-anuphan']; 14 | const wrapper = mount(Link, { 15 | propsData: { 16 | link, 17 | }, 18 | }); 19 | 20 | const box = wrapper.findComponent(BoxContainer); 21 | 22 | expect(box.classes()).toEqual(expect.arrayContaining(EXPECTED_CLASSES)); 23 | }); 24 | 25 | test('should have default props', () => { 26 | const wrapper = mount(Link); 27 | 28 | expect(wrapper.props().link.name).toBe(''); 29 | expect(wrapper.props().link.url).toBe(''); 30 | }); 31 | 32 | test('should render link name', () => { 33 | const wrapper = mount(Link, { 34 | propsData: { 35 | link, 36 | }, 37 | }); 38 | 39 | const atag = wrapper.find('a'); 40 | 41 | expect(atag.text()).toEqual(link.name); 42 | }); 43 | 44 | test('should contain link url', () => { 45 | const wrapper = mount(Link, { 46 | propsData: { 47 | link, 48 | }, 49 | }); 50 | 51 | const atag = wrapper.find('a'); 52 | 53 | expect(atag.attributes('href')).toEqual(link.url); 54 | }); 55 | 56 | test('should contain ExternalLink', () => { 57 | const wrapper = mount(Link, { 58 | propsData: { 59 | link, 60 | }, 61 | }); 62 | 63 | const externalLink = wrapper.findComponent(ExternalLink); 64 | 65 | expect(externalLink.exists()).toBeTruthy(); 66 | }); 67 | 68 | test('should not render link name and url', () => { 69 | const wrapper = mount(Link, { 70 | propsData: { 71 | link: {}, 72 | }, 73 | }); 74 | 75 | const atag = wrapper.find('a'); 76 | 77 | expect(atag.text()).toEqual(''); 78 | expect(atag.attributes('href')).toEqual(''); 79 | }); 80 | }); 81 | -------------------------------------------------------------------------------- /tests/components/promise-card/promise-card.test.ts: -------------------------------------------------------------------------------- 1 | import { mount } from '@vue/test-utils'; 2 | import PromiseCard from '@/components/promise-card/promise-card.vue'; 3 | import SingleCard from '@/components/promise-card/single-card.vue'; 4 | import ExpandedCard from '@/components/promise-card/expanded-card.vue'; 5 | 6 | describe('Promise Card', () => { 7 | const promise = { 8 | id: 94, 9 | party: 'พลังประชารัฐ', 10 | title: 'ต่อยอดบัตรสวัสดิการแห่งรัฐ', 11 | topic: 'equality', 12 | status: 'done', 13 | description: 'เป็นโครงการที่ต่อยอด', 14 | isNCPO: true, 15 | imageUrl: 16 | 'https://spreadsheet.wevis.info/dl/promise_tracker_9tvh/db/nc_9tvh__promises/image_0C6onm_ก้าวไกล_9.jpg', 17 | links: [ 18 | { 19 | name: 'ตรวจสอบสิทธิ์สวัสดิการแห่งรัฐ ', 20 | url: 'https://govwelfare.cgd.go.th/welfare/check\n', 21 | }, 22 | ], 23 | timelines: [ 24 | { 25 | label: 'เปิดโอกาสให้ผู้มีรายได้น้อยลงทะเบียน ', 26 | from: '2016-04-03', 27 | to: '2016-05-15', 28 | }, 29 | ], 30 | }; 31 | 32 | test('should have default props', () => { 33 | const wrapper = mount(PromiseCard); 34 | 35 | expect(wrapper.props().promise).toEqual({}); 36 | expect(wrapper.props().openState).toBe(false); 37 | }); 38 | 39 | test('should render single card component alone by default', () => { 40 | const wrapper = mount(PromiseCard, { 41 | propsData: { 42 | promise, 43 | }, 44 | }); 45 | 46 | const singleCard = wrapper.findComponent(SingleCard); 47 | const expandedCard = wrapper.findComponent(ExpandedCard); 48 | 49 | expect(singleCard.exists()).toBeTruthy(); 50 | expect(expandedCard.html()).toBe(''); 51 | }); 52 | 53 | test('should render expanded card if openState is set to true', () => { 54 | const wrapper = mount(PromiseCard, { 55 | propsData: { 56 | promise, 57 | openState: true, 58 | }, 59 | }); 60 | 61 | const expandedCard = wrapper.findComponent(ExpandedCard); 62 | 63 | expect(expandedCard.html()).not.toBe(''); 64 | }); 65 | 66 | test('should render expanded card on readmore-click', async () => { 67 | const wrapper = mount(PromiseCard, { 68 | propsData: { 69 | promise, 70 | }, 71 | }); 72 | 73 | const singleCard = wrapper.findComponent(SingleCard); 74 | const button = singleCard.find('button'); 75 | await button.trigger('click'); 76 | const expandedCard = wrapper.findComponent(ExpandedCard); 77 | 78 | expect(singleCard.emitted('readmore')).toBeTruthy(); 79 | expect(expandedCard.html()).not.toBe(''); 80 | }); 81 | }); 82 | -------------------------------------------------------------------------------- /tests/components/promise-card/timeline.test.ts: -------------------------------------------------------------------------------- 1 | import { mount } from '@vue/test-utils'; 2 | import Timeline from '@/components/promise-card/timeline.vue'; 3 | import { PromiseTimeline } from '@/models/promise'; 4 | import BoxContainer from '@/components/promise-card/box-container.vue'; 5 | import TimelineArrow from '@/components/promise-card/timeline-arrow.vue'; 6 | 7 | describe('Timeline', () => { 8 | test('should have ultramarine container', () => { 9 | const EXPECTED_CLASSES = ['bg-ultramarine', 'text-white']; 10 | const wrapper = mount(Timeline, { 11 | propsData: { 12 | timeline: { 13 | label: 'หาเสียงประกาศนโยบาย', 14 | from: '2019-01', 15 | }, 16 | }, 17 | }); 18 | 19 | const box = wrapper.findComponent(BoxContainer); 20 | 21 | expect(box.classes()).toEqual(expect.arrayContaining(EXPECTED_CLASSES)); 22 | }); 23 | 24 | test('should have default props', () => { 25 | const wrapper = mount(Timeline); 26 | 27 | expect(wrapper.props().timeline.label).toBe(''); 28 | expect(wrapper.props().timeline.from).toBe(''); 29 | }); 30 | 31 | test('should render partial from date', () => { 32 | const wrapper = mount(Timeline, { 33 | propsData: { 34 | timeline: { 35 | label: 'หาเสียงประกาศนโยบาย', 36 | from: '2019-01', 37 | }, 38 | }, 39 | }); 40 | 41 | const p = wrapper.findAll('p'); 42 | 43 | expect(p.at(0).text()).toEqual('มกราคม 2562'); 44 | }); 45 | 46 | test('should render full from date', () => { 47 | const wrapper = mount(Timeline, { 48 | propsData: { 49 | timeline: { 50 | label: 'หาเสียงประกาศนโยบาย', 51 | from: '2019-03-01', 52 | }, 53 | }, 54 | }); 55 | 56 | const p = wrapper.findAll('p'); 57 | 58 | expect(p.at(0).text()).toEqual('1 มีนาคม 2562'); 59 | }); 60 | 61 | test('should render partial from-to date', () => { 62 | const wrapper = mount(Timeline, { 63 | propsData: { 64 | timeline: { 65 | label: 'หาเสียงประกาศนโยบาย', 66 | from: '2019-04', 67 | to: '2020-01', 68 | }, 69 | }, 70 | }); 71 | 72 | const p = wrapper.findAll('p'); 73 | 74 | expect(p.at(0).text()).toEqual('เมษายน 2562 - มกราคม 2563'); 75 | }); 76 | 77 | test('should render full from-to date', () => { 78 | const wrapper = mount(Timeline, { 79 | propsData: { 80 | timeline: { 81 | label: 'หาเสียงประกาศนโยบาย', 82 | from: '2019-05-10', 83 | to: '2019-08-02', 84 | }, 85 | }, 86 | }); 87 | 88 | const p = wrapper.findAll('p'); 89 | 90 | expect(p.at(0).text()).toEqual('10 พฤษภาคม 2562 - 2 สิงหาคม 2562'); 91 | }); 92 | 93 | const invalidDates: PromiseTimeline[] = [ 94 | { label: 'timeline-1', from: '' }, 95 | { label: 'timeline-2', from: '2019' }, 96 | { label: 'timeline-3', from: '2019-02-01-03' }, 97 | ]; 98 | 99 | test.each(invalidDates)('should not render date = "$from"', (timeline) => { 100 | const wrapper = mount(Timeline, { 101 | propsData: { 102 | timeline, 103 | }, 104 | }); 105 | 106 | const p = wrapper.findAll('p'); 107 | 108 | expect(p.at(0).text()).toEqual(''); 109 | }); 110 | 111 | test('should render label', () => { 112 | const timeline = { 113 | label: 'หาเสียงประกาศนโยบาย', 114 | from: '2019-05-10', 115 | }; 116 | const wrapper = mount(Timeline, { 117 | propsData: { 118 | timeline, 119 | }, 120 | }); 121 | 122 | const p = wrapper.findAll('p'); 123 | 124 | expect(p.at(1).text()).toEqual(timeline.label); 125 | }); 126 | 127 | test('should render arrow', () => { 128 | const timeline = { 129 | label: 'timeline', 130 | from: '2019-05-10', 131 | }; 132 | const wrapper = mount(Timeline, { 133 | propsData: { 134 | timeline, 135 | isLastItem: false, 136 | }, 137 | }); 138 | 139 | const arrow = wrapper.findComponent(TimelineArrow); 140 | 141 | expect(arrow.exists()).toBeTruthy(); 142 | }); 143 | 144 | test('should not render arrow', () => { 145 | const timeline = { 146 | label: 'timeline', 147 | from: '2019-05-10', 148 | }; 149 | const wrapper = mount(Timeline, { 150 | propsData: { 151 | timeline, 152 | isLastItem: true, 153 | }, 154 | }); 155 | 156 | const arrow = wrapper.findComponent(TimelineArrow); 157 | 158 | expect(arrow.exists()).toBeFalsy(); 159 | }); 160 | }); 161 | -------------------------------------------------------------------------------- /tests/components/promises/meta-utils.test.ts: -------------------------------------------------------------------------------- 1 | import { titleText, imageUrl } from '@/utils/promises-meta'; 2 | import { PromiseStatus } from '@/models/promise'; 3 | 4 | test('should output title text', () => { 5 | expect(titleText('ที่ 1', 'พลังประชารวย')).toBe( 6 | `โครงการที่ 1 โดยพรรคพลังประชารวย` 7 | ); 8 | }); 9 | 10 | test('should output image url path', () => { 11 | expect(imageUrl('https://github.com', 'nodata' as PromiseStatus)).toBe( 12 | 'https://github.com/nodata.jpg' 13 | ); 14 | expect(imageUrl('https://github.com', 'proposed' as PromiseStatus)).toBe( 15 | 'https://github.com/proposed.jpg' 16 | ); 17 | expect(imageUrl('https://github.com', 'paused' as PromiseStatus)).toBe( 18 | 'https://github.com/paused.jpg' 19 | ); 20 | expect(imageUrl('https://github.com', 'working' as PromiseStatus)).toBe( 21 | 'https://github.com/working.jpg' 22 | ); 23 | expect(imageUrl('https://github.com', 'done' as PromiseStatus)).toBe( 24 | 'https://github.com/done.jpg' 25 | ); 26 | }); 27 | -------------------------------------------------------------------------------- /tests/components/scroll-icon.test.ts: -------------------------------------------------------------------------------- 1 | import { mount } from '@vue/test-utils'; 2 | import ScrollIcon from '@/components/scroll-icon.vue'; 3 | 4 | describe('ScrollIcon', () => { 5 | test('should render slot', () => { 6 | const SLOT_TEXT = 'scroll down'; 7 | const wrapper = mount(ScrollIcon, { 8 | slots: { 9 | default: SLOT_TEXT, 10 | }, 11 | }); 12 | 13 | expect(wrapper.text()).toEqual(SLOT_TEXT); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /tests/fileTransformer.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | process(filename) { 5 | return 'module.exports = ' + JSON.stringify(path.basename(filename)) + ';'; 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2018", 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "lib": ["ESNext", "ESNext.AsyncIterable", "DOM"], 7 | "esModuleInterop": true, 8 | "allowJs": true, 9 | "sourceMap": true, 10 | "strict": true, 11 | "noEmit": true, 12 | "experimentalDecorators": true, 13 | "baseUrl": ".", 14 | "paths": { 15 | "~/*": ["./*"], 16 | "@/*": ["./*"] 17 | }, 18 | "types": ["@nuxt/types", "@types/node", "@types/jest"], 19 | "resolveJsonModule": true 20 | }, 21 | "exclude": ["node_modules", ".nuxt", "dist"], 22 | "ts-node": { 23 | "require": ["tsconfig-paths/register"], 24 | "compilerOptions": { 25 | "module": "CommonJS" 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /utils/metadata.ts: -------------------------------------------------------------------------------- 1 | export const BASE_TITLE = 'Promise Tracker'; 2 | const DESCRIPTION = 3 | 'สำรวจ รับรู้ ร่วมติดตาม ให้พรรคการเมืองทำตามคำสัญญาที่ให้ไว้กับเรา'; 4 | export const DEFAULT_OG_IMAGE = 5 | 'https://raw.githubusercontent.com/wevisdemo/promise-tracker/main/static/og/default.jpg'; 6 | 7 | interface createMetadataParams { 8 | pageName?: string; 9 | description?: string; 10 | image?: string; 11 | } 12 | 13 | export const createMetadata = ({ 14 | pageName, 15 | description = DESCRIPTION, 16 | image = DEFAULT_OG_IMAGE, 17 | }: createMetadataParams = {}) => { 18 | const title = pageName ? `${pageName} - ${BASE_TITLE}` : BASE_TITLE; 19 | 20 | return { 21 | title, 22 | meta: [ 23 | { 24 | hid: 'description', 25 | name: 'description', 26 | content: description, 27 | }, 28 | { 29 | hid: 'og-title', 30 | property: 'og:title', 31 | content: title, 32 | }, 33 | { 34 | hid: 'og-description', 35 | property: 'og:description', 36 | content: description, 37 | }, 38 | { 39 | hid: 'og-image', 40 | property: 'og:image', 41 | content: image, 42 | }, 43 | { 44 | name: 'twitter:card', 45 | content: 'summary_large_image', 46 | }, 47 | ], 48 | }; 49 | }; 50 | -------------------------------------------------------------------------------- /utils/promises-meta.ts: -------------------------------------------------------------------------------- 1 | import { PromiseStatus, TrackingPromise } from '@/models/promise'; 2 | 3 | export const matchedPromise = (promises: TrackingPromise[], ID: number) => { 4 | return promises.filter((promise) => promise.id === ID)[0] as TrackingPromise; 5 | }; 6 | 7 | export const titleText = (title: string, party: string) => { 8 | return `โครงการ${title} โดยพรรค${party}`; 9 | }; 10 | 11 | export const descriptionMap = new Map([ 12 | [ 13 | PromiseStatus.NoData, 14 | 'คำสัญญานี้ยังไม่พบความเคลื่อนไหว ขอ(ทวง)ถามพรรคการเมืองถึงข้อมูลและการดำเนินการเกี่ยวกับคำสัญญานี้', 15 | ], 16 | [ 17 | PromiseStatus.Proposed, 18 | 'คำสัญญานี้ถูกเสนอต่อสภา ร่วมจับตา รอดูผล ของคำสัญญานี้...ว่าได้ไปต่อไหม ?', 19 | ], 20 | [ 21 | PromiseStatus.Paused, 22 | 'น่าเศร้า !! คำสัญญานี้ถูกระงับไว้ ร่วมแชร์ให้คำสัญญาได้ไปต่อกัน', 23 | ], 24 | [ 25 | PromiseStatus.Working, 26 | 'คำสัญญานี้กำลังดำเนินการอยู่ บอกต่อให้ทุกคนมาลุ้นไปพร้อมๆกันว่าใกล้แล้ว!!', 27 | ], 28 | [ 29 | PromiseStatus.Done, 30 | 'คำสัญญานี้ทำได้สำเร็จ บอกต่อให้ทุกคนดู นี่คือคำสัญญาที่พูดแล้วทำจริง!!', 31 | ], 32 | ]); 33 | 34 | export const imageUrl = (baseImageUrl: string, status: PromiseStatus) => { 35 | return `${baseImageUrl}/${status}.jpg`; 36 | }; 37 | --------------------------------------------------------------------------------