├── .eslintrc ├── .gcloudignore ├── .github └── workflows │ └── master.yml ├── .gitignore ├── LICENSE ├── README.md ├── app.yaml ├── components ├── Candidates │ ├── CandidateList │ │ ├── CandidateList.js │ │ └── CandidateList.module.scss │ ├── ConstituencyPlace │ │ ├── ConstituencyPlace.js │ │ └── ConstituencyPlace.scss │ └── StateRegionCandidateList │ │ ├── StateRegionCandidateList.js │ │ └── StateRegionCandidateList.scss ├── Common │ ├── Button │ │ ├── Button.js │ │ └── Button.module.scss │ ├── Card │ │ ├── Card.js │ │ └── Card.module.scss │ ├── Icons │ │ ├── activeCheckbox.js │ │ ├── activeFlag.js │ │ ├── activeLightBulb.js │ │ ├── activeNews.js │ │ ├── activePeople.js │ │ ├── activeStar.js │ │ ├── checkbox.js │ │ ├── clock4PM.js │ │ ├── clock6AM.js │ │ ├── flag.js │ │ ├── flagFill.js │ │ ├── gavel.js │ │ ├── lightbulb.js │ │ ├── news.js │ │ ├── people.js │ │ └── star.js │ ├── Link │ │ └── Link.js │ ├── Modal │ │ └── Modal.js │ ├── Prompt │ │ ├── Prompt.js │ │ └── Prompt.scss │ └── Tabs │ │ ├── Tab.js │ │ ├── TabPanel.js │ │ ├── TabPanel.module.scss │ │ ├── TabSlider.js │ │ ├── TabSlider.module.scss │ │ └── index.js ├── Countdown │ ├── ElectionCountdown.js │ └── ElectionCountdown.scss ├── Faq │ ├── FaqItem │ │ ├── FaqItem.js │ │ └── FaqItem.module.scss │ └── FaqList │ │ ├── FaqList.js │ │ └── FaqList.scss ├── Layout │ ├── AppHeader │ │ ├── AppHeader.js │ │ └── AppHeader.module.scss │ ├── Layout.js │ └── Layout.module.scss ├── Location │ ├── TownshipModal.js │ ├── WardVillageModal.js │ └── locationCollapse.scss ├── Navigation │ ├── Navigation.js │ └── Navigation.module.scss ├── News │ ├── NewsItem │ │ ├── NewsItem.js │ │ └── NewsItem.scss │ └── NewsList │ │ ├── NewsList.js │ │ └── NewsList.scss ├── Parties │ ├── PartyItem │ │ ├── PartyItem.js │ │ └── PartyItem.module.scss │ └── PartyList │ │ ├── PartyList.js │ │ └── PartyList.scss └── Search │ ├── SearchPage.js │ └── SearchPage.scss ├── context └── AuthProvider.js ├── gateway └── api.js ├── hooks └── useAPI.js ├── next.config.js ├── package-lock.json ├── package.json ├── pages ├── _app.js ├── about │ ├── about.module.scss │ └── index.js ├── api │ ├── auth.js │ ├── ballots.js │ ├── candidates.js │ ├── constituencies.js │ ├── faqs.js │ ├── locations.js │ ├── news.js │ ├── parties.js │ ├── searchCandidates.js │ ├── searchFaqs.js │ ├── searchNews.js │ └── searchParties.js ├── candidates │ ├── [candidate].js │ ├── candidate.module.scss │ ├── candidates.module.scss │ ├── index.js │ └── search.js ├── faqs │ ├── [faq].js │ ├── ballots.js │ ├── ballots.module.scss │ ├── faq.module.scss │ ├── faqs.module.scss │ ├── index.js │ ├── search.js │ ├── slick-theme.scss │ └── slick.scss ├── how_to_vote │ ├── HowToVote.module.scss │ ├── index.js │ ├── voter_list.js │ └── voter_list.module.scss ├── index.js ├── location │ ├── index.js │ └── location.module.scss ├── news │ ├── index.js │ ├── news.module.scss │ └── search.js └── parties │ ├── [party].js │ ├── index.js │ ├── parties.module.scss │ ├── party.module.scss │ └── search.js ├── public ├── UEC.png ├── about │ ├── TAFVLogo_RGB.jpg │ ├── UEC_logo_mm.png │ ├── eu_logo.png │ ├── eu_logo.webp │ ├── idea_logo.png │ ├── idea_logo.webp │ ├── maepaysoh.png │ ├── mvoter2020_new_logo.png │ ├── popstack_logo.png │ ├── popstack_logo.webp │ ├── step_logo.png │ ├── step_logo.webp │ ├── taf_logo.png │ └── taf_logo.webp ├── android-chrome-192x192.png ├── android-chrome-512x512.png ├── apple-touch-icon.png ├── ballot_stack.png ├── browserconfig.xml ├── favicon-16x16.png ├── favicon-32x32.png ├── favicon.ico ├── mstile-150x150.png ├── mvoter2020-transparent-vertical.png ├── prohibition_signs │ ├── no_photo.png │ ├── no_recording.png │ ├── no_selfie.png │ └── no_video.png ├── safari-pinned-tab.svg ├── site.webmanifest └── zeit.svg ├── styles ├── base.scss ├── helpers.scss ├── normalize.scss └── partials │ ├── _colors.scss │ ├── _extends.scss │ └── _fontsizes.scss └── utils ├── constants.js ├── helpers.js └── textFormatter.js /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "nextjs", 3 | "rules": { 4 | "jsx-a11y/no-static-element-interactions": "off", 5 | "jsx-a11y/click-events-have-key-events": "off", 6 | "jsx-a11y/img-redundant-alt": "off", 7 | "react/state-in-constructor": "off", 8 | "react/no-string-refs": "off" 9 | } 10 | } -------------------------------------------------------------------------------- /.gcloudignore: -------------------------------------------------------------------------------- 1 | .env.development 2 | /.git/ 3 | /components/ 4 | /context/ 5 | /gateway/ 6 | /hoc/ 7 | /node_modules/ 8 | /pages/ 9 | /styles/ 10 | /utils/ -------------------------------------------------------------------------------- /.github/workflows/master.yml: -------------------------------------------------------------------------------- 1 | name: Master Branch Deploy 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | build: 10 | name: Build Next 11 | runs-on: ubuntu-latest 12 | env: 13 | API_KEY: ${{ secrets.API_KEY }} 14 | BASE_URL: ${{ secrets.BASE_URL }} 15 | GA_TRACKING_ID: ${{ secrets.GA_TRACKING_ID}} 16 | GCLOUD_AUTH: ${{ secrets.GCLOUD_AUTH }} 17 | 18 | strategy: 19 | matrix: 20 | node-version: [14.x] 21 | 22 | steps: 23 | - uses: actions/checkout@v2 24 | - name: Setup Node $ 25 | uses: actions/setup-node@v1 26 | with: 27 | node-version: 14 28 | - name: Cache Node.js modules 29 | uses: actions/cache@v2 30 | with: 31 | # npm cache files are stored in `~/.npm` on Linux/macOS 32 | path: ~/.npm 33 | key: ${{ runner.OS }}-node-${{ hashFiles('**/package-lock.json') }} 34 | restore-keys: | 35 | ${{ runner.OS }}-node- 36 | ${{ runner.OS }}- 37 | 38 | - run: npm install 39 | - run: npm run lint 40 | 41 | - name: Create ENV and Build 42 | run: | 43 | touch .env 44 | echo BASE_URL=${{ secrets.BASE_URL }} >> .env 45 | echo API_KEY=${{ secrets.API_KEY }} >> .env 46 | echo GA_TRACKING_ID=${{ secrets.GA_TRACKING_ID }} >> .env 47 | cat .env 48 | - run: npm run build --if-present 49 | 50 | - uses: GoogleCloudPlatform/github-actions/setup-gcloud@master 51 | with: 52 | project_id: ${{ secrets.PROJECT_ID }} 53 | service_account_key: ${{ secrets.GCLOUD_AUTH }} 54 | export_default_credentials: true 55 | - run: gcloud info 56 | - run: gcloud app deploy --project=${{ secrets.PROJECT_ID }} --quiet 57 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | 21 | # debug 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | 26 | # local env files 27 | .env.local 28 | .env.development.local 29 | .env.test.local 30 | .env.production.local 31 | .env.* 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mVoter 2020 2 | 3 | This repo contains the code for the mVoter 2020 Web App. mVoter 2020 is an updated version of previously 2017 version with addition of new features and data for 2020. The app is open sourced for educational purpose, and because we believe that softwares made with public money should be publicly available. Read more on [Public Money, Public Code](https://publiccode.asia/) here. 4 | 5 | ## Table Of Content 6 | 7 | * [Technical Specification](#technical-specification) 8 | * [Setting up ENvironment](#setting-up-environment) 9 | * [Contributing](#contributing) 10 | * [Download](#download) 11 | 12 | ### Technical Specification 13 | 14 | The code need to be ran with following specification. 15 | 16 | - **Minimal Node Version**: 12.x 17 | 18 | 19 | ## Setting up Environment 20 | 21 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/zeit/next.js/tree/canary/packages/create-next-app). 22 | 23 | ### Getting Started 24 | First, run the development server: 25 | 26 | ```bash 27 | npm run dev 28 | # or 29 | yarn dev 30 | ``` 31 | 32 | Create local `.env` file and add the following. 33 | ``` 34 | BASE_URL=YOUR_BASE_URL 35 | API_KEY=YOUR_API_KEY 36 | GA_TRACKING_ID=YOUR_GOOGLE_ANALYTICS_TRACKING_ID 37 | ``` 38 | 39 | Open [http://localhost:3009](http://localhost:3009) with your browser to see the result. 40 | 41 | ## Contributing 42 | 43 | You're welcomed to submit issues and pull requests as long as you adhere to Github community guideline. Any form of contribution is welcomed, from typo error to submitting a new feature. 44 | 45 | ## Download 46 | 47 | The app can be downloaded from [Play Store](https://play.google.com/store/apps/details?id=com.popstack.mvoter2015). If your phone doesn't have Play Store, you can also manually download the apk from [our website](https://mvoterapp.com/). 48 | 49 | The app can also be used from the [Web](https://web.mvoterapp.com). 50 | 51 | ## License 52 | 53 | ``` 54 | mVoter 2020 55 | Copyright (C) 2020 PopStack 56 | 57 | This program is free software: you can redistribute it and/or modify 58 | it under the terms of the GNU General Public License as published by 59 | the Free Software Foundation, either version 3 of the License, or 60 | (at your option) any later version. 61 | 62 | This program is distributed in the hope that it will be useful, 63 | but WITHOUT ANY WARRANTY; without even the implied warranty of 64 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 65 | GNU General Public License for more details. 66 | 67 | You should have received a copy of the GNU General Public License 68 | along with this program. If not, see . 69 | ``` 70 | -------------------------------------------------------------------------------- /app.yaml: -------------------------------------------------------------------------------- 1 | env: standard 2 | runtime: nodejs12 3 | service: default 4 | 5 | handlers: 6 | - url: /.* 7 | secure: always 8 | script: auto -------------------------------------------------------------------------------- /components/Candidates/CandidateList/CandidateList.js: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | import Card from '../../Common/Card/Card'; 3 | import './CandidateList.module.scss'; 4 | 5 | const CandidateList = props => { 6 | const { candidates = [] } = props; 7 | 8 | return ( 9 |
10 | {candidates && 11 | candidates.map(candidate => { 12 | const { 13 | id, 14 | attributes: { name, image, individual_logo: individualLogo, party } 15 | } = candidate; 16 | 17 | const { 18 | attributes: { 19 | seal_image: partySealImage, 20 | name_burmese: partyNameBurmese 21 | } = {} 22 | } = party || {}; 23 | 24 | return ( 25 |
29 | 34 | 35 | 36 |
40 |
41 |
{name}
42 |
43 | {party ? partyNameBurmese : 'တစ်သီးပုဂ္ဂလ'} 44 |
45 | {party ? ( 46 | Party Flag 51 | ) : ( 52 | Individual Logo 57 | )} 58 |
59 | 60 |
61 | 62 |
63 | ); 64 | })} 65 |
66 | ); 67 | }; 68 | 69 | export default CandidateList; 70 | -------------------------------------------------------------------------------- /components/Candidates/CandidateList/CandidateList.module.scss: -------------------------------------------------------------------------------- 1 | .CandidateList { 2 | &__item { 3 | position: relative; 4 | box-sizing: border-box; 5 | border-radius: 5px; 6 | margin-bottom: 12px; 7 | background-color: #ffffff; 8 | display: flex; 9 | align-items: center; 10 | } 11 | &__avatar { 12 | flex: none; 13 | width: 85px; 14 | height: 85px; 15 | border-radius: 100%; 16 | background-repeat: no-repeat; 17 | background-position: center center; 18 | background-size: cover; 19 | background-color: #b6b6b6; 20 | } 21 | &__info { 22 | max-width: 60%; 23 | margin-left: 8px; 24 | .name { 25 | font-weight: 700; 26 | } 27 | } 28 | &__party { 29 | margin-top: 5px; 30 | line-height: 25px; 31 | } 32 | &__partyFlag { 33 | position: absolute; 34 | top: 50%; 35 | right: 10px; 36 | transform: translateY(-50%); 37 | width: 45px; 38 | height: auto; 39 | } 40 | } 41 | 42 | @media (min-width: 1200px) { 43 | .CandidateList { 44 | &__itemWrapper { 45 | padding-top: 5px; 46 | &:nth-of-type(2n+1) { 47 | padding-right: 5px; 48 | } 49 | } 50 | &__item { 51 | min-height: 102px; 52 | margin-bottom: 0px; 53 | padding: 10px; 54 | border: 1px solid rgba($color: #000000, $alpha: 0.1); 55 | cursor: pointer; 56 | } 57 | &__info { 58 | max-width: 230px; 59 | margin-left: 16px; 60 | } 61 | } 62 | } -------------------------------------------------------------------------------- /components/Candidates/ConstituencyPlace/ConstituencyPlace.js: -------------------------------------------------------------------------------- 1 | import './ConstituencyPlace.scss'; 2 | 3 | const ConstituencyPlace = ({ className, place }) => ( 4 |
5 |
{place}
6 |
7 | ); 8 | 9 | export default ConstituencyPlace; 10 | -------------------------------------------------------------------------------- /components/Candidates/ConstituencyPlace/ConstituencyPlace.scss: -------------------------------------------------------------------------------- 1 | @import '../../../styles/partials/colors'; 2 | 3 | .ConstituencyPlace { 4 | display: flex; 5 | justify-content: center; 6 | text-align: center; 7 | overflow: hidden; 8 | margin-top: 6px; 9 | margin-bottom: 6px; 10 | font-size: 0.8rem; 11 | &__container { 12 | box-shadow: 0px 0px 3px rgba($color: #000000, $alpha: 0.1); 13 | border-radius: 100px; 14 | padding: 6px 10px; 15 | background-color: rgba($color: $primary-color, $alpha: 0.25); 16 | } 17 | } -------------------------------------------------------------------------------- /components/Candidates/StateRegionCandidateList/StateRegionCandidateList.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import ConstituencyPlace from '../ConstituencyPlace/ConstituencyPlace'; 3 | import CandidateList from '../CandidateList/CandidateList'; 4 | import './StateRegionCandidateList.scss'; 5 | 6 | const StateRegionCandidateList = props => { 7 | const { candidates } = props; 8 | 9 | const [formattedCandidates, setCandidates] = useState([]); 10 | function groupCandidates() { 11 | const nonEthnicCandidates = candidates.filter( 12 | ({ attributes: { representing_ethnicity: representingEthnicity } }) => 13 | !representingEthnicity 14 | ); 15 | 16 | const ethnicCandidates = candidates 17 | .filter( 18 | ({ attributes: { representing_ethnicity: representingEthnicity } }) => 19 | !!representingEthnicity 20 | ) 21 | .sort( 22 | (a, b) => 23 | b.attributes.constituency.attributes.name - 24 | a.attributes.constituency.attributes.name 25 | ); 26 | 27 | return [...nonEthnicCandidates, ...ethnicCandidates].reduce( 28 | (previous, current) => { 29 | const formattedData = { 30 | constituencyId: null, 31 | constituencyName: null, 32 | candidates: [] 33 | }; 34 | 35 | if (previous.length === 0) { 36 | formattedData.constituencyId = current.attributes.constituency.id; 37 | formattedData.constituencyName = 38 | current.attributes.constituency.attributes.name; 39 | formattedData.candidates.push(current); 40 | 41 | previous.push(formattedData); 42 | 43 | return previous; 44 | } 45 | 46 | const lastElement = previous[previous.length - 1]; 47 | 48 | if (lastElement) { 49 | if ( 50 | lastElement.constituencyId === current.attributes.constituency.id 51 | ) { 52 | previous[previous.length - 1].candidates.push(current); 53 | } else { 54 | formattedData.constituencyId = current.attributes.constituency.id; 55 | formattedData.constituencyName = 56 | current.attributes.constituency.attributes.name; 57 | formattedData.candidates.push(current); 58 | 59 | previous.push(formattedData); 60 | } 61 | } 62 | 63 | return previous; 64 | }, 65 | [] 66 | ); 67 | } 68 | useEffect(() => { 69 | const groupedCandidates = groupCandidates(); 70 | setCandidates(groupedCandidates); 71 | }, [candidates]); 72 | 73 | return ( 74 |
75 | {formattedCandidates.map( 76 | ({ 77 | constituencyId, 78 | constituencyName, 79 | candidates: stateRegionCandidates 80 | }) => { 81 | return ( 82 | 83 | 87 | 88 | 89 | ); 90 | } 91 | )} 92 |
93 | ); 94 | }; 95 | 96 | export default StateRegionCandidateList; 97 | -------------------------------------------------------------------------------- /components/Candidates/StateRegionCandidateList/StateRegionCandidateList.scss: -------------------------------------------------------------------------------- 1 | @import '../../../styles/partials/colors'; 2 | 3 | .state-constituency-name { 4 | &:first-child { 5 | display: none; 6 | } 7 | } 8 | 9 | @media (min-width: 1200px) { 10 | .constituency-name { 11 | margin-top: 5px; 12 | margin-bottom: 5px; 13 | } 14 | } -------------------------------------------------------------------------------- /components/Common/Button/Button.js: -------------------------------------------------------------------------------- 1 | import './Button.module.scss'; 2 | 3 | const Button = props => { 4 | return ( 5 | // eslint-disable-next-line react/button-has-type 6 | 9 | ); 10 | }; 11 | 12 | export default Button; 13 | -------------------------------------------------------------------------------- /components/Common/Button/Button.module.scss: -------------------------------------------------------------------------------- 1 | .Button { 2 | border: 0; 3 | background: none; 4 | } -------------------------------------------------------------------------------- /components/Common/Card/Card.js: -------------------------------------------------------------------------------- 1 | import './Card.module.scss'; 2 | 3 | const Card = props => ( 4 |
5 | {props.children} 6 |
7 | ); 8 | 9 | export default Card; 10 | -------------------------------------------------------------------------------- /components/Common/Card/Card.module.scss: -------------------------------------------------------------------------------- 1 | .Card { 2 | // padding: 5px 10px; 3 | min-height: 85px; 4 | // border: 1px solid rgba($color: #000000, $alpha: 0.1); 5 | border-radius: 5px; 6 | // box-shadow: 0px 0px 3px rgba($color: #000000, $alpha: 0.1); 7 | transition: box-shadow 0.1s linear; 8 | &:hover, &:focus, &:active { 9 | // box-shadow: 0px 2px 4px rgba($color: #000000, $alpha: 0.2); 10 | } 11 | } -------------------------------------------------------------------------------- /components/Common/Icons/activeCheckbox.js: -------------------------------------------------------------------------------- 1 | const ActiveCheckbox = ( 2 | 9 | checkbox_fill 10 | 11 | 17 | 18 | 22 | 23 | 24 | 25 | 26 | ); 27 | 28 | export default ActiveCheckbox; 29 | -------------------------------------------------------------------------------- /components/Common/Icons/activeFlag.js: -------------------------------------------------------------------------------- 1 | const ActiveFlag = ( 2 | 9 | flag_fill 10 | 11 | 17 | 18 | 22 | 23 | 24 | 25 | 26 | ); 27 | 28 | export default ActiveFlag; 29 | -------------------------------------------------------------------------------- /components/Common/Icons/activeLightBulb.js: -------------------------------------------------------------------------------- 1 | const ActiveLightBulb = ( 2 | 9 | lightbulb_fill 10 | 11 | 17 | 18 | 22 | 23 | 24 | 25 | 26 | ); 27 | 28 | export default ActiveLightBulb; 29 | -------------------------------------------------------------------------------- /components/Common/Icons/activeNews.js: -------------------------------------------------------------------------------- 1 | const ActiveNews = ( 2 | 9 | news_fill 10 | 11 | 17 | 18 | 22 | 23 | 24 | 25 | 26 | ); 27 | 28 | export default ActiveNews; 29 | -------------------------------------------------------------------------------- /components/Common/Icons/activePeople.js: -------------------------------------------------------------------------------- 1 | const ActivePeople = ( 2 | 9 | people_fill 10 | 11 | 17 | 18 | 22 | 23 | 24 | 25 | 26 | ); 27 | 28 | export default ActivePeople; 29 | -------------------------------------------------------------------------------- /components/Common/Icons/activeStar.js: -------------------------------------------------------------------------------- 1 | const ActiveStar = ( 2 | 9 | star_fill 10 | 11 | 17 | 18 | 22 | 23 | 24 | 25 | 26 | ); 27 | 28 | export default ActiveStar; 29 | -------------------------------------------------------------------------------- /components/Common/Icons/checkbox.js: -------------------------------------------------------------------------------- 1 | const CheckboxIcon = ( 2 | 9 | checkbox 10 | 11 | 17 | 18 | 22 | 23 | 24 | 25 | 26 | ); 27 | 28 | export default CheckboxIcon; 29 | -------------------------------------------------------------------------------- /components/Common/Icons/clock4PM.js: -------------------------------------------------------------------------------- 1 | const Clock4PM = () => ( 2 | 9 | clock_4pm 10 | 11 | 17 | 18 | 19 | 20 | 21 | 25 | 26 | 27 | 28 | 36 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | ); 55 | 56 | export default Clock4PM; 57 | -------------------------------------------------------------------------------- /components/Common/Icons/clock6AM.js: -------------------------------------------------------------------------------- 1 | const Clock6AM = () => ( 2 | 9 | clock_6am 10 | 11 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 33 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | ); 52 | 53 | export default Clock6AM; 54 | -------------------------------------------------------------------------------- /components/Common/Icons/flag.js: -------------------------------------------------------------------------------- 1 | const FlagIcon = ( 2 | 9 | flag 10 | 11 | 17 | 18 | 22 | 23 | 24 | 25 | 26 | ); 27 | 28 | export default FlagIcon; 29 | -------------------------------------------------------------------------------- /components/Common/Icons/flagFill.js: -------------------------------------------------------------------------------- 1 | const FlagFillIcon = () => ( 2 | 9 | flag_circle_fill 10 | 19 | 24 | 25 | 26 | 27 | 􀋌 28 | 29 | 30 | 31 | 32 | 33 | 34 | ); 35 | 36 | export default FlagFillIcon; 37 | -------------------------------------------------------------------------------- /components/Common/Icons/gavel.js: -------------------------------------------------------------------------------- 1 | const GavelIcon = ({ fill = '#0071dd' }) => ( 2 | 3 | gavel 4 | 5 | 10 | 11 | 12 | 16 | 17 | 18 | 22 | 23 | 27 | 31 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | ); 42 | 43 | export default GavelIcon; 44 | -------------------------------------------------------------------------------- /components/Common/Icons/lightbulb.js: -------------------------------------------------------------------------------- 1 | const LightBulbIcon = ( 2 | 3 | lightbulb 4 | 5 | 11 | 12 | 16 | 17 | 18 | 19 | 20 | ); 21 | 22 | export default LightBulbIcon; 23 | -------------------------------------------------------------------------------- /components/Common/Icons/news.js: -------------------------------------------------------------------------------- 1 | const NewsIcon = ( 2 | 9 | news 10 | 11 | 17 | 18 | 22 | 23 | 24 | 25 | 26 | ); 27 | 28 | export default NewsIcon; 29 | -------------------------------------------------------------------------------- /components/Common/Icons/people.js: -------------------------------------------------------------------------------- 1 | const PeopleIcon = ( 2 | 9 | people 10 | 11 | 17 | 18 | 22 | 23 | 24 | 25 | 26 | ); 27 | 28 | export default PeopleIcon; 29 | -------------------------------------------------------------------------------- /components/Common/Icons/star.js: -------------------------------------------------------------------------------- 1 | const StarIcon = ( 2 | 9 | star 10 | 11 | 17 | 18 | 22 | 23 | 24 | 25 | 26 | ); 27 | 28 | export default StarIcon; 29 | -------------------------------------------------------------------------------- /components/Common/Link/Link.js: -------------------------------------------------------------------------------- 1 | import { withRouter } from 'next/router'; 2 | import Link from 'next/link'; 3 | import React, { Children } from 'react'; 4 | 5 | const ActiveLink = ({ router, children, ...props }) => { 6 | const child = Children.only(children); 7 | 8 | let className = child.props.className || null; 9 | 10 | if (router.pathname === props.href && props.activeClassName) { 11 | className = `${className !== null ? className : ''} ${ 12 | props.activeClassName 13 | }`.trim(); 14 | } 15 | 16 | // eslint-disable-next-line no-param-reassign 17 | delete props.activeClassName; 18 | 19 | return {React.cloneElement(child, { className })}; 20 | }; 21 | 22 | export default withRouter(ActiveLink); 23 | -------------------------------------------------------------------------------- /components/Common/Modal/Modal.js: -------------------------------------------------------------------------------- 1 | import ReactModal from 'react-modal'; 2 | 3 | ReactModal.setAppElement('#app'); 4 | 5 | export default function Modal(props) { 6 | const { children, isOpen = false } = props; 7 | 8 | return ( 9 | 20 | {children} 21 | 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /components/Common/Prompt/Prompt.js: -------------------------------------------------------------------------------- 1 | import './Prompt.scss'; 2 | 3 | const Prompt = props => { 4 | const { children, onClose } = props; 5 | 6 | return ( 7 |
8 |
{children}
9 |
10 | 15 |
16 |
17 | ); 18 | }; 19 | 20 | export default Prompt; 21 | -------------------------------------------------------------------------------- /components/Common/Prompt/Prompt.scss: -------------------------------------------------------------------------------- 1 | @import '../../../styles/partials/colors'; 2 | 3 | .Prompt { 4 | position: relative; 5 | background-color: $primary-color; 6 | border-radius: 8px; 7 | padding: 18px; 8 | &__children { 9 | line-height: 28px; 10 | } 11 | &__close { 12 | position: absolute; 13 | } 14 | &__buttons { 15 | button { 16 | padding: 0; 17 | border: 0; 18 | border-radius: 100%; 19 | background-color: transparent; 20 | vertical-align: bottom; 21 | color: $white-color; 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /components/Common/Tabs/Tab.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Tab = React.forwardRef((props, ref) => { 4 | const { title, value, index, active, onClick } = props; 5 | return ( 6 |
onClick(value, index)} 9 | className={` ${active ? 'tab-active' : ''}`} 10 | > 11 |
{title}
12 |
13 | ); 14 | }); 15 | 16 | export default Tab; 17 | -------------------------------------------------------------------------------- /components/Common/Tabs/TabPanel.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | import TabSlider from './TabSlider'; 3 | 4 | import './TabPanel.module.scss'; 5 | 6 | class TabPanel extends PureComponent { 7 | state = { 8 | computedSliderWidth: 0, 9 | sliderPosition: 0, 10 | activeTabIndex: 0 11 | }; 12 | 13 | componentDidMount() { 14 | this.setSliderPosition(); 15 | } 16 | 17 | setSliderPosition = (sliderIndex = 0) => { 18 | const tabX = this.refs[`tab${sliderIndex}`].getBoundingClientRect().x; 19 | const tabPanelX = this.refs.TabPanel.getBoundingClientRect().x; 20 | 21 | this.setState({ 22 | activeTabIndex: sliderIndex, 23 | computedSliderWidth: Math.floor( 24 | this.refs[`tab${sliderIndex}`].getBoundingClientRect().width 25 | ), 26 | sliderPosition: tabX - tabPanelX 27 | }); 28 | }; 29 | 30 | moveSlider = sliderIndex => { 31 | this.setSliderPosition(sliderIndex); 32 | }; 33 | 34 | onClickTab = (value, index) => { 35 | this.moveSlider(index); 36 | this.props.onClickTab(value, index); 37 | }; 38 | 39 | render() { 40 | const tabs = this.props.children; 41 | return ( 42 | <> 43 |
44 |
45 | {React.Children.map(tabs, (child, index) => 46 | React.cloneElement(child, { 47 | index, 48 | ref: `tab${index}`, 49 | onClick: this.onClickTab, 50 | active: index === this.state.activeTabIndex 51 | }) 52 | )} 53 |
54 | 58 |
59 | {/* This is rather a dirty hack */} 60 |
61 | {React.Children.map(tabs, (child, index) => ( 62 |
67 | {child.props.children} 68 |
69 | ))} 70 |
71 | 72 | ); 73 | } 74 | } 75 | 76 | export default TabPanel; 77 | -------------------------------------------------------------------------------- /components/Common/Tabs/TabPanel.module.scss: -------------------------------------------------------------------------------- 1 | .TabPanel { 2 | position: relative; 3 | padding: 10px 0; 4 | &__Tabs { 5 | display: flex; 6 | justify-content: space-evenly; 7 | align-items: center; 8 | } 9 | .tab-active { 10 | color: #0071dd; 11 | } 12 | } -------------------------------------------------------------------------------- /components/Common/Tabs/TabSlider.js: -------------------------------------------------------------------------------- 1 | import './TabSlider.module.scss'; 2 | 3 | const TabSlider = ({ left, width = 100 }) => { 4 | return ( 5 |
9 | ); 10 | }; 11 | 12 | export default TabSlider; 13 | -------------------------------------------------------------------------------- /components/Common/Tabs/TabSlider.module.scss: -------------------------------------------------------------------------------- 1 | @import '../../../styles/partials/colors'; 2 | 3 | .slider { 4 | position: absolute; 5 | height: 3px; 6 | padding-top: 4px; 7 | border-radius: 10px; 8 | background: $primary-color; 9 | transition: all 0.25s ease-in-out; 10 | } -------------------------------------------------------------------------------- /components/Common/Tabs/index.js: -------------------------------------------------------------------------------- 1 | import TabPanel from './TabPanel'; 2 | import Tab from './Tab'; 3 | 4 | export default { 5 | TabPanel, 6 | Tab 7 | }; 8 | -------------------------------------------------------------------------------- /components/Countdown/ElectionCountdown.js: -------------------------------------------------------------------------------- 1 | // TODO: PROPERLY FIX COUNTDOWN LOGIC DOWN TO MS PRECISION 2 | import myanmarNumbers from 'myanmar-numbers'; 3 | import { useEffect, useState } from 'react'; 4 | import { handleCountdown } from '../../utils/helpers'; 5 | import './ElectionCountdown.scss'; 6 | 7 | const ElectionCountdown = () => { 8 | const [time, setTime] = useState(); 9 | const [timeType, setTimeType] = useState(''); 10 | 11 | useEffect(() => { 12 | handleCountdown((value, type) => { 13 | setTime(value); 14 | setTimeType(type); 15 | }); 16 | }, []); 17 | 18 | function formatTime(timeStr) { 19 | const splitStr = timeStr.split('-'); 20 | return `${myanmarNumbers(splitStr[0], 'my')}:${myanmarNumbers( 21 | splitStr[1], 22 | 'my' 23 | )}:${myanmarNumbers(splitStr[2], 'my')}`; 24 | } 25 | 26 | return ( 27 |
28 |
29 | {timeType === 'day' && ( 30 | 31 | ရွေးကောက်ပွဲကျင်းပရန် 32 |
33 | 34 | {myanmarNumbers(time, 'my')} 35 | 36 |
ရက်သာ လိုတော့သည် 37 |
38 | )} 39 | {timeType === 'over' && ( 40 | 41 | မဲရုံများပိတ်သွားပါပြီ 42 | 43 | )} 44 | {timeType === 'start' && ( 45 | 46 | ရွေးကောက်ပွဲကျင်းပရန်
47 | 48 | {formatTime(time)} 49 | 50 |
51 | သာလိုတော့သည် 52 |
53 | )} 54 | {timeType === 'close' && ( 55 | 56 | မဲရုံများပိတ်ရန်
57 | 58 | {formatTime(time)} 59 | 60 |
61 | သာလိုတော့သည် 62 |
63 | )} 64 |
65 |
66 | ); 67 | }; 68 | 69 | export default ElectionCountdown; 70 | -------------------------------------------------------------------------------- /components/Countdown/ElectionCountdown.scss: -------------------------------------------------------------------------------- 1 | .countdown-text { 2 | font-size: 1.5rem; 3 | } -------------------------------------------------------------------------------- /components/Faq/FaqItem/FaqItem.js: -------------------------------------------------------------------------------- 1 | import { useState, useRef } from 'react'; 2 | 3 | import './FaqItem.module.scss'; 4 | 5 | const textLength = 120; 6 | 7 | const FaqItem = props => { 8 | const { 9 | faq: { 10 | attributes: { question, answer } 11 | } 12 | } = props; 13 | 14 | const answerRef = useRef(null); 15 | const [shouldAnswerTruncate, setAnswerTruncate] = useState(true); 16 | 17 | function onClickSeeMore() { 18 | setAnswerTruncate(!shouldAnswerTruncate); 19 | } 20 | 21 | return ( 22 |
  • 23 |
    24 |
    {question}
    25 |
    26 | 27 | {answer.length > textLength && shouldAnswerTruncate ? ( 28 |
    See more') 35 | }} 36 | /> 37 | ) : ( 38 | 43 | )} 44 | 45 |
    46 |
    47 |
  • 48 | ); 49 | }; 50 | 51 | export default FaqItem; 52 | -------------------------------------------------------------------------------- /components/Faq/FaqItem/FaqItem.module.scss: -------------------------------------------------------------------------------- 1 | @import '../../../styles/partials/colors'; 2 | 3 | .FaqItem { 4 | &__question { 5 | color: $primary-color; 6 | font-size: 1.15rem; 7 | line-height: 28px; 8 | margin-bottom: 6px; 9 | } 10 | &__answer { 11 | display: block; 12 | } 13 | } 14 | 15 | .seeMore { 16 | color: $grey-color; 17 | } -------------------------------------------------------------------------------- /components/Faq/FaqList/FaqList.js: -------------------------------------------------------------------------------- 1 | // Wrapper component for FAQ List 2 | import FaqItem from '../FaqItem/FaqItem'; 3 | import './FaqList.scss'; 4 | 5 | const FaqList = ({ faqs = [] }) => { 6 | return ( 7 |
      8 | {faqs.map((faq, index) => ( 9 | 10 | ))} 11 |
    12 | ); 13 | }; 14 | 15 | export default FaqList; 16 | -------------------------------------------------------------------------------- /components/Faq/FaqList/FaqList.scss: -------------------------------------------------------------------------------- 1 | .FaqList { 2 | list-style: none; 3 | margin: 0; 4 | padding: 0; 5 | } -------------------------------------------------------------------------------- /components/Layout/AppHeader/AppHeader.js: -------------------------------------------------------------------------------- 1 | import './AppHeader.module.scss'; 2 | 3 | const AppHeader = props => { 4 | return ( 5 |
    6 | {props.children} 7 |
    8 | ); 9 | }; 10 | 11 | export default AppHeader; 12 | -------------------------------------------------------------------------------- /components/Layout/AppHeader/AppHeader.module.scss: -------------------------------------------------------------------------------- 1 | .AppHeader { 2 | height: 54px; 3 | display: flex; 4 | align-items: center; 5 | justify-content: space-between; 6 | 7 | z-index: 100; 8 | } -------------------------------------------------------------------------------- /components/Layout/Layout.js: -------------------------------------------------------------------------------- 1 | import './Layout.module.scss'; 2 | import Navigation from '../Navigation/Navigation'; 3 | 4 | const Layout = props => { 5 | const { children, shouldHideBottomNav = false, ...other } = props; 6 | 7 | return ( 8 |
    9 | {/*
    10 | mVoterLogo 11 |
    */} 12 |
    13 |
    14 |
    15 | 16 |
    17 |
    18 |
    19 | {children} 20 |
    21 |
    22 |
    23 |
    24 | {!shouldHideBottomNav && ( 25 |
    26 | 27 |
    28 | )} 29 | 51 |
    52 | ); 53 | }; 54 | 55 | export default Layout; 56 | -------------------------------------------------------------------------------- /components/Layout/Layout.module.scss: -------------------------------------------------------------------------------- 1 | .Layout { 2 | &__wrapper { 3 | position: relative; 4 | padding-bottom: 80px; 5 | } 6 | } 7 | 8 | @media (min-width: 1200px) { 9 | .Layout { 10 | &__desktopLogo { 11 | img { 12 | display: block; 13 | margin: 0 auto; 14 | width: auto; 15 | height: 52px; 16 | } 17 | } 18 | &__wrapper { 19 | padding-bottom: 10px; 20 | } 21 | &__sideNav { 22 | position: relative; 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /components/Location/TownshipModal.js: -------------------------------------------------------------------------------- 1 | // TODO: Refactor this component to be able to use as a reusable collapse in the future 2 | import { useEffect, useState } from 'react'; 3 | import { AiOutlineLoading } from 'react-icons/ai'; 4 | import Collapsible from 'react-collapsible'; 5 | import Modal from '../Common/Modal/Modal'; 6 | 7 | import './locationCollapse.scss'; 8 | import useAPI from '../../hooks/useAPI'; 9 | 10 | const TownshipModal = props => { 11 | const { isModalOpen, setModalOpen, onClickTownship } = props; 12 | const [stateRegions, setStateRegions] = useState([]); 13 | const [townships, setTownships] = useState([]); 14 | const [, fetchData] = useAPI(); 15 | 16 | async function fetchStateRegions() { 17 | const response = await fetchData('/api/locations', { 18 | type: 'state_regions' 19 | }); 20 | 21 | setStateRegions(response.data); 22 | } 23 | 24 | useEffect(() => { 25 | if (isModalOpen) { 26 | fetchStateRegions(); 27 | } 28 | }, [isModalOpen]); 29 | 30 | async function fetchTownships(stateRegion) { 31 | // Use cached values if it is already loaded 32 | const townshipsLoaded = 33 | townships.findIndex(({ stateRegion: sr }) => sr === stateRegion) > -1; 34 | 35 | if (townshipsLoaded) return; 36 | 37 | const { data } = await fetchData('/api/locations', { 38 | type: 'townships', 39 | state_region: stateRegion 40 | }); 41 | 42 | const clonedTownships = [...townships]; 43 | 44 | clonedTownships.push({ 45 | stateRegion, 46 | townships: data 47 | }); 48 | 49 | setTownships(clonedTownships); 50 | } 51 | 52 | function renderTownships(stateRegion) { 53 | const containedTownships = townships.find( 54 | ({ stateRegion: sr }) => sr === stateRegion 55 | ); 56 | 57 | if (containedTownships) { 58 | return containedTownships.townships.map(township => ( 59 |
    onClickTownship(stateRegion, township)} 63 | > 64 | {township} 65 |
    66 | )); 67 | } 68 | return ; 69 | } 70 | 71 | return ( 72 |
    73 | setModalOpen(false)} 77 | > 78 |
    မြို့နယ်ရွေးပါ
    79 | {stateRegions.map((stateRegion, srIndex) => ( 80 | fetchTownships(stateRegion)} 85 | > 86 | {renderTownships(stateRegion)} 87 | 88 | ))} 89 |
    90 |
    91 | ); 92 | }; 93 | 94 | export default TownshipModal; 95 | -------------------------------------------------------------------------------- /components/Location/WardVillageModal.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | import Modal from '../Common/Modal/Modal'; 3 | import useAPI from '../../hooks/useAPI'; 4 | 5 | const WardVillageModal = props => { 6 | const { 7 | isModalOpen, 8 | setModalOpen, 9 | stateRegion, 10 | township, 11 | onClickWardVillage 12 | } = props; 13 | 14 | const [wardVillages, setWardVillages] = useState([]); 15 | const [, fetchData] = useAPI(); 16 | 17 | async function fetchWardVillage() { 18 | const { data } = await fetchData('/api/locations', { 19 | type: 'wards', 20 | state_region: stateRegion, 21 | township 22 | }); 23 | 24 | setWardVillages(data); 25 | } 26 | 27 | useEffect(() => { 28 | if (stateRegion && township) { 29 | fetchWardVillage(); 30 | } 31 | }, [stateRegion, township]); 32 | 33 | return ( 34 | setModalOpen(false)} 38 | > 39 |
    ရပ်ကွက်/ကျေးရွာအုပ်စု ရွေးပါ
    40 |
    {township}
    41 | {wardVillages.map(ward => ( 42 |
    onClickWardVillage(ward)} 46 | > 47 | {ward} 48 |
    49 | ))} 50 |
    51 | ); 52 | }; 53 | 54 | export default WardVillageModal; 55 | -------------------------------------------------------------------------------- /components/Location/locationCollapse.scss: -------------------------------------------------------------------------------- 1 | @import '../../styles/partials/colors'; 2 | 3 | %header { 4 | color: $primary-color; 5 | font-weight: 500; 6 | } 7 | 8 | .location-ward-village-header { 9 | @extend %header; 10 | margin: 12px 0; 11 | } 12 | 13 | .location-child { 14 | padding: 5px 15px; 15 | margin: 5px 0; 16 | } 17 | 18 | .Collapsible { 19 | margin: 12px 0; 20 | cursor: pointer; 21 | } 22 | 23 | .Collapsible__trigger { 24 | @extend %header; 25 | } 26 | 27 | .location-modal { 28 | position: absolute; 29 | top: 40px; 30 | left: 40px; 31 | right: 40px; 32 | bottom: 40px; 33 | border: 1px solid rgb(204, 204, 204); 34 | background: rgb(255, 255, 255); 35 | overflow: auto; 36 | border-radius: 4px; 37 | outline: none; 38 | padding: 20px; 39 | } 40 | 41 | @media (min-width: 1200px) { 42 | .location-modal { 43 | top: 50%; 44 | left: 50%; 45 | transform: translateX(-50%) translateY(-50%); 46 | max-width: 380px; 47 | max-height: 80%; 48 | } 49 | } -------------------------------------------------------------------------------- /components/Navigation/Navigation.js: -------------------------------------------------------------------------------- 1 | import { useRouter } from 'next/router'; 2 | 3 | import Link from '../Common/Link/Link'; 4 | import PeopleIcon from '../Common/Icons/people'; 5 | import FlagIcon from '../Common/Icons/flag'; 6 | import CheckboxIcon from '../Common/Icons/checkbox'; 7 | import LightBulbIcon from '../Common/Icons/lightbulb'; 8 | import NewsIcon from '../Common/Icons/news'; 9 | import ActivePeopleIcon from '../Common/Icons/activePeople'; 10 | import ActiveFlagIcon from '../Common/Icons/activeFlag'; 11 | import ActiveLightBulbIcon from '../Common/Icons/activeLightBulb'; 12 | import ActiveCheckboxIcon from '../Common/Icons/activeCheckbox'; 13 | import ActiveNewsIcon from '../Common/Icons/activeNews'; 14 | 15 | import './Navigation.module.scss'; 16 | 17 | const Navigation = () => { 18 | const router = useRouter(); 19 | const currentPath = router.pathname; 20 | 21 | const NavComponent = ({ link, activeIcon, inActiveIcon, text }) => { 22 | const isSamePath = 23 | currentPath.split('/').indexOf(link.replace('/', '')) > -1; 24 | 25 | if (isSamePath) { 26 | return ( 27 |
    28 | {activeIcon} 29 | {text} 30 |
    31 | ); 32 | } 33 | return ( 34 |
    35 | {inActiveIcon} 36 | {text} 37 |
    38 | ); 39 | }; 40 | 41 | return ( 42 | 109 | ); 110 | }; 111 | 112 | export default Navigation; 113 | -------------------------------------------------------------------------------- /components/Navigation/Navigation.module.scss: -------------------------------------------------------------------------------- 1 | @import '../../styles/partials/colors'; 2 | 3 | .Navigation { 4 | font-size: 0.85rem; 5 | padding-top: 12px; 6 | padding-bottom: 12px; 7 | padding-left: 5px; 8 | padding-right: 5px; 9 | width: 100%; 10 | position: fixed; 11 | left: 0; 12 | bottom: 0; 13 | border-top: 1px solid rgba($color: #000000, $alpha: 0.1); 14 | background: #ffffff; 15 | &__mVoterLogo { 16 | display: none; 17 | } 18 | ul { 19 | display: flex; 20 | justify-content: space-around; 21 | align-items: center; 22 | list-style: none; 23 | margin: 0; 24 | padding: 0; 25 | } 26 | li { 27 | position: relative; 28 | width: 100%; 29 | height: 48px; 30 | color: #999999; 31 | margin: 0; 32 | padding: 0; 33 | font-size: .8rem; 34 | text-align: center; 35 | &:hover { 36 | cursor: pointer; 37 | } 38 | .active { 39 | color: $primary-color; 40 | } 41 | .text { 42 | position: absolute; 43 | bottom: 0; 44 | left: 50%; 45 | transform: translateX(-50%); 46 | width: 100%; 47 | } 48 | } 49 | } 50 | 51 | @media (min-width: 1200px) { 52 | %linkDivRules { 53 | width: 100%; 54 | display: flex; 55 | height: auto; 56 | svg { 57 | width: 30px; 58 | } 59 | } 60 | .Navigation { 61 | width: 255px; 62 | display: flex; 63 | align-content: center; 64 | margin: 0 auto; 65 | height: 100%; 66 | border-top: none; 67 | padding-top: 2px; 68 | left: unset; 69 | &__mVoterLogo { 70 | display: block; 71 | img { 72 | display: block; 73 | margin: 0 auto; 74 | width: auto; 75 | height: 64px; 76 | } 77 | } 78 | ul { 79 | display: inline-block; 80 | // position: fixed; 81 | margin: 0 auto; 82 | } 83 | li { 84 | width: 180px; 85 | text-align: left; 86 | display: flex; 87 | align-items: center; 88 | transition: color 0.2s linear; 89 | div.active { 90 | width: 100%; 91 | display: flex; 92 | padding: 10px; 93 | height: auto; 94 | background-color: $primary-color; 95 | border-radius: 5px; 96 | svg { 97 | g { 98 | fill: #ffffff; 99 | } 100 | } 101 | } 102 | span.active { 103 | color: #ffffff; 104 | } 105 | > div { 106 | @extend %linkDivRules; 107 | } 108 | div.inactive { 109 | width: 100%; 110 | display: flex; 111 | padding: 10px; 112 | &:hover { 113 | color: rgba($color: $primary-color, $alpha: 0.85); 114 | g { 115 | fill: rgba($color: $primary-color, $alpha: 0.85); 116 | } 117 | } 118 | } 119 | .text { 120 | position: relative; 121 | display: inline-block; 122 | font-size: 1rem; 123 | } 124 | } 125 | } 126 | } -------------------------------------------------------------------------------- /components/News/NewsItem/NewsItem.js: -------------------------------------------------------------------------------- 1 | import { formatPublishDateToMMLocale } from '../../../utils/textFormatter'; 2 | import './NewsItem.scss'; 3 | 4 | const Article = props => { 5 | const { 6 | news: { 7 | attributes: { title, summary, published_date: publishedDate, url } 8 | } 9 | } = props; 10 | 11 | return ( 12 | 18 |
    19 |
    20 |

    {title}

    21 |

    {summary} ...

    22 |

    23 | {formatPublishDateToMMLocale(publishedDate)} 24 |

    25 |
    26 |
    27 | UEC Logo 28 |
    29 |
    30 |
    31 | ); 32 | }; 33 | 34 | export default Article; 35 | -------------------------------------------------------------------------------- /components/News/NewsItem/NewsItem.scss: -------------------------------------------------------------------------------- 1 | @import '../../../styles/partials/fontsizes'; 2 | @import '../../../styles/partials/colors'; 3 | 4 | .Article { 5 | width: 100%; 6 | display: flex; 7 | justify-content: space-between; 8 | h1 { 9 | line-height: 28px; 10 | } 11 | &__heading { 12 | font-weight: 700; 13 | font-size: $medium; 14 | } 15 | &__date { 16 | color: $grey-color; 17 | } 18 | &__newsGroup { 19 | margin-right: 10px; 20 | } 21 | &__summary { 22 | margin-bottom: 5px; 23 | } 24 | &__UECLogo { 25 | >img { 26 | width: auto; 27 | height: 65px; 28 | } 29 | } 30 | } 31 | 32 | .article-link { 33 | text-decoration: none; 34 | color: inherit; 35 | } 36 | -------------------------------------------------------------------------------- /components/News/NewsList/NewsList.js: -------------------------------------------------------------------------------- 1 | import NewsItem from '../NewsItem/NewsItem'; 2 | 3 | import './NewsList.scss'; 4 | 5 | const NewsList = ({ news = [] }) => { 6 | return ( 7 |
      8 | {news.map(newsItem => ( 9 | 10 | ))} 11 |
    12 | ); 13 | }; 14 | 15 | export default NewsList; 16 | -------------------------------------------------------------------------------- /components/News/NewsList/NewsList.scss: -------------------------------------------------------------------------------- 1 | .NewsList { 2 | list-style: none; 3 | margin: 0; 4 | padding: 0; 5 | } -------------------------------------------------------------------------------- /components/Parties/PartyItem/PartyItem.js: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | import Card from '../../Common/Card/Card'; 3 | 4 | import './PartyItem.module.scss'; 5 | 6 | const PartyCard = props => { 7 | const { 8 | party: { 9 | id, 10 | attributes: { seal_image: sealImage, name_burmese: nameBurmese, region } 11 | } 12 | } = props; 13 | 14 | const sealImageStyle = { 15 | width: 64, 16 | height: 64, 17 | backgroundImage: `url("${sealImage}")` 18 | }; 19 | 20 | return ( 21 |
    22 | 23 | 24 | 25 | 34 | ); 35 | }; 36 | 37 | export default PartyCard; 38 | -------------------------------------------------------------------------------- /components/Parties/PartyItem/PartyItem.module.scss: -------------------------------------------------------------------------------- 1 | .PartyItem { 2 | width: 100%; 3 | &__Card { 4 | display: flex; 5 | align-items: center; 6 | } 7 | &__image { 8 | background-position: center center; 9 | background-repeat: no-repeat; 10 | background-size: contain; 11 | } 12 | &__Description { 13 | margin-left: 10px; 14 | .name { 15 | margin-bottom: 5px; 16 | } 17 | } 18 | } 19 | 20 | .constituency { 21 | font-size: 0.9rem; 22 | color: rgba($color: #000000, $alpha: 0.5); 23 | } 24 | 25 | @media (min-width: 1200px) { 26 | .PartyItem { 27 | &__Card { 28 | padding: 0 15px; 29 | border: 1px solid rgba($color: #000000, $alpha: 0.1); 30 | } 31 | &__Description { 32 | .name { 33 | line-height: 25px; 34 | margin-bottom: 0; 35 | } 36 | } 37 | } 38 | } -------------------------------------------------------------------------------- /components/Parties/PartyList/PartyList.js: -------------------------------------------------------------------------------- 1 | import PartyItem from '../PartyItem/PartyItem'; 2 | import './PartyList.scss'; 3 | 4 | const PartyList = ({ parties = [] }) => { 5 | return ( 6 |
    7 |
      8 | {parties.map((party, index) => ( 9 |
    • 10 | 11 |
    • 12 | ))} 13 |
    14 |
    15 | ); 16 | }; 17 | 18 | export default PartyList; 19 | -------------------------------------------------------------------------------- /components/Parties/PartyList/PartyList.scss: -------------------------------------------------------------------------------- 1 | .PartyList { 2 | list-style: none; 3 | margin: 0; 4 | padding: 0; 5 | overflow: hidden; 6 | } 7 | 8 | @media (min-width: 1200px) { 9 | .PartyList { 10 | &__itemWrapper { 11 | padding-top: 5px; 12 | &:nth-of-type(2n+1) { 13 | padding-right: 5px; 14 | } 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /components/Search/SearchPage.js: -------------------------------------------------------------------------------- 1 | import ReactGA from 'react-ga'; 2 | import { useRouter } from 'next/router'; 3 | import InfiniteScroll from 'react-infinite-scroll-component'; 4 | import { AiOutlineLoading } from 'react-icons/ai'; 5 | import React, { useRef, useCallback, useState, useEffect } from 'react'; 6 | import Head from 'next/head'; 7 | import Layout from '../Layout/Layout'; 8 | import Button from '../Common/Button/Button'; 9 | import AppHeader from '../Layout/AppHeader/AppHeader'; 10 | import { debounce } from '../../utils/helpers'; 11 | 12 | import './SearchPage.scss'; 13 | import useAPI from '../../hooks/useAPI'; 14 | 15 | const SearchPage = props => { 16 | const { 17 | type = 'candidates', 18 | endpoint, 19 | children, 20 | inputPlaceholder = 'ရှာဖွေလိုသော အမည်ကို ရိုက်ထည့်ပါ', 21 | emptyPlaceholder = '', 22 | notFoundPlaceholder = '' 23 | } = props; 24 | 25 | const router = useRouter(); 26 | const [list, setList] = useState(null); 27 | const [page, setPage] = useState(1); 28 | const [searchString, setSearchString] = useState(''); 29 | const [hasMore, setHasMore] = useState(true); 30 | const [loading, fetchData] = useAPI(); 31 | const searchInputRef = useRef(null); 32 | 33 | useEffect(() => { 34 | ReactGA.pageview(window.location.pathname); 35 | searchInputRef.current.focus(); 36 | }, []); 37 | 38 | async function apiCall(value) { 39 | const arr = list ?? []; 40 | 41 | if (page === 1) { 42 | setList(null); 43 | } 44 | 45 | const { data } = await fetchData(`/api/${endpoint}`, { 46 | page, 47 | query: value || searchString, 48 | item_per_page: 25 49 | }); 50 | 51 | if (data.length === 0) { 52 | setHasMore(false); 53 | } else { 54 | setHasMore(true); 55 | } 56 | 57 | setPage(page + 1); 58 | 59 | return setList(arr.concat(data)); 60 | } 61 | 62 | const debouncedCall = useCallback( 63 | debounce(value => apiCall(value), 500), 64 | [] 65 | ); 66 | 67 | function loadMoreData() { 68 | apiCall(); 69 | } 70 | 71 | function onChangeSearch(e) { 72 | const { 73 | target: { value } 74 | } = e; 75 | setSearchString(value); 76 | debouncedCall(value); 77 | } 78 | 79 | return ( 80 | 81 | 82 | mVoter 2020 83 | 84 | 85 | 90 |
    91 | search 92 | 100 |
    101 |
    102 |
    103 |
    104 |
    105 | {!list && !loading && ( 106 |

    107 | {emptyPlaceholder} 108 |

    109 | )} 110 | {list && list.length === 0 && !loading && ( 111 |

    112 | {notFoundPlaceholder} 113 |

    114 | )} 115 | {loading && ( 116 | 117 | )} 118 | 123 | {React.Children.map(children, child => 124 | React.cloneElement(child, { [type]: list ?? [] }) 125 | )} 126 | 127 |
    128 |
    129 |
    130 |
    131 | ); 132 | }; 133 | 134 | export default SearchPage; 135 | -------------------------------------------------------------------------------- /components/Search/SearchPage.scss: -------------------------------------------------------------------------------- 1 | .search-input-group { 2 | width: 90%; 3 | display: flex; 4 | align-items: center; 5 | > i { 6 | color: rgba($color: #000000, $alpha: 0.5); 7 | display: inline-block; 8 | margin-right: 5px; 9 | } 10 | } 11 | 12 | .search-input { 13 | border-top: 0; 14 | border-left: 0; 15 | border-right: 0; 16 | float: right; 17 | border-bottom: 1px solid rgba($color: #000000, $alpha: 0.25); 18 | outline: 0; 19 | width: 100%; 20 | padding-bottom: 5px; 21 | } 22 | 23 | .search-page-loader { 24 | font-size: 2rem; 25 | display: block; 26 | margin: 0 auto; 27 | } 28 | -------------------------------------------------------------------------------- /context/AuthProvider.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import nookies, { parseCookies } from 'nookies'; 3 | import { createContext, useContext, useEffect, useState } from 'react'; 4 | 5 | const AuthContext = createContext({ 6 | token: null, 7 | updateToken: () => {} 8 | }); 9 | 10 | export function useAuthContext() { 11 | return useContext(AuthContext); 12 | } 13 | 14 | const AuthProvider = props => { 15 | const { children } = props; 16 | 17 | const [token, setToken] = useState(null); 18 | 19 | useEffect(() => { 20 | // TODO: Handle invalid logic 21 | async function fetchToken() { 22 | // Check token first 23 | const cookies = parseCookies(); 24 | 25 | if (cookies.token) { 26 | setToken(cookies.token); 27 | return; 28 | } 29 | 30 | const { 31 | data: { token: apiToken } 32 | } = await axios.get('/api/auth', { 33 | method: 'POST' 34 | }); 35 | 36 | nookies.set(null, 'token', apiToken, { 37 | path: '/' 38 | }); 39 | 40 | if (apiToken) { 41 | setToken(apiToken); 42 | } 43 | } 44 | 45 | fetchToken(); 46 | }, []); 47 | 48 | function updateToken(apiToken) { 49 | nookies.set(null, 'token', apiToken, { 50 | path: '/' 51 | }); 52 | setToken(apiToken); 53 | } 54 | 55 | return ( 56 | 57 | {children} 58 | 59 | ); 60 | }; 61 | 62 | export default AuthProvider; 63 | -------------------------------------------------------------------------------- /gateway/api.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | import axios from 'axios'; 3 | import { fetchToken } from '../pages/api/auth'; 4 | 5 | class MaePaySohAPI { 6 | constructor(token) { 7 | const axiosInstance = axios.create({ 8 | baseURL: process.env.BASE_URL, 9 | timeout: 10000 10 | }); 11 | 12 | axiosInstance.interceptors.request.use( 13 | async config => { 14 | if (token) { 15 | // eslint-disable-next-line no-param-reassign 16 | config.headers['api-token'] = token; 17 | } 18 | 19 | return config; 20 | }, 21 | error => { 22 | return Promise.reject(error); 23 | } 24 | ); 25 | 26 | axiosInstance.interceptors.response.use( 27 | response => { 28 | return response; 29 | }, 30 | // eslint-disable-next-line func-names 31 | async function(error) { 32 | if (error.response.status !== 401) { 33 | return Promise.reject(error); 34 | } 35 | 36 | const { config } = error; 37 | 38 | if (error.response.status === 401 && !error.response.retry) { 39 | // Token key not authorized for use 40 | const apiToken = await fetchToken(); // This is signed 41 | config.headers['api-token'] = apiToken; 42 | 43 | const response = await axios.request(config); 44 | 45 | response.retry = true; 46 | response.data.token = apiToken; 47 | 48 | return Promise.resolve(response); 49 | } 50 | return Promise.reject(error); 51 | } 52 | ); 53 | 54 | this.api = axiosInstance; 55 | } 56 | 57 | getConstituencies(query) { 58 | return this.api.get('/constituencies', { 59 | params: { 60 | query 61 | } 62 | }); 63 | } 64 | 65 | getCandidateList(constituencyId) { 66 | return this.api 67 | .get('/candidates', { 68 | params: { 69 | constituency_id: constituencyId 70 | } 71 | }) 72 | .catch(console.error); 73 | } 74 | 75 | searchCandidates({ query, page = 1, item_per_page }) { 76 | return this.api 77 | .get('/candidates', { 78 | params: { 79 | query, 80 | page, 81 | item_per_page 82 | } 83 | }) 84 | .catch(console.error); 85 | } 86 | 87 | getCandidateById(id) { 88 | return this.api.get(`/candidates/${id}`).catch(console.error); 89 | } 90 | 91 | getFaqs({ page, name, category }) { 92 | return this.api 93 | .get('/faqs', { 94 | params: { 95 | page, 96 | name, 97 | category 98 | } 99 | }) 100 | .catch(console.error); 101 | } 102 | 103 | getFaqById(id) { 104 | return this.api.get(`/faqs/${id}`).catch(console.error); 105 | } 106 | 107 | searchFaqs({ query, page = 1 }) { 108 | return this.api.get('/faqs', { 109 | params: { 110 | query, 111 | page 112 | } 113 | }); 114 | } 115 | 116 | getStateRegions() { 117 | return this.api.get('/locality/state_regions').catch(console.error); 118 | } 119 | 120 | getTownships(stateRegion) { 121 | return this.api 122 | .get('/locality/townships', { 123 | params: { 124 | state_region: stateRegion 125 | } 126 | }) 127 | .catch(console.error); 128 | } 129 | 130 | getWards(stateRegion, township) { 131 | return this.api 132 | .get('/locality/wards', { 133 | params: { 134 | state_region: stateRegion, 135 | township 136 | } 137 | }) 138 | .catch(console.error); 139 | } 140 | 141 | getWardDetails(stateRegion, township, ward) { 142 | return this.api 143 | .get('/locality/details', { 144 | params: { 145 | state_region: stateRegion, 146 | township, 147 | ward 148 | } 149 | }) 150 | .catch(console.error); 151 | } 152 | 153 | getNews({ page, itemPerPage = 10 }) { 154 | return this.api 155 | .get('/news', { 156 | params: { 157 | page, 158 | item_per_page: itemPerPage 159 | } 160 | }) 161 | .catch(console.error); 162 | } 163 | 164 | searchNews({ query, page = 1 }) { 165 | return this.api.get('/news', { 166 | params: { 167 | query, 168 | page 169 | } 170 | }); 171 | } 172 | 173 | getParties({ page, item_per_page }) { 174 | return this.api.get('/parties', { 175 | params: { 176 | page, 177 | item_per_page 178 | } 179 | }); 180 | } 181 | 182 | getPartyById(id) { 183 | return this.api.get(`/parties/${id}`).catch(console.error); 184 | } 185 | 186 | searchParties({ query, page = 1, item_per_page }) { 187 | return this.api.get('/parties', { 188 | params: { 189 | query, 190 | page, 191 | item_per_page 192 | } 193 | }); 194 | } 195 | 196 | getBallots(category = 'normal') { 197 | return this.api 198 | .get('ballots', { 199 | params: { 200 | category 201 | } 202 | }) 203 | .catch(console.error); 204 | } 205 | } 206 | 207 | module.exports = MaePaySohAPI; 208 | -------------------------------------------------------------------------------- /hooks/useAPI.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { useState } from 'react'; 3 | import { useAuthContext } from '../context/AuthProvider'; 4 | 5 | function useAPI() { 6 | const { updateToken } = useAuthContext(); 7 | const [loading, setLoading] = useState(false); 8 | 9 | async function fetchData(url, query) { 10 | try { 11 | setLoading(true); 12 | 13 | const { data } = await axios.get(url, { 14 | params: { ...query }, 15 | withCredentials: true 16 | }); 17 | 18 | // This is where we handle custom token 19 | if (data.token) { 20 | updateToken(data.token); 21 | } 22 | 23 | setLoading(false); 24 | return data; 25 | } catch (error) { 26 | setLoading(false); 27 | return error; 28 | } 29 | } 30 | 31 | return [loading, fetchData]; 32 | } 33 | 34 | export default useAPI; 35 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | const withSass = require('@zeit/next-sass'); 2 | const withCSS = require('@zeit/next-css'); 3 | 4 | module.exports = { 5 | env: { 6 | GA_TRACKING_ID: process.env.GA_TRACKING_ID 7 | }, 8 | devIndicators: { 9 | autoPrerender: false 10 | }, 11 | ...withCSS(withSass()) 12 | }; 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mvoter-app", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "NODE_ENV=development next dev -p 3009", 7 | "build": "rm -rf ./build && NODE_ENV=production next build", 8 | "start": "next start -p 8080", 9 | "deploy": "eslint . && npm run build && gcloud app deploy --silent", 10 | "test": "jest --watch", 11 | "lint": "eslint .", 12 | "lint:fix": "eslint . --fix" 13 | }, 14 | "dependencies": { 15 | "@zeit/next-css": "^1.0.1", 16 | "@zeit/next-sass": "^1.0.1", 17 | "axios": "^0.19.2", 18 | "bootstrap": "^4.5.1", 19 | "express": "^4.17.1", 20 | "html-to-text": "^5.1.1", 21 | "moment": "^2.27.0", 22 | "myanmar-numbers": "^2.1.3", 23 | "next": "9.3.4", 24 | "node-gyp": "^7.0.0", 25 | "node-sass": "^4.14.1", 26 | "nookies": "^2.4.0", 27 | "normalize.css": "^8.0.1", 28 | "react": "16.13.1", 29 | "react-collapsible": "^2.8.0", 30 | "react-dom": "16.13.1", 31 | "react-ga": "^3.1.2", 32 | "react-icons": "^3.10.0", 33 | "react-infinite-scroll-component": "^5.0.5", 34 | "react-modal": "^3.11.2", 35 | "react-select": "^3.1.0", 36 | "react-slick": "^0.27.10", 37 | "react-transition-group": "^4.4.1", 38 | "slick-carousel": "^1.8.1", 39 | "uuid": "^8.3.0" 40 | }, 41 | "devDependencies": { 42 | "babel-eslint": "^9.0.0", 43 | "babel-jest": "^26.3.0", 44 | "eslint": "^6.8.0", 45 | "eslint-config-airbnb": "^18.2.0", 46 | "eslint-config-nextjs": "^1.0.6", 47 | "eslint-config-prettier": "^4.3.0", 48 | "eslint-config-standard": "^14.1.1", 49 | "eslint-plugin-html": "^5.0.5", 50 | "eslint-plugin-import": "^2.22.1", 51 | "eslint-plugin-jsx-a11y": "^6.3.1", 52 | "eslint-plugin-node": "^11.1.0", 53 | "eslint-plugin-prettier": "^3.1.4", 54 | "eslint-plugin-promise": "^4.2.1", 55 | "eslint-plugin-react": "^7.21.3", 56 | "eslint-plugin-react-hooks": "^1.7.0", 57 | "eslint-plugin-standard": "^4.0.1", 58 | "jest": "^26.4.2", 59 | "prettier": "^1.19.1" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /pages/_app.js: -------------------------------------------------------------------------------- 1 | import ReactGA from 'react-ga'; 2 | import Head from 'next/head'; 3 | import AuthProvider from '../context/AuthProvider'; 4 | import 'bootstrap/dist/css/bootstrap-reboot.css'; 5 | import 'bootstrap/dist/css/bootstrap-grid.css'; 6 | import '../styles/base.scss'; 7 | import '../styles/helpers.scss'; 8 | 9 | ReactGA.initialize(process.env.GA_TRACKING_ID); 10 | 11 | function mVoterApp({ Component, pageProps }) { 12 | return ( 13 | <> 14 | 15 | 20 | 26 | 32 | 33 | 34 | 35 | 36 | 37 | 41 | 42 | 43 | 44 | 45 | 46 | ); 47 | } 48 | 49 | export default mVoterApp; 50 | -------------------------------------------------------------------------------- /pages/about/about.module.scss: -------------------------------------------------------------------------------- 1 | @import '../../styles/partials/colors'; 2 | 3 | .About { 4 | h1, p { 5 | margin: 0; 6 | } 7 | p { 8 | line-height: 1.5rem; 9 | } 10 | .mVoterLogo { 11 | width: auto; 12 | height: 150px; 13 | } 14 | .PopStack { 15 | width: 150px; 16 | height: auto; 17 | display: block; 18 | margin-top: 10px; 19 | margin-left: auto; 20 | margin-right: auto; 21 | } 22 | img { 23 | width: 100%; 24 | height: auto; 25 | } 26 | img.large-logo { 27 | width: 100%; 28 | height: auto; 29 | } 30 | .suggestive-links { 31 | margin-top: 10px; 32 | margin-bottom: 5px; 33 | i { 34 | display: inline-block; 35 | margin-left: 8px; 36 | margin-right: 8px; 37 | padding: 8px; 38 | font-size: 1.35rem; 39 | color: white; 40 | background-color: $primary-color; 41 | border-radius: 100%; 42 | } 43 | } 44 | } 45 | 46 | @media (min-width: 1200px) { 47 | .About { 48 | max-width: 85%; 49 | .PopStack { 50 | width: auto; 51 | height: 180px; 52 | } 53 | img.large-logo { 54 | width: auto; 55 | height: 170px; 56 | } 57 | img.step-logo { 58 | height: 130px; 59 | } 60 | img.eu-logo { 61 | height: 120px; 62 | } 63 | } 64 | } -------------------------------------------------------------------------------- /pages/about/index.js: -------------------------------------------------------------------------------- 1 | import { useRouter } from 'next/router'; 2 | import Head from 'next/head'; 3 | import Layout from '../../components/Layout/Layout'; 4 | import AppHeader from '../../components/Layout/AppHeader/AppHeader'; 5 | 6 | import './about.module.scss'; 7 | import Button from '../../components/Common/Button/Button'; 8 | 9 | const About = () => { 10 | const router = useRouter(); 11 | 12 | return ( 13 | 14 | 15 | About | mVoter 2020 16 | 17 | 18 | 23 | 24 |
    25 |
    26 |
    27 | mVoter Logo 32 |

    mVoter

    33 |

    34 | Award-winning voter education app for Myanmar elections since 35 | 2015. 36 |

    37 |
    38 |

    Proudly and voluntarily presented by

    39 |
    40 | 41 | PopStack 46 |
    47 | 48 |

    Supported by

    49 | 50 |
    51 |
    52 | UEC Logo 53 |
    54 |
    55 | 66 | 67 | 78 | 79 |
    80 |
    မေးမြန်းအကြံပြုလိုပါက ဆက်သွယ်ရန်
    81 | 96 |
    97 | ©2015-2020 Team PopStack. All rights reserved. 98 |
    99 |
    100 |
    101 |
    102 | ); 103 | }; 104 | 105 | export default About; 106 | -------------------------------------------------------------------------------- /pages/api/auth.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | const authAPI = axios.create({ 4 | baseURL: process.env.BASE_URL, 5 | headers: { 6 | 'api-key': process.env.API_KEY 7 | } 8 | }); 9 | 10 | export async function fetchToken() { 11 | // This is rather a side effect 12 | const response = await authAPI.post('/authenticate'); 13 | const { token: apiToken } = response.data; 14 | return apiToken; 15 | } 16 | 17 | export default async function auth(req, res) { 18 | try { 19 | const token = await fetchToken(); 20 | // Set token inside HTTP Cookie so we can deal with SSR endpoints 21 | return res.status(200).send({ token }); 22 | } catch (error) { 23 | return res.status(500).send('Internal server error'); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /pages/api/ballots.js: -------------------------------------------------------------------------------- 1 | import MaePaySohAPI from '../../gateway/api'; 2 | 3 | export default async function(req, res) { 4 | try { 5 | const { category = 'normal' } = req.query; 6 | 7 | const api = new MaePaySohAPI(req.cookies.token); 8 | 9 | const response = await api.getBallots(category); 10 | 11 | return res.status(200).send(response.data); 12 | } catch (error) { 13 | return res.status(500).send('Internal server error'); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /pages/api/candidates.js: -------------------------------------------------------------------------------- 1 | import MaePaySohAPI from '../../gateway/api'; 2 | 3 | export default async function(req, res) { 4 | try { 5 | const { constituency_id: constituencyId } = req.query; 6 | 7 | const api = new MaePaySohAPI(req.cookies.token); 8 | const response = await api.getCandidateList(constituencyId); 9 | const { data } = response.data; 10 | 11 | return res.status(200).send({ 12 | // pre-sort the data here before Frontend 13 | token: response.data.token, 14 | data: data.sort( 15 | (a, b) => a.attributes.ballot_order - b.attributes.ballot_order 16 | ) 17 | }); 18 | } catch (error) { 19 | return res.status(500).send('Internal server error'); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /pages/api/constituencies.js: -------------------------------------------------------------------------------- 1 | import MaePaySohAPI from '../../gateway/api'; 2 | 3 | export default async function(req, res) { 4 | try { 5 | const { query } = req.query; 6 | 7 | const api = new MaePaySohAPI(req.cookies.token); 8 | 9 | const response = await api.getConstituencies(query); 10 | 11 | return res.status(200).send(response.data); 12 | } catch (error) { 13 | return res.status(500).send('Internal server error'); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /pages/api/faqs.js: -------------------------------------------------------------------------------- 1 | import MaePaySohAPI from '../../gateway/api'; 2 | 3 | export default async function(req, res) { 4 | try { 5 | const { page, category } = req.query; 6 | 7 | const api = new MaePaySohAPI(req.cookies.token); 8 | const response = await api.getFaqs({ page, category }); 9 | 10 | return res.status(200).send(response.data); 11 | } catch (error) { 12 | return res.status(500).send('Internal server error'); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /pages/api/locations.js: -------------------------------------------------------------------------------- 1 | import MaePaySohAPI from '../../gateway/api'; 2 | 3 | // eslint-disable-next-line func-names 4 | export default async function(req, res) { 5 | try { 6 | const { type = 'state_regions', state_region, township, ward } = req.query; 7 | 8 | const api = new MaePaySohAPI(req.cookies.token); 9 | let response; 10 | 11 | if (type === 'state_regions') { 12 | response = await api.getStateRegions(); 13 | } else if (type === 'townships') { 14 | response = await api.getTownships(state_region); 15 | } else if (type === 'wards') { 16 | response = await api.getWards(state_region, township); 17 | } else if (type === 'details') { 18 | response = await api.getWardDetails(state_region, township, ward); 19 | } else { 20 | throw new Error('Location type not provided.'); 21 | } 22 | 23 | return res.status(200).send(response.data); 24 | } catch (error) { 25 | return res.status(500).send('Internal server error'); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /pages/api/news.js: -------------------------------------------------------------------------------- 1 | import MaePaySohAPI from '../../gateway/api'; 2 | 3 | export default async function(req, res) { 4 | try { 5 | const { page } = req.query; 6 | 7 | const api = new MaePaySohAPI(req.cookies.token); 8 | 9 | const response = await api.getNews({ page }); 10 | 11 | return res.status(200).send(response.data); 12 | } catch (error) { 13 | return res.status(500).send('Internal server error'); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /pages/api/parties.js: -------------------------------------------------------------------------------- 1 | import MaePaySohAPI from '../../gateway/api'; 2 | 3 | export default async function(req, res) { 4 | try { 5 | const { page, item_per_page = 25 } = req.query; 6 | 7 | const api = new MaePaySohAPI(req.cookies.token); 8 | const response = await api.getParties({ 9 | page, 10 | item_per_page 11 | }); 12 | 13 | return res.status(200).send(response.data); 14 | } catch (error) { 15 | // eslint-disable-next-line no-console 16 | console.error(error); 17 | return res.status(500).send('Internal server error'); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /pages/api/searchCandidates.js: -------------------------------------------------------------------------------- 1 | import MaePaySohAPI from '../../gateway/api'; 2 | 3 | export default async function(req, res) { 4 | try { 5 | const { query, page, item_per_page = 25 } = req.query; 6 | 7 | const api = new MaePaySohAPI(req.cookies.token); 8 | 9 | const response = await api.searchCandidates({ query, page, item_per_page }); 10 | 11 | return res.status(200).send(response.data); 12 | } catch (error) { 13 | // eslint-disable-next-line no-console 14 | console.error(error); 15 | return res.status(500).send('Internal server error'); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /pages/api/searchFaqs.js: -------------------------------------------------------------------------------- 1 | import MaePaySohAPI from '../../gateway/api'; 2 | 3 | export default async function(req, res) { 4 | try { 5 | const { query, page, item_per_page = 25 } = req.query; 6 | 7 | const api = new MaePaySohAPI(req.cookies.token); 8 | 9 | const response = await api.searchFaqs({ query, page, item_per_page }); 10 | 11 | return res.status(200).send(response.data); 12 | } catch (error) { 13 | // eslint-disable-next-line no-console 14 | console.error(error); 15 | return res.status(500).send('Internal server error'); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /pages/api/searchNews.js: -------------------------------------------------------------------------------- 1 | import MaePaySohAPI from '../../gateway/api'; 2 | 3 | export default async function(req, res) { 4 | try { 5 | const { query, page, item_per_page = 25 } = req.query; 6 | 7 | const api = new MaePaySohAPI(req.cookies.token); 8 | const response = await api.searchNews({ query, page, item_per_page }); 9 | 10 | return res.status(200).send(response.data); 11 | } catch (error) { 12 | return res.status(500).send('Internal server error'); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /pages/api/searchParties.js: -------------------------------------------------------------------------------- 1 | import MaePaySohAPI from '../../gateway/api'; 2 | 3 | export default async function(req, res) { 4 | try { 5 | const { query, page, item_per_page = 25 } = req.query; 6 | 7 | const api = new MaePaySohAPI(req.cookies.token); 8 | const response = await api.searchParties({ query, page, item_per_page }); 9 | 10 | return res.status(200).send(response.data); 11 | } catch (error) { 12 | return res.status(500).send('Internal server error'); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /pages/candidates/candidate.module.scss: -------------------------------------------------------------------------------- 1 | @import '../../styles/partials/colors'; 2 | 3 | .Candidate { 4 | line-height: 25px; 5 | &__imageWrapper { 6 | width: 130px; 7 | height: 130px; 8 | margin: 0 auto; 9 | position: relative; 10 | margin-bottom: 20px; 11 | } 12 | &__infoHeaderWrapper { 13 | text-align: center; 14 | } 15 | &__image { 16 | border-radius: 100%; 17 | background-position: center center; 18 | background-repeat: no-repeat; 19 | background-size: cover; 20 | width: 100%; 21 | height: 100%; 22 | &.is-winner { 23 | border: 3px solid $primary-color; 24 | } 25 | } 26 | &__winner { 27 | position: absolute; 28 | left: 50%; 29 | color: #ffffff; 30 | transform: translateX(-50%); 31 | bottom: -6px; 32 | background-color: $primary-color; 33 | font-size: 0.65rem; 34 | border-radius: 10px; 35 | z-index: 2; 36 | padding: 0px 8px; 37 | } 38 | &__partyFlag { 39 | display: inline-block; 40 | width: auto; 41 | height: 30px; 42 | margin-bottom: 5px; 43 | } 44 | &__name { 45 | display: inline-block; 46 | margin-left: 6px; 47 | margin-bottom: 10px; 48 | font-size: 1.25rem; 49 | } 50 | &__party { 51 | display: inline-block; 52 | margin-left: 3px; 53 | } 54 | &__partyName { 55 | color: rgba($color: #000000, $alpha: 0.75); 56 | cursor: pointer; 57 | i { 58 | vertical-align: middle; 59 | } 60 | } 61 | &__senate { 62 | font-size: 0.875rem; 63 | } 64 | &__constituency { 65 | margin-top: 10px; 66 | display: inline-block; 67 | background-color: rgba($color: $primary-color, $alpha: 0.25); 68 | border-radius: 100px; 69 | font-size: 0.85rem; 70 | font-weight: 600; 71 | padding: 8px 12px; 72 | } 73 | &__age { 74 | font-size: 1.5rem; 75 | color: $primary-color; 76 | } 77 | &__info { 78 | margin-bottom: 12px; 79 | .col-12 { 80 | margin-top: 5px; 81 | margin-bottom: 5px; 82 | } 83 | } 84 | &__infoLabel { 85 | font-size: 0.875rem; 86 | color: rgba($color: #000000, $alpha: 0.85); 87 | } 88 | .parent-info { 89 | div { 90 | &:first-child { 91 | margin-top: 0; 92 | } 93 | } 94 | } 95 | &__infoAnswer { 96 | font-size: 1rem; 97 | } 98 | } 99 | 100 | .competitors-wrapper { 101 | margin-top: 8px; 102 | margin-bottom: 0px; 103 | } 104 | 105 | .competitors-text { 106 | font-weight: 600; 107 | font-size: 1rem; 108 | margin: 5px 0; 109 | } 110 | 111 | @media (min-width: 1200px) { 112 | .Candidate { 113 | &__infoHeaderWrapper { 114 | text-align: left; 115 | } 116 | &__infoLabel { 117 | color: $primary-color; 118 | } 119 | &__infoAnswer { 120 | word-wrap: break-word; 121 | } 122 | &__partyFlag { 123 | margin-bottom: unset; 124 | } 125 | &__info { 126 | .col-12 { 127 | margin-top: 0; 128 | margin-bottom: 0; 129 | } 130 | .parent-type { 131 | color: $primary-color; 132 | } 133 | } 134 | } 135 | .competitors-wrapper { 136 | margin-top: 16px; 137 | margin-bottom: -6px; 138 | } 139 | .parent-info { 140 | margin-top: 0px; 141 | display: flex; 142 | justify-content: space-between; 143 | &-name { 144 | width: 50%; 145 | } 146 | &-ethnicity, &-religion { 147 | width: 25%; 148 | } 149 | .Candidate__infoLabel { 150 | color: rgba($color: #000000, $alpha: 0.65); 151 | } 152 | div { 153 | &:first-child { 154 | margin-left: 0; 155 | } 156 | margin-left: 5px; 157 | margin-right: 5px; 158 | } 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /pages/candidates/candidates.module.scss: -------------------------------------------------------------------------------- 1 | @import '../../styles/partials/colors'; 2 | @import '../../styles/partials/extends'; 3 | 4 | .show-location-image { 5 | width: auto; 6 | height: 96px; 7 | display: block; 8 | margin: 0 auto; 9 | } 10 | 11 | .show-location-chooser { 12 | margin-top: 20px; 13 | } 14 | 15 | .show-location-chooser-button { 16 | margin-top: 20px; 17 | background-color: $primary-color; 18 | color: #FFFFFF; 19 | border: 0; 20 | outline: 0; 21 | border-radius: 25px; 22 | padding: 5px 20px 3px 15px; 23 | transition: all 0.2s linear; 24 | font-size: 0.8rem; 25 | i { 26 | font-size: 1.2rem; 27 | } 28 | span { 29 | vertical-align: super; 30 | } 31 | &:hover, &:active { 32 | @extend %button-hover; 33 | } 34 | } 35 | 36 | .Candidates { 37 | @extend %mobile-vert-rules; 38 | } 39 | 40 | .Tabs { 41 | display: flex; 42 | justify-content: space-around; 43 | align-items: center; 44 | position: relative; 45 | padding: 10px; 46 | border-bottom: 1px solid rgba($color: #000000, $alpha: 0.1); 47 | box-shadow: 0px 3px 5px rgba($color: #000000, $alpha: 0.1); 48 | } 49 | 50 | .Tab { 51 | word-break: break-all; 52 | } 53 | 54 | .slider { 55 | position: absolute; 56 | left: 0; 57 | bottom: 0; 58 | // transform: translateX(-50%); 59 | height: 3px; 60 | width: 138px; 61 | background: #0071dd; 62 | } 63 | 64 | .no-data-text { 65 | margin-top: 55%; 66 | color: red; 67 | } 68 | -------------------------------------------------------------------------------- /pages/candidates/search.js: -------------------------------------------------------------------------------- 1 | import SearchPage from '../../components/Search/SearchPage'; 2 | import CandidateList from '../../components/Candidates/CandidateList/CandidateList'; 3 | 4 | const CandidateSearch = () => { 5 | // Why pass endpoint you may ask? Answer: I am lazy. 6 | return ( 7 | 14 | 15 | 16 | ); 17 | }; 18 | 19 | export default CandidateSearch; 20 | -------------------------------------------------------------------------------- /pages/faqs/[faq].js: -------------------------------------------------------------------------------- 1 | import ReactGA from 'react-ga'; 2 | import htmlToText from 'html-to-text'; 3 | import nookies from 'nookies'; 4 | import Head from 'next/head'; 5 | import { useEffect } from 'react'; 6 | import MaePaySohAPI from '../../gateway/api'; 7 | import Layout from '../../components/Layout/Layout'; 8 | import { formatFAQCategory } from '../../utils/textFormatter'; 9 | import { useAuthContext } from '../../context/AuthProvider'; 10 | 11 | import './faq.module.scss'; 12 | 13 | const FAQ = props => { 14 | const { 15 | faq: { id, category, question, answer, strippedAnswer }, 16 | token 17 | } = props; 18 | 19 | const { updateToken } = useAuthContext(); 20 | 21 | useEffect(() => ReactGA.pageview('/faqs/[faq]'), []); 22 | useEffect(() => { 23 | if (token) { 24 | updateToken(token); 25 | } 26 | }, [token]); 27 | 28 | return ( 29 | 30 | 31 | {formatFAQCategory(category)} သိမှတ်ဖွယ်ရာ | mVoter 2020 32 | 33 | 34 | 35 | 36 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 52 | 53 | 54 | 58 | 59 |
    60 |
    61 |
    62 |
    63 |

    {question}

    64 | 69 |
    70 |
    71 |
    72 |
    73 | {/* TODO Add more sensible information under this */} 74 |
    75 | ); 76 | }; 77 | 78 | export async function getServerSideProps(context) { 79 | const { params } = context; 80 | 81 | const cookies = nookies.get(context); 82 | const api = new MaePaySohAPI(cookies.token); 83 | const response = await api.getFaqById(params.faq); 84 | 85 | const { data, token } = response.data; 86 | 87 | // Strip HTML characters into one string 88 | const htmlStrippedAnswer = htmlToText.fromString(data.attributes.answer); 89 | data.attributes.strippedAnswer = htmlStrippedAnswer; 90 | 91 | return { 92 | props: { 93 | faq: { 94 | ...data, 95 | ...data.attributes 96 | }, 97 | ...(token && { token }) 98 | } 99 | }; 100 | } 101 | 102 | export default FAQ; 103 | -------------------------------------------------------------------------------- /pages/faqs/ballots.js: -------------------------------------------------------------------------------- 1 | import ReactGA from 'react-ga'; 2 | import { useRouter } from 'next/router'; 3 | import { useState, useEffect, useRef } from 'react'; 4 | import { AiOutlineLoading } from 'react-icons/ai'; 5 | import Head from 'next/head'; 6 | import Slider from 'react-slick'; 7 | import Select from 'react-select'; 8 | import myanmarNumbers from 'myanmar-numbers'; 9 | import { customSelectStyle, BALLOT_CATEGORIES } from '../../utils/constants'; 10 | import Button from '../../components/Common/Button/Button'; 11 | import AppHeader from '../../components/Layout/AppHeader/AppHeader'; 12 | import Layout from '../../components/Layout/Layout'; 13 | 14 | import './slick.scss'; 15 | import './slick-theme.scss'; 16 | import './ballots.module.scss'; 17 | import useAPI from '../../hooks/useAPI'; 18 | 19 | function PrevArrow(props) { 20 | const { onClick, currentSlide } = props; 21 | return ( 22 | 37 | ); 38 | } 39 | 40 | function NextArrow(props) { 41 | const { onClick, currentSlide, slideCount } = props; 42 | return ( 43 | 58 | ); 59 | } 60 | 61 | const Ballots = () => { 62 | const router = useRouter(); 63 | const [ballots, setBallots] = useState([]); 64 | const [loading, setLoading] = useState(false); 65 | const [currentSlide, setCurrentSlide] = useState(1); 66 | const sliderRef = useRef(); 67 | const [, fetchData] = useAPI(); 68 | 69 | const settings = { 70 | dots: false, 71 | infinite: false, 72 | speed: 500, 73 | slidesToShow: 1, 74 | slidesToScroll: 1, 75 | count: ballots.length, 76 | prevArrow: , 77 | nextArrow: , 78 | afterChange: current => { 79 | setCurrentSlide(current + 1); 80 | } 81 | }; 82 | 83 | async function fetchBallots(category = 'normal') { 84 | setLoading(true); 85 | 86 | const { data } = await fetchData('/api/ballots', { 87 | category 88 | }); 89 | 90 | setBallots(data); 91 | setLoading(false); 92 | } 93 | 94 | useEffect(() => { 95 | ReactGA.pageview(window.location.pathname); 96 | 97 | fetchBallots(); 98 | }, []); 99 | 100 | function onChangeCategory(value) { 101 | sliderRef.current.slickGoTo(0, true); // don't animate 102 | fetchBallots(value); 103 | setCurrentSlide(1); 104 | } 105 | 106 | return ( 107 | 108 | 109 | မဲနမူနာများ | mVoter 2020 110 | 111 | 112 |
    113 | 116 | သိမှတ်ဖွယ်ရာများ 117 |
    118 |
    119 |
    120 | ({ 108 | value: key, 109 | label: text 110 | }))} 111 | onChange={({ value }) => onChangeCategory(value)} 112 | /> 113 |
    114 |
    115 |
    116 |
    117 |
    118 | {faqCategory === 'voter_list' && ( 119 |
    120 |
    121 |
    router.push('/faqs/ballots')} 124 | > 125 |
    126 | Ballot Stack 131 |
    132 |
    ပယ်မဲ၊ ခိုင်လုံမဲ နမူနာများ
    133 |
    134 |
    135 |
    136 |
    137 |
    138 | No Selfie 142 |
    143 | Selfie
    144 | မရိုက်ရ 145 |
    146 |
    147 |
    148 | No Photo 149 |
    150 | ဓာတ်ပုံ
    151 | မရိုက်ရ 152 |
    153 |
    154 |
    155 | No Video 156 |
    157 | ဗီဒီယို
    158 | မရိုက်ရ 159 |
    160 |
    161 |
    162 | No Recording 166 |
    167 | အသံ
    168 | မသွင်းရ 169 |
    170 |
    171 |
    172 |
    173 |
    174 | )} 175 | {faqCategory === 'voter_list' && ( 176 |
    177 |
    178 | 179 |
    180 | 181 | how_to_reg 182 | 183 | မဲစာရင်းစစ်ရန် 184 |
    185 | 186 |
    187 |
    188 | )} 189 | {faqCategory === 'candidate' && ( 190 |
    191 |
    192 | 198 |
    199 |
    200 | 201 |
    202 |
    203 | ရွေးကောက်ပွဲဆိုင်ရာ ပြစ်မှု၊ ပြစ်ဒဏ်များနှင့် 204 | တရားမဲ့ပြုကျင့်မှုများ 205 |
    206 |
    207 |
    208 |
    209 |
    210 | )} 211 |
    212 | 217 | 218 | 219 |
    220 | 221 | ); 222 | }; 223 | 224 | export default FAQs; 225 | -------------------------------------------------------------------------------- /pages/faqs/search.js: -------------------------------------------------------------------------------- 1 | import SearchPage from '../../components/Search/SearchPage'; 2 | import FaqList from '../../components/Faq/FaqList/FaqList'; 3 | 4 | const FaqSearch = () => { 5 | // Why pass endpoint you may ask? Answer: I am lazy. 6 | return ( 7 | 14 | 15 | 16 | ); 17 | }; 18 | 19 | export default FaqSearch; 20 | -------------------------------------------------------------------------------- /pages/faqs/slick-theme.scss: -------------------------------------------------------------------------------- 1 | @charset 'UTF-8'; 2 | /* Slider */ 3 | .slick-loading .slick-list 4 | { 5 | // background: #fff url('./ajax-loader.gif') center center no-repeat; 6 | } 7 | /* Arrows */ 8 | .slick-prev, 9 | .slick-next 10 | { 11 | font-size: 0; 12 | line-height: 0; 13 | 14 | position: absolute; 15 | top: 50%; 16 | 17 | display: block; 18 | 19 | width: 20px; 20 | height: 20px; 21 | padding: 0; 22 | -webkit-transform: translate(0, -50%); 23 | -ms-transform: translate(0, -50%); 24 | transform: translate(0, -50%); 25 | 26 | cursor: pointer; 27 | 28 | color: transparent; 29 | border: none; 30 | outline: none; 31 | background: transparent; 32 | } 33 | .slick-prev:hover, 34 | .slick-prev:focus, 35 | .slick-next:hover, 36 | .slick-next:focus 37 | { 38 | color: transparent; 39 | outline: none; 40 | background: transparent; 41 | } 42 | .slick-prev:hover:before, 43 | .slick-prev:focus:before, 44 | .slick-next:hover:before, 45 | .slick-next:focus:before 46 | { 47 | opacity: 1; 48 | } 49 | .slick-prev.slick-disabled:before, 50 | .slick-next.slick-disabled:before 51 | { 52 | opacity: .25; 53 | } 54 | 55 | .slick-prev:before, 56 | .slick-next:before 57 | { 58 | font-family: 'slick'; 59 | font-size: 20px; 60 | line-height: 1; 61 | 62 | opacity: .75; 63 | color: white; 64 | 65 | -webkit-font-smoothing: antialiased; 66 | -moz-osx-font-smoothing: grayscale; 67 | } 68 | 69 | .slick-prev 70 | { 71 | left: -25px; 72 | } 73 | [dir='rtl'] .slick-prev 74 | { 75 | right: -25px; 76 | left: auto; 77 | } 78 | .slick-prev:before 79 | { 80 | content: '←'; 81 | } 82 | [dir='rtl'] .slick-prev:before 83 | { 84 | content: '→'; 85 | } 86 | 87 | .slick-next 88 | { 89 | right: -25px; 90 | } 91 | [dir='rtl'] .slick-next 92 | { 93 | right: auto; 94 | left: -25px; 95 | } 96 | .slick-next:before 97 | { 98 | content: '→'; 99 | } 100 | [dir='rtl'] .slick-next:before 101 | { 102 | content: '←'; 103 | } 104 | 105 | /* Dots */ 106 | .slick-dotted.slick-slider 107 | { 108 | margin-bottom: 30px; 109 | } 110 | 111 | .slick-dots 112 | { 113 | position: absolute; 114 | bottom: -25px; 115 | 116 | display: block; 117 | 118 | width: 100%; 119 | padding: 0; 120 | margin: 0; 121 | 122 | list-style: none; 123 | 124 | text-align: center; 125 | } 126 | .slick-dots li 127 | { 128 | position: relative; 129 | 130 | display: inline-block; 131 | 132 | width: 20px; 133 | height: 20px; 134 | margin: 0 5px; 135 | padding: 0; 136 | 137 | cursor: pointer; 138 | } 139 | .slick-dots li button 140 | { 141 | font-size: 0; 142 | line-height: 0; 143 | 144 | display: block; 145 | 146 | width: 20px; 147 | height: 20px; 148 | padding: 5px; 149 | 150 | cursor: pointer; 151 | 152 | color: transparent; 153 | border: 0; 154 | outline: none; 155 | background: transparent; 156 | } 157 | .slick-dots li button:hover, 158 | .slick-dots li button:focus 159 | { 160 | outline: none; 161 | } 162 | .slick-dots li button:hover:before, 163 | .slick-dots li button:focus:before 164 | { 165 | opacity: 1; 166 | } 167 | .slick-dots li button:before 168 | { 169 | font-family: 'slick'; 170 | font-size: 6px; 171 | line-height: 20px; 172 | 173 | position: absolute; 174 | top: 0; 175 | left: 0; 176 | 177 | width: 20px; 178 | height: 20px; 179 | 180 | content: '•'; 181 | text-align: center; 182 | 183 | opacity: .25; 184 | color: black; 185 | 186 | -webkit-font-smoothing: antialiased; 187 | -moz-osx-font-smoothing: grayscale; 188 | } 189 | .slick-dots li.slick-active button:before 190 | { 191 | opacity: .75; 192 | color: black; 193 | } -------------------------------------------------------------------------------- /pages/faqs/slick.scss: -------------------------------------------------------------------------------- 1 | /* Slider */ 2 | .slick-slider 3 | { 4 | position: relative; 5 | display: block; 6 | box-sizing: border-box; 7 | 8 | -webkit-user-select: none; 9 | -moz-user-select: none; 10 | -ms-user-select: none; 11 | user-select: none; 12 | 13 | -webkit-touch-callout: none; 14 | -khtml-user-select: none; 15 | -ms-touch-action: pan-y; 16 | touch-action: pan-y; 17 | -webkit-tap-highlight-color: transparent; 18 | } 19 | 20 | .slick-list 21 | { 22 | position: relative; 23 | 24 | display: block; 25 | overflow: hidden; 26 | 27 | margin: 0; 28 | padding: 0; 29 | } 30 | .slick-list:focus 31 | { 32 | outline: none; 33 | } 34 | .slick-list.dragging 35 | { 36 | cursor: pointer; 37 | cursor: hand; 38 | } 39 | 40 | .slick-slider .slick-track, 41 | .slick-slider .slick-list 42 | { 43 | -webkit-transform: translate3d(0, 0, 0); 44 | -moz-transform: translate3d(0, 0, 0); 45 | -ms-transform: translate3d(0, 0, 0); 46 | -o-transform: translate3d(0, 0, 0); 47 | transform: translate3d(0, 0, 0); 48 | } 49 | 50 | .slick-track 51 | { 52 | position: relative; 53 | top: 0; 54 | left: 0; 55 | 56 | display: block; 57 | margin-left: auto; 58 | margin-right: auto; 59 | } 60 | .slick-track:before, 61 | .slick-track:after 62 | { 63 | display: table; 64 | 65 | content: ''; 66 | } 67 | .slick-track:after 68 | { 69 | clear: both; 70 | } 71 | .slick-loading .slick-track 72 | { 73 | visibility: hidden; 74 | } 75 | 76 | .slick-slide 77 | { 78 | display: none; 79 | float: left; 80 | 81 | height: 100%; 82 | min-height: 1px; 83 | } 84 | [dir='rtl'] .slick-slide 85 | { 86 | float: right; 87 | } 88 | .slick-slide img 89 | { 90 | display: block; 91 | } 92 | .slick-slide.slick-loading img 93 | { 94 | display: none; 95 | } 96 | .slick-slide.dragging img 97 | { 98 | pointer-events: none; 99 | } 100 | .slick-initialized .slick-slide 101 | { 102 | display: block; 103 | } 104 | .slick-loading .slick-slide 105 | { 106 | visibility: hidden; 107 | } 108 | .slick-vertical .slick-slide 109 | { 110 | display: block; 111 | 112 | height: auto; 113 | 114 | border: 1px solid transparent; 115 | } 116 | .slick-arrow.slick-hidden { 117 | display: none; 118 | } -------------------------------------------------------------------------------- /pages/how_to_vote/HowToVote.module.scss: -------------------------------------------------------------------------------- 1 | @import '../../styles/partials/colors'; 2 | @import '../../styles/partials/extends'; 3 | 4 | .check-voter-list-button { 5 | display: block; 6 | margin: 0 auto; 7 | width: 100%; 8 | border: 0; 9 | border-radius: 10px; 10 | background: $primary-color; 11 | color: #ffffff; 12 | padding: 10px 30px; 13 | transition: background 0.2s linear; 14 | &:hover { 15 | background: rgba($color: $primary-color, $alpha: 0.85); 16 | } 17 | } 18 | 19 | .HowToVote { 20 | @extend %mobile-vert-rules; 21 | padding-bottom: 20px; 22 | &__announcement { 23 | padding: 15px; 24 | border-radius: 12px; 25 | background-color: rgba($color: $primary-color, $alpha: 0.25); 26 | .title, .date, .day { 27 | display: inline-block; 28 | } 29 | .title { 30 | font-weight: 700; 31 | margin-bottom: 14px; 32 | } 33 | .date { 34 | font-weight: 700; 35 | color: $primary-color; 36 | font-size: 1.4rem; 37 | } 38 | .day { 39 | color: $grey-color; 40 | font-size: 1.1rem; 41 | margin-bottom: 12px; 42 | } 43 | } 44 | &__votingTime { 45 | display: flex; 46 | justify-content: center; 47 | .opening-time, .closing-time { 48 | margin: 5px 10px; 49 | .pre { 50 | line-height: 28px; 51 | font-size: 0.8rem; 52 | font-weight: 700; 53 | } 54 | } 55 | } 56 | .instruction-list { 57 | position: relative; 58 | // &::after { 59 | // content: ''; 60 | // display: inline-block; 61 | // position: absolute; 62 | // top: 0; 63 | // left: 10px; 64 | // width: 2px; 65 | // height: 100%; 66 | // background-color: $primary-color; 67 | // } 68 | } 69 | .title { 70 | font-weight: 700; 71 | margin-top: 2px; 72 | margin-bottom: 3px; 73 | } 74 | .instruction { 75 | padding-left: 30px; 76 | position: relative; 77 | line-height: 28px; 78 | margin: 5px 0; 79 | &:last-child::before { 80 | content: ''; 81 | display: none; 82 | } 83 | &::before { 84 | content: ''; 85 | display: inline-block; 86 | position: absolute; 87 | top: 20px; 88 | left: 10px; 89 | width: 2px; 90 | height: 100%; 91 | background-color: $primary-color; 92 | } 93 | &::after { 94 | content: ''; 95 | display: inline-block; 96 | 97 | position: absolute; 98 | border-radius: 100%; 99 | top: 9px; 100 | left: 5px; 101 | width: 12px; 102 | height: 12px; 103 | background-color: $primary-color; 104 | } 105 | } 106 | } 107 | 108 | @media (min-width: 1200px) { 109 | .check-voter-list-button { 110 | width: unset; 111 | } 112 | } -------------------------------------------------------------------------------- /pages/how_to_vote/index.js: -------------------------------------------------------------------------------- 1 | import ReactGA from 'react-ga'; 2 | import { useEffect } from 'react'; 3 | import Link from 'next/link'; 4 | import Head from 'next/head'; 5 | import dynamic from 'next/dynamic'; 6 | 7 | import Layout from '../../components/Layout/Layout'; 8 | import Clock6AM from '../../components/Common/Icons/clock6AM'; 9 | import Clock4PM from '../../components/Common/Icons/clock4PM'; 10 | import AppHeader from '../../components/Layout/AppHeader/AppHeader'; 11 | 12 | import './HowToVote.module.scss'; 13 | 14 | const ElectionCountdown = dynamic( 15 | () => import('../../components/Countdown/ElectionCountdown'), 16 | { ssr: false } 17 | ); 18 | 19 | export default function howToVote() { 20 | useEffect(() => { 21 | ReactGA.pageview(window.location.pathname); 22 | }, []); 23 | 24 | return ( 25 | 26 | 27 | မဲပေးနည်း | mVoter 2020 28 | 29 | 30 | မဲပေးနည်း 31 | 32 |
    33 |
    34 |
    35 | 36 |
    37 | 38 | (၂၀၂၀) ပြည့်နှစ် အထွေထွေရွေးကောက်ပွဲနေ့ 39 | 40 |
    41 | နိုဝင်ဘာလ (၈) ရက် 42 |
    43 | တနင်္ဂနွေနေ့ 44 |
    45 |
    46 |
    47 | 48 | 49 |   မဲရုံဖွင့်ချိန် 50 | 51 |
    52 | နံနက်(၆)နာရီ 53 |
    54 |
    55 | 56 | 57 |   မဲရုံပိတ်ချိန် 58 | 59 |
    60 | ညနေ(၄)နာရီ 61 |
    62 |
    63 |
    64 |
    65 |
    66 | 67 |
    68 |
    69 | 70 | 73 | 74 |
    75 |
    76 | 77 |
    78 |
    79 |
    ရွေးကောက်ပွဲနေ့တွင်
    80 |
    81 |
    82 | ရပ်ကွက်/ကျေးရွာအုပ်စု အတွင်းရှိ မိမိ မဲပေးရမည့် မဲရုံသို့သွားပါ။ 83 |
    84 |
    85 |
    86 |
    87 |
    88 |
    89 |
    90 | ပြည်သူ့လွှတ်တော် ကိုယ်စားလှယ် ရွေးချယ်ရန် 91 |
    92 |
    93 |
    94 | ပြည်သူ့လွှတ်တော် ကိုယ်စားလှယ် ဆန္ဒမဲအတွက် မဲစာရင်းစစ်သူထံ 95 | သွားရောက်၍ မှတ်ပုံတင်ပြသပါ။ 96 |
    97 |
    98 | မဲစာရင်းတွင် မိမိအမည်ပါလျှင် မဲလက်မှတ် ထုတ်ပေးသူထံမှ မဲလက်မှတ် 99 | ရယူပါ။ 100 |
    101 |
    102 | လျှို့ဝှက်ဆန္ဒမဲပေးခန်းသို့ သွား၍ မိမိနှစ်သက်ရာ 103 | ကိုယ်စားလှယ်လောင်း၏ အကွက်တွင် တံဆိပ်တုံးနှိပ်ပါ။ 104 |
    105 |
    106 | ပြည်သူ့လွှတ်တော် ဆန္ဒမဲပုံး ရှိရာသို့သွား၍ မဲပုံးထဲသို့ 107 | မဲလက်မှတ်ကိုထည့်ပါ။ 108 |
    109 |
    110 |
    111 |
    112 |
    113 |
    114 |
    115 | အမျိုးသားလွှတ်တော် ကိုယ်စားလှယ် ရွေးချယ်ရန် 116 |
    117 |
    118 | အမျိုးသားလွှတ်တော် ကိုယ်စားလှယ် ဆန္ဒမဲအတွက် မဲစာရင်းစစ်သူထံ 119 | သွားရောက်၍ မှတ်ပုံတင်ပြသပါ။ 120 |
    121 |
    122 | မဲစာရင်းတွင် မိမိအမည်ပါလျှင် မဲလက်မှတ် ထုတ်ပေးသူထံမှ မဲလက်မှတ် 123 | ရယူပါ။ 124 |
    125 |
    126 | လျှို့ဝှက်ဆန္ဒမဲပေးခန်းသို့ သွား၍ မိမိနှစ်သက်ရာ 127 | ကိုယ်စားလှယ်လောင်း၏ အကွက်တွင် တံဆိပ်တုံးနှိပ်ပါ။ 128 |
    129 |
    130 | အမျိုးသားလွှတ်တော် ဆန္ဒမဲပုံး ရှိရာသို့သွား၍ မဲပုံးထဲသို့ 131 | မဲလက်မှတ်ကိုထည့်ပါ။ 132 |
    133 |
    134 |
    135 |
    136 |
    137 |
    138 | တိုင်းဒေသကြီး/ပြည်နယ် လွှတ်တော် ကိုယ်စားလှယ် ရွေးချယ်ရန် 139 |
    140 |
    141 | တိုင်းဒေသကြီး/ပြည်နယ် လွှတ်တော် ကိုယ်စားလှယ် ဆန္ဒမဲ အတွက် 142 | မဲစာရင်းစစ်သူထံ သွားရောက်၍ မှတ်ပုံတင်ပြသပါ။ 143 |
    144 |
    145 | မဲစာရင်းတွင် မိမိအမည်ပါလျှင် မဲလက်မှတ် ထုတ်ပေးသူထံမှ မဲလက်မှတ် 146 | ရယူပါ။ 147 |
    148 |
    149 | လျှို့ဝှက်ဆန္ဒမဲပေးခန်းသို့ သွား၍ မိမိနှစ်သက်ရာ 150 | ကိုယ်စားလှယ်လောင်း၏ အကွက်တွင် တံဆိပ်တုံးနှိပ်ပါ။ 151 |
    152 |
    153 | တိုင်းဒေသကြီး/ပြည်နယ် လွှတ်တော် ဆန္ဒမဲပုံး ရှိရာသို့သွား၍ 154 | မဲပုံးထဲသို့ မဲလက်မှတ်ကိုထည့်ပါ။ 155 |
    156 |
    157 |
    158 |
    159 |
    160 |
    161 | တိုင်းရင်းသားလူမျိုး ကိုယ်စားလှယ် ရွေးချယ်ရန်ရှိပါက 162 |
    163 |
    164 | တိုင်းရင်းသားလူမျိုး ကိုယ်စားလှယ် ဆန္ဒမဲအတွက် မဲစာရင်းစစ်သူထံ 165 | သွားရောက်၍ မှတ်ပုံတင်ပြသပါ။ 166 |
    167 |
    168 | မဲစာရင်းတွင် မိမိအမည်ပါလျှင် မဲလက်မှတ် ထုတ်ပေးသူထံမှ မဲလက်မှတ် 169 | ရယူပါ။ 170 |
    171 |
    172 | လျှို့ဝှက်ဆန္ဒမဲပေးခန်းသို့ သွား၍ မိမိနှစ်သက်ရာ 173 | ကိုယ်စားလှယ်လောင်း၏ အကွက်တွင် တံဆိပ်တုံးနှိပ်ပါ။ 174 |
    175 |
    176 | တိုင်းရင်းသားလူမျိုး ဆန္ဒမဲပုံး ရှိရာသို့သွား၍ မဲပုံးထဲသို့ 177 | မဲလက်မှတ်ကိုထည့်ပါ။ 178 |
    179 |
    180 |
    181 |
    182 |
    183 |
    အထက်ပါ အဆင့်များ လုပ်ဆောင်ပြီးလျှင်
    184 |
    185 | မဲပေးပြီးကြောင်း မင်တို့မည့် မဲရုံအဖွဲ့ဝင်ထံ သွားရောက်၍ မိမိ၏ 186 | လက်ဝဲဘက် လက်သန်းတွင် မင်တို့ပါ။ 187 |
    188 |
    189 | မဲရုံအတွင်းမှ ထွက်နိုင်ပါပြီ။ သင် နိုင်ငံသားတစ်ယောက်၏ တာဝန် 190 | ကျေပွန်ခဲ့ပြီ ဖြစ်သည်။ 191 |
    192 |
    193 |
    194 |
    195 |
    196 | ); 197 | } 198 | -------------------------------------------------------------------------------- /pages/how_to_vote/voter_list.js: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | import { useRouter } from 'next/router'; 3 | import ReactGA from 'react-ga'; 4 | import Head from 'next/head'; 5 | import Layout from '../../components/Layout/Layout'; 6 | import AppHeader from '../../components/Layout/AppHeader/AppHeader'; 7 | import { VOTER_LIST_LINKS } from '../../utils/constants'; 8 | 9 | import './voter_list.module.scss'; 10 | 11 | const VoterListPage = () => { 12 | const router = useRouter(); 13 | 14 | useEffect(() => { 15 | ReactGA.pageview(window.location.pathname); 16 | }, []); 17 | 18 | return ( 19 | 20 | 21 | မဲစာရင်းစစ်ရန် | mVoter 2020 22 | 23 | 24 |
    25 | 26 | router.back()}> 27 | arrow_back 28 | 29 | 30 |
    မဲစာရင်းစစ်ရန်
    31 |
    32 |
    33 |
    34 |

    35 | မိမိ အမည်ပါ၊ မပါ မဲစာရင်းစစ်ရန် ရွေးကောက်ပွဲကော်မရှင်မှ တရားဝင် Web 36 | Application Link များအား အသုံးပြုစစ်ဆေးနိုင်ပါတယ်။ 37 |

    38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | {VOTER_LIST_LINKS.map(({ region, url }, index) => ( 47 | 48 | 49 | 54 | 55 | ))} 56 | 57 |
    တိုင်းဒေသကြီး/ပြည်နယ်စစ်ဆေးရန် App Link
    {region} 50 | 51 | {url} 52 | 53 |
    58 |
    59 |
    60 | ); 61 | }; 62 | 63 | export default VoterListPage; 64 | -------------------------------------------------------------------------------- /pages/how_to_vote/voter_list.module.scss: -------------------------------------------------------------------------------- 1 | @import '../../styles/partials/colors'; 2 | 3 | .VoterList { 4 | &__table { 5 | width: 100%; 6 | th, tr, td { 7 | padding: 5px 10px; 8 | border: 1px solid rgba($color: #000000, $alpha: 0.85); 9 | } 10 | } 11 | } 12 | 13 | @media (min-width: 1200px) { 14 | .VoterList { 15 | &__table { 16 | th, tr, td { 17 | padding: 10px; 18 | } 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /pages/index.js: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | import Head from 'next/head'; 3 | import { useRouter } from 'next/router'; 4 | import Layout from '../components/Layout/Layout'; 5 | 6 | const Home = () => { 7 | const router = useRouter(); 8 | 9 | useEffect(() => { 10 | router.push('/candidates'); 11 | }, []); 12 | 13 | return ( 14 |
    15 | 16 | mVoter 2020 Web Application 17 | 18 | 19 | 23 | 24 | 25 | 26 | 30 | 31 | 32 | 33 | 34 | 35 | 39 | 40 | 41 | 42 |
    43 | ); 44 | }; 45 | 46 | export default Home; 47 | -------------------------------------------------------------------------------- /pages/location/index.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import Head from 'next/head'; 3 | import { useRouter } from 'next/router'; 4 | 5 | import Link from 'next/link'; 6 | import Layout from '../../components/Layout/Layout'; 7 | import ActivePeopleIcon from '../../components/Common/Icons/activePeople'; 8 | import Button from '../../components/Common/Button/Button'; 9 | import AppHeader from '../../components/Layout/AppHeader/AppHeader'; 10 | import TownshipModal from '../../components/Location/TownshipModal'; 11 | import WardVillageModal from '../../components/Location/WardVillageModal'; 12 | 13 | import './location.module.scss'; 14 | import { hasFullLocation } from '../../utils/helpers'; 15 | import { LOCALSTORAGE_KEYS } from '../../utils/constants'; 16 | 17 | const Location = () => { 18 | const [townshipModalOpen, setTownshipModalOpen] = useState(false); 19 | const [wardVillageModalOpen, setWardVillageModalOpen] = useState(false); 20 | const [stateRegion, setStateRegion] = useState(); 21 | const [township, setTownship] = useState(); 22 | const [wardVillage, setWardVillage] = useState(); 23 | const [isAppStart, setIsAppStart] = useState(true); 24 | const router = useRouter(); 25 | 26 | useEffect(() => { 27 | if (hasFullLocation()) { 28 | setIsAppStart(false); 29 | setStateRegion(localStorage.getItem(LOCALSTORAGE_KEYS.STATE_REGION)); 30 | setTownship(localStorage.getItem(LOCALSTORAGE_KEYS.TOWNSHIP)); 31 | setWardVillage(localStorage.getItem(LOCALSTORAGE_KEYS.WARD_VILLAGE)); 32 | } 33 | }, []); 34 | 35 | function onClickDone() { 36 | localStorage.setItem(LOCALSTORAGE_KEYS.STATE_REGION, stateRegion); 37 | localStorage.setItem(LOCALSTORAGE_KEYS.TOWNSHIP, township); 38 | localStorage.setItem(LOCALSTORAGE_KEYS.WARD_VILLAGE, wardVillage); 39 | router.push('/candidates'); 40 | } 41 | 42 | function onClickChooseTownship() { 43 | setTownshipModalOpen(true); 44 | } 45 | 46 | function onClickChooseWardVillage() { 47 | setWardVillageModalOpen(true); 48 | } 49 | 50 | function onClickTownship(chosenStateRegion, chosenTownship) { 51 | // Set localStorage here 52 | setStateRegion(chosenStateRegion); 53 | setTownship(chosenTownship); 54 | setWardVillage(null); 55 | setTownshipModalOpen(false); 56 | } 57 | 58 | function onClickWardVillage(chosenWardVillage) { 59 | setWardVillage(chosenWardVillage); 60 | setWardVillageModalOpen(false); 61 | } 62 | 63 | return ( 64 | 65 | 66 | မဲဆန္ဒနယ်ရွေးရန် | mVoter 2020 67 | 68 | {!isAppStart && ( 69 | 70 |
    71 | 76 | ကိုယ်စားလှယ်လောင်းများ 77 |
    78 |
    79 | )} 80 |
    81 |
    82 |
    83 |
    84 |
    {ActivePeopleIcon}
    85 |

    86 | မိမိ မဲပေးရွေးချယ်ရမည့် လွှတ်တော်ကိုယ်စားလှယ််လောင်းများကို 87 | သိရှိရန် နေထိုင်ရာအရပ်ကို ရွေးပါ 88 |

    89 |
    90 | 102 | 115 |
    116 |
    117 |
    118 |
    119 | 127 |
    128 |
    129 |
    130 |
    131 | 136 | 143 |
    144 | ); 145 | }; 146 | 147 | export default Location; 148 | -------------------------------------------------------------------------------- /pages/location/location.module.scss: -------------------------------------------------------------------------------- 1 | @import '../../styles/partials/colors'; 2 | @import '../../styles/partials/extends'; 3 | 4 | .locationSelector { 5 | // display: inline-block; 6 | display: flex; 7 | justify-content: center; 8 | align-items: center; 9 | margin: 0 auto; 10 | padding: 10px 0px 10px 5px; 11 | text-align: center; 12 | border: 0; 13 | background: none; 14 | &:disabled { 15 | span { 16 | color: rgba($color: #000000, $alpha: 0.25) !important; 17 | } 18 | } 19 | span { 20 | color: $primary-color; 21 | margin-right: 4px; 22 | font-weight: 700; 23 | font-size: 1.3rem; 24 | } 25 | svg { 26 | float: right; 27 | } 28 | } 29 | 30 | .people-icon { 31 | margin-bottom: 8px; 32 | >svg { 33 | width: 135px; 34 | height: 78px; 35 | } 36 | } 37 | 38 | .Location { 39 | height: calc(100vh - 180px); 40 | &__wrapper { 41 | position: absolute; 42 | top: 50%; 43 | transform: translateY(-50%); 44 | } 45 | &__done { 46 | margin-top: 30px; 47 | border: 0; 48 | background-color: $primary-color; 49 | color: $white-color; 50 | border-radius: 25px; 51 | padding: 5px 50px; 52 | vertical-align: middle; 53 | transition: all 0.2s linear; 54 | &:hover, &:active { 55 | @extend %button-hover; 56 | } 57 | &:disabled { 58 | cursor: not-allowed !important; 59 | background-color: rgba($color: #000000, $alpha: 0.25); 60 | } 61 | i { 62 | margin-top: 3px; 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /pages/news/index.js: -------------------------------------------------------------------------------- 1 | import ReactGA from 'react-ga'; 2 | import { useState, useEffect } from 'react'; 3 | import InfiniteScroll from 'react-infinite-scroll-component'; 4 | import Head from 'next/head'; 5 | import Link from 'next/link'; 6 | 7 | import Layout from '../../components/Layout/Layout'; 8 | import AppHeader from '../../components/Layout/AppHeader/AppHeader'; 9 | import Button from '../../components/Common/Button/Button'; 10 | import NewsList from '../../components/News/NewsList/NewsList'; 11 | 12 | import './news.module.scss'; 13 | import useAPI from '../../hooks/useAPI'; 14 | 15 | const News = () => { 16 | const [news, setNews] = useState([]); 17 | const [page, setPage] = useState(1); 18 | const [, fetchData] = useAPI(); 19 | 20 | async function fetchNews(pageToLoad = 1) { 21 | try { 22 | const { data } = await fetchData('/api/news', { 23 | page: pageToLoad 24 | }); 25 | 26 | return setNews(news.concat(data)); 27 | } catch (error) { 28 | return error; 29 | } 30 | } 31 | 32 | useEffect(() => { 33 | ReactGA.pageview(window.location.pathname); 34 | 35 | fetchNews(); 36 | }, []); 37 | 38 | function loadMoreNews() { 39 | const nextPage = page + 1; 40 | fetchNews(nextPage); 41 | setPage(nextPage); 42 | } 43 | 44 | return ( 45 | 46 | 47 | သတင်းများ | mVoter 2020 48 | 49 | 50 |
    သတင်းများ
    51 |
    52 | 53 | 54 | 57 | 58 | 59 |
    60 |
    61 |
    62 |
    63 |
    64 | 69 | 70 | 71 |
    72 |
    73 |
    74 |
    75 | ); 76 | }; 77 | 78 | export default News; 79 | -------------------------------------------------------------------------------- /pages/news/news.module.scss: -------------------------------------------------------------------------------- 1 | @import '../../styles/partials/extends'; 2 | 3 | .News { 4 | @extend %mobile-vert-rules; 5 | } -------------------------------------------------------------------------------- /pages/news/search.js: -------------------------------------------------------------------------------- 1 | import SearchPage from '../../components/Search/SearchPage'; 2 | import NewsList from '../../components/News/NewsList/NewsList'; 3 | 4 | const NewsSearch = () => { 5 | return ( 6 | 13 | 14 | 15 | ); 16 | }; 17 | 18 | export default NewsSearch; 19 | -------------------------------------------------------------------------------- /pages/parties/[party].js: -------------------------------------------------------------------------------- 1 | import ReactGA from 'react-ga'; 2 | import { useEffect } from 'react'; 3 | import { useRouter } from 'next/router'; 4 | import nookies from 'nookies'; 5 | import Head from 'next/head'; 6 | import myanmarNumbers from 'myanmar-numbers'; 7 | import Layout from '../../components/Layout/Layout'; 8 | import AppHeader from '../../components/Layout/AppHeader/AppHeader'; 9 | import MaePaySohAPI from '../../gateway/api'; 10 | import Button from '../../components/Common/Button/Button'; 11 | import { useAuthContext } from '../../context/AuthProvider'; 12 | 13 | import './party.module.scss'; 14 | 15 | const Party = props => { 16 | const { 17 | party: { 18 | id, 19 | flag_image: flagImage, 20 | seal_image: sealImage, 21 | abbreviation, 22 | name_burmese: nameBurmese, 23 | name_english: nameEnglish, 24 | policy, 25 | region, 26 | leaders_and_chairmen: leadership, 27 | member_count: memberCount, 28 | headquarter_address: headquarterAddress, 29 | contacts, 30 | establishment_application_date: establishmentApplicationDate, 31 | establishment_approval_date: establishmentApprovalDate, 32 | registration_application_date: registrationApplicationDate, 33 | registration_approved_date: registrationApprovedDate 34 | }, 35 | token = null 36 | } = props; 37 | 38 | const router = useRouter(); 39 | const { updateToken } = useAuthContext(token); 40 | 41 | useEffect(() => ReactGA.pageview('/parties/[party]'), []); 42 | useEffect(() => { 43 | if (token) { 44 | updateToken(token); 45 | } 46 | }, [token]); 47 | 48 | return ( 49 | 50 | 51 | {nameBurmese} | mVoter 2020 52 | 53 | 54 | 55 | 59 | 60 | 61 | 65 | 66 | 67 | 71 | 72 | 76 | 77 | 78 | 79 | 82 | 83 |
    84 |
    85 |
    86 | Party Seal 91 |
    92 |
    93 |

    {nameBurmese}

    94 |

    {nameEnglish}

    95 | {abbreviation && ( 96 |

    {abbreviation}

    97 | )} 98 |

    {region}

    99 | 105 |
    ပါတီ မူဝါဒ
    106 |
    107 |
    108 |
    109 |
    110 |
    111 |
    112 | 113 |
    114 |
    ပါတီအမှတ်စဥ်
    115 |
    116 | {myanmarNumbers(id, 'my')} 117 |
    118 |
    119 |
    120 |
    121 | {leadership.length > 0 && ( 122 | <> 123 |
    124 | ပါတီဥက္ကဋ္ဌနှင့် ဗဟိုအလုပ်အမှုဆောင်များ 125 |
    126 |
    127 | {leadership.join('၊ ')} 128 |
    129 | 130 | )} 131 |
    132 |
    133 |
    လျှောက်ထားစဥ် ပါတီအင်အား
    134 |
    135 | {myanmarNumbers(memberCount, 'my')} 136 |
    137 |
    138 |
    139 |
    ပါတီရုံးချုပ်
    140 |
    {headquarterAddress}
    141 |
    142 |
    143 |
    ဆက်သွယ်ရန်
    144 |
    {contacts.join('၊ ')}
    145 |
    146 |
    147 | {establishmentApplicationDate && ( 148 |
    149 |
    150 | {establishmentApplicationDate} 151 |
    152 |
    153 | ပါတီ တည်ထောင်ခွင့်လျှောက်ထားသည် 154 |
    155 |
    156 | )} 157 | {establishmentApprovalDate && ( 158 |
    159 |
    160 | {establishmentApprovalDate} 161 |
    162 |
    163 | ပါတီ တည်ထောင်ခွင့် ရရှိသည် 164 |
    165 |
    166 | )} 167 | {registrationApplicationDate && ( 168 |
    169 |
    170 | {registrationApplicationDate} 171 |
    172 |
    173 | ပါတီအဖြစ် မှတ်ပုံတင်ခွင့် လျှောက်ထားသည် 174 |
    175 |
    176 | )} 177 | {registrationApprovedDate && ( 178 |
    179 |
    180 | {registrationApprovedDate} 181 |
    182 |
    183 | ပါတီအဖြစ် မှတ်ပုံတင်ခွင့် ရရှိသည် 184 |
    185 |
    186 | )} 187 |
    188 |
    189 |
    190 |
    191 |
    192 | ); 193 | }; 194 | 195 | export async function getServerSideProps(context) { 196 | const { params } = context; 197 | 198 | const cookies = nookies.get(context); 199 | const api = new MaePaySohAPI(cookies.token); 200 | const response = await api.getPartyById(params.party); 201 | 202 | const { data, token } = response.data; 203 | // expand everything inside data attributes to primary object 204 | return { 205 | props: { 206 | party: { 207 | ...data, 208 | ...data.attributes 209 | }, 210 | ...(token && { token }) 211 | } 212 | }; 213 | } 214 | 215 | export default Party; 216 | -------------------------------------------------------------------------------- /pages/parties/index.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import ReactGA from 'react-ga'; 3 | import Head from 'next/head'; 4 | import Link from 'next/link'; 5 | import InfiniteScroll from 'react-infinite-scroll-component'; 6 | 7 | import useAPI from '../../hooks/useAPI'; 8 | import Layout from '../../components/Layout/Layout'; 9 | import AppHeader from '../../components/Layout/AppHeader/AppHeader'; 10 | import Button from '../../components/Common/Button/Button'; 11 | import PartyList from '../../components/Parties/PartyList/PartyList'; 12 | 13 | import './parties.module.scss'; 14 | 15 | const Parties = () => { 16 | // Inject AJAX call on first load 17 | const [parties, setParties] = useState([]); 18 | const [totalCount, setTotalCount] = useState(0); 19 | const [page, setPage] = useState(1); 20 | const [, fetchData] = useAPI(); 21 | 22 | async function fetchParties(pageToLoad = 1) { 23 | try { 24 | const data = await fetchData('/api/parties', { 25 | page: pageToLoad, 26 | item_per_page: 25 27 | }); 28 | 29 | return data; 30 | } catch (error) { 31 | return error; 32 | } 33 | } 34 | 35 | useEffect(() => { 36 | // initial load 37 | ReactGA.pageview(window.location.pathname); 38 | 39 | fetchParties() 40 | .then(result => { 41 | setParties(result.data); 42 | setTotalCount(result.pagination.total); 43 | }) 44 | .catch(error => error); 45 | }, []); 46 | 47 | async function loadMoreParties() { 48 | const nextPage = page + 1; 49 | fetchParties(nextPage) 50 | .then(result => setParties(parties.concat(result.data))) 51 | .then(() => setPage(nextPage)) 52 | .catch(error => error); 53 | } 54 | 55 | return ( 56 | 57 | 58 | ပါတီများ | mVoter 2020 59 | 60 | 61 |
    ပါတီများ
    62 |
    63 | 64 | 65 | 68 | 69 | 70 |
    71 |
    72 |
    73 |
    74 |
    75 |
    76 |
    77 | flag 78 |
    79 |

    80 | (၂၀၂၀) ခုနှစ် အထွေထွေ ရွေးကောက်ပွဲတွင် ဝင်ရောက်ယှဥ်ပြိုင်မည့် 81 | နိုင်ငံရေးပါတီ (၉၁) ခု 82 |

    83 |
    84 |
    85 |
    86 | 91 | 92 | 93 |
    94 |
    95 | ); 96 | }; 97 | 98 | export default Parties; 99 | -------------------------------------------------------------------------------- /pages/parties/parties.module.scss: -------------------------------------------------------------------------------- 1 | @import '../../styles/partials/colors'; 2 | @import '../../styles/partials/extends'; 3 | 4 | .Parties { 5 | @extend %mobile-vert-rules; 6 | &__Container { 7 | min-height: 100vh; 8 | } 9 | &__Wrapper { 10 | padding: 5px 10px; 11 | } 12 | &__infoHeader { 13 | display: flex; 14 | padding: 10px 15px; 15 | border-radius: 6px; 16 | background-color: rgba($color: $primary-color, $alpha: 0.25); 17 | font-weight: 600; 18 | font-size: 0.875rem; 19 | .icon-blk { 20 | position: relative; 21 | width: 30px; 22 | height: 20px; 23 | margin-top: 4px; 24 | margin-right: 8px; 25 | background-color: $primary-color; 26 | border-radius: 100%; 27 | .flag { 28 | position: absolute; 29 | left: 50%; 30 | top: 50%; 31 | transform: translateX(-50%) translateY(-50%); 32 | font-size: 1rem; 33 | color: #ffffff; 34 | } 35 | } 36 | p { 37 | margin: 0; 38 | } 39 | } 40 | &__buttonGroup { 41 | color: $primary-color; 42 | } 43 | ul { 44 | overflow: hidden; 45 | } 46 | } 47 | 48 | @media (min-width: 1200px) { 49 | .Parties { 50 | &__infoHeader { 51 | .icon-blk { 52 | height: 30px; 53 | margin-top: 0; 54 | } 55 | } 56 | } 57 | } -------------------------------------------------------------------------------- /pages/parties/party.module.scss: -------------------------------------------------------------------------------- 1 | @import '../../styles/partials/colors'; 2 | 3 | .Party { 4 | line-height: 25px; 5 | &__headInfo { 6 | display: flex; 7 | justify-content: center; 8 | text-align: center; 9 | padding-top: 5px; 10 | margin-bottom: 12px; 11 | } 12 | &__seal { 13 | width: 150px; 14 | height: auto; 15 | display: block; 16 | margin-left: auto; 17 | margin-right: auto; 18 | } 19 | &__flag { 20 | display: inline-block; 21 | width: auto; 22 | height: 40px; 23 | background-position: center center; 24 | background-repeat: no-repeat; 25 | background-size: contain; 26 | } 27 | &__title { 28 | font-size: 1.25rem; 29 | } 30 | &__engTitle { 31 | font-size: 1.25rem; 32 | font-weight: 400; 33 | margin: 0; 34 | } 35 | &__abbreviation { 36 | font-size: 1.5rem; 37 | font-weight: 600; 38 | } 39 | &__partyPolicy { 40 | background-color: $primary-color; 41 | color: #ffffff; 42 | font-size: 0.85rem; 43 | border-radius: 30px; 44 | padding: 5px 10px; 45 | box-shadow: 0px 2px 4px rgba($color: $primary-color, $alpha: 0.25); 46 | transition: all 0.2s linear; 47 | &:hover { 48 | opacity: 0.95; 49 | box-shadow: 0px 2px 6px rgba($color: $primary-color, $alpha: 1); 50 | } 51 | } 52 | &__region { 53 | font-size: 0.875rem; 54 | } 55 | &__info { 56 | margin-bottom: 12px; 57 | } 58 | &__flagInfo { 59 | display: flex; 60 | align-items: center; 61 | } 62 | &__infoLabel { 63 | font-size: 0.875rem; 64 | color: rgba($color: #000000, $alpha: 0.85); 65 | } 66 | &__infoAnswer { 67 | font-size: 1rem; 68 | } 69 | &__timeline { 70 | background: rgba($color: $primary-color, $alpha: 0.25); 71 | padding: 5px; 72 | .timeline-item { 73 | margin: 5px 0; 74 | .date { 75 | font-weight: 600; 76 | font-size: 1.15rem; 77 | } 78 | .time { 79 | padding-left: 0; 80 | } 81 | &:last-child { // Tricky hack 82 | .description { 83 | &::before { 84 | display: none; 85 | } 86 | } 87 | } 88 | .description { 89 | position: relative; 90 | &::before { 91 | content: ''; 92 | display: inline-block; 93 | position: absolute; 94 | top: 14px; 95 | left: 0px; 96 | width: 2px; 97 | height: 100%; 98 | background-color: $primary-color; 99 | } 100 | &::after { 101 | content: ''; 102 | display: inline-block; 103 | position: absolute; 104 | border-radius: 100%; 105 | top: 8px; 106 | left: -5px; 107 | width: 12px; 108 | height: 12px; 109 | background-color: $primary-color; 110 | } 111 | } 112 | } 113 | } 114 | } 115 | 116 | @media (min-width: 1200px) { 117 | .Party { 118 | &__headInfo { 119 | text-align: left; 120 | margin-bottom: 15px; 121 | } 122 | &__infoLabel { 123 | color: $primary-color; 124 | } 125 | &__timeline { 126 | padding: 5px 0px 15px 0px; 127 | } 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /pages/parties/search.js: -------------------------------------------------------------------------------- 1 | import SearchPage from '../../components/Search/SearchPage'; 2 | import PartyList from '../../components/Parties/PartyList/PartyList'; 3 | 4 | const FaqSearch = () => { 5 | // Why pass endpoint you may ask? Answer: I am lazy. 6 | return ( 7 | 14 | 15 | 16 | ); 17 | }; 18 | 19 | export default FaqSearch; 20 | -------------------------------------------------------------------------------- /public/UEC.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PopStackHack/mVoterWeb/71a6bf3da114a0688689dc93658576c8c806afe5/public/UEC.png -------------------------------------------------------------------------------- /public/about/TAFVLogo_RGB.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PopStackHack/mVoterWeb/71a6bf3da114a0688689dc93658576c8c806afe5/public/about/TAFVLogo_RGB.jpg -------------------------------------------------------------------------------- /public/about/UEC_logo_mm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PopStackHack/mVoterWeb/71a6bf3da114a0688689dc93658576c8c806afe5/public/about/UEC_logo_mm.png -------------------------------------------------------------------------------- /public/about/eu_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PopStackHack/mVoterWeb/71a6bf3da114a0688689dc93658576c8c806afe5/public/about/eu_logo.png -------------------------------------------------------------------------------- /public/about/eu_logo.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PopStackHack/mVoterWeb/71a6bf3da114a0688689dc93658576c8c806afe5/public/about/eu_logo.webp -------------------------------------------------------------------------------- /public/about/idea_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PopStackHack/mVoterWeb/71a6bf3da114a0688689dc93658576c8c806afe5/public/about/idea_logo.png -------------------------------------------------------------------------------- /public/about/idea_logo.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PopStackHack/mVoterWeb/71a6bf3da114a0688689dc93658576c8c806afe5/public/about/idea_logo.webp -------------------------------------------------------------------------------- /public/about/maepaysoh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PopStackHack/mVoterWeb/71a6bf3da114a0688689dc93658576c8c806afe5/public/about/maepaysoh.png -------------------------------------------------------------------------------- /public/about/mvoter2020_new_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PopStackHack/mVoterWeb/71a6bf3da114a0688689dc93658576c8c806afe5/public/about/mvoter2020_new_logo.png -------------------------------------------------------------------------------- /public/about/popstack_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PopStackHack/mVoterWeb/71a6bf3da114a0688689dc93658576c8c806afe5/public/about/popstack_logo.png -------------------------------------------------------------------------------- /public/about/popstack_logo.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PopStackHack/mVoterWeb/71a6bf3da114a0688689dc93658576c8c806afe5/public/about/popstack_logo.webp -------------------------------------------------------------------------------- /public/about/step_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PopStackHack/mVoterWeb/71a6bf3da114a0688689dc93658576c8c806afe5/public/about/step_logo.png -------------------------------------------------------------------------------- /public/about/step_logo.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PopStackHack/mVoterWeb/71a6bf3da114a0688689dc93658576c8c806afe5/public/about/step_logo.webp -------------------------------------------------------------------------------- /public/about/taf_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PopStackHack/mVoterWeb/71a6bf3da114a0688689dc93658576c8c806afe5/public/about/taf_logo.png -------------------------------------------------------------------------------- /public/about/taf_logo.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PopStackHack/mVoterWeb/71a6bf3da114a0688689dc93658576c8c806afe5/public/about/taf_logo.webp -------------------------------------------------------------------------------- /public/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PopStackHack/mVoterWeb/71a6bf3da114a0688689dc93658576c8c806afe5/public/android-chrome-192x192.png -------------------------------------------------------------------------------- /public/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PopStackHack/mVoterWeb/71a6bf3da114a0688689dc93658576c8c806afe5/public/android-chrome-512x512.png -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PopStackHack/mVoterWeb/71a6bf3da114a0688689dc93658576c8c806afe5/public/apple-touch-icon.png -------------------------------------------------------------------------------- /public/ballot_stack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PopStackHack/mVoterWeb/71a6bf3da114a0688689dc93658576c8c806afe5/public/ballot_stack.png -------------------------------------------------------------------------------- /public/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #da532c 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PopStackHack/mVoterWeb/71a6bf3da114a0688689dc93658576c8c806afe5/public/favicon-16x16.png -------------------------------------------------------------------------------- /public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PopStackHack/mVoterWeb/71a6bf3da114a0688689dc93658576c8c806afe5/public/favicon-32x32.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PopStackHack/mVoterWeb/71a6bf3da114a0688689dc93658576c8c806afe5/public/favicon.ico -------------------------------------------------------------------------------- /public/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PopStackHack/mVoterWeb/71a6bf3da114a0688689dc93658576c8c806afe5/public/mstile-150x150.png -------------------------------------------------------------------------------- /public/mvoter2020-transparent-vertical.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PopStackHack/mVoterWeb/71a6bf3da114a0688689dc93658576c8c806afe5/public/mvoter2020-transparent-vertical.png -------------------------------------------------------------------------------- /public/prohibition_signs/no_photo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PopStackHack/mVoterWeb/71a6bf3da114a0688689dc93658576c8c806afe5/public/prohibition_signs/no_photo.png -------------------------------------------------------------------------------- /public/prohibition_signs/no_recording.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PopStackHack/mVoterWeb/71a6bf3da114a0688689dc93658576c8c806afe5/public/prohibition_signs/no_recording.png -------------------------------------------------------------------------------- /public/prohibition_signs/no_selfie.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PopStackHack/mVoterWeb/71a6bf3da114a0688689dc93658576c8c806afe5/public/prohibition_signs/no_selfie.png -------------------------------------------------------------------------------- /public/prohibition_signs/no_video.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PopStackHack/mVoterWeb/71a6bf3da114a0688689dc93658576c8c806afe5/public/prohibition_signs/no_video.png -------------------------------------------------------------------------------- /public/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.11, written by Peter Selinger 2001-2013 9 | 10 | 12 | 20 | 26 | 32 | 38 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /public/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mVoter 2020", 3 | "short_name": "mVoter 2020", 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 | -------------------------------------------------------------------------------- /public/zeit.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /styles/base.scss: -------------------------------------------------------------------------------- 1 | @import '../styles/partials/colors'; 2 | 3 | .ReactModal__Overlay { 4 | opacity: 0; 5 | transition: all 200ms ease-in-out; 6 | } 7 | 8 | .ReactModal__Overlay--after-open { 9 | opacity: 1; 10 | } 11 | 12 | .ReactModal__Overlay--before-close { 13 | opacity: 0; 14 | } 15 | 16 | .vert-flex-center { 17 | display: flex; 18 | align-items: center; 19 | } 20 | 21 | .text-bold { 22 | font-weight: 700; 23 | } 24 | 25 | *:disabled { 26 | cursor: not-allowed !important; 27 | } 28 | 29 | a.no-style { 30 | color: inherit !important; 31 | &:hover { 32 | text-decoration: none !important; 33 | } 34 | } 35 | 36 | p { 37 | line-height: 26px; 38 | } 39 | 40 | .cursor-pointer { 41 | cursor: pointer; 42 | } 43 | 44 | .color-primary { 45 | color: $primary-color; 46 | * { 47 | color: $primary-color; 48 | } 49 | } 50 | 51 | .color-white { 52 | color: $white-color; 53 | * { 54 | color: $white-color; 55 | } 56 | } 57 | 58 | .color-danger { 59 | color: $danger-color; 60 | * { 61 | color: $danger-color; 62 | } 63 | } 64 | 65 | .no-padding { 66 | padding: 0 !important; 67 | } 68 | 69 | .no-margin { 70 | margin: 0 !important; 71 | } 72 | 73 | .no-text-decor { 74 | text-decoration: none !important; 75 | } 76 | 77 | .vert-align-middle { 78 | vertical-align: middle; 79 | } 80 | 81 | a, select { 82 | cursor: pointer; 83 | } 84 | 85 | .box-hover { 86 | transition: all 0.2s linear !important; 87 | &:hover, &:active { 88 | border-color: rgba($color: $primary-color, $alpha: 0.5); 89 | // box-shadow: 0px 2px 6px rgba($color: #000000, $alpha: 0.2); 90 | } 91 | } 92 | 93 | .loader { 94 | color: $primary-color; 95 | animation: spin 0.7s linear infinite; 96 | } 97 | 98 | @keyframes spin { 99 | 0% { 100 | transform: rotate(0deg); 101 | } 102 | 100% { 103 | transform: rotate(360deg) 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /styles/normalize.scss: -------------------------------------------------------------------------------- 1 | /*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */ 2 | 3 | /* Document 4 | ========================================================================== */ 5 | 6 | /** 7 | * 1. Correct the line height in all browsers. 8 | * 2. Prevent adjustments of font size after orientation changes in iOS. 9 | */ 10 | 11 | html { 12 | line-height: 1.15; /* 1 */ 13 | -webkit-text-size-adjust: 100%; /* 2 */ 14 | } 15 | 16 | /* Sections 17 | ========================================================================== */ 18 | 19 | /** 20 | * Remove the margin in all browsers. 21 | */ 22 | 23 | body { 24 | margin: 0; 25 | } 26 | 27 | /** 28 | * Render the `main` element consistently in IE. 29 | */ 30 | 31 | main { 32 | display: block; 33 | } 34 | 35 | /** 36 | * Correct the font size and margin on `h1` elements within `section` and 37 | * `article` contexts in Chrome, Firefox, and Safari. 38 | */ 39 | 40 | h1 { 41 | font-size: 2em; 42 | margin: 0.67em 0; 43 | } 44 | 45 | /* Grouping content 46 | ========================================================================== */ 47 | 48 | /** 49 | * 1. Add the correct box sizing in Firefox. 50 | * 2. Show the overflow in Edge and IE. 51 | */ 52 | 53 | hr { 54 | box-sizing: content-box; /* 1 */ 55 | height: 0; /* 1 */ 56 | overflow: visible; /* 2 */ 57 | } 58 | 59 | /** 60 | * 1. Correct the inheritance and scaling of font size in all browsers. 61 | * 2. Correct the odd `em` font sizing in all browsers. 62 | */ 63 | 64 | pre { 65 | font-family: monospace, monospace; /* 1 */ 66 | font-size: 1em; /* 2 */ 67 | } 68 | 69 | /* Text-level semantics 70 | ========================================================================== */ 71 | 72 | /** 73 | * Remove the gray background on active links in IE 10. 74 | */ 75 | 76 | a { 77 | background-color: transparent; 78 | } 79 | 80 | /** 81 | * 1. Remove the bottom border in Chrome 57- 82 | * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari. 83 | */ 84 | 85 | abbr[title] { 86 | border-bottom: none; /* 1 */ 87 | text-decoration: underline; /* 2 */ 88 | text-decoration: underline dotted; /* 2 */ 89 | } 90 | 91 | /** 92 | * Add the correct font weight in Chrome, Edge, and Safari. 93 | */ 94 | 95 | b, 96 | strong { 97 | font-weight: bolder; 98 | } 99 | 100 | /** 101 | * 1. Correct the inheritance and scaling of font size in all browsers. 102 | * 2. Correct the odd `em` font sizing in all browsers. 103 | */ 104 | 105 | code, 106 | kbd, 107 | samp { 108 | font-family: monospace, monospace; /* 1 */ 109 | font-size: 1em; /* 2 */ 110 | } 111 | 112 | /** 113 | * Add the correct font size in all browsers. 114 | */ 115 | 116 | small { 117 | font-size: 80%; 118 | } 119 | 120 | /** 121 | * Prevent `sub` and `sup` elements from affecting the line height in 122 | * all browsers. 123 | */ 124 | 125 | sub, 126 | sup { 127 | font-size: 75%; 128 | line-height: 0; 129 | position: relative; 130 | vertical-align: baseline; 131 | } 132 | 133 | sub { 134 | bottom: -0.25em; 135 | } 136 | 137 | sup { 138 | top: -0.5em; 139 | } 140 | 141 | /* Embedded content 142 | ========================================================================== */ 143 | 144 | /** 145 | * Remove the border on images inside links in IE 10. 146 | */ 147 | 148 | img { 149 | border-style: none; 150 | } 151 | 152 | /* Forms 153 | ========================================================================== */ 154 | 155 | /** 156 | * 1. Change the font styles in all browsers. 157 | * 2. Remove the margin in Firefox and Safari. 158 | */ 159 | 160 | button, 161 | input, 162 | optgroup, 163 | select, 164 | textarea { 165 | font-family: inherit; /* 1 */ 166 | font-size: 100%; /* 1 */ 167 | line-height: 1.15; /* 1 */ 168 | margin: 0; /* 2 */ 169 | } 170 | 171 | /** 172 | * Show the overflow in IE. 173 | * 1. Show the overflow in Edge. 174 | */ 175 | 176 | button, 177 | input { /* 1 */ 178 | overflow: visible; 179 | } 180 | 181 | /** 182 | * Remove the inheritance of text transform in Edge, Firefox, and IE. 183 | * 1. Remove the inheritance of text transform in Firefox. 184 | */ 185 | 186 | button, 187 | select { /* 1 */ 188 | text-transform: none; 189 | } 190 | 191 | /** 192 | * Correct the inability to style clickable types in iOS and Safari. 193 | */ 194 | 195 | button, 196 | [type="button"], 197 | [type="reset"], 198 | [type="submit"] { 199 | -webkit-appearance: button; 200 | } 201 | 202 | /** 203 | * Remove the inner border and padding in Firefox. 204 | */ 205 | 206 | button::-moz-focus-inner, 207 | [type="button"]::-moz-focus-inner, 208 | [type="reset"]::-moz-focus-inner, 209 | [type="submit"]::-moz-focus-inner { 210 | border-style: none; 211 | padding: 0; 212 | } 213 | 214 | /** 215 | * Restore the focus styles unset by the previous rule. 216 | */ 217 | 218 | button:-moz-focusring, 219 | [type="button"]:-moz-focusring, 220 | [type="reset"]:-moz-focusring, 221 | [type="submit"]:-moz-focusring { 222 | outline: 1px dotted ButtonText; 223 | } 224 | 225 | /** 226 | * Correct the padding in Firefox. 227 | */ 228 | 229 | fieldset { 230 | padding: 0.35em 0.75em 0.625em; 231 | } 232 | 233 | /** 234 | * 1. Correct the text wrapping in Edge and IE. 235 | * 2. Correct the color inheritance from `fieldset` elements in IE. 236 | * 3. Remove the padding so developers are not caught out when they zero out 237 | * `fieldset` elements in all browsers. 238 | */ 239 | 240 | legend { 241 | box-sizing: border-box; /* 1 */ 242 | color: inherit; /* 2 */ 243 | display: table; /* 1 */ 244 | max-width: 100%; /* 1 */ 245 | padding: 0; /* 3 */ 246 | white-space: normal; /* 1 */ 247 | } 248 | 249 | /** 250 | * Add the correct vertical alignment in Chrome, Firefox, and Opera. 251 | */ 252 | 253 | progress { 254 | vertical-align: baseline; 255 | } 256 | 257 | /** 258 | * Remove the default vertical scrollbar in IE 10+. 259 | */ 260 | 261 | textarea { 262 | overflow: auto; 263 | } 264 | 265 | /** 266 | * 1. Add the correct box sizing in IE 10. 267 | * 2. Remove the padding in IE 10. 268 | */ 269 | 270 | [type="checkbox"], 271 | [type="radio"] { 272 | box-sizing: border-box; /* 1 */ 273 | padding: 0; /* 2 */ 274 | } 275 | 276 | /** 277 | * Correct the cursor style of increment and decrement buttons in Chrome. 278 | */ 279 | 280 | [type="number"]::-webkit-inner-spin-button, 281 | [type="number"]::-webkit-outer-spin-button { 282 | height: auto; 283 | } 284 | 285 | /** 286 | * 1. Correct the odd appearance in Chrome and Safari. 287 | * 2. Correct the outline style in Safari. 288 | */ 289 | 290 | [type="search"] { 291 | -webkit-appearance: textfield; /* 1 */ 292 | outline-offset: -2px; /* 2 */ 293 | } 294 | 295 | /** 296 | * Remove the inner padding in Chrome and Safari on macOS. 297 | */ 298 | 299 | [type="search"]::-webkit-search-decoration { 300 | -webkit-appearance: none; 301 | } 302 | 303 | /** 304 | * 1. Correct the inability to style clickable types in iOS and Safari. 305 | * 2. Change font properties to `inherit` in Safari. 306 | */ 307 | 308 | ::-webkit-file-upload-button { 309 | -webkit-appearance: button; /* 1 */ 310 | font: inherit; /* 2 */ 311 | } 312 | 313 | /* Interactive 314 | ========================================================================== */ 315 | 316 | /* 317 | * Add the correct display in Edge, IE 10+, and Firefox. 318 | */ 319 | 320 | details { 321 | display: block; 322 | } 323 | 324 | /* 325 | * Add the correct display in all browsers. 326 | */ 327 | 328 | summary { 329 | display: list-item; 330 | } 331 | 332 | /* Misc 333 | ========================================================================== */ 334 | 335 | /** 336 | * Add the correct display in IE 10+. 337 | */ 338 | 339 | template { 340 | display: none; 341 | } 342 | 343 | /** 344 | * Add the correct display in IE 10. 345 | */ 346 | 347 | [hidden] { 348 | display: none; 349 | } -------------------------------------------------------------------------------- /styles/partials/_colors.scss: -------------------------------------------------------------------------------- 1 | $primary-color: #2071DA; 2 | $white-color: #FFFFFF; 3 | $grey-color: rgba($color: #000000, $alpha: 0.5); 4 | $danger-color: #942d2d; -------------------------------------------------------------------------------- /styles/partials/_extends.scss: -------------------------------------------------------------------------------- 1 | @import './colors'; 2 | 3 | %mobile-vert-rules { 4 | overflow-x: hidden; 5 | } 6 | 7 | %button-hover { 8 | background-color: rgba($primary-color, 0.85); 9 | } -------------------------------------------------------------------------------- /styles/partials/_fontsizes.scss: -------------------------------------------------------------------------------- 1 | $big: 1.5rem; 2 | $medium: 1.1rem; 3 | $normal: 1rem; 4 | $small: 0.8rem; -------------------------------------------------------------------------------- /utils/constants.js: -------------------------------------------------------------------------------- 1 | export const FAQ_CATEGORY = Object.freeze({ 2 | voter_list: 'မဲဆန္ဒရှင်စာရင်း', 3 | candidate: 'ကိုယ်စားလှယ်လောင်း', 4 | international_observer: 'စောင့်ကြည့်လေ့လာခြင်း', 5 | diplomatic: 'သံတမန်ရေးရာ', 6 | conflict_resolution: 'အငြင်းပွားမှုများဖြေရှင်းခြင်း', 7 | mediation_committees: 'စေ့စပ်ညှိနှိုင်းရေးကော်မတီများ' 8 | }); 9 | 10 | export const HOUSES = Object.freeze({ 11 | amyota: 'အမျိုးသားလွှတ်တော်', 12 | pyithu: 'ပြည်ထောင်စုလွှတ်တော်', 13 | state: 'တိုင်း/ပြည်နယ်လွှတ်တော်' 14 | }); 15 | 16 | export const BALLOT_CATEGORIES = Object.freeze([ 17 | { label: 'မဲနမူနာများ', value: 'normal' }, 18 | { label: 'ကြိုတင်မဲနမူနာများ', value: 'advanced' } 19 | ]); 20 | 21 | export const customSelectStyle = Object.freeze({ 22 | option: styles => ({ 23 | ...styles, 24 | cursor: 'pointer' 25 | }), 26 | control: styles => ({ 27 | ...styles, 28 | cursor: 'pointer' 29 | }) 30 | }); 31 | 32 | export const VOTER_LIST_LINKS = [ 33 | { 34 | region: 'တိုင်းဒေသကြီး/ပြည်နယ် အားလုံး', 35 | url: 'https://findyourpollingstation.uec.gov.mm' 36 | }, 37 | { region: 'ကချင်ပြည်နယ်', url: 'http://bit.ly/kachin-voters' }, 38 | { region: 'ကယားပြည်နယ်', url: 'http://bit.ly/kayah-voters' }, 39 | { region: 'ကရင်ပြည်နယ်', url: 'http://bit.ly/kayin-voters' }, 40 | { region: 'ချင်းပြည်နယ်', url: 'http://bit.ly/chin-voters' }, 41 | { region: 'စစ်ကိုင်းတိုင်းဒေသကြီး', url: 'http://bit.ly/sagaing-voters' }, 42 | { region: 'တနင်္သာရီတိုင်းဒေသကြီး', url: 'http://bit.ly/tanintharyi-voters' }, 43 | { region: 'ပဲခူးတိုင်းဒေသကြီး', url: 'http://bit.ly/bago-voters' }, 44 | { region: 'မကွေးတိုင်းဒေသကြီး', url: 'http://bit.ly/magway-voters' }, 45 | { region: 'မန္တလေးတိုင်းဒေသကြီး', url: 'http://bit.ly/mandalay-voters' }, 46 | { region: 'မွန်ပြည်နယ်', url: 'http://bit.ly/mon-voters' }, 47 | { region: 'ရခိုင်ပြည်နယ်', url: 'http://bit.ly/rakhine-voters' }, 48 | { region: 'ရန်ကုန်တိုင်းဒေသကြီး', url: 'http://bit.ly/yangon-voters' }, 49 | { region: 'ရှမ်းပြည်နယ်', url: 'http://bit.ly/shan-voters' }, 50 | { region: 'ဧရာဝတီတိုင်းဒေသကြီး', url: 'http://bit.ly/ayarwaddy-voters' }, 51 | { region: 'ပြည်ထောင်စုနယ်မြေ', url: 'http://bit.ly/naypyitaw-voters' } 52 | ]; 53 | 54 | export const LOCALSTORAGE_KEYS = { 55 | STATE_REGION: 'stateRegion', 56 | TOWNSHIP: 'township', 57 | WARD_VILLAGE: 'wardVillage', 58 | CANDIDATE_PROMPT_DISMISS: 'candidatePromptDismiss' 59 | }; 60 | -------------------------------------------------------------------------------- /utils/helpers.js: -------------------------------------------------------------------------------- 1 | import moment from 'moment'; 2 | import { LOCALSTORAGE_KEYS } from './constants'; 3 | 4 | export function hasFullLocation() { 5 | return ( 6 | localStorage.getItem(LOCALSTORAGE_KEYS.STATE_REGION) && 7 | localStorage.getItem(LOCALSTORAGE_KEYS.TOWNSHIP) && 8 | localStorage.getItem(LOCALSTORAGE_KEYS.WARD_VILLAGE) 9 | ); 10 | } 11 | 12 | // https://chrisboakes.com/how-a-javascript-debounce-function-works/ <- Code taken from this article 13 | export function debounce(callback, wait) { 14 | let timeout; 15 | return (...args) => { 16 | const context = this; 17 | clearTimeout(timeout); 18 | timeout = setTimeout(() => callback.apply(context, args), wait); 19 | }; 20 | } 21 | 22 | export function handleCountdown(cb) { 23 | const electionDay = moment('2020-11-08').utcOffset('+0630'); 24 | const nowEpoch = moment().unix() * 1000; 25 | 26 | const electionDayStartEpoch = 27 | electionDay 28 | .clone() 29 | .startOf('day') 30 | .unix() * 1000; 31 | 32 | const pollingStationOpenEpoch = 33 | electionDay 34 | .clone() 35 | .set({ hours: 6 }) 36 | .unix() * 1000; 37 | 38 | const pollingStationCloseEpoch = 39 | electionDay 40 | .clone() 41 | .set({ hours: 16 }) 42 | .unix() * 1000; 43 | 44 | // Check if it's still day diff >= 1 45 | const dayDiff = Math.ceil( 46 | moment.duration(moment(electionDayStartEpoch).diff(nowEpoch)).asDays() 47 | ); 48 | 49 | if (dayDiff >= 1) { 50 | return cb(dayDiff, 'day'); 51 | } 52 | 53 | // Election over 54 | if (dayDiff >= 0 && nowEpoch > pollingStationCloseEpoch) { 55 | return cb(null, 'over'); 56 | } 57 | 58 | setInterval(() => { 59 | const currentIntervalEpoch = moment().unix() * 1000; 60 | // Check if pollion station has closed 61 | if (currentIntervalEpoch > pollingStationCloseEpoch) { 62 | return cb(null, 'over'); 63 | } 64 | 65 | // Check if current interval epoch has passed election day 66 | // To show မဲရုံများဖွင့်ချိန် 67 | if ( 68 | currentIntervalEpoch > electionDayStartEpoch && 69 | currentIntervalEpoch < pollingStationOpenEpoch 70 | ) { 71 | const diffTime = pollingStationOpenEpoch - currentIntervalEpoch; 72 | const duration = moment.duration(diffTime, 'milliseconds'); 73 | const cd = moment.duration(duration - 1000, 'milliseconds'); 74 | 75 | const hours = cd.hours() < 10 ? `0${cd.hours()}` : cd.hours(); 76 | const minutes = cd.minutes() < 10 ? `0${cd.minutes()}` : cd.minutes(); 77 | const seconds = cd.seconds() < 10 ? `0${cd.seconds()}` : cd.seconds(); 78 | 79 | return cb(`${hours}-${minutes}-${seconds}`, 'start'); 80 | } 81 | 82 | // Check if pollion station has opened 83 | // To show မဲရုံများပိတ်ချိန် 84 | if ( 85 | currentIntervalEpoch > electionDayStartEpoch && 86 | currentIntervalEpoch > pollingStationOpenEpoch && 87 | currentIntervalEpoch < pollingStationCloseEpoch 88 | ) { 89 | const diffTime = pollingStationCloseEpoch - currentIntervalEpoch; 90 | const duration = moment.duration(diffTime, 'milliseconds'); 91 | const cd = moment.duration(duration - 1000, 'milliseconds'); 92 | 93 | const hours = cd.hours() < 10 ? `0${cd.hours()}` : cd.hours(); 94 | const minutes = cd.minutes() < 10 ? `0${cd.minutes()}` : cd.minutes(); 95 | const seconds = cd.seconds() < 10 ? `0${cd.seconds()}` : cd.seconds(); 96 | 97 | return cb(`${hours}-${minutes}-${seconds}`, 'close'); 98 | } 99 | 100 | return true; 101 | }, 1000); 102 | 103 | return true; 104 | } 105 | -------------------------------------------------------------------------------- /utils/textFormatter.js: -------------------------------------------------------------------------------- 1 | import myanmarNumbers from 'myanmar-numbers'; 2 | import moment from 'moment'; 3 | import { HOUSES, FAQ_CATEGORY } from './constants'; 4 | 5 | export function formatHouse(house) { 6 | return HOUSES[house]; 7 | } 8 | 9 | export function formatConstituency(stateRegion, constituencyName) { 10 | return `${stateRegion} ${constituencyName 11 | .split(' ') 12 | .slice(1) 13 | .join('')}`; 14 | } 15 | 16 | export function formatFAQCategory(category) { 17 | return FAQ_CATEGORY[category]; 18 | } 19 | 20 | // Nasty format function to quickly achieve our MM locale result 21 | // dateStr will be in YYYY-MM-DD format 22 | export function formatPublishDateToMMLocale(dateStr) { 23 | const publishedTime = moment(dateStr); 24 | const date = dateStr.split('-')[2]; // It has zero paddings so we cheat. 25 | const month = publishedTime.month(); // This is to access MM Locale months below 26 | const year = publishedTime.year(); 27 | // Text data taken from https://gist.github.com/eimg/3f115b39fbc6c48976e6cc7af1b8ddf9 28 | const months = 'ဇန်နဝါရီ_ဖေဖော်ဝါရီ_မတ်_ဧပြီ_မေ_ဇွန်_ဇူလိုင်_သြဂုတ်_စက်တင်ဘာ_အောက်တိုဘာ_နိုဝင်ဘာ_ဒီဇင်ဘာ'.split( 29 | '_' 30 | ); 31 | return `${myanmarNumbers(date, 'my')} ${months[month]} ${myanmarNumbers( 32 | year, 33 | 'my' 34 | )}`; 35 | } 36 | 37 | export function formatBirthDay(birthday) { 38 | if (!birthday) return '-'; 39 | const strArr = birthday.split('-'); 40 | return `${myanmarNumbers(strArr[2], 'my')}၊ ${myanmarNumbers( 41 | strArr[1], 42 | 'my' 43 | )}၊ ${myanmarNumbers(strArr[0], 'my')}`; 44 | } 45 | --------------------------------------------------------------------------------