├── .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 |
5 |
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 |
--------------------------------------------------------------------------------
/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 |
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 |
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 |
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 |
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 |
17 |
23 |
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 |
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 |
--------------------------------------------------------------------------------
/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 |
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 |
--------------------------------------------------------------------------------
/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 |
25 |
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 |
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 |
--------------------------------------------------------------------------------