├── .nvmrc ├── .browserslistrc ├── test ├── setup-files.ts └── setup-files-after-env.ts ├── public ├── robots.txt ├── og.png ├── favicon.ico ├── images │ ├── cet-logo.png │ ├── menu-pic.png │ ├── safety_notice │ │ ├── 1.png │ │ ├── 2.png │ │ ├── 3.png │ │ ├── 4.png │ │ └── 5.png │ ├── tutorial │ │ ├── create-1.jpg │ │ ├── create-2.jpg │ │ ├── create-3.jpg │ │ ├── create-4.jpg │ │ ├── create-5.jpg │ │ ├── create-6.jpg │ │ ├── create-7.jpg │ │ ├── update-1.jpg │ │ ├── update-2.jpg │ │ ├── update-3.jpg │ │ └── update-4.jpg │ ├── now.svg │ ├── zoom-out.svg │ ├── filter.svg │ ├── zoom-in.svg │ ├── locate.svg │ ├── search.svg │ ├── alert.svg │ ├── marker-white.svg │ ├── marker-1.svg │ ├── marker-3.svg │ ├── marker-0.svg │ ├── marker-2.svg │ ├── marker-default.svg │ ├── marker-REPORTED.svg │ ├── marker-NEW.svg │ ├── marker-EXISTING_COMPLETE.svg │ └── marker-EXISTING_INCOMPLETE.svg ├── img │ └── icons │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── mstile-150x150.png │ │ ├── apple-touch-icon.png │ │ ├── android-chrome-192x192.png │ │ ├── android-chrome-512x512.png │ │ ├── apple-touch-icon-60x60.png │ │ ├── apple-touch-icon-76x76.png │ │ ├── apple-touch-icon-120x120.png │ │ ├── apple-touch-icon-152x152.png │ │ ├── apple-touch-icon-180x180.png │ │ ├── msapplication-icon-144x144.png │ │ ├── android-chrome-maskable-192x192.png │ │ ├── android-chrome-maskable-512x512.png │ │ └── safari-pinned-tab.svg └── index.html ├── src ├── styles │ ├── index.scss │ ├── text.scss │ ├── vuetify-extend.scss │ ├── typography.scss │ ├── variables.scss │ ├── flex.scss │ ├── images-grid.scss │ ├── components │ │ ├── back-button.scss │ │ └── preview-images.scss │ ├── utils.scss │ ├── global.scss │ ├── openlayer.scss │ └── page.scss ├── vue-cool-lightbox.d.ts ├── symbols.ts ├── shims-vue.d.ts ├── vue-carousel.d.ts ├── lib │ ├── utils.ts │ ├── factory.ts │ ├── useBackPressed.ts │ ├── useGA.ts │ ├── useMapMode.ts │ ├── utils │ │ └── factoryPointCache.ts │ ├── browserCheck.ts │ ├── hooks │ │ ├── useScroll.ts │ │ └── useInsideViewport.ts │ ├── apiConfig.ts │ ├── useAlert.ts │ ├── imageUpload.ts │ ├── image.ts │ ├── permalink.ts │ ├── hooks.ts │ └── appState.ts ├── shims-vueGtags.ts ├── shims-tsx.d.ts ├── plugins │ └── vuetify.ts ├── main.ts ├── shims-ol-mapbox-style.ts ├── components │ ├── CreateFactorySuccessModal.vue │ ├── UpdateFactorySuccessModal.vue │ ├── AppTextArea.vue │ ├── Minimap.vue │ ├── AppTextField.vue │ ├── IOSVersionAlertModal.vue │ ├── ContactModal.vue │ ├── AppAlert.vue │ ├── ImgurFallbackImage.vue │ ├── AboutModal.vue │ ├── AppModal.vue │ ├── AppSelect.vue │ ├── SwitchMapModeButton.vue │ ├── MaintenanceModal.vue │ ├── SafetyModal.vue │ ├── AppNavbar.vue │ ├── GettingStartedModal.vue │ ├── AppButton.vue │ ├── AppSidebar.vue │ ├── __tests__ │ │ └── TutorialModal.spec.ts │ ├── DisplaySettingBottomSheet.vue │ ├── ApiConfigModal.vue │ ├── ImageUploadForm.vue │ ├── ImageUploadModal.vue │ ├── TutorialModal.vue │ ├── ConfirmFactory.vue │ ├── UpdateFactorySteps.vue │ └── Map.vue ├── registerServiceWorker.ts ├── types.ts ├── api │ └── index.ts └── App.vue ├── postcss.config.js ├── babel.config.js ├── docs ├── images │ └── screenshot.png ├── development.md ├── JD.md └── design.md ├── .eslintignore ├── .editorconfig ├── jest.config.js ├── assets └── images │ └── remove.svg ├── .gitignore ├── .env.example ├── .github ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md └── workflows │ └── ci.yml ├── vue.config.js ├── tsconfig.json ├── README.md ├── LICENSE ├── .eslintrc.js └── package.json /.nvmrc: -------------------------------------------------------------------------------- 1 | 16 2 | -------------------------------------------------------------------------------- /.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | -------------------------------------------------------------------------------- /test/setup-files.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: 3 | -------------------------------------------------------------------------------- /src/styles/index.scss: -------------------------------------------------------------------------------- 1 | @import './global.scss'; 2 | 3 | -------------------------------------------------------------------------------- /src/vue-cool-lightbox.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'vue-cool-lightbox' 2 | -------------------------------------------------------------------------------- /test/setup-files-after-env.ts: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom' 2 | -------------------------------------------------------------------------------- /src/styles/text.scss: -------------------------------------------------------------------------------- 1 | .text-center { 2 | text-align: center; 3 | } 4 | -------------------------------------------------------------------------------- /public/og.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Disfactory/frontend/HEAD/public/og.png -------------------------------------------------------------------------------- /src/symbols.ts: -------------------------------------------------------------------------------- 1 | export const MainMapControllerSymbol = Symbol('MAIN_MAP_CONTROLLER') 2 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Disfactory/frontend/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | autoprefixer: {} 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /docs/images/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Disfactory/frontend/HEAD/docs/images/screenshot.png -------------------------------------------------------------------------------- /public/images/cet-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Disfactory/frontend/HEAD/public/images/cet-logo.png -------------------------------------------------------------------------------- /public/images/menu-pic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Disfactory/frontend/HEAD/public/images/menu-pic.png -------------------------------------------------------------------------------- /src/shims-vue.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.vue' { 2 | import Vue from 'vue' 3 | export default Vue 4 | } 5 | -------------------------------------------------------------------------------- /src/styles/vuetify-extend.scss: -------------------------------------------------------------------------------- 1 | .v-btn.v-btn-plain:hover:before { 2 | background-color: transparent; 3 | } 4 | -------------------------------------------------------------------------------- /public/images/safety_notice/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Disfactory/frontend/HEAD/public/images/safety_notice/1.png -------------------------------------------------------------------------------- /public/images/safety_notice/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Disfactory/frontend/HEAD/public/images/safety_notice/2.png -------------------------------------------------------------------------------- /public/images/safety_notice/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Disfactory/frontend/HEAD/public/images/safety_notice/3.png -------------------------------------------------------------------------------- /public/images/safety_notice/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Disfactory/frontend/HEAD/public/images/safety_notice/4.png -------------------------------------------------------------------------------- /public/images/safety_notice/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Disfactory/frontend/HEAD/public/images/safety_notice/5.png -------------------------------------------------------------------------------- /public/img/icons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Disfactory/frontend/HEAD/public/img/icons/favicon-16x16.png -------------------------------------------------------------------------------- /public/img/icons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Disfactory/frontend/HEAD/public/img/icons/favicon-32x32.png -------------------------------------------------------------------------------- /public/images/tutorial/create-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Disfactory/frontend/HEAD/public/images/tutorial/create-1.jpg -------------------------------------------------------------------------------- /public/images/tutorial/create-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Disfactory/frontend/HEAD/public/images/tutorial/create-2.jpg -------------------------------------------------------------------------------- /public/images/tutorial/create-3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Disfactory/frontend/HEAD/public/images/tutorial/create-3.jpg -------------------------------------------------------------------------------- /public/images/tutorial/create-4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Disfactory/frontend/HEAD/public/images/tutorial/create-4.jpg -------------------------------------------------------------------------------- /public/images/tutorial/create-5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Disfactory/frontend/HEAD/public/images/tutorial/create-5.jpg -------------------------------------------------------------------------------- /public/images/tutorial/create-6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Disfactory/frontend/HEAD/public/images/tutorial/create-6.jpg -------------------------------------------------------------------------------- /public/images/tutorial/create-7.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Disfactory/frontend/HEAD/public/images/tutorial/create-7.jpg -------------------------------------------------------------------------------- /public/images/tutorial/update-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Disfactory/frontend/HEAD/public/images/tutorial/update-1.jpg -------------------------------------------------------------------------------- /public/images/tutorial/update-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Disfactory/frontend/HEAD/public/images/tutorial/update-2.jpg -------------------------------------------------------------------------------- /public/images/tutorial/update-3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Disfactory/frontend/HEAD/public/images/tutorial/update-3.jpg -------------------------------------------------------------------------------- /public/images/tutorial/update-4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Disfactory/frontend/HEAD/public/images/tutorial/update-4.jpg -------------------------------------------------------------------------------- /public/img/icons/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Disfactory/frontend/HEAD/public/img/icons/mstile-150x150.png -------------------------------------------------------------------------------- /public/img/icons/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Disfactory/frontend/HEAD/public/img/icons/apple-touch-icon.png -------------------------------------------------------------------------------- /public/img/icons/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Disfactory/frontend/HEAD/public/img/icons/android-chrome-192x192.png -------------------------------------------------------------------------------- /public/img/icons/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Disfactory/frontend/HEAD/public/img/icons/android-chrome-512x512.png -------------------------------------------------------------------------------- /public/img/icons/apple-touch-icon-60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Disfactory/frontend/HEAD/public/img/icons/apple-touch-icon-60x60.png -------------------------------------------------------------------------------- /public/img/icons/apple-touch-icon-76x76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Disfactory/frontend/HEAD/public/img/icons/apple-touch-icon-76x76.png -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .vscode 4 | babel.config.js 5 | postcss.config.js 6 | vue.config.js 7 | .eslintrc.js 8 | jest.config.js 9 | -------------------------------------------------------------------------------- /public/img/icons/apple-touch-icon-120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Disfactory/frontend/HEAD/public/img/icons/apple-touch-icon-120x120.png -------------------------------------------------------------------------------- /public/img/icons/apple-touch-icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Disfactory/frontend/HEAD/public/img/icons/apple-touch-icon-152x152.png -------------------------------------------------------------------------------- /public/img/icons/apple-touch-icon-180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Disfactory/frontend/HEAD/public/img/icons/apple-touch-icon-180x180.png -------------------------------------------------------------------------------- /public/img/icons/msapplication-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Disfactory/frontend/HEAD/public/img/icons/msapplication-icon-144x144.png -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.{js,jsx,ts,tsx,vue}] 2 | indent_style = space 3 | indent_size = 2 4 | trim_trailing_whitespace = true 5 | insert_final_newline = true 6 | -------------------------------------------------------------------------------- /public/img/icons/android-chrome-maskable-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Disfactory/frontend/HEAD/public/img/icons/android-chrome-maskable-192x192.png -------------------------------------------------------------------------------- /public/img/icons/android-chrome-maskable-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Disfactory/frontend/HEAD/public/img/icons/android-chrome-maskable-512x512.png -------------------------------------------------------------------------------- /src/vue-carousel.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'vue-carousel' { 2 | import { VueConstructor } from 'vue' 3 | 4 | export const Carousel: VueConstructor 5 | export const Slide: VueConstructor 6 | } 7 | -------------------------------------------------------------------------------- /src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { nextTick } from 'vue' 2 | 3 | export const sleep = (ms: number) => new Promise(resolve => window.setTimeout(resolve, ms)) 4 | 5 | export const waitNextTick = () => nextTick() 6 | -------------------------------------------------------------------------------- /public/images/now.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/shims-vueGtags.ts: -------------------------------------------------------------------------------- 1 | declare module 'vue-gtag' 2 | 3 | declare module 'vue/types/vue' { 4 | interface Vue { 5 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 6 | $gtag: any 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: '@vue/cli-plugin-unit-jest/presets/typescript-and-babel', 3 | setupFiles: ['/test/setup-files.ts'], 4 | setupFilesAfterEnv: ['/test/setup-files-after-env.ts'] 5 | } 6 | -------------------------------------------------------------------------------- /assets/images/remove.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /public/images/zoom-out.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/images/filter.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/styles/typography.scss: -------------------------------------------------------------------------------- 1 | @import "./variables"; 2 | 3 | h2 { 4 | color: $dark-green-color; 5 | font-size: 20px; 6 | line-height: 30px; 7 | } 8 | 9 | p { 10 | font-size: 16px; 11 | line-height: 24px; 12 | } 13 | 14 | a { 15 | color: $second-color; 16 | text-decoration: underline; 17 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | # local env files 6 | .env.local 7 | .env.*.local 8 | .env 9 | 10 | # Log files 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | 15 | # Editor directories and files 16 | .idea 17 | .vscode 18 | *.suo 19 | *.ntvs* 20 | *.njsproj 21 | *.sln 22 | *.sw? 23 | -------------------------------------------------------------------------------- /public/images/zoom-in.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/shims-tsx.d.ts: -------------------------------------------------------------------------------- 1 | import Vue, { VNode } from 'vue' 2 | 3 | declare global { 4 | namespace JSX { 5 | // tslint:disable no-empty-interface 6 | interface Element extends VNode {} 7 | // tslint:disable no-empty-interface 8 | interface ElementClass extends Vue {} 9 | interface IntrinsicElements { 10 | [elem: string]: any 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/styles/variables.scss: -------------------------------------------------------------------------------- 1 | $font-color: #000000; 2 | $dark-font-color: #fff; 3 | 4 | $primary-color: #6E8501; 5 | $second-color: #99a639; 6 | 7 | $form-font-size: 1.3rem; 8 | 9 | $background-color: #fff; 10 | 11 | $red-color: #bb4a37; 12 | $blue-color: #447287; 13 | $gray-color: #58585B; 14 | $dark-green-color: #364516; 15 | $light-green-color: #697F01; 16 | $gray-light-color: #A1A1A1; 17 | -------------------------------------------------------------------------------- /src/plugins/vuetify.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuetify from 'vuetify/lib' 3 | import colors from 'vuetify/lib/util/colors' 4 | 5 | Vue.use(Vuetify) 6 | 7 | export default new Vuetify({ 8 | theme: { 9 | themes: { 10 | light: { 11 | primary: '#697F01', 12 | secondary: '#364516', 13 | accent: colors.green.base 14 | } 15 | } 16 | } 17 | }) 18 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | VUE_APP_BASE_URL=https://staging.disfactory.tw/api 2 | VUE_APP_IMGUR_FALLBACK_URL=https://api.disfactory.tw/imgur 3 | 4 | # Image upload configuration 5 | # Set to 'backend' to use the backend filesystem upload, or 'imgur' to use Imgur (default) 6 | VUE_APP_IMAGE_UPLOAD_PROVIDER=imgur 7 | # Backend upload URL (only used when VUE_APP_IMAGE_UPLOAD_PROVIDER=backend) 8 | VUE_APP_IMAGE_UPLOAD_URL=https://api.disfactory.tw/api/upload 9 | -------------------------------------------------------------------------------- /docs/development.md: -------------------------------------------------------------------------------- 1 | # Disfactory Development Guide 2 | 3 | ## Pre-requisites 4 | 5 | - Node.js 16 6 | 7 | ## Project setup 8 | 9 | ```bash 10 | cp .env.example .env 11 | 12 | # Install dependencies 13 | npm install 14 | ``` 15 | 16 | ### Start development server 17 | 18 | ```bash 19 | # Compiles and hot-reloads for development 20 | npm run serve 21 | ``` 22 | 23 | ## Compiles and minifies for production 24 | 25 | ```bash 26 | npm run build 27 | ``` 28 | -------------------------------------------------------------------------------- /public/images/locate.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/lib/factory.ts: -------------------------------------------------------------------------------- 1 | import { FactoryData, FACTORY_TYPE, FactoryType } from '../types' 2 | 3 | const factoryTypeMap: { [key: string]: string } = FACTORY_TYPE.reduce((acc, obj) => { 4 | return { 5 | ...acc, 6 | [obj.value]: obj.text 7 | } 8 | }, {}) 9 | 10 | export const getFactoryTypeText = (factory: FactoryData) => { 11 | if (factory.type) { 12 | return factoryTypeMap[factory.type] as FactoryType 13 | } else { 14 | return null 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/styles/flex.scss: -------------------------------------------------------------------------------- 1 | .flex { 2 | display: flex; 3 | } 4 | 5 | .justify-center { 6 | justify-content: center; 7 | } 8 | 9 | .justify-between { 10 | justify-content: space-between; 11 | } 12 | 13 | .align-items-center { 14 | align-items: center; 15 | } 16 | 17 | .align-items-end { 18 | align-items: flex-end; 19 | } 20 | 21 | .flex-column { 22 | flex-direction: column; 23 | } 24 | 25 | .flex-row { 26 | flex-direction: row; 27 | } 28 | 29 | .flex-auto { 30 | flex: 1; 31 | } 32 | 33 | -------------------------------------------------------------------------------- /src/styles/images-grid.scss: -------------------------------------------------------------------------------- 1 | .images-grid { 2 | .image-card { 3 | position: relative; 4 | width: calc(50% - 4px); 5 | display: inline-block; 6 | padding-top: 35%; 7 | height: 0px; 8 | 9 | &:nth-child(2n + 1) { 10 | margin-right: 4px; 11 | } 12 | 13 | &:nth-child(2n) { 14 | margin-left: 4px; 15 | } 16 | } 17 | 18 | img { 19 | position: absolute; 20 | top: 0; 21 | height: 100%; 22 | width: 100%; 23 | object-fit: cover; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/lib/useBackPressed.ts: -------------------------------------------------------------------------------- 1 | import { onUnmounted } from 'vue' 2 | 3 | export function useBackPressed (onBack: () => void) { 4 | const hideModal = (event: PopStateEvent) => { 5 | if (event.state === 'backPressed') { 6 | onBack() 7 | } 8 | } 9 | 10 | window.history.pushState('backPressed', '', null) 11 | window.history.pushState('dummy', '', null) 12 | window.addEventListener('popstate', hideModal, { once: true }) 13 | 14 | onUnmounted(() => { 15 | window.removeEventListener('popstate', hideModal) 16 | }) 17 | } 18 | -------------------------------------------------------------------------------- /public/images/search.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/lib/useGA.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | import { getCurrentInstance } from 'vue' 3 | 4 | export function useGA () { 5 | const instance = getCurrentInstance() 6 | const gtag = (instance?.proxy as any)?.$gtag 7 | 8 | const pageview = (path: string) => { 9 | if (gtag) { 10 | gtag.pageview({ page_path: path }) 11 | } 12 | } 13 | 14 | const event = (event: string, data: any = {}) => { 15 | if (gtag) { 16 | gtag.event(event, data) 17 | } 18 | } 19 | 20 | return { pageview, event } 21 | } 22 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import App from './App.vue' 3 | import CoolLightBox from 'vue-cool-lightbox' 4 | 5 | import './registerServiceWorker' 6 | import VueGtag from 'vue-gtag' 7 | import vuetify from './plugins/vuetify' 8 | 9 | import 'vue-cool-lightbox/dist/vue-cool-lightbox.min.css' 10 | 11 | Vue.config.productionTip = false 12 | Vue.use(CoolLightBox) 13 | 14 | Vue.use(VueGtag, { 15 | config: { id: 'UA-154739393-1' }, 16 | enabled: process.env.NODE_ENV === 'production' 17 | }) 18 | 19 | new Vue({ 20 | vuetify, 21 | render: (h) => h(App) 22 | }).$mount('#app') 23 | -------------------------------------------------------------------------------- /src/lib/useMapMode.ts: -------------------------------------------------------------------------------- 1 | import { inject, InjectionKey, provide, Ref, ref } from 'vue' 2 | import { BASE_MAP } from './map' 3 | 4 | const mapModeSymbol: InjectionKey<{ currentMapMode: Ref }> = Symbol('MapModeSymbol') 5 | 6 | export function provideMapMode () { 7 | const currentMapMode = ref(BASE_MAP.OSM) 8 | 9 | return provide(mapModeSymbol, { currentMapMode }) 10 | } 11 | 12 | export function useMapMode () { 13 | const context = inject(mapModeSymbol) 14 | 15 | if (!context) { 16 | throw new Error('Please use provideMapMode before useMapMode.') 17 | } 18 | 19 | return context 20 | } 21 | -------------------------------------------------------------------------------- /src/styles/components/back-button.scss: -------------------------------------------------------------------------------- 1 | .back-button { 2 | width: 30px; 3 | height: 20px; 4 | position: relative; 5 | cursor: pointer; 6 | 7 | span { 8 | position: absolute; 9 | width: 100%; 10 | height: 4px; 11 | border-radius: 5px; 12 | background-color: $primary-color; 13 | 14 | left: 0; 15 | } 16 | 17 | span:nth-child(1) { 18 | top: 7px; 19 | } 20 | span:nth-child(2) { 21 | transform: rotate(-45deg); 22 | width: 65%; 23 | top: 1px; 24 | left: -4px; 25 | } 26 | span:nth-child(3) { 27 | transform: rotate(45deg); 28 | width: 65%; 29 | top: 12px; 30 | left: -4px; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/shims-ol-mapbox-style.ts: -------------------------------------------------------------------------------- 1 | declare module 'ol-mapbox-style' 2 | 3 | declare module 'ol-mapbox-style/dist/stylefunction' { 4 | import VectorLayer from 'ol/layer/Vector' 5 | import VectorTileLayer from 'ol/layer/VectorTile' 6 | import { StyleFunction } from 'ol/style/Style' 7 | 8 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 9 | export default function StyleFunction(olLayer: VectorLayer|VectorTileLayer, glStyle: string|Record, source: string|Array, resolutions?: Array, spriteData?: Record, spriteImageUrl?: Record, getFonts?: (arg: Array) => Array): StyleFunction 10 | } 11 | -------------------------------------------------------------------------------- /src/lib/utils/factoryPointCache.ts: -------------------------------------------------------------------------------- 1 | import { FactoryData } from '@/types' 2 | import Feature from 'ol/Feature' 3 | 4 | const featurePointCachedByCoord = new Map() 5 | export class FactoryPointCache { 6 | static getFactoryCacheKey (factory: FactoryData) { 7 | return `${factory.lat},${factory.lng}` 8 | } 9 | 10 | static getFactoryCache (factory: FactoryData) { 11 | return featurePointCachedByCoord.get(this.getFactoryCacheKey(factory)) 12 | } 13 | 14 | static setFactoryCache (factory: FactoryData, feature: Feature) { 15 | featurePointCachedByCoord.set(this.getFactoryCacheKey(factory), feature) 16 | } 17 | } 18 | 19 | export default FactoryPointCache 20 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /src/styles/utils.scss: -------------------------------------------------------------------------------- 1 | @import './flex'; 2 | @import './text'; 3 | 4 | 5 | .width-auto { 6 | width: auto; 7 | } 8 | 9 | .w-100 { 10 | width: 100%; 11 | } 12 | 13 | @mixin close-button { 14 | position: absolute; 15 | top: 24px; 16 | right: 22px; 17 | 18 | width: 24px; 19 | height: 24px; 20 | padding-top: 12px; 21 | cursor: pointer; 22 | 23 | &::before, &::after { 24 | display: block; 25 | content: ''; 26 | width: 100%; 27 | height: 3px; 28 | background: #000; 29 | transform-origin: center; 30 | position: absolute; 31 | border-radius: 5px; 32 | } 33 | 34 | &::before { 35 | transform: rotate(45deg); 36 | } 37 | 38 | &::after { 39 | transform: rotate(-45deg); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /public/images/alert.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/images/marker-white.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | lintOnSave: false, 3 | devServer: { 4 | allowedHosts: 'all', 5 | proxy: { 6 | '/server': { 7 | target: 'https://staging.disfactory.tw', 8 | changeOrigin: true, 9 | pathRewrite: { 10 | '^/server': '' 11 | } 12 | } 13 | }, 14 | headers: { 15 | 'Cache-Control': 'no-store' 16 | } 17 | }, 18 | pwa: { 19 | name: '農地違章工廠', 20 | themeColor: '#697F01', 21 | workboxOptions: { 22 | skipWaiting: true, 23 | clientsClaim: true 24 | } 25 | }, 26 | transpileDependencies: [ 27 | 'vuetify' 28 | ], 29 | chainWebpack: (config) => { 30 | if (process.env.NODE_ENV === 'development') { 31 | config.plugins.delete('preload') 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /docs/JD.md: -------------------------------------------------------------------------------- 1 | # Job Description for Disfactory Project 2 | 3 | [Disfactory - 違章工廠檢舉系統](https://github.com/Disfactory/frontend/) 徵求各路前端工程師,專案描述如下: 4 | 5 | - 前後端分離的 Vue 2~3 專案。會說 2 到 3 是因為雖然用的版本是 Vue 2,但有用到 Vue 3 [composition API](https://github.com/vuejs/composition-api) 的寫法,未來升級會相對容易 6 | - Vue CLI 開的專案,符合一般習慣 7 | - 使用 TypeScript 並開啟嚴格模式 8 | - [OpenLayers](https://openlayers.org/) 來呈現地圖,與國土測繪圖資服務雲用同一套方便作業,如果你換一套也 OK 9 | - 使用 [Vuetify](https://vuetifyjs.com/) 這套 Material Design 風格的 Vue Framework 10 | - 專案用 GitHub Actions 設定了 CI (linting & typecheck,目前並無測試) 及 CD ([staging](https://dev.disfactory.tw) & [production](https://disfactory.tw)),也會為每個 PR 自動部屬 Review App (via Netlify),手動測試驗證相信會非常舒適 11 | - 並「沒有」用到 Vuex 及 Vue-router 等函式庫,列在這不是優缺點,而是現況 12 | - 目前還在使用 npm,若有升級到 yarn 的想法,這會是你第一個 PR :stuck_out_tongue_closed_eyes: 13 | - 與設計使用 Figma 溝通畫面 14 | -------------------------------------------------------------------------------- /src/styles/components/preview-images.scss: -------------------------------------------------------------------------------- 1 | .preview-images-container { 2 | .uploaded-image { 3 | position: relative; 4 | width: calc(50% - 7.5px); 5 | height: 0; 6 | padding-top: 35%; 7 | overflow: hidden; 8 | display: inline-block; 9 | 10 | &:nth-child(odd) { 11 | margin-right: 7.5px; 12 | } 13 | 14 | &:nth-child(even) { 15 | margin-left: 7.5px; 16 | } 17 | 18 | img { 19 | object-fit: cover; 20 | position: absolute; 21 | width: 100%; 22 | height: 100%; 23 | top: 0; 24 | left: 0; 25 | } 26 | 27 | .remove-image-btn { 28 | position: absolute; 29 | top: 5px; 30 | right: 5px; 31 | 32 | width: 20px; 33 | height: 20px; 34 | background-image: url(/assets/images/remove.svg); 35 | cursor: pointer; 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/styles/global.scss: -------------------------------------------------------------------------------- 1 | @import './variables.scss'; 2 | @import '~ol/ol.css'; 3 | @import './openlayer'; 4 | @import './vuetify-extend'; 5 | @import './utils'; 6 | 7 | html, 8 | body { 9 | font-size: 16px; 10 | line-height: 1; 11 | color: $font-color; 12 | -moz-osx-font-smoothing: grayscale; 13 | -webkit-font-smoothing: auto; 14 | font-family: system-ui -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; 15 | 16 | touch-action: manipulation; 17 | } 18 | 19 | html { 20 | overflow-y: hidden; 21 | } 22 | 23 | * { 24 | line-height: 1; 25 | box-sizing: border-box; 26 | } 27 | 28 | 29 | *:focus { 30 | outline:none 31 | } 32 | 33 | .required:after { 34 | content: '*'; 35 | color: #A22A29; 36 | font-size: 13px; 37 | position: absolute; 38 | } 39 | 40 | .color-gray-light { 41 | color: $gray-light-color; 42 | } 43 | -------------------------------------------------------------------------------- /src/lib/browserCheck.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/prefer-regexp-exec 2 | export const isiOS = !!(navigator.userAgent.match(/(iPod|iPhone|iPad)/) && navigator.userAgent.match(/AppleWebKit/)) 3 | 4 | export const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent) 5 | 6 | export function iOSversion () { 7 | if (/iP(hone|od|ad)/.test(navigator.platform)) { 8 | /* eslint-disable-next-line @typescript-eslint/prefer-regexp-exec */ 9 | const v = (navigator.appVersion).match(/OS (\d+)_(\d+)_?(\d+)?/) 10 | if (v) { 11 | return [parseInt(v[1], 10), parseInt(v[2], 10), parseInt(v[3] || '0', 10)].join('.') 12 | } 13 | } 14 | } 15 | 16 | export function isNotSupportedIOS () { 17 | let version 18 | /* eslint-disable-next-line @typescript-eslint/prefer-regexp-exec */ 19 | return !!isiOS && !!(version = iOSversion()) && !!version.match(/^13\.(1|2|3)/) 20 | } 21 | -------------------------------------------------------------------------------- /src/components/CreateFactorySuccessModal.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 30 | 31 | 39 | -------------------------------------------------------------------------------- /src/components/UpdateFactorySuccessModal.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 30 | 31 | 39 | -------------------------------------------------------------------------------- /docs/design.md: -------------------------------------------------------------------------------- 1 | # Design 2 | 3 | ## Overview 4 | 5 | The Disfactory frontend uses [Vue.js 2.x](https://v2.vuejs.org/) as the main framework. It is a single-page application (SPA) that communicates with the [backend](https://github.com/Disfactory/Disfactory) API server. 6 | 7 | For the map, we use [OpenLayers](https://openlayers.org/) as the map library. The map is rendered in the [`Map.vue`](./src/components/Map.vue) component, with [`map.ts`](./src/lib/map.ts) as the main logic for the map. 8 | 9 | ## Libraries 10 | 11 | - [Vue.js 2.x](https://v2.vuejs.org/) 12 | - Vue composition API 13 | - [OpenLayers](https://openlayers.org/) 14 | - [Vuetify 2.x](https://v2.vuetifyjs.com/): Material Design component framework for Vue.js. Most of the UI components are built with Vuetify. 15 | 16 | ## Directory Structure 17 | 18 | - `public/`: Static files 19 | - `src/`: Main source code directory 20 | - `components/`: Vue components 21 | - `lib/`: Libraries and utilities 22 | -------------------------------------------------------------------------------- /public/images/marker-1.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/marker-3.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/marker-0.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/marker-2.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/marker-default.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2015", 4 | "module": "esnext", 5 | "strict": true, 6 | "jsx": "preserve", 7 | "importHelpers": true, 8 | "moduleResolution": "node", 9 | "experimentalDecorators": true, 10 | "esModuleInterop": true, 11 | "skipLibCheck": true, 12 | "allowSyntheticDefaultImports": true, 13 | "sourceMap": true, 14 | "allowJs": true, 15 | "baseUrl": ".", 16 | "types": [ 17 | "webpack-env", 18 | "vuetify", 19 | "jest" 20 | ], 21 | "paths": { 22 | "@/*": [ 23 | "src/*" 24 | ] 25 | }, 26 | "lib": [ 27 | "esnext", 28 | "dom", 29 | "dom.iterable", 30 | "scripthost" 31 | ] 32 | }, 33 | "include": [ 34 | "src/**/*.ts", 35 | "src/**/*.tsx", 36 | "src/**/*.vue", 37 | "src/lib/browserCheck.js", 38 | "test/**/*.ts", 39 | "src/lib/vectorTileLoader.js" 40 | ], 41 | "exclude": [ 42 | "node_modules" 43 | ] 44 | } 45 | -------------------------------------------------------------------------------- /src/components/AppTextArea.vue: -------------------------------------------------------------------------------- 1 |