├── .nvmrc ├── public ├── favicon.ico ├── images │ ├── map.jpg │ ├── logo192.png │ └── logo512.png ├── _headers ├── manifest.json └── robots.txt ├── .npmrc ├── src ├── helpers │ ├── permissions │ │ ├── index.ts │ │ └── location.ts │ ├── environment.ts │ ├── index.ts │ ├── translate.tsx │ ├── firebase │ │ ├── index.ts │ │ └── incident.ts │ ├── url.ts │ ├── date.ts │ └── analytics.ts ├── assets │ └── images │ │ ├── appStoreIcon.jpg │ │ ├── appStoreDownload.png │ │ ├── googlePlayDownload.png │ │ ├── googlePlayDownload.svg │ │ └── appStoreDownload.svg ├── lib │ └── utils.ts ├── store │ ├── actions.ts │ ├── slices │ │ ├── cameras.ts │ │ ├── user.ts │ │ ├── ui.ts │ │ └── incidents.ts │ └── index.ts ├── components │ ├── Listeners │ │ ├── index.tsx │ │ ├── Cameras.tsx │ │ ├── location.tsx │ │ └── incident.tsx │ ├── Icon │ │ ├── custom │ │ │ └── Twitter.tsx │ │ └── index.tsx │ ├── ui │ │ ├── separator.tsx │ │ ├── textarea.tsx │ │ ├── input.tsx │ │ ├── sonner.tsx │ │ ├── container.tsx │ │ ├── alert.tsx │ │ ├── accordion.tsx │ │ ├── button.tsx │ │ ├── card.tsx │ │ ├── button-group.tsx │ │ ├── empty.tsx │ │ ├── dialog.tsx │ │ ├── sheet.tsx │ │ ├── item.tsx │ │ └── input-group.tsx │ ├── MapMarker │ │ ├── index.tsx │ │ └── Animated.tsx │ ├── MapSidebar │ │ ├── parts │ │ │ └── Item.tsx │ │ └── index.tsx │ ├── Modal │ │ ├── index.tsx │ │ ├── MobileApp.tsx │ │ └── ProjectInfo.tsx │ ├── Typography │ │ └── index.tsx │ ├── Loader │ │ └── index.tsx │ ├── SafeArea │ │ └── index.tsx │ ├── MapCameraInfo │ │ └── index.tsx │ └── MapIncidentInfo │ │ ├── parts │ │ └── CameraSection.tsx │ │ └── index.tsx ├── main.tsx ├── hooks │ ├── useAnalyticsPageView.tsx │ └── useIsMobile.ts ├── types.ts ├── config │ └── index.ts ├── test │ └── setup.ts ├── theme-provider.tsx ├── App.tsx ├── routes │ ├── Contact.tsx │ ├── Download.tsx │ ├── TrafficCams.tsx │ └── Map.tsx └── index.css ├── .prettierrc ├── vitest.config.ts ├── components.json ├── .gitignore ├── API.md ├── tsconfig.json ├── README.md ├── vite.config.ts ├── index.html ├── release_notes.md ├── eslint.config.js └── package.json /.nvmrc: -------------------------------------------------------------------------------- 1 | 20.19.4 -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rdrnt/tps-calls/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/images/map.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rdrnt/tps-calls/HEAD/public/images/map.jpg -------------------------------------------------------------------------------- /public/images/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rdrnt/tps-calls/HEAD/public/images/logo192.png -------------------------------------------------------------------------------- /public/images/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rdrnt/tps-calls/HEAD/public/images/logo512.png -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | //npm.pkg.github.com/:_authToken=$PERSONAL_GITHUB_TOKEN 2 | @rdrnt:registry=https://npm.pkg.github.com -------------------------------------------------------------------------------- /src/helpers/permissions/index.ts: -------------------------------------------------------------------------------- 1 | import * as location from './location'; 2 | 3 | export { location }; 4 | -------------------------------------------------------------------------------- /src/assets/images/appStoreIcon.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rdrnt/tps-calls/HEAD/src/assets/images/appStoreIcon.jpg -------------------------------------------------------------------------------- /src/assets/images/appStoreDownload.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rdrnt/tps-calls/HEAD/src/assets/images/appStoreDownload.png -------------------------------------------------------------------------------- /src/assets/images/googlePlayDownload.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rdrnt/tps-calls/HEAD/src/assets/images/googlePlayDownload.png -------------------------------------------------------------------------------- /public/_headers: -------------------------------------------------------------------------------- 1 | /* 2 | X-Frame-Options: DENY 3 | X-Content-Type-Options: nosniff 4 | Referrer-Policy: strict-origin-when-cross-origin 5 | Permissions-Policy: geolocation=(self), camera=() -------------------------------------------------------------------------------- /src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { clsx, type ClassValue } from "clsx" 2 | import { twMerge } from "tailwind-merge" 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | -------------------------------------------------------------------------------- /src/store/actions.ts: -------------------------------------------------------------------------------- 1 | // Re-export all actions for convenience 2 | export * from './slices/incidents'; 3 | export * from './slices/ui'; 4 | export * from './slices/user'; 5 | export * from './slices/cameras'; 6 | -------------------------------------------------------------------------------- /src/components/Listeners/index.tsx: -------------------------------------------------------------------------------- 1 | import LocationListener from './location'; 2 | import IncidentListener from './incident'; 3 | import CameraListener from './Cameras'; 4 | 5 | export { LocationListener, IncidentListener, CameraListener }; 6 | -------------------------------------------------------------------------------- /src/helpers/environment.ts: -------------------------------------------------------------------------------- 1 | export const config = { 2 | SENTRY_DSN: import.meta.env.VITE_SENTRY_DSN as string, 3 | MAPBOX_API_KEY: import.meta.env.VITE_MAPBOX_API_KEY as string, 4 | GOOGLEANALYTICS_KEY: import.meta.env.VITE_GANALYTICS_KEY as string, 5 | }; 6 | 7 | export const isDevelopment: boolean = import.meta.env.DEV; 8 | -------------------------------------------------------------------------------- /src/main.tsx: -------------------------------------------------------------------------------- 1 | import { createRoot, hydrateRoot } from 'react-dom/client'; 2 | import './index.css'; 3 | 4 | import App from './App'; 5 | 6 | import { Analytics } from './helpers'; 7 | 8 | Analytics.initialize(); 9 | 10 | const domNode = document.getElementById('root'); 11 | const root = createRoot(domNode as Element); 12 | root.render(); 13 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "singleQuote": true, 4 | "trailingComma": "es5", 5 | "printWidth": 80, 6 | "tabWidth": 2, 7 | "useTabs": false, 8 | "quoteProps": "as-needed", 9 | "bracketSpacing": true, 10 | "bracketSameLine": false, 11 | "arrowParens": "avoid", 12 | "endOfLine": "lf", 13 | "embeddedLanguageFormatting": "auto" 14 | } -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config'; 2 | import react from '@vitejs/plugin-react'; 3 | import { resolve } from 'path'; 4 | 5 | export default defineConfig({ 6 | plugins: [react()], 7 | test: { 8 | globals: true, 9 | environment: 'jsdom', 10 | setupFiles: ['./src/test/setup.ts'], 11 | }, 12 | resolve: { 13 | alias: { 14 | '@': resolve(__dirname, './src'), 15 | }, 16 | }, 17 | }); 18 | -------------------------------------------------------------------------------- /src/hooks/useAnalyticsPageView.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | import { Analytics } from '../helpers'; 3 | 4 | interface UseAnalyticsPageViewProps { 5 | path: string; 6 | onRun?: () => void; 7 | } 8 | 9 | const useAnalyticsPageView = ({ path, onRun }: UseAnalyticsPageViewProps) => { 10 | useEffect(() => { 11 | Analytics.pageview(path); 12 | onRun?.(); 13 | }, [path, onRun]); 14 | }; 15 | 16 | export default useAnalyticsPageView; 17 | -------------------------------------------------------------------------------- /src/helpers/index.ts: -------------------------------------------------------------------------------- 1 | import * as Environment from './environment'; 2 | import * as Firebase from './firebase'; 3 | import * as DateHelper from './date'; 4 | import * as Translate from './translate'; 5 | import * as URL from './url'; 6 | import * as Permissions from './permissions'; 7 | import * as Analytics from './analytics'; 8 | 9 | export { 10 | Environment, 11 | Firebase, 12 | DateHelper, 13 | Translate, 14 | URL, 15 | Permissions, 16 | Analytics, 17 | }; 18 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": false, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "", 8 | "css": "src/index.css", 9 | "baseColor": "neutral", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "iconLibrary": "lucide", 14 | "aliases": { 15 | "components": "@/components", 16 | "utils": "@/lib/utils", 17 | "ui": "@/components/ui", 18 | "lib": "@/lib", 19 | "hooks": "@/hooks" 20 | }, 21 | "registries": {} 22 | } 23 | -------------------------------------------------------------------------------- /.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 | # production 12 | /build 13 | /dist 14 | 15 | # misc 16 | .DS_Store 17 | .env.local 18 | .env.development.local 19 | .env.production 20 | .env.test.local 21 | .env.production.local 22 | .env 23 | src/config/firebase 24 | .netlify 25 | netlify.toml 26 | .sentryclirc 27 | 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | 32 | # Sentry Config File 33 | .env.sentry-build-plugin 34 | -------------------------------------------------------------------------------- /src/helpers/permissions/location.ts: -------------------------------------------------------------------------------- 1 | import { Coordinates } from '@rdrnt/tps-calls-shared'; 2 | 3 | export const isSupported = (): boolean => 4 | Boolean('geolocation' in window.navigator); 5 | 6 | interface RequestPermissionParams { 7 | success: (coordinates: Coordinates) => void; 8 | error: () => void; 9 | } 10 | export const requestPermission = ({ 11 | success, 12 | error, 13 | }: RequestPermissionParams): void => 14 | navigator.geolocation.getCurrentPosition( 15 | event => { 16 | success(event.coords); 17 | }, 18 | () => { 19 | error(); 20 | }, 21 | { 22 | enableHighAccuracy: true, 23 | timeout: 5000, 24 | maximumAge: 9000, 25 | } 26 | ); 27 | -------------------------------------------------------------------------------- /src/helpers/translate.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { IncidentType, IncidentSourceType } from '@rdrnt/tps-calls-shared'; 3 | import { GiKnifeThrust } from 'react-icons/gi'; 4 | 5 | export const getIconForIncidentType = (incidentType: IncidentType) => { 6 | switch (incidentType) { 7 | case IncidentType.STABBING: 8 | return ; 9 | default: 10 | return ''; 11 | } 12 | }; 13 | 14 | export const getNameForIncidentSource = ( 15 | source: IncidentSourceType, 16 | shorthand?: boolean 17 | ): string => { 18 | switch (source) { 19 | case IncidentSourceType.TORONTO_POLICE: 20 | return 'Toronto Police Services'; 21 | 22 | default: 23 | return ''; 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /src/components/Icon/custom/Twitter.tsx: -------------------------------------------------------------------------------- 1 | import { LucideProps } from 'lucide-react'; 2 | 3 | export const TwitterIcon = (props: LucideProps) => ( 4 | 14 | 15 | 16 | 17 | {' '} 18 | {' '} 19 | 20 | 21 | ); 22 | -------------------------------------------------------------------------------- /API.md: -------------------------------------------------------------------------------- 1 | # Request Access to tpscalls REST API 2 | 3 | Thank you for your interest in using the tpscalls REST API. To get started, follow the steps below. 4 | 5 | ## How to Apply for Access 6 | 7 | Please follow the steps below to apply for API access: 8 | 9 | 1. **Send an Email:** Here's a link to [my email](mailto:riley@drnt.ca). Please include "tpscalls API Access Request" as the subject. 10 | 11 | 2. **Provide a Brief Reason for Access:** Explain what you plan to do with the API. This helps me understand your needs and tailor the API to better suit all users. 12 | 13 | Once I receive your request, I will respond as soon as possible. If additional information is required, I'll reach out to you. 14 | 15 | I look forward to seeing what you build with the tpscalls REST API :) 16 | -------------------------------------------------------------------------------- /src/components/ui/separator.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as SeparatorPrimitive from "@radix-ui/react-separator" 3 | 4 | import { cn } from "@/lib/utils" 5 | 6 | function Separator({ 7 | className, 8 | orientation = "horizontal", 9 | decorative = true, 10 | ...props 11 | }: React.ComponentProps) { 12 | return ( 13 | 23 | ) 24 | } 25 | 26 | export { Separator } 27 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import { Incident } from '@rdrnt/tps-calls-shared'; 2 | import { Timestamp } from 'firebase/firestore'; 3 | 4 | export type LocalIncident = Omit, 'date'> & { 5 | date: number; 6 | }; 7 | 8 | // Toronto Traffic Cameras 9 | // Server types 10 | export interface TorontoTrafficCameraView { 11 | direction: string; 12 | imageUrl: string; 13 | } 14 | 15 | export interface ServerTorontoTrafficCamera { 16 | id: string; // REC_ID as string 17 | name: string; // " & " 18 | location: { latitude: number; longitude: number }; 19 | date: Timestamp; // when mapped 20 | cameras: TorontoTrafficCameraView[]; // IMAGEURL/REFURL* + DIRECTION* 21 | } 22 | 23 | export type LocalTorontoTrafficCamera = Omit< 24 | ServerTorontoTrafficCamera, 25 | 'date' 26 | > & { 27 | date: number; 28 | }; 29 | -------------------------------------------------------------------------------- /src/components/ui/textarea.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | function Textarea({ className, ...props }: React.ComponentProps<"textarea">) { 6 | return ( 7 |