├── .npmrc ├── apps ├── graphql │ ├── src │ │ ├── utils │ │ │ ├── is-dev.ts │ │ │ ├── schema.ts │ │ │ ├── cors.ts │ │ │ └── yoga.ts │ │ ├── types │ │ │ ├── user-role.ts │ │ │ ├── ticket-type.ts │ │ │ ├── refund-status.ts │ │ │ ├── verification-status.ts │ │ │ ├── revenue.ts │ │ │ ├── status-arg.ts │ │ │ ├── lng-lat.ts │ │ │ ├── lng-lat-input.ts │ │ │ ├── user-role-enum-arg.ts │ │ │ ├── ride-ticket-schedule.ts │ │ │ ├── ticket-type-enum-arg.ts │ │ │ ├── trip-route.ts │ │ │ ├── refund-status-enum-arg.ts │ │ │ ├── schedule-time.ts │ │ │ ├── lines-and-stations-type.ts │ │ │ ├── passengers-input.ts │ │ │ ├── verification-status-enum-arg.ts │ │ │ ├── refund-analytics.ts │ │ │ ├── line.ts │ │ │ ├── one-time-input.ts │ │ │ ├── invitation.ts │ │ │ ├── station-position-in-line.ts │ │ │ ├── pricing.ts │ │ │ ├── refund.ts │ │ │ ├── user-card.ts │ │ │ ├── users-analytics.ts │ │ │ ├── subscription-input.ts │ │ │ ├── ride-ticket-data.ts │ │ │ ├── payment-args.ts │ │ │ ├── station.ts │ │ │ ├── user-ticket.ts │ │ │ ├── user.ts │ │ │ └── subscription.ts │ │ ├── lib │ │ │ ├── is-email-valid.ts │ │ │ ├── capitalize-first-letters.ts │ │ │ ├── encrypt.ts │ │ │ ├── decrypt.ts │ │ │ ├── email-defaults.ts │ │ │ ├── generate-access-token.ts │ │ │ ├── magic-link.ts │ │ │ ├── verify-access-token.ts │ │ │ ├── rate-limit.ts │ │ │ ├── get-mjml-template.ts │ │ │ ├── calculate-travel-duration.ts │ │ │ ├── get-saved-card.ts │ │ │ ├── refund-subscription.ts │ │ │ ├── convert-location-to-lat-lng.ts │ │ │ ├── save-user-card.ts │ │ │ ├── otp.ts │ │ │ ├── convert-lat-lng-to-location.ts │ │ │ └── calculate-route.ts │ │ ├── permissions │ │ │ ├── authenticated.ts │ │ │ ├── admin.ts │ │ │ └── secret-path.ts │ │ ├── resolvers │ │ │ ├── queries │ │ │ │ ├── stations.ts │ │ │ │ ├── me.ts │ │ │ │ ├── station-by-id.ts │ │ │ │ ├── admin │ │ │ │ │ ├── analytics-total-subscribers.ts │ │ │ │ │ ├── analytics-active-lines-and-stations.ts │ │ │ │ │ ├── analytics-average-response.ts │ │ │ │ │ ├── analytics-sold-tickets.ts │ │ │ │ │ ├── pending-invitations.ts │ │ │ │ │ └── team-members.ts │ │ │ │ ├── get-price.ts │ │ │ │ ├── invitation.ts │ │ │ │ ├── user-subscription-history.ts │ │ │ │ ├── lines.ts │ │ │ │ ├── monthly-revenue.ts │ │ │ │ └── user-purchase-history.ts │ │ │ └── mutations │ │ │ │ ├── migrations │ │ │ │ └── create-main-admin-account.ts │ │ │ │ ├── admin-remove-teammate.ts │ │ │ │ ├── add-line.ts │ │ │ │ └── update-verification-status.ts │ │ ├── api │ │ │ └── auth │ │ │ │ └── logout.ts │ │ ├── context.ts │ │ ├── notifications │ │ │ └── invitation-response.mjml │ │ └── index.ts │ ├── prisma │ │ └── nexus-prisma.ts │ ├── nodemon.json │ ├── .eslintrc.js │ ├── .env.example │ ├── vercel.json │ └── tsconfig.json └── home │ ├── .eslintrc.js │ ├── public │ ├── assets │ │ ├── logo.png │ │ ├── emails │ │ │ ├── login.png │ │ │ ├── signup.png │ │ │ └── invitation.png │ │ └── metro-black.png │ ├── icon-192x192.png │ ├── icon-256x256.png │ ├── icon-384x384.png │ ├── icon-512x512.png │ ├── locales │ │ ├── ar │ │ │ ├── tickets-search.json │ │ │ ├── purchase.json │ │ │ ├── find-ticket.json │ │ │ ├── user-ticket.json │ │ │ ├── login.json │ │ │ ├── user-subscriptions.json │ │ │ ├── tickets-details.json │ │ │ ├── subscriptions.json │ │ │ ├── home.json │ │ │ └── faq.json │ │ └── en │ │ │ ├── tickets-search.json │ │ │ ├── purchase.json │ │ │ ├── find-ticket.json │ │ │ ├── user-ticket.json │ │ │ ├── login.json │ │ │ ├── user-subscriptions.json │ │ │ ├── tickets-details.json │ │ │ ├── subscriptions.json │ │ │ └── home.json │ └── manifest.json │ ├── postcss.config.js │ ├── .env.example │ ├── next-sitemap.config.js │ ├── components │ ├── admin │ │ ├── change-indicator.tsx │ │ ├── card.tsx │ │ └── hero-gradient.tsx │ ├── ticket-skeleton.tsx │ ├── help │ │ ├── help-map.tsx │ │ ├── enjoy-your-journey.tsx │ │ ├── card.tsx │ │ └── contact-us.tsx │ ├── authentication │ │ ├── login │ │ │ ├── login-screen-animation.tsx │ │ │ └── email-view.tsx │ │ └── signup │ │ │ └── policy.tsx │ ├── tickets │ │ ├── details │ │ │ └── index.tsx │ │ └── hero │ │ │ └── index.tsx │ ├── home │ │ └── index.tsx │ ├── user │ │ └── tickets │ │ │ └── index.tsx │ ├── or-separator.tsx │ ├── input.tsx │ ├── window-size-wrapper.tsx │ ├── separator.tsx │ ├── default-seo-settings.tsx │ ├── navigation │ │ ├── hamburger-menu.tsx │ │ ├── navigation-menu.tsx │ │ ├── navigation-trigger.tsx │ │ └── index.tsx │ ├── checkbox.tsx │ ├── modal │ │ └── purchase │ │ │ ├── default-card.tsx │ │ │ ├── visa-card.tsx │ │ │ └── mastercard-card.tsx │ ├── ticket-search │ │ └── calendar │ │ │ └── calendar-popover.tsx │ └── footer │ │ └── install-pwa.tsx │ ├── tsconfig.json │ ├── icons │ ├── add.svg │ ├── close.svg │ ├── remove-outline.svg │ ├── checkmark.svg │ ├── add-outline.svg │ ├── caret-up.svg │ ├── chevron-down.svg │ ├── chevron-left.svg │ ├── chevron-right.svg │ ├── menu.svg │ ├── reorder-two.svg │ ├── caret-down.svg │ ├── chevron-back-outline.svg │ ├── chevron-forward-outline.svg │ ├── arrow-forward.svg │ ├── arrow-up-outline.svg │ ├── arrow-back-outline.svg │ ├── arrow-down-outline.svg │ ├── navigate-outline.svg │ ├── checkmark-circle.svg │ ├── information-circle.svg │ ├── card-outline.svg │ ├── checkbox-outline.svg │ ├── return-up-forward.svg │ ├── search-outline.svg │ ├── mail-outline.svg │ ├── location.svg │ ├── close-circle-outline.svg │ ├── document-text.svg │ ├── blind.svg │ ├── credit-default-effect.svg │ ├── document-outline.svg │ ├── location-outline.svg │ ├── person-outline.svg │ ├── train-outline.svg │ ├── cloud-upload-outline.svg │ ├── logo-pwa.svg │ ├── assist-walker.svg │ ├── pricetags-outline.svg │ ├── logo-google.svg │ ├── home.svg │ ├── trash.svg │ ├── person-remove-outline.svg │ ├── mastercard-effect.svg │ ├── create.svg │ ├── heart-outline-gradient.svg │ ├── call-outline.svg │ ├── hearing.svg │ ├── hourglass-outline.svg │ ├── calendar.svg │ ├── calendar-outline.svg │ ├── accessible.svg │ ├── analytics-outline.svg │ ├── accessibility-outline.svg │ ├── train.svg │ ├── qr-code.svg │ ├── visa-effect.svg │ ├── ticket-outline.svg │ ├── timer-outline-gradient.svg │ ├── ticket.svg │ ├── chatbubbles.svg │ └── ticket-outline-gradient.svg │ ├── next-env.d.ts │ ├── types │ ├── refund.ts │ ├── invitee.ts │ ├── pricing.ts │ ├── line.ts │ ├── user.ts │ ├── recommendation.ts │ └── station.ts │ ├── lib │ ├── capitalize-first-letters.ts │ ├── fetcher.ts │ └── use-window-size.tsx │ ├── graphql │ ├── user │ │ ├── logout.ts │ │ ├── login.ts │ │ ├── magic-link.ts │ │ ├── user-cards.ts │ │ ├── subscription-history.ts │ │ ├── me.ts │ │ ├── request-refund.ts │ │ ├── signup.ts │ │ └── purchase-history.ts │ ├── admin │ │ ├── analytics │ │ │ ├── average-response.ts │ │ │ ├── sold-tickets.ts │ │ │ ├── total-subscribers.ts │ │ │ ├── refunds-analytics.ts │ │ │ ├── seniors-analytics.ts │ │ │ ├── total-lines-and-stations.ts │ │ │ └── total-users.ts │ │ ├── monthly-revenue.ts │ │ ├── team.ts │ │ ├── remove-teammate.ts │ │ ├── pending-invitations.ts │ │ ├── invite-teammate.ts │ │ ├── refunds │ │ │ ├── update-refund-request.ts │ │ │ └── refunds.ts │ │ └── verifications │ │ │ ├── update-verification-request.ts │ │ │ └── verifications.ts │ ├── stations │ │ ├── delete-station.ts │ │ ├── reorder-station.ts │ │ ├── station-by-id.ts │ │ ├── stations.ts │ │ ├── ride-route.ts │ │ ├── update-station.ts │ │ └── add-station.ts │ ├── update-invitation.ts │ ├── mutate.ts │ ├── recommendations.ts │ ├── get-price.ts │ ├── get-invitation.ts │ ├── graphql-fetcher.ts │ ├── lines │ │ └── lines.ts │ └── payment │ │ └── create-subscription.ts │ ├── next-i18next.config.js │ ├── layouts │ ├── user.tsx │ ├── authentication.tsx │ ├── help-layout.tsx │ ├── app.tsx │ └── admin.tsx │ ├── pages │ ├── admin │ │ ├── team.tsx │ │ ├── index.tsx │ │ ├── refunds.tsx │ │ ├── lines-and-stations.tsx │ │ └── seniors-verification.tsx │ ├── tickets │ │ └── [from] │ │ │ └── [to] │ │ │ └── [departure] │ │ │ └── index.tsx │ ├── subscriptions.tsx │ ├── index.tsx │ ├── _app.tsx │ ├── magic-link │ │ └── [link].tsx │ ├── user │ │ ├── tickets.tsx │ │ └── subscription.tsx │ └── signup.tsx │ ├── .gitignore │ └── next.config.js ├── .idea ├── .gitignore ├── i18nSettings.xml ├── modules.xml └── turbo-template-new.iml ├── packages ├── tsconfig │ ├── package.json │ ├── react-library.json │ ├── base.json │ └── nextjs.json └── eslint-config-custom │ └── package.json ├── .eslintrc.js ├── turbo.json ├── .gitignore ├── .vscode └── settings.json └── package.json /.npmrc: -------------------------------------------------------------------------------- 1 | auto-install-peers = true 2 | -------------------------------------------------------------------------------- /apps/graphql/src/utils/is-dev.ts: -------------------------------------------------------------------------------- 1 | export const isDev = process.env.NODE_ENV !== 'production' 2 | -------------------------------------------------------------------------------- /apps/home/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: ['custom'], 4 | } 5 | -------------------------------------------------------------------------------- /apps/home/public/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skittlesaur/cairometro/HEAD/apps/home/public/assets/logo.png -------------------------------------------------------------------------------- /apps/home/public/icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skittlesaur/cairometro/HEAD/apps/home/public/icon-192x192.png -------------------------------------------------------------------------------- /apps/home/public/icon-256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skittlesaur/cairometro/HEAD/apps/home/public/icon-256x256.png -------------------------------------------------------------------------------- /apps/home/public/icon-384x384.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skittlesaur/cairometro/HEAD/apps/home/public/icon-384x384.png -------------------------------------------------------------------------------- /apps/home/public/icon-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skittlesaur/cairometro/HEAD/apps/home/public/icon-512x512.png -------------------------------------------------------------------------------- /apps/home/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Editor-based HTTP Client requests 5 | /httpRequests/ 6 | -------------------------------------------------------------------------------- /apps/home/public/assets/emails/login.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skittlesaur/cairometro/HEAD/apps/home/public/assets/emails/login.png -------------------------------------------------------------------------------- /apps/home/public/assets/metro-black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skittlesaur/cairometro/HEAD/apps/home/public/assets/metro-black.png -------------------------------------------------------------------------------- /apps/home/public/assets/emails/signup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skittlesaur/cairometro/HEAD/apps/home/public/assets/emails/signup.png -------------------------------------------------------------------------------- /apps/home/public/assets/emails/invitation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skittlesaur/cairometro/HEAD/apps/home/public/assets/emails/invitation.png -------------------------------------------------------------------------------- /apps/graphql/prisma/nexus-prisma.ts: -------------------------------------------------------------------------------- 1 | import { settings } from 'nexus-prisma/generator' 2 | 3 | settings({ 4 | prismaClientImportId: '@prisma/client', 5 | }) -------------------------------------------------------------------------------- /apps/graphql/nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "watch": [ 3 | "src" 4 | ], 5 | "ext": "ts", 6 | "execMap": { 7 | "ts": "sucrase-node src/index.ts" 8 | } 9 | } -------------------------------------------------------------------------------- /apps/home/.env.example: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_GOOGLE_MAPS_API_KEY= 2 | NEXT_PUBLIC_MAP_ID= 3 | NEXT_PUBLIC_API_URL=http://api.cairometro:1111 4 | NEXT_PUBLIC_GOOGLE_CLIENT_ID= -------------------------------------------------------------------------------- /apps/graphql/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: ['custom'], 4 | rules: { 5 | 'react-hooks/rules-of-hooks': 'off', 6 | }, 7 | } 8 | -------------------------------------------------------------------------------- /apps/home/next-sitemap.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next-sitemap').IConfig} */ 2 | module.exports = { 3 | siteUrl: process.env.NEXT_PUBLIC_SITE_URL, 4 | generateRobotsTxt: true, 5 | } -------------------------------------------------------------------------------- /apps/graphql/src/types/user-role.ts: -------------------------------------------------------------------------------- 1 | import { enumType } from 'nexus' 2 | import { UserRole } from 'nexus-prisma' 3 | 4 | const UserRoleEnum = enumType(UserRole) 5 | 6 | export default UserRoleEnum -------------------------------------------------------------------------------- /apps/graphql/src/types/ticket-type.ts: -------------------------------------------------------------------------------- 1 | import { enumType } from 'nexus' 2 | import { TicketType } from 'nexus-prisma' 3 | 4 | const TicketTypeEnum = enumType(TicketType) 5 | 6 | export default TicketTypeEnum -------------------------------------------------------------------------------- /packages/tsconfig/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tsconfig", 3 | "version": "0.0.0", 4 | "private": true, 5 | "license": "MIT", 6 | "publishConfig": { 7 | "access": "public" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /apps/graphql/src/types/refund-status.ts: -------------------------------------------------------------------------------- 1 | import { enumType } from 'nexus' 2 | import { RefundStatus } from 'nexus-prisma' 3 | 4 | const RefundStatusEnum = enumType(RefundStatus) 5 | 6 | export default RefundStatusEnum -------------------------------------------------------------------------------- /.idea/i18nSettings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /apps/graphql/src/lib/is-email-valid.ts: -------------------------------------------------------------------------------- 1 | const isEmailValid = (email: string) => { 2 | const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/ 3 | 4 | return emailPattern.test(email) 5 | } 6 | 7 | export default isEmailValid -------------------------------------------------------------------------------- /apps/home/components/admin/change-indicator.tsx: -------------------------------------------------------------------------------- 1 | const ChangeIndicator = () => { 2 | return ( 3 | 4 | * 5 | 6 | ) 7 | } 8 | 9 | export default ChangeIndicator -------------------------------------------------------------------------------- /apps/home/public/locales/ar/tickets-search.json: -------------------------------------------------------------------------------- 1 | { 2 | "result": "نتيجة البحث عن {0} إلى {1}", 3 | "viewFullSchedule": "عرض الجدول الكامل ل {0} إلى {1}", 4 | "fullSchedule": "الجدول الكامل", 5 | "loadMore": "عرض المزيد" 6 | } -------------------------------------------------------------------------------- /apps/home/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | }, 5 | "extends": "tsconfig/nextjs.json", 6 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 7 | "exclude": ["node_modules"] 8 | } 9 | -------------------------------------------------------------------------------- /apps/home/public/locales/en/tickets-search.json: -------------------------------------------------------------------------------- 1 | { 2 | "result": "Search Result for {0} to {1}", 3 | "viewFullSchedule": "View full schedule for {0} to {1}", 4 | "fullSchedule": "Full Schedule", 5 | "loadMore": "Load More" 6 | } -------------------------------------------------------------------------------- /apps/graphql/.env.example: -------------------------------------------------------------------------------- 1 | FRONTEND_URL="http://cairometro:3000" 2 | DATABASE_URL= 3 | SENDGRID_API_KEY= 4 | SENDGRID_FROM= 5 | HELP_EMAIL= 6 | SECRET_ENDPOINT_TOKEN= 7 | JWT_SECRET= 8 | ACCESS_TOKEN_COOKIE=".cairometro" 9 | MAIN_ADMIN_EMAIL= -------------------------------------------------------------------------------- /apps/home/icons/add.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/home/icons/close.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/home/icons/remove-outline.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/graphql/src/types/verification-status.ts: -------------------------------------------------------------------------------- 1 | import { enumType } from 'nexus' 2 | import { VerificationStatus } from 'nexus-prisma' 3 | 4 | const VerificationStatusEnum = enumType(VerificationStatus) 5 | 6 | export default VerificationStatusEnum -------------------------------------------------------------------------------- /apps/home/icons/checkmark.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/home/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /apps/home/icons/add-outline.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/home/icons/caret-up.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/home/icons/chevron-down.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/home/icons/chevron-left.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/home/icons/chevron-right.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/home/icons/menu.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/home/icons/reorder-two.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/home/icons/caret-down.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/home/components/ticket-skeleton.tsx: -------------------------------------------------------------------------------- 1 | const TicketSkeleton = () => { 2 | return ( 3 |
4 | 5 |
6 | ) 7 | } 8 | 9 | export default TicketSkeleton -------------------------------------------------------------------------------- /apps/home/icons/chevron-back-outline.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/home/icons/chevron-forward-outline.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/home/types/refund.ts: -------------------------------------------------------------------------------- 1 | interface Refund { 2 | status: string; 3 | id: string 4 | user: { 5 | name: string 6 | email: string 7 | } 8 | createdAt: Date 9 | price: number 10 | message: string 11 | } 12 | 13 | export default Refund -------------------------------------------------------------------------------- /apps/graphql/src/types/revenue.ts: -------------------------------------------------------------------------------- 1 | import { objectType } from 'nexus' 2 | 3 | const RevenueType = objectType({ 4 | name: 'Revenue', 5 | definition(t) { 6 | t.int('month') 7 | t.float('revenue') 8 | }, 9 | }) 10 | 11 | export default RevenueType -------------------------------------------------------------------------------- /apps/graphql/src/types/status-arg.ts: -------------------------------------------------------------------------------- 1 | import { enumType } from 'nexus' 2 | 3 | const StatusEnum = enumType({ 4 | name: 'Status', 5 | members: { 6 | ACCEPTED: 'ACCEPTED', 7 | REJECTED: 'REJECTED', 8 | }, 9 | }) 10 | 11 | export default StatusEnum -------------------------------------------------------------------------------- /apps/home/icons/arrow-forward.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/home/icons/arrow-up-outline.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | // This tells ESLint to load the config from the package `eslint-config-custom` 4 | extends: ['custom'], 5 | settings: { 6 | next: { 7 | rootDir: ['apps/*/'], 8 | }, 9 | }, 10 | } 11 | -------------------------------------------------------------------------------- /apps/graphql/src/types/lng-lat.ts: -------------------------------------------------------------------------------- 1 | import { objectType } from 'nexus' 2 | 3 | 4 | const LngLatType = objectType({ 5 | name: 'LngLat', 6 | definition(t) { 7 | t.float('lng') 8 | t.float('lat') 9 | }, 10 | }) 11 | 12 | 13 | export default LngLatType -------------------------------------------------------------------------------- /apps/home/icons/arrow-back-outline.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/home/icons/arrow-down-outline.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/home/icons/navigate-outline.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/graphql/vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 2, 3 | "builds": [ 4 | { 5 | "src": "src/index.ts", 6 | "use": "@vercel/node" 7 | } 8 | ], 9 | "routes": [ 10 | { 11 | "src": "/(.*)", 12 | "dest": "/src/index.ts" 13 | } 14 | ] 15 | } -------------------------------------------------------------------------------- /apps/graphql/src/types/lng-lat-input.ts: -------------------------------------------------------------------------------- 1 | import { inputObjectType } from 'nexus' 2 | 3 | const LngLatInputType = inputObjectType({ 4 | name: 'LngLatInput', 5 | definition(t) { 6 | t.float('lng') 7 | t.float('lat') 8 | }, 9 | }) 10 | 11 | export default LngLatInputType -------------------------------------------------------------------------------- /apps/home/lib/capitalize-first-letters.ts: -------------------------------------------------------------------------------- 1 | const capitalizeFirstLetters = (str: string): string => { 2 | return str 3 | .split(' ') 4 | .map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()) 5 | .join(' ') 6 | } 7 | 8 | export default capitalizeFirstLetters -------------------------------------------------------------------------------- /apps/graphql/src/lib/capitalize-first-letters.ts: -------------------------------------------------------------------------------- 1 | const capitalizeFirstLetters = (str: string): string => { 2 | return str 3 | .split(' ') 4 | .map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()) 5 | .join(' ') 6 | } 7 | 8 | export default capitalizeFirstLetters -------------------------------------------------------------------------------- /apps/home/graphql/user/logout.ts: -------------------------------------------------------------------------------- 1 | import mutate from '@/graphql/mutate' 2 | 3 | const LOGIN_MUTATION = /* GraphQL */ ` 4 | mutation { 5 | logout 6 | } 7 | ` 8 | 9 | const logoutMutation = async () => { 10 | return mutate(LOGIN_MUTATION) 11 | } 12 | 13 | export default logoutMutation -------------------------------------------------------------------------------- /apps/home/types/invitee.ts: -------------------------------------------------------------------------------- 1 | import User from '@/types/user' 2 | 3 | interface Invitee { 4 | id: string 5 | name: string 6 | email: string 7 | role: 'ADMIN' | 'CUSTOMER_SUPPORT' | 'SENIOR' | 'ADULT' 8 | createdAt: string 9 | invitedBy?: User 10 | } 11 | 12 | export default Invitee -------------------------------------------------------------------------------- /apps/home/components/help/help-map.tsx: -------------------------------------------------------------------------------- 1 | import Map from '@/components/map' 2 | 3 | const HelpMap = () => { 4 | return ( 5 |
6 | 7 |
8 | ) 9 | } 10 | 11 | export default HelpMap -------------------------------------------------------------------------------- /apps/graphql/src/utils/schema.ts: -------------------------------------------------------------------------------- 1 | import { makeSchema } from 'nexus' 2 | 3 | import mutations from '../mutations' 4 | import queries from '../queries' 5 | import types from '../types' 6 | 7 | const schema = makeSchema({ 8 | types: [types, queries, mutations], 9 | }) 10 | 11 | export default schema -------------------------------------------------------------------------------- /apps/graphql/src/lib/encrypt.ts: -------------------------------------------------------------------------------- 1 | import CryptoJS from 'crypto-js' 2 | 3 | const encrypt = (stringToEncrypt: string) => { 4 | 5 | const encryptedString = CryptoJS.AES.encrypt(stringToEncrypt, process.env.ENCRYPTION_KEY ?? '') 6 | 7 | return encryptedString.toString() 8 | } 9 | 10 | export default encrypt -------------------------------------------------------------------------------- /apps/home/types/pricing.ts: -------------------------------------------------------------------------------- 1 | interface Pricing { 2 | id: string 3 | lineId: string 4 | priceZoneOne: number 5 | priceZoneOneSeniors: number 6 | priceZoneTwo: number 7 | priceZoneTwoSeniors: number 8 | priceZoneThree: number 9 | priceZoneThreeSeniors: number 10 | } 11 | 12 | export default Pricing -------------------------------------------------------------------------------- /packages/tsconfig/react-library.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "display": "React Library", 4 | "extends": "./base.json", 5 | "compilerOptions": { 6 | "jsx": "react-jsx", 7 | "lib": ["ES2015"], 8 | "module": "ESNext", 9 | "target": "es6" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /apps/graphql/src/lib/decrypt.ts: -------------------------------------------------------------------------------- 1 | import CryptoJS from 'crypto-js' 2 | 3 | const decrypt = (stringToDecrypt: string) => { 4 | 5 | const decryptedString = CryptoJS.AES.decrypt(stringToDecrypt, process.env.ENCRYPTION_KEY ?? '') 6 | 7 | return decryptedString.toString(CryptoJS.enc.Utf8) 8 | } 9 | 10 | export default decrypt -------------------------------------------------------------------------------- /apps/home/types/line.ts: -------------------------------------------------------------------------------- 1 | import Pricing from '@/types/pricing' 2 | import Station from '@/types/station' 3 | 4 | interface Line { 5 | id: string 6 | name: string 7 | name_ar: string 8 | color: string 9 | stations: Station[] 10 | sortedStations: Station[] 11 | pricing: Pricing 12 | } 13 | 14 | export default Line -------------------------------------------------------------------------------- /apps/home/types/user.ts: -------------------------------------------------------------------------------- 1 | interface User { 2 | id: string 3 | name: string 4 | email: string 5 | role: 'ADMIN' | 'CUSTOMER_SUPPORT' | 'SENIOR' | 'ADULT' 6 | createdAt: string 7 | documentVerified: 'ACCEPTED' | 'REJECTED' | 'PENDING' 8 | documentUrl: string 9 | picture?: string 10 | } 11 | 12 | export default User -------------------------------------------------------------------------------- /apps/home/types/recommendation.ts: -------------------------------------------------------------------------------- 1 | import Station from '@/types/station' 2 | 3 | interface Recommendation { 4 | from: Station 5 | to: Station 6 | schedule: { 7 | departureTime: string 8 | arrivalTime: string 9 | }[] 10 | price: number, 11 | noOfStationsOnPath: number 12 | } 13 | 14 | export default Recommendation -------------------------------------------------------------------------------- /apps/graphql/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2016", 4 | "module": "commonjs", 5 | "esModuleInterop": true, 6 | "forceConsistentCasingInFileNames": true, 7 | "strict": true, 8 | "skipLibCheck": true 9 | }, 10 | "include": ["src/**/*"], 11 | "exclude": ["node_modules", "dist"] 12 | } -------------------------------------------------------------------------------- /apps/graphql/src/permissions/authenticated.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLError } from 'graphql/error' 2 | 3 | import { Context } from '../context' 4 | 5 | const authenticatedPermission = (ctx: Context) => { 6 | if (!ctx.user) throw new GraphQLError('Not authenticated') 7 | 8 | return true 9 | } 10 | 11 | export default authenticatedPermission -------------------------------------------------------------------------------- /turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turbo.build/schema.json", 3 | "globalDependencies": ["**/.env.*local"], 4 | "pipeline": { 5 | "build": { 6 | "outputs": [".next/**", "!.next/cache/**"] 7 | }, 8 | "lint": {}, 9 | "dev": { 10 | "cache": false, 11 | "persistent": true 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /apps/graphql/src/types/user-role-enum-arg.ts: -------------------------------------------------------------------------------- 1 | import { inputObjectType } from 'nexus' 2 | 3 | import UserRoleEnum from './user-role' 4 | 5 | const UserRoleEnumArg = inputObjectType({ 6 | name: 'UserRoleEnumArg', 7 | definition(t) { 8 | t.field('userRole', { type: UserRoleEnum }) 9 | }, 10 | }) 11 | 12 | export default UserRoleEnumArg -------------------------------------------------------------------------------- /apps/graphql/src/types/ride-ticket-schedule.ts: -------------------------------------------------------------------------------- 1 | import { objectType } from 'nexus' 2 | 3 | const RideTicketSchedule = objectType({ 4 | name: 'RideTicketSchedule', 5 | definition(t) { 6 | t.field('departureTime', { type: 'DateTime' }) 7 | t.field('arrivalTime', { type: 'DateTime' }) 8 | }, 9 | }) 10 | 11 | export default RideTicketSchedule -------------------------------------------------------------------------------- /apps/home/icons/checkmark-circle.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/graphql/src/types/ticket-type-enum-arg.ts: -------------------------------------------------------------------------------- 1 | import { inputObjectType } from 'nexus' 2 | 3 | import TicketTypeEnum from './ticket-type' 4 | 5 | const TicketTypeEnumArg = inputObjectType({ 6 | name: 'TicketTypeEnumArg', 7 | definition(t) { 8 | t.field('ticketType', { type: TicketTypeEnum }) 9 | }, 10 | }) 11 | 12 | export default TicketTypeEnumArg -------------------------------------------------------------------------------- /apps/graphql/src/types/trip-route.ts: -------------------------------------------------------------------------------- 1 | import { objectType } from 'nexus' 2 | 3 | import StationType from './station' 4 | 5 | const TripRouteType = objectType({ 6 | name: 'TripRoute', 7 | definition(t) { 8 | t.field('station', { type: StationType }) 9 | t.field('time', { type: 'String' }) 10 | }, 11 | }) 12 | 13 | 14 | export default TripRouteType -------------------------------------------------------------------------------- /apps/graphql/src/types/refund-status-enum-arg.ts: -------------------------------------------------------------------------------- 1 | import { inputObjectType } from 'nexus' 2 | 3 | import RefundStatusEnum from './refund-status' 4 | 5 | const RefundStatusEnumArg = inputObjectType({ 6 | name: 'RefundStatusEnumArg', 7 | definition(t) { 8 | t.field('refundStatus', { type: RefundStatusEnum }) 9 | }, 10 | }) 11 | 12 | export default RefundStatusEnumArg -------------------------------------------------------------------------------- /apps/graphql/src/types/schedule-time.ts: -------------------------------------------------------------------------------- 1 | import { inputObjectType } from 'nexus' 2 | 3 | const scheduleTimeType = inputObjectType({ 4 | name: 'scheduleTimeType', 5 | definition(t) { 6 | t.field('hour', { type: 'Int' }) 7 | t.field('minute', { type: 'Int' }) 8 | t.field('meridiem', { type: 'String' }) 9 | }, 10 | }) 11 | 12 | export default scheduleTimeType -------------------------------------------------------------------------------- /apps/graphql/src/lib/email-defaults.ts: -------------------------------------------------------------------------------- 1 | import { EmailTemplate, EmailVariables } from './send-email' 2 | 3 | const getEmailDefaultVariables = (variables: EmailVariables) => { 4 | if (!variables.helpEmail) { 5 | variables.helpEmail = process.env.HELP_EMAIL 6 | } 7 | 8 | return variables 9 | } 10 | 11 | export default getEmailDefaultVariables -------------------------------------------------------------------------------- /apps/home/components/help/enjoy-your-journey.tsx: -------------------------------------------------------------------------------- 1 | const EnjoyYourJourney = () => { 2 | return ( 3 |
4 |

5 | Enjoy your journey on the Cairo Metro! 6 |

7 |
8 | ) 9 | } 10 | 11 | export default EnjoyYourJourney -------------------------------------------------------------------------------- /apps/home/icons/information-circle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /apps/home/next-i18next.config.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/no-var-requires 2 | const path = require('path') 3 | 4 | module.exports = { 5 | i18n: { 6 | defaultLocale: 'en', 7 | locales: ['en', 'ar'], 8 | }, 9 | ...(typeof window === undefined ? ( 10 | { localePath: path.resolve('./public/locales') } 11 | ) : ( 12 | {} 13 | )), 14 | } -------------------------------------------------------------------------------- /apps/graphql/src/types/lines-and-stations-type.ts: -------------------------------------------------------------------------------- 1 | import { objectType } from 'nexus' 2 | 3 | 4 | const LinesAndStationsAnalyticsType = objectType({ 5 | name: 'LinesAndStationsAnalytics', 6 | definition(t) { 7 | t.field('totalLines', { type: 'Int' }) 8 | t.field('totalStations', { type: 'Int' }) 9 | }, 10 | }) 11 | 12 | 13 | export default LinesAndStationsAnalyticsType -------------------------------------------------------------------------------- /apps/graphql/src/types/passengers-input.ts: -------------------------------------------------------------------------------- 1 | import { inputObjectType } from 'nexus' 2 | 3 | const passengersInputType = inputObjectType({ 4 | name: 'passengersInputType', 5 | definition(t) { 6 | t.field('adults', { type: 'Int' }) 7 | t.field('children', { type: 'Int' }) 8 | t.field('seniors', { type: 'Int' }) 9 | }, 10 | }) 11 | 12 | export default passengersInputType -------------------------------------------------------------------------------- /apps/home/icons/card-outline.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/home/icons/checkbox-outline.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/home/icons/return-up-forward.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/home/icons/search-outline.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/home/icons/mail-outline.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/home/icons/location.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/home/icons/close-circle-outline.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/home/types/station.ts: -------------------------------------------------------------------------------- 1 | import Line from '@/types/line' 2 | 3 | interface Station { 4 | id: string 5 | name: string 6 | name_ar: string 7 | location: string 8 | locationLngLat: { 9 | lng: number 10 | lat: number 11 | } 12 | stationPositionInLine: { 13 | position: number 14 | line: Line 15 | }[] 16 | lines: Line[] 17 | lineIds: string[] 18 | } 19 | 20 | export default Station -------------------------------------------------------------------------------- /apps/graphql/src/types/verification-status-enum-arg.ts: -------------------------------------------------------------------------------- 1 | import { inputObjectType } from 'nexus' 2 | 3 | import VerificationStatusEnum from './verification-status' 4 | 5 | const VerificationStatusEnumArg = inputObjectType({ 6 | name: 'VerificationStatusEnumArg', 7 | definition(t) { 8 | t.field('verificationstatus', { type: VerificationStatusEnum }) 9 | }, 10 | }) 11 | 12 | export default VerificationStatusEnumArg -------------------------------------------------------------------------------- /apps/graphql/src/resolvers/queries/stations.ts: -------------------------------------------------------------------------------- 1 | import { FieldResolver } from 'nexus/src/typegenTypeHelpers' 2 | 3 | import { Context } from '../../context' 4 | 5 | const stations: FieldResolver<'Query', 'stations'> = 6 | async (_, args, ctx: Context) => { 7 | const { prisma } = ctx 8 | 9 | const stations = await prisma.station.findMany() 10 | 11 | return stations 12 | } 13 | 14 | export default stations 15 | -------------------------------------------------------------------------------- /apps/graphql/src/utils/cors.ts: -------------------------------------------------------------------------------- 1 | import cors, { CorsOptions } from 'cors' 2 | 3 | /** 4 | * This is a custom CORS middleware that allows us to use cookies with CORS 5 | */ 6 | export default cors((req, callback) => { 7 | const corsOptions: CorsOptions = { 8 | origin: process.env.FRONTEND_URL ?? '', 9 | credentials: true, 10 | exposedHeaders: ['Set-Cookie'], 11 | } 12 | 13 | callback(null, corsOptions) 14 | }) 15 | -------------------------------------------------------------------------------- /apps/home/components/authentication/login/login-screen-animation.tsx: -------------------------------------------------------------------------------- 1 | import PerspectiveGrid from './perspective-grid' 2 | 3 | const LoginScreenAnimation = () => { 4 | 5 | return ( 6 |
7 | 8 |
9 | ) 10 | } 11 | 12 | export default LoginScreenAnimation -------------------------------------------------------------------------------- /apps/home/components/tickets/details/index.tsx: -------------------------------------------------------------------------------- 1 | import TicketPurchaseDetails from '@/components/tickets/details/ticket-purchase-details' 2 | import Hero from '@/components/tickets/hero' 3 | 4 | const TicketDetails = () => { 5 | return ( 6 |
7 | 8 | 9 |
10 | ) 11 | } 12 | 13 | export default TicketDetails -------------------------------------------------------------------------------- /apps/home/icons/document-text.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/graphql/src/types/refund-analytics.ts: -------------------------------------------------------------------------------- 1 | import { objectType } from 'nexus' 2 | 3 | 4 | const refundAnalyticsType = objectType({ 5 | name: 'RefundAnalytics', 6 | definition(t) { 7 | t.field('total', { type: 'Int' }) 8 | t.field('totalApproved', { type: 'Int' }) 9 | t.field('totalRejected', { type: 'Int' }) 10 | t.field('totalThisMonth', { type: 'Int' }) 11 | }, 12 | }) 13 | 14 | 15 | export default refundAnalyticsType -------------------------------------------------------------------------------- /apps/home/components/admin/card.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react' 2 | 3 | const Card = ({ children }: {children?: ReactNode}) => { 4 | return ( 5 |
6 | {children} 7 |
8 | ) 9 | } 10 | 11 | export default Card -------------------------------------------------------------------------------- /apps/graphql/src/lib/generate-access-token.ts: -------------------------------------------------------------------------------- 1 | import { User } from '@prisma/client' 2 | import dotenv from 'dotenv' 3 | import jwt from 'jsonwebtoken' 4 | 5 | dotenv.config() 6 | 7 | const JWT_SECRET = process.env.JWT_SECRET ?? 'dev' 8 | 9 | const generateAccessToken = (user: { id: string } & Partial) => { 10 | return jwt.sign({ id: user.id }, JWT_SECRET, { 11 | expiresIn: '7d', 12 | }) 13 | } 14 | 15 | export default generateAccessToken -------------------------------------------------------------------------------- /apps/home/icons/blind.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/home/icons/credit-default-effect.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /apps/home/public/locales/en/purchase.json: -------------------------------------------------------------------------------- 1 | { 2 | "cardNumber": "Card Number", 3 | "cardHolder": "Card Holder", 4 | "cardHolderPlaceholder": "Enter Card Holder Name", 5 | "validThru": "Valid Thru", 6 | "validThruPlaceholder": "MM/YY", 7 | "CVC": "CVC", 8 | "saveCard": "Save Card", 9 | "pay": "Pay", 10 | "savedCards": "Saved Cards", 11 | "total": "Total", 12 | "egp": "EGP", 13 | "poweredBy": "Powered By", 14 | "endsWith": "Ends with" 15 | } -------------------------------------------------------------------------------- /apps/home/icons/document-outline.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/home/public/locales/ar/purchase.json: -------------------------------------------------------------------------------- 1 | { 2 | "cardNumber": "رقم البطاقة", 3 | "cardHolder": "اسم صاحب البطاقة", 4 | "cardHolderPlaceholder": "اسم صاحب البطاقة", 5 | "validThru": "صالحة حتى", 6 | "validThruPlaceholder": "شهر/سنة", 7 | "CVC": "CVC", 8 | "saveCard": "حفظ البطاقة", 9 | "pay": "دفع", 10 | "savedCards": "البطاقات المحفوظة", 11 | "total": "المجموع", 12 | "egp": "جنيه", 13 | "poweredBy": "مدعوم بواسطة", 14 | "endsWith": "تنتهي بـ" 15 | } -------------------------------------------------------------------------------- /apps/graphql/src/resolvers/queries/me.ts: -------------------------------------------------------------------------------- 1 | import { FieldResolver } from 'nexus/src/typegenTypeHelpers' 2 | 3 | import { Context } from '../../context' 4 | import authenticatedPermission from '../../permissions/authenticated' 5 | 6 | const me: FieldResolver<'Query', 'me'> = 7 | async (_, args, ctx: Context) => { 8 | try { 9 | authenticatedPermission(ctx) 10 | return ctx.user 11 | } catch (e) { 12 | return null 13 | } 14 | } 15 | 16 | export default me -------------------------------------------------------------------------------- /apps/graphql/src/permissions/admin.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLError } from 'graphql/error' 2 | 3 | import { UserRole } from '@prisma/client' 4 | 5 | import { Context } from '../context' 6 | 7 | import authenticatedPermission from './authenticated' 8 | 9 | const adminPermission = (ctx: Context) => { 10 | authenticatedPermission(ctx) 11 | 12 | if (ctx.user?.role !== UserRole.ADMIN) throw new GraphQLError('Not authorized') 13 | 14 | return true 15 | } 16 | 17 | export default adminPermission -------------------------------------------------------------------------------- /apps/home/icons/location-outline.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/graphql/src/lib/magic-link.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient, User } from '@prisma/client' 2 | 3 | const generateMagicLink = async (user: {id: string} & Partial, prisma: PrismaClient) => { 4 | const token = await prisma.magicToken.create({ 5 | data: { 6 | createdAt: new Date(), 7 | expiryDate: new Date(new Date().getTime() + 30 * 60000), // 30 minutes 8 | userID: user.id, 9 | }, 10 | }) 11 | 12 | return token 13 | } 14 | 15 | export default generateMagicLink -------------------------------------------------------------------------------- /apps/home/components/home/index.tsx: -------------------------------------------------------------------------------- 1 | import Faq from '@/components/faq' 2 | import Companion from '@/components/home/companion' 3 | import Discover from '@/components/home/discover' 4 | import Hero from '@/components/home/hero' 5 | 6 | 7 | const Home = () => { 8 | return ( 9 |
10 | 11 | 12 | 13 | 14 |
15 | ) 16 | } 17 | 18 | export default Home 19 | -------------------------------------------------------------------------------- /apps/graphql/src/types/line.ts: -------------------------------------------------------------------------------- 1 | import { objectType } from 'nexus' 2 | import { Line } from 'nexus-prisma' 3 | 4 | 5 | const LineType = objectType({ 6 | name: Line.$name, 7 | definition(t) { 8 | t.field(Line.id) 9 | t.field(Line.name) 10 | t.field(Line.name_ar) 11 | t.field(Line.color) 12 | t.field(Line.stations) 13 | t.field(Line.pricing) 14 | t.list.field('sortedStations', { 15 | type: 'Station', 16 | }) 17 | }, 18 | }) 19 | 20 | 21 | export default LineType -------------------------------------------------------------------------------- /apps/graphql/src/types/one-time-input.ts: -------------------------------------------------------------------------------- 1 | import { inputObjectType } from 'nexus' 2 | 3 | import passengersInputType from './passengers-input' 4 | 5 | 6 | const oneTimeInput = inputObjectType({ 7 | name: 'oneTimeInput', 8 | definition(t) { 9 | t.field('from', { type: 'String' }) 10 | t.field('to', { type: 'String' }) 11 | t.field('passengers', { type: passengersInputType }) 12 | t.field('departureTime', { type: 'String' }) 13 | 14 | }, 15 | }) 16 | export default oneTimeInput 17 | -------------------------------------------------------------------------------- /apps/home/graphql/admin/analytics/average-response.ts: -------------------------------------------------------------------------------- 1 | 2 | import graphqlFetcher from '@/graphql/graphql-fetcher' 3 | 4 | import useSWR from 'swr' 5 | 6 | const TOTAL_SOLD_TICKETS_QUERY = /* GraphQL */ ` 7 | { 8 | totalSoldTickets 9 | } 10 | ` 11 | 12 | const useTotalSoldTickets = () => { 13 | const result = useSWR( 14 | [TOTAL_SOLD_TICKETS_QUERY], 15 | (queryStr: string) => graphqlFetcher(queryStr) 16 | ) 17 | 18 | return result 19 | } 20 | 21 | export default useTotalSoldTickets -------------------------------------------------------------------------------- /apps/home/graphql/user/login.ts: -------------------------------------------------------------------------------- 1 | import { Variables } from 'graphql-request' 2 | 3 | import mutate from '@/graphql/mutate' 4 | 5 | interface LoginMutationVariables extends Variables { 6 | email: string 7 | } 8 | 9 | const LOGIN_MUTATION = /* GraphQL */ ` 10 | mutation login($email: String!) { 11 | login(email: $email) 12 | } 13 | ` 14 | 15 | const loginMutation = async (variables: LoginMutationVariables) => { 16 | return mutate(LOGIN_MUTATION, variables) 17 | } 18 | 19 | export default loginMutation -------------------------------------------------------------------------------- /apps/graphql/src/resolvers/queries/station-by-id.ts: -------------------------------------------------------------------------------- 1 | import { FieldResolver } from 'nexus/src/typegenTypeHelpers' 2 | 3 | import { Context } from '../../context' 4 | 5 | const stationById: FieldResolver<'Query', 'stationById'> = 6 | async (_, args, ctx: Context) => { 7 | const { prisma } = ctx 8 | 9 | const station = await prisma.station.findUnique({ 10 | where: { 11 | id: args.id, 12 | }, 13 | }) 14 | 15 | return station 16 | } 17 | 18 | export default stationById 19 | -------------------------------------------------------------------------------- /apps/home/icons/person-outline.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/home/graphql/admin/analytics/sold-tickets.ts: -------------------------------------------------------------------------------- 1 | import graphqlFetcher from '@/graphql/graphql-fetcher' 2 | 3 | import useSWR from 'swr' 4 | 5 | const ANALYTICS_SOLD_TICKETS_QUERY = /* GraphQL */ ` 6 | { 7 | analyticsSoldTickets 8 | } 9 | ` 10 | 11 | const useAnalyticsSoldTickets = () => { 12 | const result = useSWR( 13 | [ANALYTICS_SOLD_TICKETS_QUERY], 14 | (queryStr: string) => graphqlFetcher(queryStr) 15 | ) 16 | 17 | return result 18 | } 19 | 20 | export default useAnalyticsSoldTickets -------------------------------------------------------------------------------- /apps/graphql/src/types/invitation.ts: -------------------------------------------------------------------------------- 1 | import { objectType } from 'nexus' 2 | import { StaffInvitation } from 'nexus-prisma' 3 | 4 | const InvitationType = objectType({ 5 | name: StaffInvitation.$name, 6 | definition(t) { 7 | t.field(StaffInvitation.id) 8 | t.field(StaffInvitation.email) 9 | t.field(StaffInvitation.name) 10 | t.field(StaffInvitation.role) 11 | t.field(StaffInvitation.invitedBy) 12 | t.field(StaffInvitation.createdAt) 13 | }, 14 | }) 15 | 16 | 17 | export default InvitationType -------------------------------------------------------------------------------- /apps/home/icons/train-outline.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.idea/turbo-template-new.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /apps/graphql/src/types/station-position-in-line.ts: -------------------------------------------------------------------------------- 1 | import { objectType } from 'nexus' 2 | import { StationPositionInLine } from 'nexus-prisma' 3 | 4 | 5 | 6 | const StationPositionInLineType = objectType({ 7 | name: StationPositionInLine.$name, 8 | definition(t) { 9 | t.field(StationPositionInLine.id) 10 | t.field(StationPositionInLine.line) 11 | t.field(StationPositionInLine.station) 12 | t.field(StationPositionInLine.position) 13 | }, 14 | }) 15 | 16 | 17 | export default StationPositionInLineType -------------------------------------------------------------------------------- /apps/home/icons/cloud-upload-outline.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/graphql/src/resolvers/queries/admin/analytics-total-subscribers.ts: -------------------------------------------------------------------------------- 1 | import { FieldResolver } from 'nexus/src/typegenTypeHelpers' 2 | 3 | import { Context } from '../../../context' 4 | import adminPermission from '../../../permissions/admin' 5 | 6 | const analyticsTotalSubscribers: FieldResolver<'Query', 'analyticsTotalSubscribers'> = 7 | async (_, _args, ctx: Context) => { 8 | adminPermission(ctx) 9 | // @todo: implement 10 | return await ctx.prisma.user.count() 11 | } 12 | 13 | export default analyticsTotalSubscribers -------------------------------------------------------------------------------- /apps/home/layouts/user.tsx: -------------------------------------------------------------------------------- 1 | import { useRouter } from 'next/router' 2 | 3 | import useUser from '@/graphql/user/me' 4 | 5 | const AuthenticatedUser = ({ children }: {children: React.ReactNode}) => { 6 | const { data: user, isLoading: userLoading } = useUser() 7 | const router = useRouter() 8 | 9 | if (userLoading) return null 10 | 11 | if (!user) { 12 | router.push('/') 13 | return null 14 | } 15 | 16 | return ( 17 | <> 18 | {children} 19 | 20 | ) 21 | } 22 | 23 | export default AuthenticatedUser -------------------------------------------------------------------------------- /apps/graphql/src/types/pricing.ts: -------------------------------------------------------------------------------- 1 | import { objectType } from 'nexus' 2 | import { Pricing } from 'nexus-prisma' 3 | 4 | 5 | const PricingType = objectType({ 6 | name: Pricing.$name, 7 | definition(t) { 8 | t.field(Pricing.id) 9 | t.field(Pricing.priceZoneOne) 10 | t.field(Pricing.priceZoneOneSeniors) 11 | t.field(Pricing.priceZoneTwo) 12 | t.field(Pricing.priceZoneTwoSeniors) 13 | t.field(Pricing.priceZoneThree) 14 | t.field(Pricing.priceZoneThreeSeniors) 15 | }, 16 | }) 17 | 18 | 19 | export default PricingType -------------------------------------------------------------------------------- /apps/home/graphql/admin/analytics/total-subscribers.ts: -------------------------------------------------------------------------------- 1 | import graphqlFetcher from '@/graphql/graphql-fetcher' 2 | 3 | import useSWR from 'swr' 4 | 5 | const ANALYTICS_TOTAL_SUBSCRIBERS_QUERY = /* GraphQL */ ` 6 | { 7 | analyticsTotalSubscribers 8 | } 9 | ` 10 | 11 | const useAnalyticsTotalSubscribers = () => { 12 | const result = useSWR( 13 | [ANALYTICS_TOTAL_SUBSCRIBERS_QUERY], 14 | (queryStr: string) => graphqlFetcher(queryStr) 15 | ) 16 | 17 | return result 18 | } 19 | 20 | export default useAnalyticsTotalSubscribers -------------------------------------------------------------------------------- /apps/home/graphql/admin/monthly-revenue.ts: -------------------------------------------------------------------------------- 1 | import graphqlFetcher from '@/graphql/graphql-fetcher' 2 | 3 | import useSWR from 'swr' 4 | 5 | const MONTHLY_REVENUE_QUERY = /* GraphQL */ ` 6 | query monthlyRevenue { 7 | monthlyRevenue { 8 | month 9 | revenue 10 | } 11 | } 12 | ` 13 | 14 | const useMonthlyRevenue = () => { 15 | const result = useSWR( 16 | [MONTHLY_REVENUE_QUERY], 17 | (queryStr: string) => graphqlFetcher(queryStr), 18 | ) 19 | 20 | return result 21 | } 22 | 23 | export default useMonthlyRevenue -------------------------------------------------------------------------------- /apps/home/pages/admin/team.tsx: -------------------------------------------------------------------------------- 1 | import Team from '@/components/admin/team' 2 | import AdminLayout from '@/layouts/admin' 3 | 4 | import { NextSeo } from 'next-seo' 5 | 6 | const TeamPage = () => { 7 | return ( 8 | 13 | 18 | 19 | 20 | ) 21 | } 22 | 23 | export default TeamPage -------------------------------------------------------------------------------- /apps/home/graphql/admin/team.ts: -------------------------------------------------------------------------------- 1 | import graphqlFetcher from '@/graphql/graphql-fetcher' 2 | 3 | import useSWR from 'swr' 4 | 5 | const TEAM_MEMBERS_QUERY = /* GraphQL */ ` 6 | query adminTeamMembers { 7 | adminTeamMembers { 8 | id 9 | name 10 | email 11 | role 12 | createdAt 13 | } 14 | } 15 | ` 16 | 17 | const useTeamMembers = () => { 18 | const result = useSWR( 19 | [TEAM_MEMBERS_QUERY], 20 | (queryStr: string) => graphqlFetcher(queryStr), 21 | ) 22 | 23 | return result 24 | } 25 | 26 | export default useTeamMembers -------------------------------------------------------------------------------- /apps/home/icons/logo-pwa.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/home/graphql/user/magic-link.ts: -------------------------------------------------------------------------------- 1 | import { Variables } from 'graphql-request' 2 | 3 | import mutate from '@/graphql/mutate' 4 | 5 | interface LinkMutationVariables extends Variables { 6 | 7 | link: string 8 | } 9 | 10 | const MAGIC_LINK_MUTATION = /* GraphQL */ ` 11 | mutation magicLinkVerification($link: String!) { 12 | magicLinkVerification(link:$link) 13 | } 14 | ` 15 | 16 | const magicLinkMutation = async (variables: LinkMutationVariables) => { 17 | return mutate(MAGIC_LINK_MUTATION, variables) 18 | } 19 | 20 | export default magicLinkMutation -------------------------------------------------------------------------------- /apps/graphql/src/api/auth/logout.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from 'express' 2 | 3 | const logout = async (req: Request, res: Response) => { 4 | try { 5 | const accessTokenCookieDomain = process.env.ACCESS_TOKEN_COOKIE ?? '' 6 | 7 | res.clearCookie('access', { 8 | domain: accessTokenCookieDomain, 9 | }) 10 | 11 | return res.status(200).json({ 12 | success: true, 13 | }) 14 | } catch (e) { 15 | return res.status(500).json({ 16 | message: 'Internal Server Error', 17 | }) 18 | } 19 | } 20 | 21 | export default logout -------------------------------------------------------------------------------- /apps/graphql/src/lib/verify-access-token.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from '@prisma/client' 2 | import jwt from 'jsonwebtoken' 3 | 4 | const JWT_SECRET = process.env.JWT_SECRET ?? 'dev' 5 | 6 | const verifyAccessToken = async (token: string, prisma: PrismaClient) => { 7 | const verify = jwt.verify(token, JWT_SECRET) 8 | const { id } = verify as { id: string } 9 | 10 | if (!id) throw new Error('User id not found') 11 | 12 | return await prisma.user.findUnique({ 13 | where: { 14 | id, 15 | }, 16 | }) 17 | } 18 | 19 | export default verifyAccessToken -------------------------------------------------------------------------------- /apps/home/graphql/user/user-cards.ts: -------------------------------------------------------------------------------- 1 | import graphqlFetcher from '@/graphql/graphql-fetcher' 2 | 3 | import useSWR from 'swr' 4 | 5 | const USER_CARDS_QUERY = /* GraphQL */ ` 6 | { 7 | userCards { 8 | id 9 | cardHolder 10 | last4 11 | brand 12 | expiryMonth 13 | expiryYear 14 | } 15 | } 16 | ` 17 | 18 | const useUserCards = () => { 19 | const result = useSWR( 20 | [USER_CARDS_QUERY], 21 | (queryStr: string) => graphqlFetcher(queryStr) 22 | ) 23 | 24 | return result 25 | } 26 | 27 | export default useUserCards -------------------------------------------------------------------------------- /apps/home/icons/assist-walker.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/graphql/src/lib/rate-limit.ts: -------------------------------------------------------------------------------- 1 | import rateLimit from 'express-rate-limit' 2 | 3 | /** 4 | * Limit the rate of requests to a given endpoint 5 | * @param maxRate The maximum number of requests allowed in the given time window 6 | * @param minutes The time window in minutes 7 | */ 8 | export const limitRate = (maxRate: number, minutes: number) => rateLimit({ 9 | windowMs: minutes * 60 * 1000, 10 | max: maxRate, 11 | standardHeaders: true, 12 | legacyHeaders: false, 13 | message: `Too many requests from this IP, please try again in ${minutes} minutes`, 14 | }) -------------------------------------------------------------------------------- /apps/graphql/src/types/refund.ts: -------------------------------------------------------------------------------- 1 | import { objectType } from 'nexus' 2 | import { Refund } from 'nexus-prisma' 3 | 4 | 5 | const RefundType = objectType({ 6 | name: Refund.$name, 7 | definition(t) { 8 | t.field(Refund.id) 9 | t.field(Refund.user) 10 | t.field(Refund.userId) 11 | t.field(Refund.status) 12 | t.field(Refund.ticketType) 13 | t.field('createdAt', { type: 'DateTime' }) 14 | t.field('updatedAt', { type: 'DateTime' }) 15 | t.field(Refund.message) 16 | t.field(Refund.price) 17 | }, 18 | }) 19 | 20 | 21 | export default RefundType -------------------------------------------------------------------------------- /apps/graphql/src/types/user-card.ts: -------------------------------------------------------------------------------- 1 | import { objectType } from 'nexus' 2 | import { UserCreditCard } from 'nexus-prisma' 3 | 4 | 5 | 6 | const UserCardType = objectType({ 7 | name: UserCreditCard.$name, 8 | definition(t) { 9 | t.field(UserCreditCard.id) 10 | t.field(UserCreditCard.cardHolder) 11 | t.field('last4', { 12 | type: 'String', 13 | }) 14 | t.field('brand', { 15 | type: 'String', 16 | }) 17 | t.field(UserCreditCard.expiryMonth) 18 | t.field(UserCreditCard.expiryYear) 19 | }, 20 | }) 21 | 22 | 23 | export default UserCardType -------------------------------------------------------------------------------- /apps/home/graphql/stations/delete-station.ts: -------------------------------------------------------------------------------- 1 | import { Variables } from 'graphql-request' 2 | 3 | import mutate from '@/graphql/mutate' 4 | 5 | interface DeleteStationVariables extends Variables { 6 | stationId: string 7 | } 8 | 9 | const DELETE_QUERY = /* GraphQL */ ` 10 | mutation adminDeleteStation($stationId: String!) { 11 | adminDeleteStation(stationId: $stationId) 12 | } 13 | ` 14 | 15 | const adminDeleteStationMutation = (variables: DeleteStationVariables) => { 16 | return mutate(DELETE_QUERY, variables) 17 | } 18 | 19 | export default adminDeleteStationMutation -------------------------------------------------------------------------------- /apps/graphql/src/types/users-analytics.ts: -------------------------------------------------------------------------------- 1 | import { objectType } from 'nexus' 2 | 3 | 4 | const UserAnalyticsType = objectType({ 5 | name: 'UsersAnalytics', 6 | definition(t) { 7 | t.field('totalUsers', { type: 'Int' }) 8 | t.field('totalSeniors', { type: 'Int' }) 9 | t.field('totalAdults', { type: 'Int' }) 10 | t.field('weeklyUsers', { type: 'Int' }) 11 | t.field('weeklyUsersDiff', { type: 'Float' }) 12 | t.field('monthlyUsers', { type: 'Int' }) 13 | t.field('monthlyUsersDiff', { type: 'Float' }) 14 | }, 15 | }) 16 | 17 | export default UserAnalyticsType -------------------------------------------------------------------------------- /apps/graphql/src/types/subscription-input.ts: -------------------------------------------------------------------------------- 1 | import { enumType, inputObjectType } from 'nexus' 2 | import { SubscriptionTier, SubscriptionType } from 'nexus-prisma' 3 | 4 | const SubscriptionTypeEnum = enumType(SubscriptionType) 5 | const SubscriptionTierEnum = enumType(SubscriptionTier) 6 | 7 | const subscriptionEnumArg = inputObjectType({ 8 | name: 'subscriptionEnumArg', 9 | definition(t) { 10 | t.field('subscriptionType', { type: SubscriptionTypeEnum }) 11 | t.field('subscriptionTier', { type: SubscriptionTierEnum }) 12 | }, 13 | }) 14 | 15 | export default subscriptionEnumArg -------------------------------------------------------------------------------- /apps/home/components/help/card.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link' 2 | 3 | interface CardProps extends React.HTMLAttributes { 4 | href: string 5 | } 6 | 7 | const HelpCard = ({ children, href, ...props }: CardProps) => { 8 | return ( 9 | 14 | {children} 15 | 16 | ) 17 | } 18 | 19 | export default HelpCard -------------------------------------------------------------------------------- /apps/home/icons/pricetags-outline.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/graphql/src/types/ride-ticket-data.ts: -------------------------------------------------------------------------------- 1 | import { objectType } from 'nexus' 2 | 3 | import RideTicketSchedule from './ride-ticket-schedule' 4 | import StationType from './station' 5 | 6 | 7 | const RideTicketDataType = objectType({ 8 | name: 'RideTicketData', 9 | definition(t) { 10 | t.field('from', { type: StationType }) 11 | t.field('to', { type: StationType }) 12 | t.field('noOfStationsOnPath', { type: 'Int' }) 13 | t.field('price', { type: 'Float' }) 14 | t.list.field('schedule', { type: RideTicketSchedule }) 15 | }, 16 | }) 17 | 18 | export default RideTicketDataType -------------------------------------------------------------------------------- /.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 | # build output 9 | dist 10 | build 11 | out 12 | 13 | # testing 14 | coverage 15 | 16 | # next.js 17 | .next/ 18 | out/ 19 | 20 | # misc 21 | .DS_Store 22 | *.pem 23 | 24 | # debug 25 | npm-debug.log* 26 | yarn-debug.log* 27 | yarn-error.log* 28 | 29 | # local env files 30 | .env 31 | .env.local 32 | .env.development.local 33 | .env.test.local 34 | .env.production.local 35 | 36 | # turbo 37 | .turbo 38 | 39 | # vercel 40 | .vercel 41 | -------------------------------------------------------------------------------- /apps/home/graphql/admin/remove-teammate.ts: -------------------------------------------------------------------------------- 1 | import { Variables } from 'graphql-request' 2 | 3 | import mutate from '@/graphql/mutate' 4 | 5 | interface AdminRemoveTeammateVariables extends Variables { 6 | email: string 7 | } 8 | 9 | const ADMIN_REMOVE_TEAMMATES_QUERY = /* GraphQL */ ` 10 | mutation adminRemoveTeammate($email: String!) { 11 | adminRemoveTeammate(email: $email) 12 | } 13 | ` 14 | 15 | const adminRemoveTeammateMutation = (variables: AdminRemoveTeammateVariables) => { 16 | return mutate(ADMIN_REMOVE_TEAMMATES_QUERY, variables) 17 | } 18 | 19 | export default adminRemoveTeammateMutation -------------------------------------------------------------------------------- /apps/home/graphql/user/subscription-history.ts: -------------------------------------------------------------------------------- 1 | import graphqlFetcher from '@/graphql/graphql-fetcher' 2 | 3 | import useSWR from 'swr' 4 | 5 | const SUBSCRIPTION_HISTORY_QUERY = /* GraphQL */ ` 6 | { 7 | userSubscriptionsHistory { 8 | id 9 | type 10 | tier 11 | createdAt 12 | expiresAt 13 | } 14 | } 15 | ` 16 | 17 | const useSubscriptionHistory = () => { 18 | const result = useSWR( 19 | [SUBSCRIPTION_HISTORY_QUERY], 20 | (queryStr: string) => graphqlFetcher(queryStr) 21 | ) 22 | 23 | return result 24 | } 25 | 26 | export default useSubscriptionHistory -------------------------------------------------------------------------------- /apps/home/pages/admin/index.tsx: -------------------------------------------------------------------------------- 1 | import type { NextPage } from 'next' 2 | 3 | import Admin from '@/components/admin/home' 4 | import AdminLayout from '@/layouts/admin' 5 | 6 | import { NextSeo } from 'next-seo' 7 | 8 | const AdminPage: NextPage = () => { 9 | return ( 10 | 15 | 20 | 21 | 22 | ) 23 | } 24 | 25 | export default AdminPage -------------------------------------------------------------------------------- /apps/home/icons/logo-google.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/graphql/src/resolvers/queries/get-price.ts: -------------------------------------------------------------------------------- 1 | import { FieldResolver } from 'nexus/src/typegenTypeHelpers' 2 | 3 | import { Context } from '../../context' 4 | import calculatePricing from '../../lib/calculate-pricing' 5 | import findRoute from '../../lib/find-route' 6 | 7 | const getPrice: FieldResolver<'Query', 'getPrice'> = 8 | async (_, args, ctx: Context) => { 9 | const { from, to, passengers } = args 10 | 11 | const path = await findRoute(from, to, ctx) 12 | const price = await calculatePricing(path, passengers, ctx) 13 | 14 | return price 15 | } 16 | 17 | export default getPrice 18 | -------------------------------------------------------------------------------- /apps/home/icons/home.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/home/.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 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env.local 29 | .env.development.local 30 | .env.test.local 31 | .env.production.local 32 | 33 | # vercel 34 | .vercel 35 | 36 | 37 | # PWA 38 | **/public/sw.js 39 | **/public/workbox-*.js -------------------------------------------------------------------------------- /apps/home/graphql/admin/analytics/refunds-analytics.ts: -------------------------------------------------------------------------------- 1 | import graphqlFetcher from '@/graphql/graphql-fetcher' 2 | 3 | import useSWR from 'swr' 4 | 5 | const REFUNDS_ANALYTICS_QUERY = /* GraphQL */ ` 6 | query refundsAnalytics { 7 | refundsAnalytics { 8 | total 9 | totalApproved 10 | totalRejected 11 | totalThisMonth 12 | } 13 | } 14 | ` 15 | 16 | const useRefundsAnalytics = () => { 17 | const result = useSWR( 18 | [REFUNDS_ANALYTICS_QUERY], 19 | (queryStr: string) => graphqlFetcher(queryStr) 20 | ) 21 | 22 | return result 23 | } 24 | 25 | export default useRefundsAnalytics -------------------------------------------------------------------------------- /apps/home/graphql/admin/analytics/seniors-analytics.ts: -------------------------------------------------------------------------------- 1 | import graphqlFetcher from '@/graphql/graphql-fetcher' 2 | 3 | import useSWR from 'swr' 4 | 5 | const SENIORS_ANALYTICS_QUERY = /* GraphQL */ ` 6 | query seniorsAnalytics { 7 | seniorsAnalytics { 8 | total 9 | totalApproved 10 | totalRejected 11 | totalThisMonth 12 | } 13 | } 14 | ` 15 | 16 | const useSeniorsAnalytics = () => { 17 | const result = useSWR( 18 | [SENIORS_ANALYTICS_QUERY], 19 | (queryStr: string) => graphqlFetcher(queryStr) 20 | ) 21 | 22 | return result 23 | } 24 | 25 | export default useSeniorsAnalytics -------------------------------------------------------------------------------- /apps/home/graphql/admin/analytics/total-lines-and-stations.ts: -------------------------------------------------------------------------------- 1 | import graphqlFetcher from '@/graphql/graphql-fetcher' 2 | 3 | import useSWR from 'swr' 4 | 5 | const ANALYTICS_TOTAL_LINES_AND_STATIONS_QUERY = /* GraphQL */ ` 6 | { 7 | analyticsActiveLinesAndStations { 8 | totalLines 9 | totalStations 10 | } 11 | } 12 | ` 13 | 14 | const useTotalLinesAndStations = () => { 15 | const result = useSWR( 16 | [ANALYTICS_TOTAL_LINES_AND_STATIONS_QUERY], 17 | (queryStr: string) => graphqlFetcher(queryStr) 18 | ) 19 | 20 | return result 21 | } 22 | 23 | export default useTotalLinesAndStations -------------------------------------------------------------------------------- /apps/home/icons/trash.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/home/pages/admin/refunds.tsx: -------------------------------------------------------------------------------- 1 | import type { NextPage } from 'next' 2 | 3 | import Refunds from '@/components/admin/refunds' 4 | import AdminLayout from '@/layouts/admin' 5 | 6 | import { NextSeo } from 'next-seo' 7 | 8 | const RefundsPage: NextPage = () => { 9 | return ( 10 | 15 | 20 | 21 | 22 | ) 23 | } 24 | 25 | export default RefundsPage -------------------------------------------------------------------------------- /packages/tsconfig/base.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "display": "Default", 4 | "compilerOptions": { 5 | "composite": false, 6 | "declaration": true, 7 | "declarationMap": true, 8 | "esModuleInterop": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "inlineSources": false, 11 | "isolatedModules": true, 12 | "moduleResolution": "node", 13 | "noUnusedLocals": false, 14 | "noUnusedParameters": false, 15 | "preserveWatchOutput": true, 16 | "skipLibCheck": true, 17 | "strict": true 18 | }, 19 | "exclude": ["node_modules"] 20 | } 21 | -------------------------------------------------------------------------------- /apps/home/icons/person-remove-outline.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/graphql/src/resolvers/queries/invitation.ts: -------------------------------------------------------------------------------- 1 | import { FieldResolver } from 'nexus/src/typegenTypeHelpers' 2 | 3 | import { Context } from '../../context' 4 | 5 | const invitation: FieldResolver<'Query', 'invitation'> = 6 | async (_, args, ctx: Context) => { 7 | const { prisma } = ctx 8 | const { token } = args 9 | 10 | const invitation = await prisma.staffInvitation.findUnique({ 11 | where: { 12 | id: token, 13 | }, 14 | }) 15 | 16 | if (!invitation) { 17 | throw new Error('Invitation not found.') 18 | } 19 | 20 | return invitation 21 | } 22 | 23 | export default invitation -------------------------------------------------------------------------------- /packages/eslint-config-custom/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eslint-config-custom", 3 | "version": "0.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "dependencies": { 7 | "@typescript-eslint/eslint-plugin": "^5.59.0", 8 | "@typescript-eslint/parser": "^5.59.0", 9 | "@typescript-eslint/typescript-estree": "^5.59.0", 10 | "eslint-config-next": "latest", 11 | "eslint-config-prettier": "^8.3.0", 12 | "eslint-config-turbo": "latest", 13 | "eslint-plugin-react": "7.28.0", 14 | "eslint-plugin-simple-import-sort": "^10.0.0" 15 | }, 16 | "publishConfig": { 17 | "access": "public" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /apps/home/graphql/update-invitation.ts: -------------------------------------------------------------------------------- 1 | import { Variables } from 'graphql-request' 2 | 3 | import mutate from '@/graphql/mutate' 4 | 5 | interface UpdateInvitationVariables extends Variables { 6 | token: string 7 | status: 'ACCEPTED' | 'REJECTED' 8 | } 9 | 10 | const UPDATE_INVITATION_QUERY = /* GraphQL */ ` 11 | mutation updateInvitation($token: String!, $status: Status!) { 12 | updateInvitation(token: $token, status: $status) 13 | } 14 | ` 15 | 16 | const updateInvitationMutation = (variables: UpdateInvitationVariables) => { 17 | return mutate(UPDATE_INVITATION_QUERY, variables) 18 | } 19 | 20 | export default updateInvitationMutation -------------------------------------------------------------------------------- /apps/home/graphql/user/me.ts: -------------------------------------------------------------------------------- 1 | import graphqlFetcher from '@/graphql/graphql-fetcher' 2 | 3 | import useSWR from 'swr' 4 | 5 | const USER_QUERY = /* GraphQL */ ` 6 | { 7 | me { 8 | id 9 | name 10 | role 11 | picture 12 | subscription { 13 | id 14 | type 15 | tier 16 | isActive 17 | expiresAt 18 | refundRequest 19 | } 20 | } 21 | } 22 | ` 23 | 24 | const useUser = () => { 25 | const result = useSWR( 26 | [USER_QUERY], 27 | (queryStr: string) => graphqlFetcher(queryStr) 28 | ) 29 | 30 | return result 31 | } 32 | 33 | export default useUser -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.defaultFormatter": "rvest.vs-code-prettier-eslint", 3 | "editor.formatOnPaste": false, // required 4 | "editor.formatOnType": false, // required 5 | "editor.formatOnSave": false, // optional 6 | "editor.formatOnSaveMode": "file", // required to format on save 7 | "files.autoSave": "onFocusChange", // optional but recommended 8 | "vs-code-prettier-eslint.prettierLast": false, // set as "true" to run 'prettier' last not first 9 | "editor.codeActionsOnSave": { 10 | "source.fixAll.eslint": true 11 | }, 12 | "eslint.validate": ["javascript"], 13 | "eslint.codeActionsOnSave.rules": null 14 | } 15 | -------------------------------------------------------------------------------- /apps/graphql/src/lib/get-mjml-template.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs' 2 | import * as Handlebars from 'handlebars' 3 | import * as path from 'path' 4 | 5 | import { EmailTemplate, EmailVariables } from './send-email' 6 | 7 | 8 | const getMjmlTemplate = (template: T, variables: EmailVariables) => { 9 | const templateFile = path.join(__dirname, `../notifications/${template}.mjml`) 10 | const templateContent = fs.readFileSync(templateFile, 'utf8') 11 | 12 | const compiledTemplate = Handlebars.compile(templateContent.toString())(variables) 13 | 14 | return compiledTemplate 15 | } 16 | 17 | export default getMjmlTemplate -------------------------------------------------------------------------------- /apps/home/icons/mastercard-effect.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /apps/home/icons/create.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/graphql/src/resolvers/queries/user-subscription-history.ts: -------------------------------------------------------------------------------- 1 | import { FieldResolver } from 'nexus/src/typegenTypeHelpers' 2 | 3 | import { Context } from '../../context' 4 | import authenticatedPermission from '../../permissions/authenticated' 5 | 6 | const userSubscriptionHistory: FieldResolver<'Query', 'userSubscriptionHistory'> = 7 | async (_, args, ctx: Context) => { 8 | authenticatedPermission(ctx) 9 | 10 | const subscriptions = await ctx.prisma.subscriptions.findMany({ 11 | where: { 12 | userId: ctx.user?.id, 13 | }, 14 | }) 15 | 16 | return subscriptions 17 | } 18 | 19 | export default userSubscriptionHistory -------------------------------------------------------------------------------- /apps/home/pages/admin/lines-and-stations.tsx: -------------------------------------------------------------------------------- 1 | import LinesAndStations from '@/components/admin/lines-and-stations' 2 | import AdminLayout from '@/layouts/admin' 3 | 4 | import { NextSeo } from 'next-seo' 5 | 6 | const LinesAndStationsPage = () => { 7 | return ( 8 | 13 | 18 | 19 | 20 | ) 21 | } 22 | 23 | export default LinesAndStationsPage -------------------------------------------------------------------------------- /apps/graphql/src/resolvers/queries/admin/analytics-active-lines-and-stations.ts: -------------------------------------------------------------------------------- 1 | import { FieldResolver } from 'nexus/src/typegenTypeHelpers' 2 | 3 | import { Context } from '../../../context' 4 | 5 | const analyticsActiveLinesAndStations: FieldResolver<'Query', 'analyticsActiveLinesAndStations'> = 6 | async (_, _args, ctx: Context) => { 7 | // adminPermission(ctx) 8 | 9 | const { prisma } = ctx 10 | 11 | const totalLines = await prisma.line.count() 12 | const totalStations = await prisma.station.count() 13 | 14 | return { 15 | totalLines, 16 | totalStations, 17 | } 18 | } 19 | 20 | export default analyticsActiveLinesAndStations -------------------------------------------------------------------------------- /apps/graphql/src/context.ts: -------------------------------------------------------------------------------- 1 | import { YogaInitialContext } from 'graphql-yoga' 2 | 3 | import { PrismaClient, User } from '@prisma/client' 4 | 5 | import authenticateUser from './lib/authenticate-user' 6 | 7 | const prisma = new PrismaClient() 8 | 9 | export interface Context extends YogaInitialContext { 10 | prisma: PrismaClient 11 | user?: User | Partial | null 12 | } 13 | 14 | export const createContext = async (initialContext: YogaInitialContext): Promise => { 15 | const user = initialContext.request ? await authenticateUser(prisma, initialContext.request) : null 16 | return { 17 | ...initialContext, 18 | prisma, 19 | user, 20 | } 21 | } -------------------------------------------------------------------------------- /apps/graphql/src/permissions/secret-path.ts: -------------------------------------------------------------------------------- 1 | 2 | import { GraphQLError } from 'graphql/error' 3 | 4 | import { Context } from '../context' 5 | import { isDev } from '../utils/is-dev' 6 | 7 | const secretPathPermission = (ctx: Context) => { 8 | if (isDev) { 9 | return true 10 | } 11 | 12 | const secretToken = process.env.SECRET_ENDPOINT_TOKEN 13 | 14 | if (!secretToken) throw new GraphQLError('No secret token provided') 15 | 16 | const isVerified = ctx.request.headers.get('x-secret-endpoint-token') === secretToken 17 | 18 | if (!isVerified) throw new GraphQLError('Not authorized') 19 | 20 | return true 21 | } 22 | 23 | export default secretPathPermission -------------------------------------------------------------------------------- /apps/home/icons/heart-outline-gradient.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /apps/home/icons/call-outline.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/home/public/locales/ar/find-ticket.json: -------------------------------------------------------------------------------- 1 | { 2 | "from": { 3 | "title": "من", 4 | "placeholder": "رحيل" 5 | }, 6 | "to": { 7 | "title": "الى", 8 | "placeholder": "وصول" 9 | }, 10 | "travelTime": { 11 | "title": "وقت السفر", 12 | "placeholder": "وقت المغادرة" 13 | }, 14 | "passengers": { 15 | "title": "ركاب", 16 | "adult": "بالغ", 17 | "adults": "راشدون", 18 | "senior": "أقدم", 19 | "seniors": "كبار السن", 20 | "child": "طفل", 21 | "children": "أطفال", 22 | "noPassengersSelected": "لم يتم اختيار أي ركاب", 23 | "age": "عمر", 24 | "ageUnder": "العمر تحت" 25 | }, 26 | "findARide": "ابحث عن رحلة" 27 | } 28 | -------------------------------------------------------------------------------- /apps/home/components/user/tickets/index.tsx: -------------------------------------------------------------------------------- 1 | import History from '@/components/user/tickets/history' 2 | 3 | import { useTranslation } from 'next-i18next' 4 | 5 | const UserTickets = () => { 6 | const { t } = useTranslation('user-ticket') 7 | return ( 8 |
9 |
10 |

11 | {t('History')} 12 |

13 |
14 | {/* @todo: calendar */} 15 |
16 |
17 | 18 |
19 | ) 20 | } 21 | 22 | export default UserTickets -------------------------------------------------------------------------------- /apps/home/graphql/admin/analytics/total-users.ts: -------------------------------------------------------------------------------- 1 | import graphqlFetcher from '@/graphql/graphql-fetcher' 2 | 3 | import useSWR from 'swr' 4 | 5 | const ANALYTICS_TOTAL_USERS_QUERY = /* GraphQL */ ` 6 | { 7 | analyticsTotalUsers { 8 | totalUsers 9 | totalSeniors 10 | totalAdults 11 | weeklyUsers 12 | weeklyUsersDiff 13 | monthlyUsers 14 | monthlyUsersDiff 15 | } 16 | } 17 | ` 18 | 19 | const useAnalyticsTotalUsers = () => { 20 | const result = useSWR( 21 | [ANALYTICS_TOTAL_USERS_QUERY], 22 | (queryStr: string) => graphqlFetcher(queryStr) 23 | ) 24 | 25 | return result 26 | } 27 | 28 | export default useAnalyticsTotalUsers -------------------------------------------------------------------------------- /apps/home/graphql/user/request-refund.ts: -------------------------------------------------------------------------------- 1 | import { Variables } from 'graphql-request' 2 | 3 | import mutate from '@/graphql/mutate' 4 | 5 | interface RequestRefundVariables extends Variables { 6 | id: string 7 | ticketType: { 8 | ticketType: 'TICKET' | 'SUBSCRIPTION' 9 | } 10 | } 11 | 12 | const REQUEST_REFUND_MUTATION = /* GraphQL */ ` 13 | mutation requestRefund($id: String!, $ticketType: TicketTypeEnumArg!) { 14 | requestRefund(id: $id, ticketType: $ticketType) 15 | } 16 | ` 17 | 18 | const requestRefundMutation = async (variables: RequestRefundVariables) => { 19 | return mutate(REQUEST_REFUND_MUTATION, variables) 20 | } 21 | 22 | export default requestRefundMutation -------------------------------------------------------------------------------- /apps/home/graphql/admin/pending-invitations.ts: -------------------------------------------------------------------------------- 1 | import graphqlFetcher from '@/graphql/graphql-fetcher' 2 | 3 | import useSWR from 'swr' 4 | 5 | const PENDING_INVITATIONS_QUERY = /* GraphQL */ ` 6 | query adminPendingInvitations { 7 | adminPendingInvitations { 8 | id 9 | name 10 | email 11 | role 12 | createdAt 13 | invitedBy { 14 | id 15 | name 16 | email 17 | } 18 | } 19 | } 20 | ` 21 | 22 | const usePendingInvitations = () => { 23 | const result = useSWR( 24 | [PENDING_INVITATIONS_QUERY], 25 | (queryStr: string) => graphqlFetcher(queryStr), 26 | ) 27 | 28 | return result 29 | } 30 | 31 | export default usePendingInvitations -------------------------------------------------------------------------------- /apps/home/components/tickets/hero/index.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react' 2 | 3 | import { Gradient } from '@/components/gradient' 4 | 5 | const Hero = () => { 6 | 7 | useEffect(() => { 8 | const gradient = new Gradient() 9 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 10 | // @ts-ignore 11 | gradient.initGradient('#gradient-canvas') 12 | }, []) 13 | 14 | return ( 15 |
18 | 22 |
23 | ) 24 | } 25 | 26 | export default Hero -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "build": "turbo run build", 5 | "dev": "turbo run dev", 6 | "lint": "turbo run lint", 7 | "format": "prettier --write \"**/*.{ts,tsx,md}\"" 8 | }, 9 | "devDependencies": { 10 | "@typescript-eslint/parser": "^5.0.1", 11 | "eslint": "^8.7.0", 12 | "eslint-config-custom": "*", 13 | "prettier": "^2.5.1", 14 | "turbo": "latest", 15 | "typescript": "^4.4.4" 16 | }, 17 | "name": "turbo-template-new", 18 | "packageManager": "yarn@1.22.19", 19 | "workspaces": [ 20 | "apps/*", 21 | "packages/*" 22 | ], 23 | "dependencies": { 24 | "@typescript-eslint/parser": "^5.0.1" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /apps/home/pages/admin/seniors-verification.tsx: -------------------------------------------------------------------------------- 1 | import type { NextPage } from 'next' 2 | 3 | import Verifications from '@/components/admin/verification' 4 | import AdminLayout from '@/layouts/admin' 5 | 6 | import { NextSeo } from 'next-seo' 7 | 8 | const VerficationPage: NextPage = () => { 9 | return ( 10 | 15 | 20 | 21 | 22 | ) 23 | } 24 | 25 | export default VerficationPage -------------------------------------------------------------------------------- /apps/home/public/locales/en/find-ticket.json: -------------------------------------------------------------------------------- 1 | { 2 | "from": { 3 | "title": "From", 4 | "placeholder": "Departure" 5 | }, 6 | "to": { 7 | "title": "To", 8 | "placeholder": "Destination" 9 | }, 10 | "travelTime": { 11 | "title": "Travel Time", 12 | "placeholder": "Departure Time" 13 | }, 14 | "passengers": { 15 | "title": "Passengers", 16 | "adult": "Adult", 17 | "adults": "Adults", 18 | "senior": "Senior", 19 | "seniors": "Seniors", 20 | "child": "Child", 21 | "children": "Children", 22 | "noPassengersSelected": "No Passengers Selected", 23 | "age": "Age", 24 | "ageUnder": "Age under" 25 | }, 26 | "findARide": "Find a Ride" 27 | } 28 | -------------------------------------------------------------------------------- /apps/home/graphql/stations/reorder-station.ts: -------------------------------------------------------------------------------- 1 | import { Variables } from 'graphql-request' 2 | 3 | import mutate from '@/graphql/mutate' 4 | 5 | interface ReorderStationVariables extends Variables { 6 | lineId: string 7 | stationId: string 8 | newPosition: number 9 | } 10 | 11 | const REORDER_QUERY = /* GraphQL */ ` 12 | mutation adminReorderStation($lineId: String!, $stationId: String!, $newPosition: Int!) { 13 | adminReorderStation(lineId: $lineId, stationId: $stationId, newPosition: $newPosition) 14 | } 15 | ` 16 | 17 | const adminReorderStationsMutation = (variables: ReorderStationVariables) => { 18 | return mutate(REORDER_QUERY, variables) 19 | } 20 | 21 | export default adminReorderStationsMutation -------------------------------------------------------------------------------- /apps/home/icons/hearing.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/graphql/src/types/payment-args.ts: -------------------------------------------------------------------------------- 1 | import { booleanArg, nonNull, stringArg } from 'nexus' 2 | 3 | import oneTimeInput from './one-time-input' 4 | import subscriptionEnumArg from './subscription-input' 5 | 6 | const createPaymentArgs = { 7 | cardNumber: nonNull(stringArg()), 8 | expiryMonth: nonNull(stringArg()), 9 | expiryYear: nonNull(stringArg()), 10 | cardCvc: nonNull(stringArg()), 11 | saveCard: nonNull(booleanArg()), 12 | isSubscription: nonNull(booleanArg()), 13 | } 14 | 15 | const paymentArgs = { 16 | ...createPaymentArgs, 17 | metadata: nonNull( 18 | (createPaymentArgs.isSubscription) 19 | ? subscriptionEnumArg 20 | : oneTimeInput 21 | ), 22 | } 23 | export default paymentArgs -------------------------------------------------------------------------------- /apps/home/public/locales/ar/user-ticket.json: -------------------------------------------------------------------------------- 1 | { 2 | "seo": { 3 | "title": "تذاكري", 4 | "description": "عرض التذاكر الخاصة بك وإدارة حجوزاتك" 5 | }, 6 | "ride": "الرحلة", 7 | "to": "الى", 8 | "price": "السعر", 9 | "History": "قائمة الرحلات", 10 | "NoHistory": "لا يوجد رحلات سابقة", 11 | "Date": "التاريخ", 12 | "at": "الساعة", 13 | "EGP": "ج.م", 14 | "adults": "كبارالسن", 15 | "child": "طفل", 16 | "children": "اطفال", 17 | "senior": "مسن", 18 | "seniors": "مسنين", 19 | "passengers": "الركاب", 20 | "noPassengers": "لا يوجد ركاب", 21 | "ReviewedAt": "تمت المراجعة في", 22 | "RequestedAt": "تم الطلب في", 23 | "Status": "الحالة", 24 | "Request": "طلب", 25 | "Refund": "استرجاع" 26 | } 27 | -------------------------------------------------------------------------------- /apps/home/icons/hourglass-outline.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/home/graphql/user/signup.ts: -------------------------------------------------------------------------------- 1 | import { Variables } from 'graphql-request' 2 | 3 | import mutate from '@/graphql/mutate' 4 | 5 | interface SignUpMutationVariables extends Variables { 6 | userRole: { 7 | userRole: 'ADULT' | 'SENIOR' 8 | } 9 | email: string 10 | name: string 11 | documentUrl?: string 12 | } 13 | 14 | const SIGNUP_MUTATION = /* GraphQL */ ` 15 | mutation signUp($userRole: UserRoleEnumArg!, $email: String!, $name: String!, $documentUrl: String) { 16 | signUp(userRole: $userRole, email: $email, name: $name, documentUrl: $documentUrl) 17 | } 18 | ` 19 | 20 | const signupMutation = async (variables: SignUpMutationVariables) => { 21 | return mutate(SIGNUP_MUTATION, variables) 22 | } 23 | 24 | export default signupMutation -------------------------------------------------------------------------------- /apps/home/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "theme_color": "#000", 3 | "background_color": "#fff", 4 | "display": "standalone", 5 | "scope": "/", 6 | "start_url": "/", 7 | "name": "Cairo Metro", 8 | "short_name": "Cairo Metro", 9 | "icons": [ 10 | { 11 | "src": "/icon-192x192.png", 12 | "sizes": "192x192", 13 | "type": "image/png" 14 | }, 15 | { 16 | "src": "/icon-256x256.png", 17 | "sizes": "256x256", 18 | "type": "image/png" 19 | }, 20 | { 21 | "src": "/icon-384x384.png", 22 | "sizes": "384x384", 23 | "type": "image/png" 24 | }, 25 | { 26 | "src": "/icon-512x512.png", 27 | "sizes": "512x512", 28 | "type": "image/png" 29 | } 30 | ] 31 | } -------------------------------------------------------------------------------- /apps/home/icons/calendar.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/home/graphql/admin/invite-teammate.ts: -------------------------------------------------------------------------------- 1 | import { Variables } from 'graphql-request' 2 | 3 | import mutate from '@/graphql/mutate' 4 | 5 | interface AdminInviteTeammateVariables extends Variables { 6 | email: string 7 | name: string 8 | role: { 9 | userRole: 'ADMIN' | 'CUSTOMER_SUPPORT' 10 | } 11 | } 12 | 13 | const ADMIN_INVITE_TEAMMATE_QUERY = /* GraphQL */ ` 14 | mutation adminInviteTeammate($email: String!, $name: String!, $role: UserRoleEnumArg!) { 15 | adminInviteTeammate(email: $email, name: $name, role: $role) 16 | } 17 | ` 18 | 19 | const adminInviteTeammateMutation = (variables: AdminInviteTeammateVariables) => { 20 | return mutate(ADMIN_INVITE_TEAMMATE_QUERY, variables) 21 | } 22 | 23 | export default adminInviteTeammateMutation -------------------------------------------------------------------------------- /apps/home/public/locales/en/user-ticket.json: -------------------------------------------------------------------------------- 1 | { 2 | "seo": { 3 | "title": "My Tickets", 4 | "description": "View your tickets and manage your bookings" 5 | }, 6 | "ride": "Ride", 7 | "to": "to", 8 | "price": "Price", 9 | "History": "Purchase History", 10 | "NoHistory": "No tickets found", 11 | "Date": "Date", 12 | "at": "at", 13 | "EGP": "EGP", 14 | "adults": "Adults", 15 | "child": "Child", 16 | "children": "Children", 17 | "senior": "Senior", 18 | "seniors": "Seniors", 19 | "passengers": "Passengers", 20 | "noPassengersSelected": "No passengers selected", 21 | "ReviewedAt": "Reviewed at", 22 | "RequestedAt": "Requested at", 23 | "Status": "Status", 24 | "Request": "Request", 25 | "Refund": "Refund" 26 | } 27 | -------------------------------------------------------------------------------- /apps/home/graphql/stations/station-by-id.ts: -------------------------------------------------------------------------------- 1 | import { Variables } from 'graphql-request' 2 | 3 | import graphqlFetcher from '@/graphql/graphql-fetcher' 4 | 5 | import useSWR from 'swr' 6 | 7 | interface StationByIdVariables extends Variables { 8 | id: string 9 | } 10 | 11 | export const STATION_BY_ID_QUERY = /* GraphQL */ ` 12 | query StationById($id: String!) { 13 | stationById(id: $id) { 14 | id 15 | name 16 | name_ar 17 | } 18 | } 19 | ` 20 | 21 | const useStationById = (variables: StationByIdVariables) => { 22 | const result = useSWR( 23 | [STATION_BY_ID_QUERY, variables], 24 | (queryStr: string) => graphqlFetcher(queryStr, variables), 25 | ) 26 | 27 | return result 28 | } 29 | 30 | export default useStationById -------------------------------------------------------------------------------- /apps/home/icons/calendar-outline.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/tsconfig/nextjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "display": "Next.js", 4 | "extends": "./base.json", 5 | "compilerOptions": { 6 | "target": "es5", 7 | "lib": ["dom", "dom.iterable", "esnext"], 8 | "allowJs": true, 9 | "skipLibCheck": true, 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "noEmit": true, 13 | "incremental": true, 14 | "esModuleInterop": true, 15 | "module": "esnext", 16 | "resolveJsonModule": true, 17 | "isolatedModules": true, 18 | "jsx": "preserve", 19 | "baseUrl": ".", 20 | "paths": { 21 | "@/*": ["./*"] 22 | } 23 | }, 24 | "include": ["src", "next-env.d.ts"], 25 | "exclude": ["node_modules"] 26 | } -------------------------------------------------------------------------------- /apps/home/lib/fetcher.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import Cookies from 'js-cookie' 3 | 4 | const DEFAULT_API_URL = process.env.NEXT_PUBLIC_API_URL 5 | 6 | const fetcher = (path: string) => { 7 | const isPathFullURL = path.startsWith('http') 8 | const isPathRelativeToDefault = path.startsWith('/') 9 | const url = isPathFullURL ? path : `${DEFAULT_API_URL}${isPathRelativeToDefault ? '' : '/'}${path}` 10 | 11 | const access = Cookies.get('access') 12 | if (access) axios.defaults.headers.common['x-auth-access'] = `Bearer ${access}` 13 | 14 | return ( 15 | axios 16 | .get(url, {}) 17 | .then((res) => res.data) 18 | .catch((err) => { 19 | throw err.response.data 20 | }) 21 | ) 22 | } 23 | 24 | export default fetcher 25 | -------------------------------------------------------------------------------- /apps/home/icons/accessible.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/home/icons/analytics-outline.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/graphql/src/types/station.ts: -------------------------------------------------------------------------------- 1 | import { objectType } from 'nexus' 2 | import { Station } from 'nexus-prisma' 3 | 4 | import convertLocationToLatLng from '../lib/convert-location-to-lat-lng' 5 | 6 | const StationType = objectType({ 7 | name: Station.$name, 8 | definition(t) { 9 | t.field(Station.id) 10 | t.field(Station.name) 11 | t.field(Station.name_ar) 12 | t.field(Station.location) 13 | t.field(Station.lines) 14 | t.field(Station.stationPositionInLine) 15 | t.field({ 16 | name: 'locationLngLat', 17 | type: 'LngLat', 18 | resolve: (station) => { 19 | const coordinates = station.location 20 | return convertLocationToLatLng(coordinates) 21 | }, 22 | }) 23 | }, 24 | }) 25 | 26 | 27 | export default StationType -------------------------------------------------------------------------------- /apps/graphql/src/lib/calculate-travel-duration.ts: -------------------------------------------------------------------------------- 1 | interface Location { 2 | lat: number 3 | lng: number 4 | } 5 | 6 | const metroSpeed: [number, number] = [30, 40] 7 | const calculateTravelDuration = (station1Location: Location, station2Location: Location, metroSpeedRange: [number, number] = metroSpeed) => { 8 | const speed = Math.floor(Math.random() * (metroSpeedRange[1] - metroSpeedRange[0] + 1) + metroSpeedRange[0]) 9 | 10 | // calculate distance between two stations in km 11 | const distance = Math.sqrt(Math.pow(station1Location.lat - station2Location.lat, 2) + Math.pow(station1Location.lng - station2Location.lng, 2)) * 111.2 12 | const duration = distance / speed 13 | 14 | // duration in minutes 15 | return duration * 60 16 | } 17 | 18 | export default calculateTravelDuration -------------------------------------------------------------------------------- /apps/graphql/src/utils/yoga.ts: -------------------------------------------------------------------------------- 1 | import { createYoga } from 'graphql-yoga' 2 | 3 | import { useDisableIntrospection } from '@graphql-yoga/plugin-disable-introspection' 4 | 5 | import { createContext } from '../context' 6 | 7 | import { isDev } from './is-dev' 8 | import schema from './schema' 9 | 10 | 11 | const yoga = createYoga({ 12 | schema, 13 | context: async (initialContext) => await createContext(initialContext), 14 | graphiql: isDev, 15 | plugins: [ 16 | useDisableIntrospection({ 17 | isDisabled: (request) =>{ 18 | const isIntrospectionSecretPresent = request.headers.get('x-allow-introspection') === process.env.introspectionSecret 19 | return isDev || isIntrospectionSecretPresent 20 | }, 21 | }), 22 | ], 23 | }) 24 | 25 | export default yoga -------------------------------------------------------------------------------- /apps/home/icons/accessibility-outline.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/home/layouts/authentication.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react' 2 | 3 | import AuthenticationNavigation from '@/components/authentication/navigation' 4 | 5 | import { useTranslation } from 'next-i18next' 6 | 7 | interface AuthenticationProps { 8 | children: ReactNode 9 | type: 'Login' | 'Register' 10 | } 11 | 12 | const AuthenticationLayout = ({ children, type }: AuthenticationProps) => { 13 | const { i18n } = useTranslation('common') 14 | 15 | return ( 16 |
20 | 21 |
{children}
22 |
23 | ) 24 | } 25 | 26 | export default AuthenticationLayout 27 | -------------------------------------------------------------------------------- /apps/home/graphql/admin/refunds/update-refund-request.ts: -------------------------------------------------------------------------------- 1 | import { Variables } from 'graphql-request' 2 | 3 | import mutate from '@/graphql/mutate' 4 | 5 | 6 | export interface UpdateRefundRequestVariables extends Variables { 7 | refundRequestId: string, 8 | status: { 9 | refundStatus: 'ACCEPTED' | 'REJECTED' 10 | } 11 | } 12 | 13 | const UPDATE_REFUND_REQUEST_MUTATION = /* GraphQL */ ` 14 | mutation adminUpdateRefundRequest($refundRequestId: String!, $status: RefundStatusEnumArg!) { 15 | adminUpdateRefundRequest(refundRequestId: $refundRequestId, status: $status) 16 | } 17 | ` 18 | 19 | const updateRefundRequestMutation = (variables: UpdateRefundRequestVariables) => { 20 | return mutate(UPDATE_REFUND_REQUEST_MUTATION, variables) 21 | } 22 | 23 | export default updateRefundRequestMutation -------------------------------------------------------------------------------- /apps/home/graphql/mutate.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLClient, Variables } from 'graphql-request' 2 | 3 | const DEFAULT_API_URL = process.env.NEXT_PUBLIC_API_URL ?? 'http://localhost:1111' 4 | 5 | const graphqlUrl = `${DEFAULT_API_URL}/graphql` 6 | 7 | const graphQLClient = new GraphQLClient(graphqlUrl, { 8 | credentials: 'include', 9 | mode: 'cors', 10 | }) 11 | 12 | const mutate = (mutation: string, variables?: Variables) => { 13 | const queryName = mutation.split('{')[1].split('(')[0].trim() 14 | 15 | return graphQLClient.request(mutation, variables) 16 | /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ 17 | .then((res: any) => { 18 | return res[queryName] 19 | }) 20 | .catch((err) => { 21 | throw err.response.errors 22 | }) 23 | } 24 | 25 | export default mutate -------------------------------------------------------------------------------- /apps/graphql/src/resolvers/mutations/migrations/create-main-admin-account.ts: -------------------------------------------------------------------------------- 1 | import { UserRole } from '@prisma/client' 2 | import { FieldResolver } from 'nexus' 3 | 4 | import secretPathPermission from '../../../permissions/secret-path' 5 | 6 | const secretCreateMainAdminAccount: FieldResolver<'Mutation', 'secretCreateMainAdminAccount'> = 7 | async (_, args, ctx) => { 8 | secretPathPermission(ctx) 9 | const { prisma } = ctx 10 | 11 | await prisma.user.upsert({ 12 | where: { 13 | email: process.env.MAIN_ADMIN_EMAIL, 14 | }, 15 | update: {}, 16 | create: { 17 | email: process.env.MAIN_ADMIN_EMAIL, 18 | name: 'Admin', 19 | role: UserRole.ADMIN, 20 | }, 21 | }) 22 | 23 | return true 24 | } 25 | 26 | export default secretCreateMainAdminAccount -------------------------------------------------------------------------------- /apps/home/icons/train.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/home/graphql/recommendations.ts: -------------------------------------------------------------------------------- 1 | import graphqlFetcher from '@/graphql/graphql-fetcher' 2 | 3 | import useSWR from 'swr' 4 | 5 | const USER_QUERY = /* GraphQL */ ` 6 | { 7 | recommendations{ 8 | from { 9 | id 10 | name 11 | name_ar 12 | } 13 | to { 14 | id 15 | name 16 | name_ar 17 | } 18 | noOfStationsOnPath 19 | price 20 | schedule{ 21 | departureTime 22 | arrivalTime 23 | } 24 | } 25 | } 26 | ` 27 | 28 | const useRecommendations = () => { 29 | const result = useSWR( 30 | [USER_QUERY], 31 | (queryStr: string) => graphqlFetcher(queryStr), 32 | { 33 | revalidateIfStale: false, 34 | revalidateOnFocus: false, 35 | revalidateOnReconnect: false, 36 | } 37 | ) 38 | 39 | return result 40 | } 41 | 42 | export default useRecommendations -------------------------------------------------------------------------------- /apps/home/graphql/get-price.ts: -------------------------------------------------------------------------------- 1 | import { Variables } from 'graphql-request' 2 | 3 | import graphqlFetcher from '@/graphql/graphql-fetcher' 4 | 5 | import useSWR from 'swr' 6 | 7 | interface PriceVariables extends Variables { 8 | from: string 9 | to: string 10 | passengers: { 11 | seniors: number 12 | adults: number 13 | children: number 14 | } 15 | } 16 | 17 | const GET_PRICE_QUERY = /* GraphQL */ ` 18 | query getPrice($from: String!, $to: String!, $passengers: passengersInputType!) { 19 | getPrice(from: $from, to: $to, passengers: $passengers) 20 | } 21 | ` 22 | 23 | const useGetPrice = (variables: PriceVariables) => { 24 | const result = useSWR( 25 | [GET_PRICE_QUERY, variables], 26 | (queryStr: string) => graphqlFetcher(queryStr, variables), 27 | ) 28 | 29 | return result 30 | } 31 | 32 | export default useGetPrice -------------------------------------------------------------------------------- /apps/home/components/authentication/login/email-view.tsx: -------------------------------------------------------------------------------- 1 | import LoginForm from '@/components/authentication/login/login-form' 2 | import LoginScreenAnimation from '@/components/authentication/login/login-screen-animation' 3 | 4 | import { motion } from 'framer-motion' 5 | 6 | interface EmailViewProps { 7 | nextView: (email: string)=> void 8 | } 9 | 10 | const EmailView = ({ nextView }: EmailViewProps) => { 11 | return ( 12 | 20 | 21 | 22 | 23 | ) 24 | } 25 | 26 | export default EmailView -------------------------------------------------------------------------------- /apps/home/icons/qr-code.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/graphql/src/resolvers/queries/admin/analytics-average-response.ts: -------------------------------------------------------------------------------- 1 | import { RefundStatus } from '@prisma/client' 2 | import { FieldResolver } from 'nexus/src/typegenTypeHelpers' 3 | 4 | import { Context } from '../../../context' 5 | import adminPermission from '../../../permissions/admin' 6 | 7 | const analyticsTotalSoldTickets: FieldResolver<'Query', 'analyticsAverageCustomerSupportResponse'> = 8 | async (_, _args, ctx: Context) => { 9 | adminPermission(ctx) 10 | 11 | const { prisma } = ctx 12 | 13 | const refundedTickets = await prisma.refund.count({ 14 | where: { 15 | status: RefundStatus.ACCEPTED, 16 | }, 17 | }) 18 | 19 | const totalSoldTickets = await prisma.userTickets.count() 20 | 21 | const total = totalSoldTickets - refundedTickets 22 | 23 | return total 24 | } 25 | 26 | export default analyticsTotalSoldTickets -------------------------------------------------------------------------------- /apps/home/graphql/stations/stations.ts: -------------------------------------------------------------------------------- 1 | import graphqlFetcher from '@/graphql/graphql-fetcher' 2 | 3 | import useSWR from 'swr' 4 | 5 | const STATIONS_QUERY = /* GraphQL */ ` 6 | { 7 | stations { 8 | id 9 | name 10 | name_ar 11 | location 12 | lines { 13 | id 14 | name 15 | name_ar 16 | color 17 | } 18 | stationPositionInLine { 19 | position 20 | line { 21 | id 22 | name 23 | name_ar 24 | color 25 | } 26 | } 27 | locationLngLat { 28 | lng 29 | lat 30 | } 31 | } 32 | } 33 | ` 34 | 35 | const useStations = () => { 36 | const result = useSWR( 37 | [STATIONS_QUERY], 38 | (queryStr: string) => graphqlFetcher(queryStr), 39 | ) 40 | 41 | return result 42 | } 43 | 44 | export default useStations -------------------------------------------------------------------------------- /apps/home/layouts/help-layout.tsx: -------------------------------------------------------------------------------- 1 | import Hero from '@/components/help/hero' 2 | import HelpNavigation from '@/components/help/navigation' 3 | 4 | import cn from 'classnames' 5 | 6 | interface HelpLayoutProps { 7 | headerChildren?: React.ReactNode 8 | children: React.ReactNode 9 | contentBackground?: `bg-${string}` 10 | } 11 | 12 | const HelpLayout = ({ headerChildren, children, contentBackground }: HelpLayoutProps) => { 13 | return ( 14 |
15 | 16 | 17 | {headerChildren} 18 | 19 |
20 | {children} 21 |
22 |
23 | ) 24 | } 25 | 26 | export default HelpLayout -------------------------------------------------------------------------------- /apps/home/graphql/admin/verifications/update-verification-request.ts: -------------------------------------------------------------------------------- 1 | import { Variables } from 'graphql-request' 2 | 3 | import mutate from '@/graphql/mutate' 4 | 5 | export interface UpdateVerificationRequestVariables extends Variables { 6 | userId: string, 7 | documentVerified: { 8 | verificationstatus: 'ACCEPTED' | 'REJECTED' | 'PENDING' 9 | } 10 | } 11 | 12 | const UPDATE_VERIFICATION_REQUEST_MUTATION = /* GraphQL */ ` 13 | mutation adminUpdateVerificationRequest($userId: String!, $documentVerified: VerificationStatusEnumArg!) { 14 | adminUpdateVerificationRequest(userId: $userId, documentVerified: $documentVerified) 15 | } 16 | ` 17 | 18 | const updateVerificationRequestMutation = (variables: UpdateVerificationRequestVariables) => { 19 | return mutate(UPDATE_VERIFICATION_REQUEST_MUTATION, variables) 20 | } 21 | 22 | export default updateVerificationRequestMutation -------------------------------------------------------------------------------- /apps/home/graphql/get-invitation.ts: -------------------------------------------------------------------------------- 1 | import { Variables } from 'graphql-request' 2 | 3 | import graphqlFetcher from '@/graphql/graphql-fetcher' 4 | 5 | import useSWR from 'swr' 6 | 7 | interface InvitationVariables extends Variables { 8 | token: string 9 | } 10 | 11 | const GET_INVITATION_QUERY = /* GraphQL */ ` 12 | query getInvitation($token: String!) { 13 | getInvitation(token: $token) { 14 | id 15 | name 16 | email 17 | role 18 | createdAt 19 | invitedBy { 20 | id 21 | name 22 | email 23 | } 24 | } 25 | } 26 | ` 27 | 28 | const useGetInvitation = (variables: InvitationVariables) => { 29 | const result = useSWR( 30 | [GET_INVITATION_QUERY, variables], 31 | (queryStr: string) => graphqlFetcher(queryStr, variables), 32 | ) 33 | 34 | return result 35 | } 36 | 37 | export default useGetInvitation -------------------------------------------------------------------------------- /apps/home/lib/use-window-size.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react' 2 | 3 | const useWindowSize = () => { 4 | const [windowSize, setWindowSize] = useState({ 5 | width: 0, 6 | height: 0, 7 | }) 8 | 9 | useEffect(() => { 10 | const handleResize = () => { 11 | setWindowSize({ 12 | width: window.innerWidth, 13 | height: window.innerHeight, 14 | }) 15 | } 16 | 17 | handleResize() 18 | 19 | window.addEventListener('resize', handleResize) 20 | 21 | return () => window.removeEventListener('resize', handleResize) 22 | }, []) 23 | 24 | return { 25 | width: windowSize.width, 26 | height: windowSize.height, 27 | isMobile: windowSize.width < 640, 28 | isTablet: windowSize.width >= 640 && windowSize.width < 1024, 29 | isDesktop: windowSize.width >= 1024, 30 | } 31 | } 32 | 33 | export default useWindowSize -------------------------------------------------------------------------------- /apps/home/components/help/contact-us.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link' 2 | 3 | import { buttonVariants } from '@/components/button' 4 | 5 | import cn from 'classnames' 6 | 7 | const ContactUs = () => { 8 | return ( 9 |
10 |

11 | Didn{"'"}t find what you were looking for? 12 |

13 |

14 | Contact our support team in live chat 15 |

16 | 23 | Contact Us 24 | 25 |
26 | ) 27 | } 28 | 29 | export default ContactUs -------------------------------------------------------------------------------- /apps/home/components/or-separator.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react' 2 | 3 | interface OrSeparatorProps { 4 | children: ReactNode 5 | } 6 | 7 | const OrSeparator = ({ children }: OrSeparatorProps) => { 8 | return ( 9 |
10 |

11 | {children} 12 |

13 | 24 | ) 25 | } 26 | 27 | export default OrSeparator -------------------------------------------------------------------------------- /apps/home/pages/tickets/[from]/[to]/[departure]/index.tsx: -------------------------------------------------------------------------------- 1 | import TicketDetails from '@/components/tickets/details' 2 | import AppLayout from '@/layouts/app' 3 | 4 | import { serverSideTranslations } from 'next-i18next/serverSideTranslations' 5 | 6 | const TicketDetailsPage = ()=>{ 7 | return ( 8 | 14 | 15 | 16 | ) 17 | } 18 | 19 | export const getStaticProps = async ({ locale }: { locale: string }) => ({ 20 | props: { 21 | ...(await serverSideTranslations(locale, ['common', 22 | 'tickets-details', 23 | 'find-ticket', 24 | 'purchase'])), 25 | }, 26 | }) 27 | 28 | export const getStaticPaths = async () => ({ 29 | paths: [], 30 | fallback: true, 31 | }) 32 | 33 | export default TicketDetailsPage -------------------------------------------------------------------------------- /apps/home/public/locales/en/login.json: -------------------------------------------------------------------------------- 1 | { 2 | "seo": { 3 | "title": "Welcome Back to Cairo Metro - Sign In for Seamless Travel", 4 | "description": "Access your Cairo Metro Web App account and continue your journey with ease. Sign in to view your routes, saved destinations, and personalized preferences. Start your Cairo adventures right where you left off, effortlessly!" 5 | }, 6 | "login": "Login", 7 | "email": "Email Address", 8 | "placeholder": "Enter an email address", 9 | "or": "OR", 10 | "signupInstead": { 11 | "question": "You don’t have an account yet? {0}", 12 | "signup": "Sign up" 13 | }, 14 | "userNotFound": "User not found, please sign up", 15 | "invalidEmail": "Invalid email address", 16 | "somethingWentWrong": "Something went wrong, please try again", 17 | "errors": { 18 | "invalidLink": "Magic link is invalid or expired" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /apps/graphql/src/lib/get-saved-card.ts: -------------------------------------------------------------------------------- 1 | import { Context } from '../context' 2 | 3 | import decrypt from './decrypt' 4 | 5 | const getSavedCard = async (cardId: string, ctx: Context) => { 6 | const { prisma, user } = ctx 7 | 8 | const card = await prisma.userCreditCard.findUnique({ 9 | where: { 10 | id: cardId, 11 | }, 12 | }) 13 | 14 | if (!card) { 15 | throw new Error('Card not found') 16 | } 17 | 18 | if (card.userId !== user?.id) { 19 | throw new Error('Card not found') 20 | } 21 | 22 | const cardNumber = decrypt(card.cardNumber) 23 | const expiryMonth = card.expiryMonth 24 | const expiryYear = card.expiryYear 25 | const cardCvc = decrypt(card.cardCvc) 26 | 27 | return { 28 | number: cardNumber, 29 | exp_month: expiryMonth, 30 | exp_year: expiryYear, 31 | cvc: cardCvc, 32 | } 33 | } 34 | 35 | export default getSavedCard -------------------------------------------------------------------------------- /apps/home/layouts/app.tsx: -------------------------------------------------------------------------------- 1 | import Footer from '@/components/footer' 2 | import Navigation, { NavigationProps } from '@/components/navigation' 3 | 4 | import { useTranslation } from 'next-i18next' 5 | 6 | interface AppLayoutProps { 7 | children: React.ReactNode 8 | navigation: NavigationProps 9 | } 10 | 11 | const AppLayout = ({ children, navigation }: AppLayoutProps) => { 12 | const { i18n } = useTranslation('common') 13 | 14 | return ( 15 |
19 | 23 |
24 | {children} 25 |
26 |
27 |
28 | ) 29 | } 30 | 31 | export default AppLayout -------------------------------------------------------------------------------- /apps/home/public/locales/ar/login.json: -------------------------------------------------------------------------------- 1 | { 2 | "seo": { 3 | "title": "مرحبًا بك مرة أخرى في مترو القاهرة - قم بتسجيل الدخول لسفر سلس", 4 | "description": "قم بالوصول إلى حساب تطبيق الويب الخاص بمترو القاهرة واستمر في رحلتك بكل سهولة. قم بتسجيل الدخول لعرض مساراتك ووجهاتك المحفوظة والتفضيلات الشخصية. ابدأ مغامراتك في القاهرة من حيث توقفت، دون عناء!" 5 | }, 6 | "login": "تسجيل الدخول", 7 | "email": "عنوان البريد الإلكتروني", 8 | "placeholder": "أدخل عنوان البريد الإلكتروني", 9 | "or": "أو", 10 | "signupInstead": { 11 | "question": "ليس لديك حساب حتى الآن؟ {0}", 12 | "signup": "التسجيل" 13 | }, 14 | "userNotFound": "لم يتم العثور على المستخدم ، يرجى التسجيل", 15 | "invalidEmail": "عنوان البريد الإلكتروني غير صالح", 16 | "somethingWentWrong": "حدث خطأ ما، يرجى المحاولة مرة أخرى", 17 | "errors": { 18 | "invalidLink": "الرابط غير صالح أو منتهي الصلاحية" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /apps/home/components/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | import cn from 'classnames' 4 | 5 | export type InputProps = React.InputHTMLAttributes 6 | 7 | const Input = React.forwardRef( 8 | ({ className, type, ...props }, ref) => { 9 | return ( 10 | 19 | ) 20 | } 21 | ) 22 | 23 | Input.displayName = 'Input' 24 | 25 | export default Input -------------------------------------------------------------------------------- /apps/home/icons/visa-effect.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /apps/home/icons/ticket-outline.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/graphql/src/lib/refund-subscription.ts: -------------------------------------------------------------------------------- 1 | import Stripe from 'stripe' 2 | 3 | export const refundSubscription = async (subscription: Stripe.Subscription, stripe: Stripe) => { 4 | if (subscription && subscription.latest_invoice){ 5 | const currentPeriodStart = subscription.current_period_start * 1000 6 | const currentPeriodEnd = subscription.current_period_end * 1000 7 | const paymentIntent = subscription.latest_invoice.payment_intent 8 | const { amount, id } = paymentIntent 9 | 10 | 11 | const remainingPeriod = currentPeriodEnd - Date.now() 12 | const totalPeriod = currentPeriodEnd - currentPeriodStart 13 | 14 | const remainingAmount = (amount * remainingPeriod) / totalPeriod 15 | const refundAmount = Math.round(remainingAmount) 16 | 17 | await stripe.refunds.create({ 18 | payment_intent: id, 19 | amount: refundAmount, 20 | }) 21 | } 22 | 23 | return 24 | } -------------------------------------------------------------------------------- /apps/home/icons/timer-outline-gradient.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 13 | -------------------------------------------------------------------------------- /apps/home/graphql/stations/ride-route.ts: -------------------------------------------------------------------------------- 1 | import { Variables } from 'graphql-request' 2 | 3 | import graphqlFetcher from '@/graphql/graphql-fetcher' 4 | 5 | import useSWR from 'swr' 6 | 7 | interface RideRoute extends Variables { 8 | from: string 9 | to: string 10 | date: string 11 | } 12 | 13 | const RIDE_ROUTE_QUERY = /* GraphQL */ ` 14 | query rideRouteByDate($from: String!, $to: String!, $date: String!) { 15 | rideRouteByDate(from: $from, to: $to, date: $date) { 16 | station { 17 | id 18 | name 19 | name_ar 20 | lines { 21 | id 22 | } 23 | } 24 | time 25 | } 26 | } 27 | ` 28 | 29 | const useRideRoute = (variables: RideRoute) => { 30 | const result = useSWR( 31 | [RIDE_ROUTE_QUERY, variables], 32 | (queryStr: string) => graphqlFetcher(queryStr, variables), 33 | ) 34 | 35 | return result 36 | } 37 | 38 | export default useRideRoute -------------------------------------------------------------------------------- /apps/graphql/src/lib/convert-location-to-lat-lng.ts: -------------------------------------------------------------------------------- 1 | const extractCoordinates = (coordinate: string) => { 2 | const regex = /(\d+)°(\d+)′(\d+)″/ 3 | const match = regex.exec(coordinate) 4 | 5 | if (match) { 6 | const deg = parseInt(match[1]) 7 | const min = parseInt(match[2]) 8 | const sec = parseInt(match[3]) 9 | 10 | return { deg, min, sec } 11 | } 12 | 13 | return { deg: 0, min: 0, sec: 0 } 14 | } 15 | 16 | const convertLocationToLatLng = (location: string) => { 17 | const [lat, lng] = location.split(' ') 18 | 19 | const { deg: latDeg, min: latMin, sec: latSec } = extractCoordinates(lat) 20 | const { deg: lngDeg, min: lngMin, sec: lngSec } = extractCoordinates(lng) 21 | 22 | const latNum = latDeg + latMin / 60 + latSec / 3600 23 | const lngNum = lngDeg + lngMin / 60 + lngSec / 3600 24 | 25 | return { 26 | lat: latNum, 27 | lng: lngNum, 28 | } 29 | } 30 | 31 | export default convertLocationToLatLng -------------------------------------------------------------------------------- /apps/home/graphql/graphql-fetcher.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLClient, Variables } from 'graphql-request' 2 | 3 | const DEFAULT_API_URL = process.env.NEXT_PUBLIC_API_URL ?? 'http://localhost:1111' 4 | 5 | const graphqlUrl = `${DEFAULT_API_URL}/graphql` 6 | 7 | const graphQLClient = new GraphQLClient(graphqlUrl, { 8 | credentials: 'include', 9 | mode: 'cors', 10 | }) 11 | 12 | const graphqlFetcher = (query: string | string[], variables?: Variables) => { 13 | const queryString = Array.isArray(query) ? query[0] : query 14 | const queryName = queryString.split('{')[1].split('(')[0].split('}')[0].replace('\n', '').trim() 15 | 16 | return graphQLClient.request(queryString, variables) 17 | /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ 18 | .then((res: any) => { 19 | return res[queryName] 20 | }) 21 | .catch((err) => { 22 | throw err.response.errors 23 | }) 24 | } 25 | 26 | export default graphqlFetcher -------------------------------------------------------------------------------- /apps/home/public/locales/ar/user-subscriptions.json: -------------------------------------------------------------------------------- 1 | { 2 | "seo": { 3 | "title": "اشتراكاتي", 4 | "description": "عرض اشتراكاتك وإدارتها" 5 | }, 6 | "mySubscriptions": "اشتراكاتي", 7 | "ONE_AREA": "منطقة واحدة", 8 | "TWO_AREAS": "منطقتان", 9 | "THREE_AREAS": "ثلاث مناطق", 10 | "MONTHLY": "شهري", 11 | "QUARTERLY": "ربع سنوي", 12 | "YEARLY": "السنويه", 13 | "subscription": "اكتتاب", 14 | "description": "استمتع بالاستكشاف والتنقل في شوارع القاهرة مع اشتراكك الشهري في ما يصل إلى {0} محطة مجانا.", 15 | "refund": "تم طلب استرداد الأموال لهذا الاشتراك في {0} وتتم مراجعته حاليا من قبل المسؤول", 16 | "expire": "ستنتهي صلاحية اشتراكك في {0}", 17 | "cancel": "إلغاء الاشتراك", 18 | "ticketsCovered": "التذاكر التي يغطيها اشتراكك", 19 | "subscriptionHistory": "سجل اشتراكاتك", 20 | "subscribedAt": "تم الاشتراك في", 21 | "expirationDate": "تاريخ انتهاء الصلاحية", 22 | "noSubscriptionsFound": "لم يتم العثور على اشتراكات" 23 | } -------------------------------------------------------------------------------- /apps/graphql/src/lib/save-user-card.ts: -------------------------------------------------------------------------------- 1 | import { Context } from '../context' 2 | 3 | import encrypt from './encrypt' 4 | 5 | interface Card { 6 | cardNumber: string 7 | expiryMonth: string 8 | expiryYear: string 9 | holderName: string 10 | cardCvc: string 11 | } 12 | 13 | const saveUserCard = async (card: Card, ctx: Context) => { 14 | const { prisma, user } = ctx 15 | 16 | if (!user) { 17 | return 18 | } 19 | 20 | const cardNumber = encrypt(card.cardNumber) 21 | const cardCvc = encrypt(card.cardCvc) 22 | const cardHolder = encrypt(card.holderName ?? '') 23 | 24 | await prisma.userCreditCard.create({ 25 | data: { 26 | cardNumber, 27 | expiryMonth: card.expiryMonth, 28 | expiryYear: card.expiryYear, 29 | cardHolder, 30 | cardCvc, 31 | user: { 32 | connect: { 33 | id: user.id, 34 | }, 35 | }, 36 | }, 37 | }) 38 | } 39 | 40 | export default saveUserCard -------------------------------------------------------------------------------- /apps/graphql/src/resolvers/queries/admin/analytics-sold-tickets.ts: -------------------------------------------------------------------------------- 1 | import { RefundStatus } from '@prisma/client' 2 | import { FieldResolver } from 'nexus/src/typegenTypeHelpers' 3 | 4 | import { Context } from '../../../context' 5 | import adminPermission from '../../../permissions/admin' 6 | 7 | const analyticsSoldTickets: FieldResolver<'Query', 'analyticsSoldTickets'> = 8 | async (_, _args, ctx: Context) => { 9 | adminPermission(ctx) 10 | 11 | const { prisma } = ctx 12 | 13 | const refundsPromise = prisma.refund.aggregate({ 14 | where: { 15 | status: RefundStatus.ACCEPTED, 16 | }, 17 | _count: true, 18 | }) 19 | 20 | const ticketsPromise = prisma.userTickets.aggregate({ 21 | _count: true, 22 | }) 23 | 24 | const [refunds, tickets] = await Promise.all([refundsPromise, ticketsPromise]) 25 | 26 | return tickets._count - refunds._count 27 | } 28 | 29 | export default analyticsSoldTickets -------------------------------------------------------------------------------- /apps/home/icons/ticket.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/graphql/src/resolvers/queries/admin/pending-invitations.ts: -------------------------------------------------------------------------------- 1 | import { User } from '@prisma/client' 2 | import { FieldResolver } from 'nexus' 3 | 4 | import adminPermission from '../../../permissions/admin' 5 | 6 | const pendingInvitations: FieldResolver<'Query', 'pendingInvitations'> = async ( 7 | _, args, ctx, 8 | ) => { 9 | adminPermission(ctx) 10 | 11 | const { prisma } = ctx 12 | 13 | const invitations = await prisma.staffInvitation.findMany({ 14 | include: { 15 | invitedBy: true, 16 | }, 17 | }) 18 | 19 | // sort by role, created at, and name 20 | invitations.sort((a: User, b: User) => { 21 | if (a.role === b.role) { 22 | if (a.createdAt === b.createdAt) { 23 | return a.name.localeCompare(b.name) 24 | } 25 | 26 | return a.createdAt > b.createdAt ? -1 : 1 27 | } 28 | 29 | return a.role > b.role ? -1 : 1 30 | }) 31 | 32 | return invitations 33 | } 34 | 35 | export default pendingInvitations -------------------------------------------------------------------------------- /apps/graphql/src/lib/otp.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient, User } from '@prisma/client' 2 | import date from 'date-and-time' 3 | import otpGenerator from 'otp-generator' 4 | 5 | const generateOTP = async (user: {id: string} & Partial, prisma: PrismaClient) => { 6 | let otpCode, existingOtp 7 | 8 | // make sure the otp code is unique 9 | do { 10 | otpCode = otpGenerator.generate(4, { lowerCaseAlphabets: false, upperCaseAlphabets: false, specialChars: false }) 11 | // eslint-disable-next-line no-await-in-loop 12 | existingOtp = await prisma.otp.findUnique({ 13 | where: { code: otpCode }, 14 | }) 15 | } while (existingOtp !== null) 16 | 17 | const now = new Date() 18 | 19 | const otp = await prisma.otp.create({ 20 | data: { 21 | code: otpCode, 22 | createdAt: now, 23 | expiryDate: date.addMinutes(now, 30), 24 | userId: user.id, 25 | }, 26 | 27 | }) 28 | 29 | return otp 30 | } 31 | 32 | export default generateOTP -------------------------------------------------------------------------------- /apps/graphql/src/resolvers/queries/admin/team-members.ts: -------------------------------------------------------------------------------- 1 | import { User, UserRole } from '@prisma/client' 2 | import { FieldResolver } from 'nexus' 3 | 4 | import adminPermission from '../../../permissions/admin' 5 | 6 | const teamMembers: FieldResolver<'Query', 'teamMembers'> = async ( 7 | _, args, ctx, 8 | ) => { 9 | adminPermission(ctx) 10 | 11 | const { prisma } = ctx 12 | 13 | const users = await prisma.user.findMany({ 14 | where: { 15 | role: { 16 | in: [UserRole.ADMIN, UserRole.CUSTOMER_SUPPORT], 17 | }, 18 | }, 19 | }) 20 | 21 | // sort by role, created at, and name 22 | users.sort((a: User, b: User) => { 23 | if (a.role === b.role) { 24 | if (a.createdAt === b.createdAt) { 25 | return a.name.localeCompare(b.name) 26 | } 27 | 28 | return a.createdAt > b.createdAt ? -1 : 1 29 | } 30 | 31 | return a.role > b.role ? -1 : 1 32 | }) 33 | 34 | return users 35 | } 36 | 37 | export default teamMembers -------------------------------------------------------------------------------- /apps/graphql/src/lib/convert-lat-lng-to-location.ts: -------------------------------------------------------------------------------- 1 | const convertLatLngToLocation = (latLng: { lat: number; lng: number }) => { 2 | const latDirection = latLng.lat >= 0 ? 'N' : 'S' 3 | const lonDirection = latLng.lng >= 0 ? 'E' : 'W' 4 | 5 | const latDegrees = Math.abs(latLng.lat) 6 | const lonDegrees = Math.abs(latLng.lng) 7 | 8 | const latHours = Math.floor(latDegrees) 9 | const lonHours = Math.floor(lonDegrees) 10 | 11 | const latMinutesFloat = (latDegrees - latHours) * 60 12 | const lonMinutesFloat = (lonDegrees - lonHours) * 60 13 | 14 | const latMinutes = Math.floor(latMinutesFloat) 15 | const lonMinutes = Math.floor(lonMinutesFloat) 16 | 17 | const latSeconds = Math.round((latMinutesFloat - latMinutes) * 60) 18 | const lonSeconds = Math.round((lonMinutesFloat - lonMinutes) * 60) 19 | 20 | return `${latHours}°${latMinutes}′${latSeconds}″${latDirection} ${lonHours}°${lonMinutes}′${lonSeconds}″${lonDirection}` 21 | } 22 | 23 | export default convertLatLngToLocation -------------------------------------------------------------------------------- /apps/graphql/src/resolvers/mutations/admin-remove-teammate.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLError } from 'graphql/error' 2 | 3 | import { FieldResolver } from 'nexus' 4 | 5 | import { Context } from '../../context' 6 | 7 | const adminRemoveTeammate: FieldResolver<'Mutation', 'adminRemoveTeammate'> = 8 | async (_, args, ctx: Context) => { 9 | const { prisma } = ctx 10 | const { email } = args 11 | const mainAdminEmail = process.env.MAIN_ADMIN_EMAIL 12 | 13 | if (email === mainAdminEmail) { 14 | throw new GraphQLError('You cannot remove the main admin') 15 | } 16 | 17 | const userByEmail = await prisma.user.findUnique({ 18 | where: { 19 | email, 20 | }, 21 | }) 22 | 23 | if (!userByEmail) { 24 | throw new GraphQLError('User not found') 25 | } 26 | 27 | await prisma.user.delete({ 28 | where: { 29 | email, 30 | }, 31 | }) 32 | 33 | return true 34 | } 35 | 36 | export default adminRemoveTeammate -------------------------------------------------------------------------------- /apps/graphql/src/types/user-ticket.ts: -------------------------------------------------------------------------------- 1 | import { objectType } from 'nexus' 2 | import { UserTickets } from 'nexus-prisma' 3 | 4 | import RefundType from './refund' 5 | 6 | const UserTicketType = objectType({ 7 | name: UserTickets.$name, 8 | definition(t) { 9 | t.field(UserTickets.id) 10 | t.field(UserTickets.user) 11 | t.field(UserTickets.userId) 12 | t.field(UserTickets.from) 13 | t.field(UserTickets.to) 14 | t.field(UserTickets.price) 15 | t.field(UserTickets.date) 16 | t.field(UserTickets.adults) 17 | t.field(UserTickets.seniors) 18 | t.field(UserTickets.children) 19 | 20 | t.field('refundRequest', { 21 | type: RefundType, 22 | resolve: async (parent, _, ctx) => { 23 | const refund = await ctx.prisma.refund.findFirst({ 24 | where: { 25 | referenceId: parent.id, 26 | }, 27 | }) 28 | 29 | return refund 30 | }, 31 | }) 32 | }, 33 | }) 34 | 35 | 36 | export default UserTicketType -------------------------------------------------------------------------------- /apps/home/components/window-size-wrapper.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | import React from 'react' 3 | 4 | import useWindowSize from '@/lib/use-window-size' 5 | 6 | interface WindowSizeWrapperProps { 7 | MobileComponent: React.ComponentType 8 | TabletComponent?: React.ComponentType 9 | DesktopComponent: React.ComponentType 10 | } 11 | 12 | const WindowSizeWrapper = ({ 13 | MobileComponent, 14 | TabletComponent, 15 | DesktopComponent, 16 | }: WindowSizeWrapperProps) => { 17 | const ResizerComponent = (props: any) => { 18 | const { isMobile, isTablet } = useWindowSize() 19 | 20 | if (isMobile && MobileComponent) { 21 | return 22 | } 23 | 24 | if ((isMobile || isTablet) && TabletComponent) { 25 | return 26 | } 27 | 28 | return 29 | } 30 | 31 | return ResizerComponent 32 | } 33 | 34 | export default WindowSizeWrapper -------------------------------------------------------------------------------- /apps/home/graphql/stations/update-station.ts: -------------------------------------------------------------------------------- 1 | import { Variables } from 'graphql-request' 2 | 3 | import mutate from '@/graphql/mutate' 4 | 5 | export interface UpdateStationVariables extends Variables { 6 | stationId: string 7 | name?: string 8 | name_ar?: string 9 | location?: { 10 | lng: number 11 | lat: number 12 | }, 13 | lineIds?: string[] 14 | } 15 | 16 | const UPDATE_STATION_QUERY = /* GraphQL */ ` 17 | mutation adminUpdateStation( 18 | $stationId: String! 19 | $name: String 20 | $name_ar: String 21 | $locationLngLat: LngLatInput 22 | $lineIds: [String!] 23 | ) { 24 | adminUpdateStation( 25 | stationId: $stationId 26 | name: $name 27 | name_ar: $name_ar 28 | locationLngLat: $locationLngLat 29 | lineIds: $lineIds 30 | ) 31 | } 32 | ` 33 | 34 | const adminUpdateStationMutation = (variables: UpdateStationVariables) => { 35 | return mutate(UPDATE_STATION_QUERY, variables) 36 | } 37 | 38 | export default adminUpdateStationMutation -------------------------------------------------------------------------------- /apps/graphql/src/types/user.ts: -------------------------------------------------------------------------------- 1 | import { objectType } from 'nexus' 2 | import { User } from 'nexus-prisma' 3 | 4 | import Subscription from './subscription' 5 | import UserRoleEnum from './user-role' 6 | 7 | 8 | const UserType = objectType({ 9 | name: User.$name, 10 | definition(t) { 11 | t.field(User.id) 12 | t.field('role', { type: UserRoleEnum }) 13 | t.field(User.email) 14 | t.field(User.name) 15 | t.field(User.createdAt) 16 | t.field(User.documentUrl) 17 | t.field(User.documentVerified) 18 | t.field(User.picture) 19 | 20 | t.field('subscription', { 21 | type: Subscription, 22 | resolve: async (parent, _, ctx) => { 23 | const subscription = await ctx.prisma.subscriptions.findFirst({ 24 | where: { 25 | userId: parent.id, 26 | }, 27 | orderBy: { 28 | createdAt: 'desc', 29 | }, 30 | }) 31 | 32 | return subscription 33 | }, 34 | }) 35 | }, 36 | }) 37 | 38 | 39 | export default UserType -------------------------------------------------------------------------------- /apps/home/components/admin/hero-gradient.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode, useEffect } from 'react' 2 | 3 | import { Gradient } from '@/components/gradient' 4 | 5 | const HeroGradient = ({ children }: {children?: ReactNode}) => { 6 | 7 | useEffect(() => { 8 | const gradient = new Gradient() 9 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 10 | // @ts-ignore 11 | gradient.initGradient('#gradient-canvas') 12 | }, []) 13 | 14 | return ( 15 |
18 |
19 | 23 |
24 |
25 | {children} 26 |
27 |
28 | ) 29 | } 30 | 31 | export default HeroGradient -------------------------------------------------------------------------------- /apps/home/components/separator.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | import * as SeparatorPrimitive from '@radix-ui/react-separator' 4 | import cn from 'classnames' 5 | 6 | type SeparatorProps = React.ComponentPropsWithoutRef & { 7 | vertical?: boolean 8 | horizontal?: boolean 9 | color?: `bg-${string}` 10 | } 11 | 12 | const Separator = React.forwardRef< 13 | React.ElementRef, 14 | SeparatorProps 15 | >( 16 | ( 17 | { 18 | vertical, horizontal, color = 'bg-gray-900/20', ...props 19 | }, 20 | ref 21 | ) => ( 22 | 33 | ) 34 | ) 35 | Separator.displayName = SeparatorPrimitive.Root.displayName 36 | 37 | export { Separator } 38 | -------------------------------------------------------------------------------- /apps/graphql/src/resolvers/queries/lines.ts: -------------------------------------------------------------------------------- 1 | import { FieldResolver } from 'nexus/src/typegenTypeHelpers' 2 | 3 | import { Context } from '../../context' 4 | 5 | const stations: FieldResolver<'Query', 'lines'> = 6 | async (_, args, ctx: Context) => { 7 | const { prisma } = ctx 8 | 9 | const lines = await prisma.line.findMany({ 10 | include: { 11 | StationPositionInLine: { 12 | include: { 13 | station: true, 14 | }, 15 | }, 16 | pricing: true, 17 | }, 18 | orderBy: { 19 | name: 'asc', 20 | }, 21 | }) 22 | 23 | // sort stations by position in line 24 | const sortedLines = lines.map((line) => { 25 | const sortedStations = line.StationPositionInLine.sort( 26 | (a, b) => a.position - b.position, 27 | ) 28 | 29 | return { 30 | ...line, 31 | sortedStations: sortedStations.map((station) => station.station), 32 | } 33 | }) 34 | 35 | return sortedLines 36 | } 37 | 38 | export default stations 39 | -------------------------------------------------------------------------------- /apps/home/next.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | const {i18n} = require('./next-i18next.config') 3 | 4 | const withMDX = require('@next/mdx')({ 5 | extension: /\.mdx?$/, 6 | options: { 7 | // If you use remark-gfm, you'll need to use next.config.mjs 8 | // as the package is ESM only 9 | // https://github.com/remarkjs/remark-gfm#install 10 | remarkPlugins: [], 11 | rehypePlugins: [], 12 | // If you use `MDXProvider`, uncomment the following line. 13 | // providerImportSource: "@mdx-js/react", 14 | }, 15 | }) 16 | 17 | const withPWA = require('next-pwa')({ 18 | dest: 'public', 19 | register: true, 20 | skipWaiting: true, 21 | disable: process.env.NODE_ENV === 'development', 22 | }) 23 | 24 | module.exports = withPWA(withMDX({ 25 | pageExtensions: ['ts', 'tsx', 'js', 'jsx', 'md', 'mdx'], 26 | reactStrictMode: true, 27 | i18n, 28 | 29 | webpack: (config) => { 30 | config.module.rules.push({ 31 | test: /\.svg$/, 32 | use: ['@svgr/webpack'], 33 | }) 34 | 35 | return config 36 | }, 37 | })) -------------------------------------------------------------------------------- /apps/home/pages/subscriptions.tsx: -------------------------------------------------------------------------------- 1 | import type { NextPage } from 'next' 2 | 3 | import Subscriptions from '@/components/subscriptions' 4 | import AppLayout from '@/layouts/app' 5 | 6 | import { useTranslation } from 'next-i18next' 7 | import { serverSideTranslations } from 'next-i18next/serverSideTranslations' 8 | import { NextSeo } from 'next-seo' 9 | 10 | const SubscriptionsPage: NextPage = () => { 11 | const { t } = useTranslation('subscriptions') 12 | 13 | return ( 14 | 19 | 23 | 24 | 25 | ) 26 | } 27 | 28 | export const getStaticProps = async ({ locale }: { locale: string }) => ({ 29 | props: { 30 | ...(await serverSideTranslations(locale, [ 31 | 'common', 'subscriptions', 'purchase', 32 | ])), 33 | }, 34 | }) 35 | 36 | export default SubscriptionsPage -------------------------------------------------------------------------------- /apps/home/public/locales/en/user-subscriptions.json: -------------------------------------------------------------------------------- 1 | { 2 | "seo": { 3 | "title": "My Subscriptions", 4 | "description": "View your subscriptions and manage them" 5 | }, 6 | "mySubscriptions": "My Subscriptions", 7 | "ONE_AREA": "One Area", 8 | "TWO_AREAS": "Two Areas", 9 | "THREE_AREAS": "Three Areas", 10 | "MONTHLY": "Monthly", 11 | "QUARTERLY": "Quarterly", 12 | "YEARLY": "Yearly", 13 | "subscription": "Subscription", 14 | "description": "Enjoy exploring and commuting in the streets of Cairo with your monthly subscription to up to {0} stations for free.", 15 | "refund": "A refund has been requested for this subscription at {0} and is currently being reviewed by an admin", 16 | "expire": "Your subscription will expire on {0}", 17 | "cancel": "Cancel Subscription", 18 | "ticketsCovered": "Tickets covered by your subscription", 19 | "subscriptionHistory": "Your Subscriptions History", 20 | "subscribedAt": "Subscribed at", 21 | "expirationDate": "Expiration Date", 22 | "noSubscriptionsFound": "No subscriptions found" 23 | } -------------------------------------------------------------------------------- /apps/home/graphql/admin/refunds/refunds.ts: -------------------------------------------------------------------------------- 1 | import { Variables } from 'graphql-request' 2 | 3 | import graphqlFetcher from '@/graphql/graphql-fetcher' 4 | 5 | import useSWR from 'swr' 6 | 7 | export interface RefundsVariables extends Variables { 8 | page: number 9 | take?: number 10 | filterBy?: 'ALL' | 'PENDING' | 'ACCEPTED' | 'REJECTED' 11 | search?: string 12 | } 13 | 14 | const REFUNDS_QUERY = /* GraphQL */ ` 15 | query ($page: Int!, $take: Int, $filterBy: String, $search: String) { 16 | adminGetRefundRequests(page: $page, take: $take, filterBy: $filterBy, search: $search) { 17 | id 18 | createdAt 19 | status 20 | message 21 | price 22 | user{ 23 | id 24 | email 25 | name 26 | } 27 | } 28 | } 29 | ` 30 | 31 | const useRefunds = (variables: RefundsVariables) => { 32 | const result = useSWR( 33 | [REFUNDS_QUERY, ...Object.values(variables)], 34 | (queryStr: string) => graphqlFetcher(queryStr, variables), 35 | ) 36 | 37 | return result 38 | } 39 | 40 | export default useRefunds -------------------------------------------------------------------------------- /apps/home/graphql/stations/add-station.ts: -------------------------------------------------------------------------------- 1 | import { Variables } from 'graphql-request' 2 | 3 | import mutate from '@/graphql/mutate' 4 | 5 | export interface AddStationVariables extends Variables { 6 | name: string 7 | name_ar: string 8 | location: { 9 | lng: number 10 | lat: number 11 | } 12 | lineIds: string[] 13 | } 14 | 15 | const UPDATE_STATION_QUERY = /* GraphQL */ ` 16 | mutation adminAddStation( 17 | $name: String! 18 | $name_ar: String! 19 | $location: LngLatInput! 20 | $lineIds: [String!]! 21 | ) { 22 | adminAddStation( 23 | name: $name 24 | name_ar: $name_ar 25 | location: $location 26 | lineIds: $lineIds 27 | ) { 28 | id 29 | name 30 | name_ar 31 | location 32 | lines { 33 | id 34 | name 35 | name_ar 36 | color 37 | } 38 | } 39 | } 40 | ` 41 | 42 | const adminAddStationMutation = (variables: AddStationVariables) => { 43 | return mutate(UPDATE_STATION_QUERY, variables) 44 | } 45 | 46 | export default adminAddStationMutation -------------------------------------------------------------------------------- /apps/home/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import type { NextPage } from 'next' 2 | 3 | import Home from '@/components/home' 4 | import AppLayout from '@/layouts/app' 5 | 6 | import { useTranslation } from 'next-i18next' 7 | import { serverSideTranslations } from 'next-i18next/serverSideTranslations' 8 | import { NextSeo } from 'next-seo' 9 | 10 | const HomePage: NextPage = () => { 11 | const { t } = useTranslation('home') 12 | 13 | return ( 14 | 20 | 25 | 26 | 27 | ) 28 | } 29 | 30 | export const getStaticProps = async ({ locale }: { locale: string }) => ({ 31 | props: { 32 | ...(await serverSideTranslations(locale, [ 33 | 'common', 34 | 'home', 35 | 'find-ticket', 36 | 'faq', 37 | ])), 38 | }, 39 | }) 40 | 41 | export default HomePage -------------------------------------------------------------------------------- /apps/home/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import type { AppProps } from 'next/app' 2 | import dynamic from 'next/dynamic' 3 | 4 | import DefaultSeoSettings from '@/components/default-seo-settings' 5 | import PurchaseModal from '@/components/modal/purchase' 6 | import { AppProvider } from '@/context/app-context' 7 | import fetcher from '@/lib/fetcher' 8 | 9 | import { appWithTranslation } from 'next-i18next' 10 | import { SWRConfig } from 'swr' 11 | 12 | import '../styles/globals.css' 13 | 14 | const Toaster = dynamic(() => import('react-hot-toast').then((mod) => mod.Toaster), { 15 | ssr: false, 16 | }) 17 | 18 | const App = ({ Component, pageProps }: AppProps) => { 19 | return ( 20 | <> 21 | 22 | 23 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | ) 35 | } 36 | 37 | export default appWithTranslation(App) -------------------------------------------------------------------------------- /apps/home/graphql/admin/verifications/verifications.ts: -------------------------------------------------------------------------------- 1 | import { Variables } from 'graphql-request' 2 | 3 | import graphqlFetcher from '@/graphql/graphql-fetcher' 4 | 5 | import useSWR from 'swr' 6 | 7 | export interface VerificationVariables extends Variables { 8 | page: number 9 | take?: number 10 | filterBy?: 'ALL' | 'PENDING' | 'ACCEPTED' | 'REJECTED' 11 | search?: string 12 | } 13 | 14 | const VERIFICATION_QUERY = /* GraphQL */ ` 15 | query ($page: Int!, $take: Int, $filterBy: String, $search: String) { 16 | adminGetVerificationRequests(page: $page, take: $take, filterBy: $filterBy, search: $search) { 17 | id 18 | name 19 | email 20 | role 21 | documentVerified 22 | createdAt 23 | documentUrl 24 | } 25 | } 26 | ` 27 | 28 | const useVerifications = (variables: VerificationVariables) => { 29 | const result = useSWR( 30 | [VERIFICATION_QUERY, ...Object.values(variables)], 31 | (queryStr: string) => graphqlFetcher(queryStr, variables), 32 | ) 33 | 34 | return result 35 | } 36 | 37 | export default useVerifications -------------------------------------------------------------------------------- /apps/home/layouts/admin.tsx: -------------------------------------------------------------------------------- 1 | // import { useRouter } from 'next/router' 2 | 3 | import { useRouter } from 'next/router' 4 | 5 | import AdminNavigation, { AdminNavigationProps } from '@/components/admin/navigation' 6 | import useUser from '@/graphql/user/me' 7 | // import useUser from '@/graphql/user/me' 8 | 9 | interface AdminProps { 10 | children: React.ReactNode 11 | navigationProps?: AdminNavigationProps 12 | } 13 | 14 | const AdminLayout = ({ children, navigationProps }: AdminProps) => { 15 | const { data: user, isLoading: userLoading } = useUser() 16 | const router = useRouter() 17 | 18 | if (userLoading) return null 19 | 20 | if (!user || user.role !== 'ADMIN') { 21 | router.push('/') 22 | return null 23 | } 24 | 25 | 26 | return ( 27 |
28 | 29 |
30 | {children} 31 |
32 |
33 | ) 34 | } 35 | 36 | export default AdminLayout -------------------------------------------------------------------------------- /apps/home/public/locales/ar/tickets-details.json: -------------------------------------------------------------------------------- 1 | { 2 | "seo": { 3 | "title": "{0} إلى {1}", 4 | "description": "اشتر تذكرة مترو القاهرة من {0} إلى {1}. قارن بين طرق المترو وأوقاته ومحطاته للتخطيط لرحلتك." 5 | }, 6 | "reviewTicketOptions": "مراجعة خيارات التذاكر", 7 | "freeWithSubscription": "مجاني مع الاشتراك", 8 | "purchase": "شراء", 9 | "tripRoute": { 10 | "title": "مسار الرحلة", 11 | "transfer": "نقل" 12 | }, 13 | "flexibilityAndConditions": "المرونة والظروف", 14 | "boardingRequirements": { 15 | "title": "متطلبات الصعود إلى المترو", 16 | "qrCode": "مطلوب رقم الحجز أو رمز الاستجابة السريعة", 17 | "ticket": "التذكرة صالحة للاستخدام لمرة واحدة فقط", 18 | "learnMore": { 19 | "text": "لمعرفة المزيد حول متطلبات المترو، يرجى {0}", 20 | "link": "الضغط هنا" 21 | } 22 | }, 23 | "adult": "بالغ", 24 | "adults": "بالغين", 25 | "child": "طفل", 26 | "children": "أطفال", 27 | "senior": "مسن", 28 | "seniors": "مسنون", 29 | "noPassengersSelected": "لم يتم تحديد ركاب", 30 | "standard": "قياسي", 31 | "purchaseTicket": "شراء تذكرة", 32 | "egp": "ج.م." 33 | } -------------------------------------------------------------------------------- /apps/home/graphql/user/purchase-history.ts: -------------------------------------------------------------------------------- 1 | import { Variables } from 'graphql-request' 2 | 3 | import graphqlFetcher from '@/graphql/graphql-fetcher' 4 | 5 | import useSWR from 'swr' 6 | 7 | interface PurchaseHistoryVariables extends Variables { 8 | subscriptionOnly?: boolean 9 | } 10 | 11 | const PURCHASE_HISTORY_QUERY = /* GraphQL */ ` 12 | query purchaseHistory($subscriptionOnly: Boolean) { 13 | purchaseHistory(subscriptionOnly: $subscriptionOnly) { 14 | id 15 | from { 16 | id 17 | name 18 | name_ar 19 | } 20 | to { 21 | id 22 | name 23 | name_ar 24 | } 25 | price 26 | date 27 | adults 28 | seniors 29 | children 30 | refundRequest { 31 | id 32 | status 33 | createdAt 34 | message 35 | } 36 | } 37 | } 38 | ` 39 | 40 | const usePurchaseHistory = (variables: PurchaseHistoryVariables) => { 41 | const result = useSWR( 42 | [PURCHASE_HISTORY_QUERY, variables], 43 | (queryStr: string) => graphqlFetcher(queryStr, variables) 44 | ) 45 | 46 | return result 47 | } 48 | 49 | export default usePurchaseHistory -------------------------------------------------------------------------------- /apps/home/pages/magic-link/[link].tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react' 2 | import type { NextPage } from 'next' 3 | import { useRouter } from 'next/router' 4 | 5 | import Loader from '@/components/loader' 6 | 7 | import axios from 'axios' 8 | 9 | const LinkPage: NextPage = () => { 10 | const router = useRouter() 11 | const magicLink = router.query.link as string 12 | 13 | useEffect(() => { 14 | const verifyMagicLink = async () => { 15 | try { 16 | if (!magicLink) return 17 | await axios.post(`${process.env.NEXT_PUBLIC_API_URL}/auth/magic-link`, { 18 | link: magicLink, 19 | }, { 20 | withCredentials: true, 21 | }) 22 | // close window 23 | setTimeout(() => window.close(), 1000) 24 | } catch (e) { 25 | // navigate to login page 26 | router.push('/login?error=invalid-link') 27 | } 28 | } 29 | verifyMagicLink() 30 | }, [magicLink, router]) 31 | 32 | return ( 33 |
34 | 35 |
36 | ) 37 | } 38 | 39 | 40 | export default LinkPage -------------------------------------------------------------------------------- /apps/home/components/default-seo-settings.tsx: -------------------------------------------------------------------------------- 1 | import { useTranslation } from 'next-i18next' 2 | import { DefaultSeo } from 'next-seo' 3 | 4 | const DefaultSeoSettings = () => { 5 | const { t } = useTranslation('common') 6 | 7 | return ( 8 | 40 | ) 41 | } 42 | 43 | export default DefaultSeoSettings -------------------------------------------------------------------------------- /apps/home/components/navigation/hamburger-menu.tsx: -------------------------------------------------------------------------------- 1 | import { motion, MotionProps } from 'framer-motion' 2 | 3 | interface PathProps extends MotionProps { 4 | d?: string 5 | } 6 | 7 | const Path = (props: PathProps) => ( 8 | 15 | ) 16 | 17 | const HamburgerMenu = () => { 18 | return ( 19 | 24 | 30 | 38 | 44 | 45 | ) 46 | } 47 | 48 | export default HamburgerMenu -------------------------------------------------------------------------------- /apps/home/public/locales/en/tickets-details.json: -------------------------------------------------------------------------------- 1 | { 2 | "seo": { 3 | "title": "{0} to {1}", 4 | "description": "Purchase a Cairo Metro ticket from {0} to {1}. Compare metro routes, times and stations to plan your trip." 5 | }, 6 | "reviewTicketOptions": "Review ticket options", 7 | "freeWithSubscription": "Free with subscription", 8 | "purchase": "Purchase", 9 | "tripRoute": { 10 | "title": "Trip route", 11 | "transfer": "Transfer" 12 | }, 13 | "flexibilityAndConditions": "Flexibility and conditions", 14 | "boardingRequirements": { 15 | "title": "Boarding requirements", 16 | "qrCode": "Booking number or QR code are required", 17 | "ticket": "A ticket is only valid for one time use", 18 | "learnMore": { 19 | "text": "To learn more about metro requirements, please {0}", 20 | "link": "click here" 21 | } 22 | }, 23 | "adult": "Adult", 24 | "adults": "Adults", 25 | "child": "Child", 26 | "children": "Children", 27 | "senior": "Senior", 28 | "seniors": "Seniors", 29 | "noPassengersSelected": "No passengers selected", 30 | "standard": "Standard", 31 | "purchaseTicket": "Purchase ticket", 32 | "egp": "EGP" 33 | } -------------------------------------------------------------------------------- /apps/home/icons/chatbubbles.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/home/pages/user/tickets.tsx: -------------------------------------------------------------------------------- 1 | import type { NextPage } from 'next' 2 | 3 | import UserTickets from '@/components/user/tickets' 4 | import AppLayout from '@/layouts/app' 5 | import AuthenticatedUser from '@/layouts/user' 6 | 7 | import { useTranslation } from 'next-i18next' 8 | import { serverSideTranslations } from 'next-i18next/serverSideTranslations' 9 | import { NextSeo } from 'next-seo' 10 | 11 | const UserTicketsPage: NextPage = () => { 12 | const { t } = useTranslation('user-ticket') 13 | 14 | return ( 15 | 16 | 22 | 26 | 27 | 28 | 29 | ) 30 | } 31 | 32 | export const getStaticProps = async ({ locale }: { locale: string }) => ({ 33 | props: { 34 | ...(await serverSideTranslations(locale, [ 35 | 'common', 'user-ticket', 36 | ])), 37 | }, 38 | }) 39 | 40 | export default UserTicketsPage -------------------------------------------------------------------------------- /apps/home/icons/ticket-outline-gradient.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 | 11 | -------------------------------------------------------------------------------- /apps/graphql/src/types/subscription.ts: -------------------------------------------------------------------------------- 1 | import { objectType } from 'nexus' 2 | import { Subscriptions } from 'nexus-prisma' 3 | 4 | 5 | const Subscription = objectType({ 6 | name: Subscriptions.$name, 7 | definition(t) { 8 | t.field(Subscriptions.id) 9 | t.field(Subscriptions.user) 10 | t.field(Subscriptions.type) 11 | t.field(Subscriptions.tier) 12 | t.field(Subscriptions.createdAt) 13 | t.field(Subscriptions.expiresAt) 14 | t.field('isActive', { 15 | type: 'Boolean', 16 | resolve: async (parent) => { 17 | const expiresAt = parent.expiresAt 18 | const now = new Date() 19 | 20 | return expiresAt > now 21 | }, 22 | }) 23 | t.field('refundRequest', { 24 | type: 'DateTime', 25 | resolve: async (parent, _, ctx) => { 26 | const { prisma, user } = ctx 27 | 28 | const refund = await prisma.refund.findFirst({ 29 | where: { 30 | referenceId: parent.id, 31 | ticketType: 'SUBSCRIPTION', 32 | userId: user?.id, 33 | }, 34 | }) 35 | 36 | return refund?.createdAt 37 | }, 38 | }) 39 | }, 40 | }) 41 | 42 | export default Subscription -------------------------------------------------------------------------------- /apps/home/public/locales/ar/subscriptions.json: -------------------------------------------------------------------------------- 1 | { 2 | "seo": { 3 | "title": "اكتشف المزايا الحصرية مع اشتراكات مترو القاهرة", 4 | "description": "ارفع مستوى تجربتك في مترو القاهرة من خلال خطط الاشتراك الخاصة بنا. يمكنك الوصول إلى الميزات المميزة والتحديثات ذات الأولوية والعروض الخاصة. استكشف القاهرة بسلاسة من خلال خدمتنا القائمة على الاشتراك واستمتع بطريقة أكثر ملاءمة للتنقل في عاصمة مصر" 5 | }, 6 | "title": "الاشتراكات", 7 | "description": "اشتراكات شهرية وربع سنوية وسنوية لمترو القاهرة. وفر الوقت والمال من خلال الاشتراك.", 8 | "types": { 9 | "monthly": "شهري", 10 | "quarterly": "ربع سنوي", 11 | "yearly": "سنوي" 12 | }, 13 | "subscription": "اشتراك", 14 | "upToStations": "ما يصل إلى {0} محطة", 15 | "moreThanStations": "أكثر من {0} محطة", 16 | "benefits": { 17 | "rides": "المشاوير ب {0} جنيه بدلا من {1} جنيه (خصم {2})", 18 | "customerPriority": "أولوية الوصول إلى دعم العملاء", 19 | "seniors": "{0} جنيه مصري لكبار السن" 20 | }, 21 | "upgrade": "الترقية إلى اشتراك {0}", 22 | "areas": { 23 | "one": "منطقة واحدة", 24 | "two": "منطقتان", 25 | "three": "ثلاث مناطق" 26 | }, 27 | "egp": "جنيه مصري", 28 | "purchaseSubscription": "شراء اشتراك {0}" 29 | } -------------------------------------------------------------------------------- /apps/home/components/navigation/navigation-menu.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | import * as NavigationMenuPrimitive from '@radix-ui/react-navigation-menu' 4 | import cn from 'classnames' 5 | 6 | interface NavigationMenuProps extends React.ComponentPropsWithoutRef { 7 | list?: React.ComponentPropsWithoutRef 8 | } 9 | 10 | const NavigationMenu = React.forwardRef< 11 | React.ElementRef, 12 | NavigationMenuProps 13 | >(({ 14 | className, children, list, ...props 15 | }, ref) => ( 16 | 24 | 30 | {children} 31 | 32 | 33 | )) 34 | 35 | NavigationMenu.displayName = NavigationMenuPrimitive.Root.displayName 36 | 37 | export default NavigationMenu -------------------------------------------------------------------------------- /apps/home/components/checkbox.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import * as React from 'react' 4 | 5 | import CheckmarkIcon from '@/icons/checkmark.svg' 6 | 7 | import * as CheckboxPrimitive from '@radix-ui/react-checkbox' 8 | import cn from 'classnames' 9 | const Checkbox = React.forwardRef< 10 | React.ElementRef, 11 | React.ComponentPropsWithoutRef 12 | >(({ className, ...props }, ref) => ( 13 | 21 | 24 | 25 | 26 | 27 | )) 28 | Checkbox.displayName = CheckboxPrimitive.Root.displayName 29 | 30 | export { Checkbox } 31 | -------------------------------------------------------------------------------- /apps/home/components/modal/purchase/default-card.tsx: -------------------------------------------------------------------------------- 1 | import CreditDefaultEffect from '@/icons/credit-default-effect.svg' 2 | 3 | import { motion } from 'framer-motion' 4 | 5 | interface DefaultCardProps { 6 | validThru?: string 7 | cardHolder?: string 8 | formattedCardNumber?: string 9 | } 10 | const DefaultCard = ({ validThru, cardHolder, formattedCardNumber }: DefaultCardProps) => { 11 | return ( 12 | 19 |

20 | {validThru || 'MM/YY'} 21 |

22 |

23 | {cardHolder || 'Card Holder'} 24 |

25 |

26 | {formattedCardNumber} 27 |

28 |
29 | 30 |
31 |
32 | ) 33 | } 34 | 35 | export default DefaultCard -------------------------------------------------------------------------------- /apps/graphql/src/resolvers/queries/monthly-revenue.ts: -------------------------------------------------------------------------------- 1 | import { FieldResolver } from 'nexus/src/typegenTypeHelpers' 2 | 3 | import { Context } from '../../context' 4 | import adminPermission from '../../permissions/admin' 5 | 6 | const monthlyRevenue: FieldResolver<'Query', 'monthlyRevenue'> = 7 | async (_, args, ctx: Context) => { 8 | adminPermission(ctx) 9 | 10 | const now = new Date() 11 | const year = now.getFullYear() 12 | 13 | const months = Array.from({ length: 12 }, (_, i) => i + 1) 14 | 15 | const monthlyRevenue = await Promise.all( 16 | months.map(async month => { 17 | const start = new Date(year, month - 1, 1) 18 | const end = new Date(year, month, 0) 19 | 20 | const revenue = await ctx.prisma.userTickets.aggregate({ 21 | _sum: { 22 | price: true, 23 | }, 24 | where: { 25 | createdAt: { 26 | gte: start, 27 | lte: end, 28 | }, 29 | }, 30 | }) 31 | 32 | return { 33 | month, 34 | revenue: revenue._sum.price || 0, 35 | } 36 | }) 37 | ) 38 | 39 | return monthlyRevenue 40 | } 41 | 42 | export default monthlyRevenue -------------------------------------------------------------------------------- /apps/home/pages/user/subscription.tsx: -------------------------------------------------------------------------------- 1 | import type { NextPage } from 'next' 2 | 3 | import Subscription from '@/components/user/subscription' 4 | import AppLayout from '@/layouts/app' 5 | import AuthenticatedUser from '@/layouts/user' 6 | 7 | import { useTranslation } from 'next-i18next' 8 | import { serverSideTranslations } from 'next-i18next/serverSideTranslations' 9 | import { NextSeo } from 'next-seo' 10 | 11 | const UserSubscriptionPage: NextPage = () => { 12 | const { t } = useTranslation('user-subscriptions') 13 | 14 | return ( 15 | 16 | 22 | 26 | 27 | 28 | 29 | ) 30 | } 31 | 32 | export const getStaticProps = async ({ locale }: { locale: string }) => ({ 33 | props: { 34 | ...(await serverSideTranslations(locale, [ 35 | 'common', 'user-subscriptions', 'user-ticket', 36 | ])), 37 | }, 38 | }) 39 | 40 | export default UserSubscriptionPage -------------------------------------------------------------------------------- /apps/graphql/src/resolvers/queries/user-purchase-history.ts: -------------------------------------------------------------------------------- 1 | import { FieldResolver } from 'nexus/src/typegenTypeHelpers' 2 | 3 | import { Context } from '../../context' 4 | import authenticatedPermission from '../../permissions/authenticated' 5 | 6 | const purchaseHistory: FieldResolver<'Query', 'purchaseHistory'> = 7 | async (_, args, ctx: Context) => { 8 | authenticatedPermission(ctx) 9 | const { prisma, user } = ctx 10 | 11 | if (!args.subscriptionOnly) { 12 | const purchaseHistory = await prisma.userTickets.findMany({ 13 | where: { 14 | userId: user?.id, 15 | }, 16 | }) 17 | 18 | return purchaseHistory 19 | } 20 | 21 | const lastSubscription = await prisma.subscriptions.findFirst({ 22 | where: { 23 | userId: user?.id, 24 | }, 25 | orderBy: { 26 | createdAt: 'desc', 27 | }, 28 | }) 29 | 30 | const purchaseHistory = await prisma.userTickets.findMany({ 31 | where: { 32 | userId: user?.id, 33 | price: 0, 34 | createdAt: { 35 | gte: lastSubscription?.createdAt, 36 | }, 37 | }, 38 | }) 39 | 40 | return purchaseHistory 41 | } 42 | 43 | export default purchaseHistory 44 | -------------------------------------------------------------------------------- /apps/home/public/locales/ar/home.json: -------------------------------------------------------------------------------- 1 | { 2 | "seo": { 3 | "title": "مترو القاهرة - دليلك للتنقل في عاصمة مصر", 4 | "description": "اكتشف الطريقة الأسرع والأكثر ملاءمة لاستكشاف القاهرة من خلال تطبيق الويب الخاص بمترو القاهرة. قم بتخطيط مساراتك، وتحقق من الجداول الزمنية، واحصل على تحديثات في الوقت الفعلي على نظام النقل الفعال هذا. اجعل مغامراتك في القاهرة خالية من المتاعب من خلال تطبيق مترو سهل الاستخدام!" 5 | }, 6 | "companion": { 7 | "title": "رفيقك في مترو القاهرة", 8 | "subtitle": "استمتع برحلة خالية من الإجهاد مع جداول المترو المريحة وخيارات الدفع والتنبيهات والمساعدة.", 9 | "liveSupport": { 10 | "title": "الدعم المباشر", 11 | "description": "احصل على مساعدة فورية مع الدعم المباشر" 12 | }, 13 | "easyTracking": { 14 | "title": "تتبع سهل", 15 | "description": "شراء وتخزين تذاكر المترو الخاصة بك" 16 | }, 17 | "realtimeSchedule": { 18 | "title": "جدول الوقت الحقيقي", 19 | "description": "خطط لرحلتك بثقة مع وصول ومغادرة المترو في الوقت الفعلي" 20 | } 21 | }, 22 | "discover": { 23 | "title": "لست متأكدا من أين تكتشف؟", 24 | "subtitle": "إليك بعض الاقتراحات", 25 | "viewDetails": "عرض التفاصيل", 26 | "egp": "ج.م", 27 | "stations": "محطات" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /apps/graphql/src/lib/calculate-route.ts: -------------------------------------------------------------------------------- 1 | import { Station } from '@prisma/client' 2 | 3 | import { getScheduleBasedOnGivenTime } from './calculate-schedule-based-on-time' 4 | import calculateTravelDuration from './calculate-travel-duration' 5 | import convertLocationToLatLng from './convert-location-to-lat-lng' 6 | 7 | const calculateRoute = (stations: Station[], duration: number, departure: Date) => { 8 | const route: { station: Station, time: Date }[] = [] 9 | 10 | let currentTime = new Date(departure) 11 | for (let i = 0; i < stations.length - 1; i++) { 12 | const currentStation = stations[i] 13 | const nextStation = stations[i + 1] 14 | const currentLocation = convertLocationToLatLng(currentStation.location) 15 | const nextLocation = convertLocationToLatLng(nextStation.location) 16 | const duration = calculateTravelDuration(currentLocation, nextLocation) 17 | const arrivalTime = getScheduleBasedOnGivenTime(duration, currentTime, 1, 0)[0].arrivalTime 18 | route.push({ station: currentStation, time: currentTime }) 19 | currentTime = new Date(arrivalTime) 20 | } 21 | 22 | route.push({ station: stations[stations.length - 1], time: currentTime }) 23 | 24 | return route 25 | } 26 | 27 | export default calculateRoute -------------------------------------------------------------------------------- /apps/home/graphql/lines/lines.ts: -------------------------------------------------------------------------------- 1 | import graphqlFetcher from '@/graphql/graphql-fetcher' 2 | 3 | import useSWR from 'swr' 4 | 5 | const LINES_QUERY = /* GraphQL */ ` 6 | { 7 | lines { 8 | id 9 | name 10 | name_ar 11 | color 12 | sortedStations { 13 | id 14 | name 15 | name_ar 16 | locationLngLat { 17 | lng 18 | lat 19 | } 20 | lines { 21 | id 22 | name 23 | color 24 | } 25 | } 26 | stations { 27 | id 28 | name 29 | name_ar 30 | location 31 | locationLngLat { 32 | lng 33 | lat 34 | } 35 | lines { 36 | id 37 | name 38 | color 39 | } 40 | } 41 | pricing { 42 | priceZoneOne 43 | priceZoneOneSeniors 44 | priceZoneTwo 45 | priceZoneTwoSeniors 46 | priceZoneThree 47 | priceZoneThreeSeniors 48 | } 49 | } 50 | } 51 | ` 52 | 53 | const useLines = () => { 54 | const result = useSWR( 55 | [LINES_QUERY], 56 | (queryStr: string) => graphqlFetcher(queryStr), 57 | ) 58 | 59 | return result 60 | } 61 | 62 | export default useLines -------------------------------------------------------------------------------- /apps/graphql/src/notifications/invitation-response.mjml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Invitation Update 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | {{name}} ({{email}}) has {{status}} your invitation as {{role}}. 17 | 18 | 19 | 20 | 21 | 22 | 23 | Need help? Email 24 | us at {{helpEmail}} 25 | 26 | Cairo Metro 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /apps/graphql/src/resolvers/mutations/add-line.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLError } from 'graphql/error' 2 | 3 | import { FieldResolver } from 'nexus' 4 | 5 | const addLine: FieldResolver<'Mutation', 'addLine'> = 6 | async (_, args, ctx) => { 7 | // adminPermission(ctx) 8 | 9 | const { 10 | name, color, name_ar: nameAr, 11 | priceZoneOne, priceZoneOneSeniors, 12 | priceZoneTwo, priceZoneTwoSeniors, 13 | priceZoneThree, priceZoneThreeSeniors, 14 | } = args 15 | 16 | if (!name || !nameAr || !color || !priceZoneOne || !priceZoneOneSeniors || !priceZoneTwo || !priceZoneTwoSeniors || !priceZoneThree || !priceZoneThreeSeniors) 17 | throw new GraphQLError('Invalid input') 18 | 19 | const { prisma } = ctx 20 | 21 | const line = await prisma.line.create({ 22 | data: { 23 | name, 24 | name_ar: nameAr, 25 | color, 26 | pricing: { 27 | create: { 28 | priceZoneOne, 29 | priceZoneOneSeniors, 30 | priceZoneTwo, 31 | priceZoneTwoSeniors, 32 | priceZoneThree, 33 | priceZoneThreeSeniors, 34 | }, 35 | }, 36 | }, 37 | }) 38 | 39 | return line 40 | } 41 | export default addLine -------------------------------------------------------------------------------- /apps/home/public/locales/en/subscriptions.json: -------------------------------------------------------------------------------- 1 | { 2 | "seo": { 3 | "title": "Unlock Exclusive Benefits with Cairo Metro Subscriptions", 4 | "description": "Elevate your Cairo Metro experience with our subscription plans. Gain access to premium features, priority updates, and special offers. Explore Cairo seamlessly with our subscription-based service and enjoy a more convenient way to navigate Egypt's capital" 5 | }, 6 | "title": "Subscriptions", 7 | "description": "Monthly, quarterly, and annual subscriptions for Cairo Metro. Save time and money with a subscription.", 8 | "types": { 9 | "monthly": "Monthly", 10 | "quarterly": "Quarterly", 11 | "yearly": "Yearly" 12 | }, 13 | "subscription": "Subscription", 14 | "upToStations": "Up to {0} stations", 15 | "moreThanStations": "More than {0} stations", 16 | "benefits": { 17 | "rides": "Rides for {0} EGP instead of {1} EGP ({2} off)", 18 | "customerPriority": "Priority access to customer support", 19 | "seniors": "{0} EGP for seniors" 20 | }, 21 | "upgrade": "Upgrade to {0} Subscription", 22 | "areas": { 23 | "one": "One Area", 24 | "two": "Two Areas", 25 | "three": "Three Areas" 26 | }, 27 | "egp": "EGP", 28 | "purchaseSubscription": "Purchase {0} Subscription" 29 | } -------------------------------------------------------------------------------- /apps/graphql/src/resolvers/mutations/update-verification-status.ts: -------------------------------------------------------------------------------- 1 | import { FieldResolver } from 'nexus' 2 | 3 | import adminPermission from '../../permissions/admin' 4 | import sendEmail from '../../lib/send-email' 5 | import { EmailTemplate } from '../../lib/send-email' 6 | import capitalizeFirstLetters from '../../lib/capitalize-first-letters' 7 | 8 | 9 | const updateVerificationStatus: FieldResolver< 'Mutation', 'updateVerificationStatus' > = async(_, args, ctx)=>{ 10 | adminPermission(ctx) 11 | 12 | const { prisma } = ctx 13 | var statusColor = 'green' 14 | if(args.documentVerified.verificationstatus == 'REJECTED') statusColor = 'red' 15 | const user = await prisma.user.update({ 16 | where: { 17 | id: args.userId, 18 | }, 19 | data: { 20 | documentVerified: args.documentVerified.verificationstatus, 21 | }, 22 | }) 23 | await sendEmail(user.email, 'Account Verifications', EmailTemplate.VERIFICATION_REQUEST_RESPONSE, { 24 | name: user.name, 25 | documentVerified: capitalizeFirstLetters(args.documentVerified.verificationstatus), 26 | documentUrl: user.documentUrl, 27 | statusColor: statusColor 28 | }) 29 | 30 | return true 31 | } 32 | 33 | export default updateVerificationStatus -------------------------------------------------------------------------------- /apps/home/components/ticket-search/calendar/calendar-popover.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import * as PopoverPrimitive from '@radix-ui/react-popover' 4 | import cn from 'classnames' 5 | 6 | const Popover = PopoverPrimitive.Root 7 | 8 | const PopoverTrigger = PopoverPrimitive.Trigger 9 | 10 | const PopoverContent = React.forwardRef< 11 | React.ElementRef, 12 | React.ComponentPropsWithoutRef 13 | >(({ 14 | className, align = 'center', sideOffset = 4, ...props 15 | }, ref) => ( 16 | 17 | 27 | 28 | )) 29 | PopoverContent.displayName = PopoverPrimitive.Content.displayName 30 | 31 | export { Popover, PopoverTrigger, PopoverContent } -------------------------------------------------------------------------------- /apps/home/components/authentication/signup/policy.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Link from 'next/link' 3 | 4 | import { useTranslation } from 'next-i18next' 5 | 6 | const Policy = () => { 7 | const { t } = useTranslation('signup') 8 | 9 | return ( 10 |

11 | {( 12 | t('policy.full') 13 | .split(' ') 14 | .map((word, index) => ( 15 | 16 | {index !== 0 && ' '} 17 | {word === '{0}' ? ( 18 | 22 | {t('policy.terms')} 23 | 24 | ) : word === '{1}' ? ( 25 | 29 | {t('policy.privacy')} 30 | 31 | ) : ( 32 | word 33 | )} 34 | 35 | )) 36 | )} 37 |

38 | ) 39 | } 40 | 41 | export default Policy -------------------------------------------------------------------------------- /apps/home/pages/signup.tsx: -------------------------------------------------------------------------------- 1 | import type { NextPage } from 'next' 2 | import { useRouter } from 'next/router' 3 | 4 | import Signup from '@/components/authentication/signup' 5 | import useUser from '@/graphql/user/me' 6 | import AuthenticationLayout from '@/layouts/authentication' 7 | 8 | import { useTranslation } from 'next-i18next' 9 | import { serverSideTranslations } from 'next-i18next/serverSideTranslations' 10 | import { NextSeo } from 'next-seo' 11 | 12 | const SignupPage: NextPage = () => { 13 | const { t } = useTranslation('signup') 14 | const { data, isLoading, error } = useUser() 15 | const router = useRouter() 16 | 17 | if (data && !isLoading && !error) { 18 | if (router.query.redirect) 19 | router.push(router.query.redirect as string) 20 | else 21 | router.push('/') 22 | } 23 | 24 | return ( 25 | 26 | 30 | 31 | 32 | ) 33 | } 34 | 35 | export const getStaticProps = async ({ locale }: { locale: string }) => ({ 36 | props: { 37 | ...(await serverSideTranslations(locale, ['common', 'signup'])), 38 | }, 39 | }) 40 | 41 | export default SignupPage -------------------------------------------------------------------------------- /apps/home/components/footer/install-pwa.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react' 2 | 3 | import PwaLogo from '@/icons/logo-pwa.svg' 4 | 5 | interface BeforeInstallPromptEvent extends Event { 6 | prompt: ()=> void 7 | } 8 | 9 | const InstallPwa = () => { 10 | const [defferedPrompt, setDefferedPrompt] = useState(null) 11 | 12 | useEffect(() => { 13 | window.addEventListener('beforeinstallprompt', (e) => { 14 | e.preventDefault() 15 | setDefferedPrompt(e as BeforeInstallPromptEvent) 16 | }) 17 | }, []) 18 | 19 | const handleClick = () => { 20 | if (defferedPrompt) { 21 | defferedPrompt.prompt() 22 | } 23 | } 24 | 25 | return ( 26 | 40 | ) 41 | } 42 | 43 | export default InstallPwa -------------------------------------------------------------------------------- /apps/home/components/modal/purchase/visa-card.tsx: -------------------------------------------------------------------------------- 1 | import LogoVisa from '@/icons/logo-visa-current-color.svg' 2 | import VisaEffect from '@/icons/visa-effect.svg' 3 | 4 | import { motion } from 'framer-motion' 5 | 6 | interface DefaultCardProps { 7 | validThru?: string 8 | cardHolder?: string 9 | formattedCardNumber?: string 10 | } 11 | const VisaCard = ({ validThru, cardHolder, formattedCardNumber }: DefaultCardProps) => { 12 | return ( 13 | 20 |

21 | {validThru || 'MM/YY'} 22 |

23 |

24 | {cardHolder || 'Card Holder'} 25 |

26 |

27 | {formattedCardNumber} 28 |

29 |
30 | 31 |
32 | 33 |
34 | ) 35 | } 36 | 37 | export default VisaCard -------------------------------------------------------------------------------- /apps/home/public/locales/en/home.json: -------------------------------------------------------------------------------- 1 | { 2 | "seo": { 3 | "title": "Cairo Metro - Your Ultimate Guide to Navigating Egypt's Capital", 4 | "description": "Discover the fastest and most convenient way to explore Cairo with our Cairo Metro webapp. Plan your routes, check schedules, and get real-time updates on this efficient transportation system. Make your Cairo adventures hassle-free with our user-friendly Metro App!" 5 | }, 6 | "companion": { 7 | "title": "Your Cairo Metro Companion", 8 | "subtitle": "Enjoy a stress-free commute with convenient metro schedules, payment options, alerts, and assistance.", 9 | "liveSupport": { 10 | "title": "Live Support", 11 | "description": "Get immediate assistance with the live support" 12 | }, 13 | "easyTracking": { 14 | "title": "Easy Tracking", 15 | "description": "Purchase and store your metro tickets" 16 | }, 17 | "realtimeSchedule": { 18 | "title": "Realtime Schedule", 19 | "description": "Plan your journey with confidence with realtime metro arrivals and departures" 20 | } 21 | }, 22 | "discover": { 23 | "title": "Not sure where to discover?", 24 | "subtitle": "Here are some suggestions for you", 25 | "viewDetails": "View Details", 26 | "egp": "EGP", 27 | "stations": "Stations" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /apps/home/graphql/payment/create-subscription.ts: -------------------------------------------------------------------------------- 1 | import { Variables } from 'graphql-request' 2 | 3 | import mutate from '@/graphql/mutate' 4 | 5 | interface CreateSubscriptionVariables extends Variables { 6 | cardNumber: string 7 | expiryMonth: string 8 | expiryYear: string 9 | cardCvc: string 10 | saveCard: boolean 11 | metaData: { 12 | subscriptionType: 'MONTHLY' | 'QUARTERLY' | 'YEARLY' 13 | subscriptionTier: 'ONE_AREA' | 'TWO_AREAS' | 'THREE_AREAS' 14 | } 15 | } 16 | 17 | const CREATE_SUBSCRIPTION_QUERY = /* GraphQL */ ` 18 | mutation createSubscription( 19 | $cardId: String 20 | $cardHolder: String! 21 | $cardNumber: String! 22 | $expiryMonth: String! 23 | $expiryYear: String! 24 | $cardCvc: String! 25 | $saveCard: Boolean! 26 | $metaData: subscriptionEnumArg! 27 | ) { 28 | createSubscription( 29 | cardHolder : $cardHolder 30 | cardId: $cardId 31 | cardNumber: $cardNumber 32 | expiryMonth: $expiryMonth 33 | expiryYear: $expiryYear 34 | cardCvc: $cardCvc 35 | saveCard: $saveCard 36 | metaData: $metaData 37 | ) 38 | } 39 | ` 40 | 41 | const createSubscriptionMutation = (variables: CreateSubscriptionVariables) => { 42 | return mutate(CREATE_SUBSCRIPTION_QUERY, variables) 43 | } 44 | 45 | export default createSubscriptionMutation -------------------------------------------------------------------------------- /apps/home/components/navigation/navigation-trigger.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | import ChevronDown from '@/icons/chevron-down.svg' 4 | 5 | import * as NavigationMenuPrimitive from '@radix-ui/react-navigation-menu' 6 | import cn from 'classnames' 7 | 8 | const NavigationMenuTrigger = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >(({ className, children, ...props }, ref) => ( 12 | 20 | {children}{' '} 21 | 26 | )) 27 | 28 | NavigationMenuTrigger.displayName = NavigationMenuPrimitive.Trigger.displayName 29 | 30 | export default NavigationMenuTrigger -------------------------------------------------------------------------------- /apps/home/components/modal/purchase/mastercard-card.tsx: -------------------------------------------------------------------------------- 1 | import LogoMastercard from '@/icons/logo-mastercard.svg' 2 | import MastercardEffect from '@/icons/mastercard-effect.svg' 3 | 4 | import { motion } from 'framer-motion' 5 | 6 | interface DefaultCardProps { 7 | validThru?: string 8 | cardHolder?: string 9 | formattedCardNumber?: string 10 | } 11 | const MastercardCard = ({ validThru, cardHolder, formattedCardNumber }: DefaultCardProps) => { 12 | return ( 13 | 20 |

21 | {validThru || 'MM/YY'} 22 |

23 |

24 | {cardHolder || 'Card Holder'} 25 |

26 |

27 | {formattedCardNumber} 28 |

29 |
30 | 31 |
32 | 33 |
34 | ) 35 | } 36 | 37 | export default MastercardCard -------------------------------------------------------------------------------- /apps/graphql/src/index.ts: -------------------------------------------------------------------------------- 1 | import express, { Response } from 'express' 2 | 3 | import sgMail from '@sendgrid/mail' 4 | import * as bodyParser from 'body-parser' 5 | import compression from 'compression' 6 | import cookieParser from 'cookie-parser' 7 | import dotenv from 'dotenv' 8 | 9 | import googleAuth from './api/auth/google-auth' 10 | import logout from './api/auth/logout' 11 | import magicLinkVerification from './api/auth/magic-link-verification' 12 | import otpVerification from './api/auth/otp-verification' 13 | import cors from './utils/cors' 14 | import yoga from './utils/yoga' 15 | 16 | dotenv.config() 17 | 18 | const PORT = process.env.PORT ?? 1111 19 | 20 | const app = express() 21 | 22 | app.use(cors) 23 | app.use(compression()) 24 | app.use(cookieParser()) 25 | app.use(bodyParser.json()) 26 | app.use(bodyParser.urlencoded({ extended: true })) 27 | 28 | const apiKey = process.env.SENDGRID_API_KEY ?? '' 29 | sgMail.setApiKey(apiKey) 30 | 31 | 32 | app.get('/ping', (_, res: Response) => { 33 | res.status(200).send('pong') 34 | }) 35 | 36 | app.post('/auth/magic-link', magicLinkVerification) 37 | app.post('/auth/otp', otpVerification) 38 | app.post('/auth/google', googleAuth) 39 | app.post('/auth/logout', logout) 40 | 41 | app.use('/graphql', yoga) 42 | 43 | app.listen(PORT, () => { 44 | console.log(`GraphQL Server is listening on port ${PORT}`) 45 | }) 46 | 47 | export default app -------------------------------------------------------------------------------- /apps/home/components/navigation/index.tsx: -------------------------------------------------------------------------------- 1 | import DesktopNavigation from '@/components/navigation/desktop-navigation' 2 | import MobileNavigation from '@/components/navigation/mobile-navigation' 3 | 4 | import { cva, VariantProps } from 'class-variance-authority' 5 | import cn from 'classnames' 6 | 7 | const navigationVariants = cva( 8 | 'w-full border-b p-2 fixed top-0 z-50', 9 | { 10 | variants: { 11 | variant: { 12 | default: 'bg-white border-gray-200', 13 | transparent: 'bg-transparent', 14 | blur: 'before:absolute before:inset-0 before:z-[-1] before:bg-white/80 before:backdrop-blur border-gray-200', 15 | 'blur-sm': 'before:absolute before:inset-0 before:z-[-1] before:bg-white/80 before:backdrop-blur-sm border-gray-200', 16 | }, 17 | }, 18 | defaultVariants: { 19 | variant: 'default', 20 | }, 21 | } 22 | ) 23 | 24 | export interface NavigationProps extends VariantProps { 25 | activePath?: '/' | '/support' | '/subscriptions' | '/instructions' | `/${string}` 26 | } 27 | 28 | const Navigation = ({ activePath, variant }: NavigationProps) => { 29 | return ( 30 |
31 | 32 | 33 |
34 | ) 35 | } 36 | 37 | export default Navigation -------------------------------------------------------------------------------- /apps/home/public/locales/ar/faq.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "أسئلة مكررة", 3 | "question1": { 4 | "title": "كيفية الاشتراك في مترو القاهرة؟", 5 | "answer": "قم بإنشاء حساب جديد عن طريق التسجيل باستخدام معلوماتك الشخصية وعنوان بريد إلكتروني أو رقم هاتف صالح. انتقل إلى قسم {0} في النظام الأساسي. حدد الاشتراك الذي يلبي احتياجاتك وقم بمعالجة تفاصيل الدفع.", 6 | "subscription": "اكتتاب" 7 | }, 8 | "question2": { 9 | "title": "كم يكلف مترو القاهرة؟", 10 | "answer": "تعتمد تكلفة الرحلة الواحدة على مترو القاهرة على المسافة المقطوعة. حاليا ، تتراوح الأجرة من 5 إلى 10 جنيه مصري." 11 | }, 12 | "question3": { 13 | "title": "ما هي ساعات عمل مترو القاهرة؟", 14 | "answer": "يعمل مترو القاهرة من الساعة 5:00 صباحا حتى الساعة 12:00 منتصف الليل يوميا. ومع ذلك ، قد يختلف الجدول الزمني في أيام العطل الرسمية أو المناسبات الخاصة." 15 | }, 16 | "question4": { 17 | "title": "هل يمكن للأطفال السفر مجانا؟", 18 | "answer": "يمكن للأطفال دون سن 3 سنوات السفر مجانا في مترو القاهرة. الأطفال الذين تتراوح أعمارهم بين 3 و 9 سنوات مؤهلون للحصول على أسعار مخفضة." 19 | }, 20 | "question5": { 21 | "title": "هل يمكنك أخذ الطعام والشراب في القطارات؟", 22 | "answer": "لا يسمح بالأكل والشرب في قطارات مترو القاهرة. ومع ذلك ، يسمح للركاب بحمل المياه المعبأة في زجاجات أو غيرها من المشروبات غير الكحولية معهم. يوصى بالحفاظ على نظافة عربات القطار والتخلص من أي قمامة بشكل صحيح." 23 | } 24 | } 25 | --------------------------------------------------------------------------------