├── .github └── FUNDING.yml ├── app ├── types │ ├── types.d.ts │ ├── search-nearest.ts │ ├── search.ts │ ├── search-detail.ts │ ├── common.ts │ └── index.ts ├── favicon.ico ├── assets │ ├── images │ │ ├── marker.png │ │ ├── sarjdev.png │ │ ├── sarjdev-logo.png │ │ ├── sarjdev_logo.png │ │ ├── mz.svg │ │ ├── mtrugo.svg │ │ ├── mvoltrun.svg │ │ ├── mtesla.svg │ │ ├── me.svg │ │ ├── mastor.svg │ │ ├── mwatt.svg │ │ ├── marker.svg │ │ └── mb.svg │ └── styles │ │ ├── _index.scss │ │ ├── _variables.scss │ │ └── _reset.scss ├── services │ └── axiosInstance.ts ├── components │ ├── Marker │ │ ├── styles.scss │ │ ├── LoadingPopup │ │ │ ├── LoadingPopup.tsx │ │ │ └── style.scss │ │ ├── ErrorPopup │ │ │ ├── style.scss │ │ │ └── ErrorPopup.tsx │ │ ├── actions.ts │ │ ├── MarkerIcons.ts │ │ ├── MarkerComponent.tsx │ │ └── CustomPopup │ │ │ ├── style.scss │ │ │ └── CustomPopup.tsx │ ├── Map │ │ ├── styles.scss │ │ ├── actions.ts │ │ └── MapContent.tsx │ ├── Loading │ │ ├── Loading.tsx │ │ └── styles.scss │ ├── HelperButtons │ │ ├── HelperButtonGroup.tsx │ │ ├── styles.scss │ │ ├── FilterButton │ │ │ └── FilterButton.tsx │ │ └── LocationButton │ │ │ └── LocationButton.tsx │ ├── Form │ │ ├── FormProvider │ │ │ └── FormProvider.tsx │ │ └── RangeInput │ │ │ ├── RangeInput.tsx │ │ │ └── styles.scss │ ├── Accordion │ │ ├── styles.scss │ │ └── Accordion.tsx │ ├── Filter │ │ ├── FilterForm │ │ │ ├── actions.ts │ │ │ ├── styles.scss │ │ │ └── FilterForm.tsx │ │ └── FilteredCard │ │ │ ├── FilteredCard.tsx │ │ │ └── styles.scss │ ├── Search │ │ ├── actions.ts │ │ ├── styles.scss │ │ └── SearchBar.tsx │ ├── Header │ │ ├── styles.scss │ │ ├── Header.tsx │ │ └── HeaderDialog │ │ │ ├── styles.scss │ │ │ └── HeaderDialog.tsx │ ├── Button │ │ ├── styles.scss │ │ └── Button.tsx │ ├── BottomSheet │ │ ├── BottomSheet.tsx │ │ └── styles.scss │ └── Cluster │ │ ├── ClusterData.ts │ │ ├── styles.scss │ │ └── Cluster.tsx ├── hooks │ ├── useDebounce.ts │ ├── useMapEvents.ts │ ├── useResponsive.ts │ └── useUserLocation.ts ├── stores │ ├── mapGeographyStore.ts │ └── generalStore.ts ├── schema │ └── filterFormSchema.ts ├── utils │ ├── zustand.ts │ ├── notistack.ts │ └── general-utils.ts ├── page.tsx ├── layout.tsx └── data │ └── operators.ts ├── public ├── favicon.ico ├── apple-icon.png ├── robots.txt ├── favicon-16x16.png ├── favicon-32x32.png ├── favicon-96x96.png ├── ms-icon-70x70.png ├── sarjdev-logo.png ├── apple-icon-57x57.png ├── apple-icon-60x60.png ├── apple-icon-72x72.png ├── apple-icon-76x76.png ├── ms-icon-144x144.png ├── ms-icon-150x150.png ├── ms-icon-310x310.png ├── android-icon-36x36.png ├── android-icon-48x48.png ├── android-icon-72x72.png ├── android-icon-96x96.png ├── apple-icon-114x114.png ├── apple-icon-120x120.png ├── apple-icon-144x144.png ├── apple-icon-152x152.png ├── apple-icon-180x180.png ├── android-icon-144x144.png ├── android-icon-192x192.png ├── apple-icon-precomposed.png ├── assets │ └── images │ │ └── loading-background.png ├── browserconfig.xml ├── sitemap.xml ├── vercel.svg ├── manifest.json └── next.svg ├── postcss.config.js ├── next.config.js ├── .prettierrc ├── .gitignore ├── tsconfig.json ├── package.json ├── README.md └── .eslintrc.json /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: sarjdev 2 | -------------------------------------------------------------------------------- /app/types/types.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'leaflet.markercluster'; -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sarjdev/front-end/HEAD/app/favicon.ico -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sarjdev/front-end/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | autoprefixer: {}, 4 | }, 5 | } 6 | -------------------------------------------------------------------------------- /public/apple-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sarjdev/front-end/HEAD/public/apple-icon.png -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sarjdev/front-end/HEAD/public/favicon-16x16.png -------------------------------------------------------------------------------- /public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sarjdev/front-end/HEAD/public/favicon-32x32.png -------------------------------------------------------------------------------- /public/favicon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sarjdev/front-end/HEAD/public/favicon-96x96.png -------------------------------------------------------------------------------- /public/ms-icon-70x70.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sarjdev/front-end/HEAD/public/ms-icon-70x70.png -------------------------------------------------------------------------------- /public/sarjdev-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sarjdev/front-end/HEAD/public/sarjdev-logo.png -------------------------------------------------------------------------------- /public/apple-icon-57x57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sarjdev/front-end/HEAD/public/apple-icon-57x57.png -------------------------------------------------------------------------------- /public/apple-icon-60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sarjdev/front-end/HEAD/public/apple-icon-60x60.png -------------------------------------------------------------------------------- /public/apple-icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sarjdev/front-end/HEAD/public/apple-icon-72x72.png -------------------------------------------------------------------------------- /public/apple-icon-76x76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sarjdev/front-end/HEAD/public/apple-icon-76x76.png -------------------------------------------------------------------------------- /public/ms-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sarjdev/front-end/HEAD/public/ms-icon-144x144.png -------------------------------------------------------------------------------- /public/ms-icon-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sarjdev/front-end/HEAD/public/ms-icon-150x150.png -------------------------------------------------------------------------------- /public/ms-icon-310x310.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sarjdev/front-end/HEAD/public/ms-icon-310x310.png -------------------------------------------------------------------------------- /app/assets/images/marker.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sarjdev/front-end/HEAD/app/assets/images/marker.png -------------------------------------------------------------------------------- /app/assets/images/sarjdev.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sarjdev/front-end/HEAD/app/assets/images/sarjdev.png -------------------------------------------------------------------------------- /public/android-icon-36x36.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sarjdev/front-end/HEAD/public/android-icon-36x36.png -------------------------------------------------------------------------------- /public/android-icon-48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sarjdev/front-end/HEAD/public/android-icon-48x48.png -------------------------------------------------------------------------------- /public/android-icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sarjdev/front-end/HEAD/public/android-icon-72x72.png -------------------------------------------------------------------------------- /public/android-icon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sarjdev/front-end/HEAD/public/android-icon-96x96.png -------------------------------------------------------------------------------- /public/apple-icon-114x114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sarjdev/front-end/HEAD/public/apple-icon-114x114.png -------------------------------------------------------------------------------- /public/apple-icon-120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sarjdev/front-end/HEAD/public/apple-icon-120x120.png -------------------------------------------------------------------------------- /public/apple-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sarjdev/front-end/HEAD/public/apple-icon-144x144.png -------------------------------------------------------------------------------- /public/apple-icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sarjdev/front-end/HEAD/public/apple-icon-152x152.png -------------------------------------------------------------------------------- /public/apple-icon-180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sarjdev/front-end/HEAD/public/apple-icon-180x180.png -------------------------------------------------------------------------------- /public/android-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sarjdev/front-end/HEAD/public/android-icon-144x144.png -------------------------------------------------------------------------------- /public/android-icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sarjdev/front-end/HEAD/public/android-icon-192x192.png -------------------------------------------------------------------------------- /app/assets/images/sarjdev-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sarjdev/front-end/HEAD/app/assets/images/sarjdev-logo.png -------------------------------------------------------------------------------- /app/assets/images/sarjdev_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sarjdev/front-end/HEAD/app/assets/images/sarjdev_logo.png -------------------------------------------------------------------------------- /public/apple-icon-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sarjdev/front-end/HEAD/public/apple-icon-precomposed.png -------------------------------------------------------------------------------- /public/assets/images/loading-background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sarjdev/front-end/HEAD/public/assets/images/loading-background.png -------------------------------------------------------------------------------- /app/services/axiosInstance.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | const axiosInstance = axios.create({ 4 | baseURL: 'https://api.sarj.dev/v1', 5 | withCredentials: true 6 | }); 7 | 8 | export default axiosInstance; 9 | -------------------------------------------------------------------------------- /app/components/Marker/styles.scss: -------------------------------------------------------------------------------- 1 | @import "../../../app/assets/styles/variables"; 2 | 3 | .leaflet-popup-close-button { 4 | span { 5 | font-size: 2rem; 6 | padding: 0.2rem; 7 | } 8 | } 9 | 10 | .popup { 11 | bottom: 10px !important; 12 | margin-left: 10px; 13 | } 14 | -------------------------------------------------------------------------------- /public/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | #ffffff -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | /** @type {import('next').NextConfig} */ 4 | 5 | const nextConfig = { 6 | webpack: (config) => { 7 | config.resolve.alias["public"] = path.join(__dirname, "public"); 8 | return config; 9 | } 10 | }; 11 | 12 | module.exports = nextConfig; 13 | -------------------------------------------------------------------------------- /app/assets/styles/_index.scss: -------------------------------------------------------------------------------- 1 | @import "./_reset.scss"; 2 | @import "./_variables.scss"; 3 | 4 | :root { 5 | font-size: 12px; 6 | } 7 | 8 | @media (max-width: $tablet-device) { 9 | :root { 10 | font-size: 10px; 11 | } 12 | } 13 | 14 | @media (max-width: $mobile-device) { 15 | :root { 16 | font-size: 8px; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/components/Map/styles.scss: -------------------------------------------------------------------------------- 1 | .map { 2 | width: 100vw; 3 | height: 100vh; 4 | 5 | &-not-clickable { 6 | pointer-events: none; 7 | } 8 | 9 | &-bottom-sheet { 10 | &-loading { 11 | width: 100%; 12 | display: flex; 13 | align-items: center; 14 | justify-content: center; 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/types/search-nearest.ts: -------------------------------------------------------------------------------- 1 | import { LocationInfo } from "./common"; 2 | 3 | export interface SearchNearest { 4 | total: number; 5 | distance: number; 6 | distanceUnit: string; 7 | chargingStations: SearchNearestChargingStation[]; 8 | } 9 | 10 | export interface SearchNearestChargingStation extends LocationInfo { 11 | distance: number; 12 | } 13 | -------------------------------------------------------------------------------- /app/components/Marker/LoadingPopup/LoadingPopup.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import "./style.scss"; 4 | 5 | const LoadingPopup = () => { 6 | return ( 7 |
8 |
9 |
10 |
11 |
12 |
13 | ); 14 | }; 15 | 16 | export default LoadingPopup; 17 | -------------------------------------------------------------------------------- /app/types/search.ts: -------------------------------------------------------------------------------- 1 | import { GeoLocation } from "./common"; 2 | 3 | export interface Search { 4 | chargingStations: ChargingStation[]; 5 | } 6 | 7 | export interface ChargingStation { 8 | id: number; 9 | geoLocation: GeoLocation; 10 | operator: ChargingStationOperator; 11 | } 12 | 13 | export interface ChargingStationOperator { 14 | id: number; 15 | } 16 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": false, 3 | "printWidth": 100, 4 | "tabWidth": 2, 5 | "semi": true, 6 | "singleQuote": false, 7 | "trailingComma": "none", 8 | "jsxBracketSameLine": true, 9 | "bracketSpacing": true, 10 | "rcVerbose": true, 11 | "javascript.implicitProjectConfig.experimentalDecorators": true, 12 | "breadcrumbs.enabled": true 13 | } -------------------------------------------------------------------------------- /app/components/Loading/Loading.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from "react"; 2 | 3 | import "./styles.scss"; 4 | 5 | const Loading: FC = () => { 6 | return ( 7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | ); 15 | }; 16 | 17 | export default Loading; 18 | -------------------------------------------------------------------------------- /app/components/HelperButtons/HelperButtonGroup.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from "react"; 2 | import FilterButton from "./FilterButton/FilterButton"; 3 | import LocationButton from "./LocationButton/LocationButton"; 4 | 5 | import "./styles.scss"; 6 | 7 | const HelperButtonGroup: FC = () => { 8 | return ( 9 |
10 | 11 | 12 |
13 | ); 14 | }; 15 | 16 | export default HelperButtonGroup; 17 | -------------------------------------------------------------------------------- /app/components/HelperButtons/styles.scss: -------------------------------------------------------------------------------- 1 | @import "../../../app/assets/styles/variables"; 2 | 3 | .helper { 4 | width: 400; 5 | position: absolute; 6 | top: 10rem; 7 | right: 2rem; 8 | z-index: 400; 9 | display: flex; 10 | align-items: center; 11 | justify-content: flex-end; 12 | gap: 1rem; 13 | } 14 | 15 | @media (max-width: $mobile-device) { 16 | .filter { 17 | width: calc(100% - 4rem); 18 | top: 10rem; 19 | justify-content: "center"; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /public/sitemap.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 10 | https://www.sarj.dev/ 11 | 2024-03-31T20:05:04+00:00 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /app/hooks/useDebounce.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | 3 | type ReturnType = string; 4 | 5 | export function useDebounce(value: string, delay = 500): ReturnType { 6 | const [debouncedValue, setDebouncedValue] = useState(value); 7 | 8 | useEffect(() => { 9 | const handler = setTimeout(() => { 10 | setDebouncedValue(value); 11 | }, delay); 12 | 13 | return () => { 14 | clearTimeout(handler); 15 | }; 16 | }, [value, delay]); 17 | 18 | return debouncedValue; 19 | } 20 | -------------------------------------------------------------------------------- /.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 | 30 | # vercel 31 | .vercel 32 | 33 | # typescript 34 | *.tsbuildinfo 35 | next-env.d.ts 36 | -------------------------------------------------------------------------------- /app/stores/mapGeographyStore.ts: -------------------------------------------------------------------------------- 1 | import { create } from "zustand"; 2 | 3 | interface State { 4 | zoom: number; 5 | location: [number, number] | null; 6 | actions: { 7 | setZoom: (_zoom: number) => void; 8 | setLocation: (location: [number, number] | null) => void; 9 | }; 10 | } 11 | 12 | export const useMapGeographyStore = create((set) => ({ 13 | zoom: 4, 14 | location: null, 15 | actions: { 16 | setZoom: (zoom) => set(() => ({ zoom })), 17 | setLocation: (location) => set(() => ({ location })) 18 | } 19 | })); 20 | -------------------------------------------------------------------------------- /app/types/search-detail.ts: -------------------------------------------------------------------------------- 1 | import { LocationInfo, PaymentTypes } from "./common"; 2 | 3 | export type SearchDetail = LocationInfo; 4 | 5 | export interface SearchDetailLocation { 6 | cityId: number; 7 | cityName: null; 8 | districtId: number; 9 | districtName: null; 10 | address: string; 11 | lat: number; 12 | lon: number; 13 | } 14 | 15 | export interface SearchDetailOperator { 16 | id: number; 17 | title: string; 18 | brand: string; 19 | } 20 | 21 | export interface SearchDetailPaymentType { 22 | name: PaymentTypes; 23 | } 24 | -------------------------------------------------------------------------------- /app/schema/filterFormSchema.ts: -------------------------------------------------------------------------------- 1 | import * as yup from "yup"; 2 | 3 | export const FilterFormSchema = yup 4 | .object({ 5 | distance: yup 6 | .number() 7 | .positive() 8 | .integer() 9 | .min(1, "En az 1 km girebilirsiniz.") 10 | .max(20, "En fazla 20 km girebilirsiniz.") 11 | .required("Bu alan zorunludur!"), 12 | size: yup 13 | .number() 14 | .positive() 15 | .integer() 16 | .min(1, "En az 1 km girebilirsiniz.") 17 | .max(30, "En fazla 30 km girebilirsiniz.") 18 | .required("Bu alan zorunludur!") 19 | }) 20 | .required(); 21 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/components/Marker/ErrorPopup/style.scss: -------------------------------------------------------------------------------- 1 | @import "../../../../app/assets/styles/variables"; 2 | 3 | .error-popup { 4 | margin-top: 2.5rem; 5 | font-size: 1.3rem; 6 | color: $color-red; 7 | font-weight: 600; 8 | display: flex; 9 | flex-direction: column; 10 | align-items: center; 11 | justify-content: center; 12 | 13 | &-icon { 14 | font-size: 5rem; 15 | } 16 | 17 | p { 18 | text-align: center; 19 | } 20 | 21 | &-link { 22 | text-decoration: none; 23 | color: $color-gray-3 !important; 24 | 25 | &:hover, 26 | :focus, 27 | :active { 28 | color: $color-black !important; 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/components/Form/FormProvider/FormProvider.tsx: -------------------------------------------------------------------------------- 1 | import classNames from "classnames"; 2 | import { FC } from "react"; 3 | import { FormProvider as Form, UseFormReturn } from "react-hook-form"; 4 | 5 | type Props = { 6 | children: React.ReactNode; 7 | methods: UseFormReturn; 8 | className: string; 9 | onSubmit?: VoidFunction; 10 | }; 11 | 12 | const FormProvider: FC = ({ children, onSubmit, methods, className }) => { 13 | return ( 14 |
15 | 16 | {children} 17 |
18 | 19 | ); 20 | }; 21 | 22 | export default FormProvider; 23 | -------------------------------------------------------------------------------- /app/assets/styles/_variables.scss: -------------------------------------------------------------------------------- 1 | $tablet-device: 1024px; 2 | $mobile-device: 480px; 3 | 4 | $color-white: #ffffff; 5 | $color-white-2: #f4f4f4; 6 | $color-black: #000000; 7 | $color-gray: #dee2e6; 8 | $color-gray-2: #343a40; 9 | $color-gray-3: #495057; 10 | $color-gray-4: #e9ecef; 11 | $color-blue: #2789c9; 12 | $color-blue-2: #8ab4f8; 13 | $color-green: #147d35; 14 | $color-green-2: #2d6a4f; 15 | $color-yellow: #f3eddf; 16 | $color-red: #c40d3c; 17 | $color-red-2: #ff595e; 18 | $color-orange: #ff671f; 19 | 20 | $color-safe: #dee2e6; 21 | $color-low: #ced4da; 22 | $color-mid-low: #adb5bd; 23 | $color-mid: #6c757d; 24 | $color-mid-high: #343a40; 25 | $color-high: #212529; 26 | -------------------------------------------------------------------------------- /app/hooks/useMapEvents.ts: -------------------------------------------------------------------------------- 1 | import { useMapEvents as useLeafletMapEvents } from "react-leaflet"; 2 | import { useDebouncedCallback } from "use-debounce"; 3 | import { useMapGeographyStore } from "../stores/mapGeographyStore"; 4 | 5 | export const useMapEvents = () => { 6 | const { actions } = useMapGeographyStore(); 7 | 8 | const debouncedZoom = useDebouncedCallback(() => { 9 | const zoom = map.getZoom(); 10 | actions.setZoom(zoom); 11 | }, 100); 12 | 13 | const map = useLeafletMapEvents({ 14 | moveend: () => { 15 | debouncedZoom(); 16 | }, 17 | zoomend: () => { 18 | debouncedZoom(); 19 | } 20 | }); 21 | 22 | return map; 23 | }; 24 | -------------------------------------------------------------------------------- /app/components/Map/actions.ts: -------------------------------------------------------------------------------- 1 | import axiosInstance from "@/app/services/axiosInstance"; 2 | import { Search } from "@/app/types/search"; 3 | import { AxiosError } from "axios"; 4 | import { useQuery } from "react-query"; 5 | 6 | export const useGetLocations = () => { 7 | return useQuery>( 8 | ["get-location-data"], 9 | () => { 10 | return axiosInstance.get(`/search`)?.then(({ data }) => data); 11 | }, 12 | { 13 | enabled: true, 14 | keepPreviousData: true, 15 | refetchOnWindowFocus: false, 16 | refetchInterval: false, 17 | refetchOnMount: false, 18 | refetchOnReconnect: false 19 | } 20 | ); 21 | }; 22 | -------------------------------------------------------------------------------- /app/components/Accordion/styles.scss: -------------------------------------------------------------------------------- 1 | @import "../../../app/assets/styles/variables"; 2 | 3 | .accordion { 4 | &-item { 5 | width: 100%; 6 | height: 100%; 7 | } 8 | 9 | &-title { 10 | padding: 8px; 11 | cursor: pointer; 12 | width: 100%; 13 | height: 100%; 14 | display: flex; 15 | justify-content: space-between; 16 | align-items: center; 17 | background-color: $color-gray-4; 18 | } 19 | 20 | &-content { 21 | padding: 8px; 22 | transition: all ease-in-out 0.3s; 23 | border: 2px solid $color-gray-4; 24 | background-color: $color-white; 25 | max-height: calc(100vh / 3); 26 | overflow-y: auto; 27 | scrollbar-width: none; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "plugins": [ 17 | { 18 | "name": "next" 19 | } 20 | ], 21 | "paths": { 22 | "@/*": ["./*"] 23 | } 24 | }, 25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 26 | "exclude": ["node_modules"] 27 | } 28 | -------------------------------------------------------------------------------- /app/components/Marker/ErrorPopup/ErrorPopup.tsx: -------------------------------------------------------------------------------- 1 | import { Icon } from "@iconify-icon/react/dist/iconify.js"; 2 | import Link from "next/link"; 3 | import { FC } from "react"; 4 | 5 | import "./style.scss"; 6 | 7 | export interface ErrorPopupType { 8 | locationLink: string; 9 | } 10 | 11 | const ErrorPopup: FC = ({ locationLink }) => { 12 | return ( 13 |
14 | 15 |

Şarj istasyonu verisi çekilirken bir sorun yaşandı!

16 | 17 | Harita'da aç 18 | 19 |
20 | ); 21 | }; 22 | 23 | export default ErrorPopup; 24 | -------------------------------------------------------------------------------- /app/components/Filter/FilterForm/actions.ts: -------------------------------------------------------------------------------- 1 | import axiosInstance from "@/app/services/axiosInstance"; 2 | import { FilterFormRequest } from "@/app/types"; 3 | import { SearchNearest } from "@/app/types/search-nearest"; 4 | import { AxiosError } from "axios"; 5 | import { useMutation } from "react-query"; 6 | 7 | export const useGetFilteredData = () => { 8 | return useMutation, FilterFormRequest>({ 9 | mutationFn: (data: FilterFormRequest) => { 10 | return axiosInstance 11 | .get( 12 | `/search/nearest?latitude=${data?.latitude}&longitude=${data?.longitude}&distance=${data?.distance}&size=${data?.size}` 13 | ) 14 | ?.then(({ data }) => data); 15 | } 16 | }); 17 | }; 18 | -------------------------------------------------------------------------------- /app/utils/zustand.ts: -------------------------------------------------------------------------------- 1 | import queryString from "query-string"; 2 | import { StateStorage } from "zustand/middleware"; 3 | 4 | export const getHashStorage = (): StateStorage => ({ 5 | getItem: (key) => { 6 | const query = queryString.parse(location.hash, { arrayFormat: "comma" }); 7 | return query[key] as any; 8 | }, 9 | setItem: (key, newValue) => { 10 | const query = queryString.parse(location.hash, { arrayFormat: "comma" }); 11 | query[key] = newValue; 12 | location.hash = queryString.stringify(query, { arrayFormat: "comma" }); 13 | }, 14 | removeItem: (key) => { 15 | const query = queryString.parse(location.hash, { arrayFormat: "comma" }); 16 | delete query[key]; 17 | location.hash = queryString.stringify(query, { arrayFormat: "comma" }); 18 | }, 19 | }); -------------------------------------------------------------------------------- /app/components/Search/actions.ts: -------------------------------------------------------------------------------- 1 | import axiosInstance from "@/app/services/axiosInstance"; 2 | import { SuggestionSearchResponse } from "@/app/types"; 3 | import { AxiosError } from "axios"; 4 | import { useQuery } from "react-query"; 5 | 6 | export const useSearch = ({ q, size = 5 }: { q: string; size?: number }) => { 7 | return useQuery>( 8 | ["get-search"], 9 | () => { 10 | return axiosInstance.get(`/search/suggest?q=${q}&size=${size}`)?.then(({ data }) => data); 11 | }, 12 | { 13 | enabled: false, 14 | keepPreviousData: true, 15 | refetchOnWindowFocus: false, 16 | refetchInterval: false, 17 | refetchOnMount: false, 18 | refetchOnReconnect: false, 19 | refetchIntervalInBackground: false 20 | } 21 | ); 22 | }; 23 | -------------------------------------------------------------------------------- /app/components/Marker/actions.ts: -------------------------------------------------------------------------------- 1 | import axiosInstance from "@/app/services/axiosInstance"; 2 | import { SearchDetail } from "@/app/types/search-detail"; 3 | import { AxiosError } from "axios"; 4 | import { useQuery } from "react-query"; 5 | 6 | export const useGetCertaionLocation = ({ chargingStationId }: { chargingStationId: string }) => { 7 | return useQuery>( 8 | ["get-certain-location-data"], 9 | () => { 10 | return axiosInstance 11 | .get(`/charging-stations/${chargingStationId}/detail`) 12 | ?.then(({ data }) => data); 13 | }, 14 | { 15 | enabled: false, 16 | keepPreviousData: true, 17 | refetchOnWindowFocus: false, 18 | refetchInterval: false, 19 | refetchOnMount: false, 20 | refetchOnReconnect: false 21 | } 22 | ); 23 | }; 24 | -------------------------------------------------------------------------------- /app/components/Form/RangeInput/RangeInput.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from "react"; 2 | import { Controller, useFormContext } from "react-hook-form"; 3 | 4 | import "./styles.scss"; 5 | 6 | type RangeInputType = { 7 | min: number; 8 | max: number; 9 | name: string; 10 | }; 11 | 12 | const RangeInput: FC = ({ min, max, name }) => { 13 | const { control } = useFormContext(); 14 | 15 | return ( 16 |
17 | ( 21 |
22 | 23 | {error ?

{error.message}

: null} 24 |
25 | )} 26 | /> 27 |
28 | ); 29 | }; 30 | 31 | export default RangeInput; 32 | -------------------------------------------------------------------------------- /app/components/Header/styles.scss: -------------------------------------------------------------------------------- 1 | @import "../../../app/assets/styles/variables"; 2 | 3 | .navbar { 4 | background-color: $color-black; 5 | position: fixed; 6 | top: 0; 7 | left: 0; 8 | right: 0; 9 | z-index: 500; 10 | height: 7.5rem; 11 | display: flex; 12 | align-items: center; 13 | justify-content: space-between; 14 | padding: 0 2rem; 15 | 16 | &-link { 17 | text-decoration: none; 18 | color: $color-white; 19 | background-color: transparent; 20 | border: none; 21 | padding: 1rem; 22 | font-size: 1.2rem; 23 | font-weight: bold; 24 | transition: all 0.3s ease-in-out; 25 | cursor: pointer; 26 | 27 | &:hover { 28 | color: $color-gray; 29 | } 30 | } 31 | } 32 | 33 | @media (max-width: $mobile-device) { 34 | .navbar { 35 | height: 8.5rem; 36 | 37 | &-link { 38 | font-size: 1.5rem; 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /app/components/Button/styles.scss: -------------------------------------------------------------------------------- 1 | @import "../../../app/assets/styles/variables"; 2 | 3 | .button { 4 | font-size: 1.2rem; 5 | font-weight: bold; 6 | text-transform: unset; 7 | border-radius: 4px; 8 | padding: 1rem 1.5rem; 9 | cursor: pointer; 10 | transition: 0.3s ease-in-out; 11 | 12 | &-outlined { 13 | background-color: $color-white; 14 | border: 1px solid $color-black; 15 | 16 | &:hover { 17 | background-color: $color-gray-3; 18 | color: $color-white !important; 19 | } 20 | } 21 | 22 | &-contained { 23 | background-color: $color-black; 24 | color: $color-white; 25 | border: 1px solid $color-black; 26 | 27 | &:hover { 28 | background-color: $color-gray-3; 29 | color: $color-white !important; 30 | } 31 | } 32 | } 33 | 34 | @media (max-width: $mobile-device) { 35 | .button { 36 | font-size: 1.4rem; 37 | min-height: 5rem; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /app/components/Accordion/Accordion.tsx: -------------------------------------------------------------------------------- 1 | import { FC, ReactNode } from "react"; 2 | 3 | import { Icon } from "@iconify-icon/react/dist/iconify.js"; 4 | import "./styles.scss"; 5 | 6 | interface Props { 7 | title: ReactNode | string; 8 | content: ReactNode | string; 9 | isOpen: boolean; 10 | onToggle: VoidFunction; 11 | } 12 | 13 | const Accordion: FC = ({ title, content, isOpen, onToggle }) => { 14 | return ( 15 |
16 |
17 |

{title}

18 | 19 | {isOpen ? ( 20 | 21 | ) : ( 22 | 23 | )} 24 | 25 |
26 | {isOpen && ( 27 |
28 |

{content}

29 |
30 | )} 31 |
32 | ); 33 | }; 34 | 35 | export default Accordion; 36 | -------------------------------------------------------------------------------- /app/components/Button/Button.tsx: -------------------------------------------------------------------------------- 1 | import { Icon } from "@iconify-icon/react/dist/iconify.js"; 2 | import classNames from "classnames"; 3 | import { FC, ReactNode } from "react"; 4 | 5 | import "./styles.scss"; 6 | 7 | type ButtonType = { 8 | type?: "button" | "submit" | "reset"; 9 | variant?: "contained" | "outlined"; 10 | classes?: string; 11 | onClick?: VoidFunction; 12 | isLoading?: boolean; 13 | children?: ReactNode; 14 | }; 15 | 16 | const Button: FC = ({ 17 | variant, 18 | classes, 19 | isLoading, 20 | children, 21 | type = "submit", 22 | onClick 23 | }) => { 24 | return ( 25 | 35 | ); 36 | }; 37 | 38 | export default Button; 39 | -------------------------------------------------------------------------------- /app/hooks/useResponsive.ts: -------------------------------------------------------------------------------- 1 | import { Breakpoint, useTheme } from "@mui/material/styles"; 2 | import useMediaQuery from "@mui/material/useMediaQuery"; 3 | 4 | type ReturnType = boolean; 5 | 6 | export type Query = "up" | "down" | "between" | "only"; 7 | 8 | export type Value = Breakpoint | number; 9 | 10 | export function useResponsive(query: Query, start?: Value, end?: Value): ReturnType { 11 | const theme = useTheme(); 12 | 13 | const mediaUp = useMediaQuery(theme.breakpoints.up(start as Value)); 14 | 15 | const mediaDown = useMediaQuery(theme.breakpoints.down(start as Value)); 16 | 17 | const mediaBetween = useMediaQuery(theme.breakpoints.between(start as Value, end as Value)); 18 | 19 | const mediaOnly = useMediaQuery(theme.breakpoints.only(start as Breakpoint)); 20 | 21 | if (query === "up") { 22 | return mediaUp; 23 | } 24 | 25 | if (query === "down") { 26 | return mediaDown; 27 | } 28 | 29 | if (query === "between") { 30 | return mediaBetween; 31 | } 32 | 33 | return mediaOnly; 34 | } 35 | -------------------------------------------------------------------------------- /app/components/BottomSheet/BottomSheet.tsx: -------------------------------------------------------------------------------- 1 | import { useResponsive } from "@/app/hooks/useResponsive"; 2 | import classNames from "classnames"; 3 | import { FC, ReactNode } from "react"; 4 | 5 | import "./styles.scss"; 6 | 7 | interface BottomSheetModalProps { 8 | isOpen: boolean; 9 | isForResponsiveMarker?: boolean; 10 | onClose: () => void; 11 | children: ReactNode; 12 | } 13 | 14 | const BottomSheet: FC = ({ 15 | isOpen, 16 | onClose, 17 | children, 18 | isForResponsiveMarker = false 19 | }) => { 20 | const mdUp = useResponsive("up", "md"); 21 | 22 | return isOpen ? ( 23 |
28 |
29 |
{children}
30 |
31 | ) : null; 32 | }; 33 | 34 | export default BottomSheet; 35 | -------------------------------------------------------------------------------- /app/components/HelperButtons/FilterButton/FilterButton.tsx: -------------------------------------------------------------------------------- 1 | import { useGeneralStore } from "@/app/stores/generalStore"; 2 | import { useMapGeographyStore } from "@/app/stores/mapGeographyStore"; 3 | import { Icon } from "@iconify-icon/react/dist/iconify.js"; 4 | import { useSnackbar } from "notistack"; 5 | import { FC } from "react"; 6 | import Button from "../../Button/Button"; 7 | 8 | const FilterButton: FC = () => { 9 | const { actions } = useGeneralStore(); 10 | const { location } = useMapGeographyStore(); 11 | const { enqueueSnackbar } = useSnackbar(); 12 | 13 | const handleClickFilterButton = () => { 14 | if (location) { 15 | actions.setBottomSheetOpen(true); 16 | } else { 17 | enqueueSnackbar("Filtreleme yapabilmek için konum erişimine izin vermeniz gerekmektedir!", { 18 | variant: "warning" 19 | }); 20 | } 21 | }; 22 | return ( 23 | 36 | 37 | 38 | 39 | 40 | ); 41 | }; 42 | 43 | export default Header; 44 | -------------------------------------------------------------------------------- /app/types/common.ts: -------------------------------------------------------------------------------- 1 | export interface LocationInfo { 2 | id: number; 3 | title: string; 4 | location: LocationDetail; 5 | geoLocation: GeoLocation; 6 | operator: Operators; 7 | reservationUrl: string; 8 | phone: string; 9 | stationActive: boolean; 10 | plugs: Plug[]; 11 | plugsTotal: number; 12 | provider: string; 13 | paymentTypes: PaymentType[]; 14 | provideLiveStats: boolean; 15 | } 16 | 17 | export interface GeoLocation { 18 | lat: number; 19 | lon: number; 20 | } 21 | 22 | export interface LocationDetail { 23 | cityId: number; 24 | cityName: null; 25 | districtId: number; 26 | districtName: null; 27 | address: string; 28 | lat: number; 29 | lon: number; 30 | } 31 | 32 | export enum PaymentTypes { 33 | Mobilodeme = "MOBILODEME" 34 | } 35 | 36 | export interface Operators { 37 | id: number; 38 | title: string; 39 | brand: string; 40 | } 41 | 42 | export interface PaymentType { 43 | name: PaymentTypes; 44 | } 45 | 46 | export interface Plug { 47 | id: number; 48 | type: Types; 49 | subType: SubTypes; 50 | socketNumber: string; 51 | power: number; 52 | price: number; 53 | count: number; 54 | } 55 | 56 | export enum SubTypes { 57 | ACType2 = "AC_TYPE2", 58 | DcCcs = "DC_CCS" 59 | } 60 | 61 | export enum Types { 62 | AC = "AC", 63 | Dc = "DC" 64 | } 65 | 66 | export enum Providers { 67 | Epdk = "EPDK" 68 | } 69 | -------------------------------------------------------------------------------- /app/components/BottomSheet/styles.scss: -------------------------------------------------------------------------------- 1 | @import "../../../app/assets/styles/variables"; 2 | 3 | .bottom-sheet { 4 | &-open { 5 | position: fixed; 6 | bottom: 0; 7 | left: 0; 8 | right: 0; 9 | width: 100%; 10 | z-index: 999; 11 | height: 460px; 12 | 13 | .bottom-sheet-content { 14 | transform: translateY(0); 15 | } 16 | } 17 | 18 | &-overlay { 19 | position: fixed; 20 | top: 0; 21 | left: 0; 22 | width: 100%; 23 | height: 100%; 24 | background-color: rgba(0, 0, 0, 0.5); 25 | } 26 | 27 | &-content { 28 | position: relative; 29 | left: 0; 30 | right: 0; 31 | bottom: 0; 32 | border: 1px solid $color-black; 33 | border-bottom: none; 34 | border-top-left-radius: 1rem; 35 | border-top-right-radius: 1rem; 36 | background-color: $color-white; 37 | padding: 32px; 38 | max-width: 100%; 39 | height: 100%; 40 | transform: translateY(100%); 41 | animation: slideIn 0.5s ease-in-out forwards; 42 | } 43 | } 44 | 45 | @media (max-width: $mobile-device) { 46 | .bottom-sheet { 47 | &-open { 48 | height: 80%; 49 | } 50 | 51 | &-responsive { 52 | &-marker { 53 | height: auto; 54 | } 55 | } 56 | } 57 | } 58 | 59 | @keyframes slideIn { 60 | from { 61 | transform: translateY(100%); 62 | } 63 | to { 64 | transform: translateY(0); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /app/components/Cluster/styles.scss: -------------------------------------------------------------------------------- 1 | @import "../../assets/styles/variables"; 2 | 3 | .custom-cluster { 4 | border-radius: 50%; 5 | color: #212121; 6 | width: 40px !important; 7 | height: 40px !important; 8 | opacity: 0.9; 9 | display: flex; 10 | align-items: center; 11 | justify-content: center; 12 | line-height: 20px !important; 13 | font-size: 14px !important; 14 | font-weight: bold; 15 | padding: 12px; 16 | 17 | &-inner { 18 | &-safe { 19 | background-color: $color-safe; 20 | border: 5px solid rgba($color-white, 10); 21 | color: $color-high; 22 | } 23 | 24 | &-low { 25 | background-color: $color-low; 26 | border: 5px solid rgba($color-safe, 10); 27 | color: $color-high; 28 | } 29 | 30 | &-mid-low { 31 | background-color: $color-mid-low; 32 | border: 5px solid rgba($color-low, 10); 33 | color: $color-high; 34 | } 35 | 36 | &-mid { 37 | background-color: $color-mid; 38 | border: 5px solid rgba($color-mid-low, 10); 39 | color: $color-safe; 40 | } 41 | 42 | &-mid-high { 43 | background-color: $color-mid-high; 44 | border: 5px solid rgba($color-mid, 10); 45 | color: $color-safe; 46 | } 47 | 48 | &-high { 49 | background-color: $color-black; 50 | border: 5px solid rgba($color-mid-high, 10); 51 | color: $color-safe; 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /app/components/Filter/FilterForm/styles.scss: -------------------------------------------------------------------------------- 1 | @import "../../../../app/assets/styles/variables"; 2 | 3 | .filter { 4 | &-section { 5 | display: flex; 6 | flex-direction: row; 7 | width: 100%; 8 | height: 100%; 9 | z-index: 999; 10 | 11 | &-form { 12 | display: flex; 13 | flex-direction: column; 14 | justify-content: flex-start; 15 | gap: 2rem; 16 | width: 30%; 17 | height: 100%; 18 | padding-right: 2rem; 19 | border-right: 1px solid $color-gray-4; 20 | 21 | div { 22 | display: flex; 23 | flex-direction: column; 24 | gap: 1rem; 25 | 26 | span { 27 | font-size: 1.4rem; 28 | } 29 | 30 | button { 31 | width: 100%; 32 | } 33 | } 34 | 35 | h3 { 36 | font-size: 2rem; 37 | font-weight: bold; 38 | } 39 | } 40 | } 41 | } 42 | 43 | @media (max-width: $mobile-device) { 44 | .filter { 45 | &-section { 46 | flex-direction: column; 47 | justify-content: flex-start; 48 | 49 | &-form { 50 | width: 100%; 51 | height: auto; 52 | border: none; 53 | padding: 0px; 54 | padding-bottom: 2rem; 55 | border-bottom: 1px solid $color-gray-4; 56 | 57 | h3 { 58 | font-size: 2.4rem; 59 | } 60 | 61 | div { 62 | span { 63 | font-size: 1.8rem; 64 | } 65 | } 66 | } 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /app/components/Marker/MarkerIcons.ts: -------------------------------------------------------------------------------- 1 | import maIcon from "@/app/assets/images/ma.svg"; 2 | import defaultMarker from "@/app/assets/images/marker.svg"; 3 | import mastorIcon from "@/app/assets/images/mastor.svg"; 4 | import mbIcon from "@/app/assets/images/mb.svg"; 5 | import mcwIcon from "@/app/assets/images/mcw.svg"; 6 | import mdchargeIcon from "@/app/assets/images/mdcharge.svg"; 7 | import meIcon from "@/app/assets/images/me.svg"; 8 | import mpowersarjIcon from "@/app/assets/images/mpowersarj.svg"; 9 | import msIcon from "@/app/assets/images/ms.svg"; 10 | import mteslaIcon from "@/app/assets/images/mtesla.svg"; 11 | import mtrugoIcon from "@/app/assets/images/mtrugo.svg"; 12 | import mtuncIcon from "@/app/assets/images/mtunc.svg"; 13 | import mvoltrunIcon from "@/app/assets/images/mvoltrun.svg"; 14 | import mwattIcon from "@/app/assets/images/mwatt.svg"; 15 | import mzIcon from "@/app/assets/images/mz.svg"; 16 | 17 | export const renderIcon = (id: string): any => { 18 | const Icons: Record = { 19 | "919287": mzIcon.src, 20 | "992950": mbIcon.src, 21 | "952581": maIcon.src, 22 | "930848": meIcon.src, 23 | "929899": msIcon.src, 24 | "929964": mtrugoIcon.src, 25 | "993744": mteslaIcon.src, 26 | "929436": mwattIcon.src, 27 | "1093161": mvoltrunIcon.src, 28 | "1006519": mtuncIcon.src, 29 | "919121": mastorIcon.src, 30 | "919069": mcwIcon.src, 31 | "1092971": mdchargeIcon.src, 32 | "931220": mpowersarjIcon.src 33 | }; 34 | 35 | return Icons[id] || defaultMarker.src; 36 | }; 37 | -------------------------------------------------------------------------------- /app/utils/notistack.ts: -------------------------------------------------------------------------------- 1 | import { alpha, styled } from "@mui/material/styles"; 2 | import { MaterialDesignContent } from "notistack"; 3 | 4 | export const StyledNotistack = styled(MaterialDesignContent)(({ theme }) => { 5 | const isLight = theme.palette.mode === "light"; 6 | 7 | return { 8 | "& #notistack-snackbar": { 9 | ...theme.typography.subtitle2, 10 | padding: 0, 11 | flexGrow: 1, 12 | fontSize: "14px" 13 | }, 14 | "&.notistack-MuiContent": { 15 | padding: theme.spacing(0.5), 16 | paddingRight: theme.spacing(2), 17 | color: theme.palette.text.primary, 18 | boxShadow: theme.shadows, 19 | borderRadius: theme.shape.borderRadius, 20 | backgroundColor: theme.palette.background.paper 21 | }, 22 | "&.notistack-MuiContent-default": { 23 | padding: theme.spacing(1), 24 | color: isLight ? theme.palette.common.white : theme.palette.grey[800], 25 | backgroundColor: isLight ? theme.palette.grey[800] : theme.palette.common.white 26 | } 27 | }; 28 | }); 29 | 30 | type StyledIconProps = { 31 | color: "info" | "success" | "warning" | "error"; 32 | }; 33 | 34 | export const StyledIcon = styled("span")(({ color, theme }) => ({ 35 | width: 44, 36 | height: 44, 37 | display: "flex", 38 | alignItems: "center", 39 | justifyContent: "center", 40 | marginRight: theme.spacing(1.5), 41 | color: theme.palette[color].main, 42 | borderRadius: theme.shape.borderRadius, 43 | backgroundColor: alpha(theme.palette[color].main, 0.16) 44 | })); 45 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "esarj", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@next/third-parties": "14.1.4", 13 | "@emotion/react": "^11.11.1", 14 | "@emotion/styled": "^11.11.0", 15 | "@hookform/resolvers": "^3.3.4", 16 | "@mui/material": "^5.14.18", 17 | "@types/leaflet": "^1.9.3", 18 | "@types/leaflet.markercluster": "^1.5.2", 19 | "@types/lodash.omit": "^4.5.7", 20 | "@types/node": "20.5.1", 21 | "@types/react": "18.2.20", 22 | "@types/react-dom": "18.2.7", 23 | "@types/react-html-parser": "^2.0.5", 24 | "autoprefixer": "10.4.15", 25 | "axios": "^1.5.0", 26 | "classnames": "^2.3.2", 27 | "eslint": "8.47.0", 28 | "eslint-config-next": "13.4.19", 29 | "leaflet": "^1.9.4", 30 | "leaflet.markercluster": "^1.5.3", 31 | "localforage": "^1.10.0", 32 | "lodash.omit": "^4.5.0", 33 | "next": "13.4.19", 34 | "notistack": "^3.0.1", 35 | "postcss": "8.4.31", 36 | "query-string": "^8.1.0", 37 | "react": "18.2.0", 38 | "react-dom": "18.2.0", 39 | "react-hook-form": "^7.51.2", 40 | "react-html-parser": "^2.0.2", 41 | "react-leaflet": "^4.2.1", 42 | "react-query": "^3.39.3", 43 | "sass": "^1.66.1", 44 | "supercluster": "^7.1.5", 45 | "typescript": "5.1.6", 46 | "use-debounce": "^9.0.4", 47 | "use-supercluster": "^0.4.0", 48 | "yup": "^1.4.0", 49 | "zustand": "^4.4.1" 50 | }, 51 | "devDependencies": { 52 | "@iconify-icon/react": "^1.0.8" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /app/hooks/useUserLocation.ts: -------------------------------------------------------------------------------- 1 | import { LatLngTuple } from "leaflet"; 2 | import { useSnackbar } from "notistack"; 3 | import { useEffect, useState } from "react"; 4 | import { useMapGeographyStore } from "../stores/mapGeographyStore"; 5 | 6 | function useUserLocation() { 7 | const [location, setLocation] = useState(null); 8 | const [error, setError] = useState(null); 9 | const [loading, setLoading] = useState(true); 10 | const { enqueueSnackbar } = useSnackbar(); 11 | const { actions } = useMapGeographyStore(); 12 | const { 13 | actions: { setZoom } 14 | } = useMapGeographyStore(); 15 | 16 | useEffect(() => { 17 | if (navigator.geolocation) { 18 | navigator.geolocation.getCurrentPosition( 19 | (position) => { 20 | const { latitude, longitude } = position.coords; 21 | setLocation([latitude, longitude]); 22 | setZoom(14); 23 | actions.setLocation([latitude, longitude]); 24 | setLoading(false); 25 | }, 26 | (error) => { 27 | setError(`Konum bilgisi alınırken hata oluştu: ${error.message}`); 28 | setLoading(false); 29 | enqueueSnackbar("Konum bilgisi alınamadı!", { variant: "error" }); 30 | console.error(`Konum bilgisi alınırken hata oluştun: ${error.message}`); 31 | }, 32 | { enableHighAccuracy: false, timeout: 8000, maximumAge: Infinity } 33 | ); 34 | } else { 35 | setError("Konum bilgisi bu tarayıcıda kullanılamamaktadır!"); 36 | setLoading(false); 37 | enqueueSnackbar("Konum bilgisi bu tarayıcıda kullanılamamaktadır!", { variant: "error" }); 38 | } 39 | }, []); 40 | 41 | return { location, error, loading }; 42 | } 43 | 44 | export default useUserLocation; 45 | -------------------------------------------------------------------------------- /app/assets/styles/_reset.scss: -------------------------------------------------------------------------------- 1 | html, 2 | body, 3 | div, 4 | span, 5 | applet, 6 | object, 7 | iframe, 8 | h1, 9 | h2, 10 | h3, 11 | h4, 12 | h5, 13 | h6, 14 | p, 15 | blockquote, 16 | pre, 17 | a, 18 | abbr, 19 | acronym, 20 | address, 21 | big, 22 | cite, 23 | code, 24 | del, 25 | dfn, 26 | em, 27 | img, 28 | ins, 29 | kbd, 30 | q, 31 | s, 32 | samp, 33 | small, 34 | strike, 35 | strong, 36 | sub, 37 | sup, 38 | tt, 39 | var, 40 | b, 41 | u, 42 | i, 43 | center, 44 | dl, 45 | dt, 46 | dd, 47 | ol, 48 | ul, 49 | li, 50 | fieldset, 51 | form, 52 | label, 53 | legend, 54 | table, 55 | caption, 56 | tbody, 57 | tfoot, 58 | thead, 59 | tr, 60 | th, 61 | td, 62 | article, 63 | aside, 64 | canvas, 65 | details, 66 | embed, 67 | figure, 68 | figcaption, 69 | footer, 70 | header, 71 | hgroup, 72 | menu, 73 | nav, 74 | output, 75 | ruby, 76 | section, 77 | summary, 78 | time, 79 | mark, 80 | audio, 81 | video { 82 | margin: 0; 83 | padding: 0; 84 | border: 0; 85 | font-size: 100%; 86 | font: inherit; 87 | vertical-align: baseline; 88 | box-sizing: border-box; 89 | } 90 | /* HTML5 display-role reset for older browsers */ 91 | article, 92 | aside, 93 | details, 94 | figcaption, 95 | figure, 96 | footer, 97 | header, 98 | hgroup, 99 | menu, 100 | nav, 101 | section { 102 | display: block; 103 | } 104 | body { 105 | line-height: 1; 106 | } 107 | ol, 108 | ul { 109 | list-style: none; 110 | } 111 | blockquote, 112 | q { 113 | quotes: none; 114 | } 115 | blockquote:before, 116 | blockquote:after, 117 | q:before, 118 | q:after { 119 | content: ""; 120 | content: none; 121 | } 122 | table { 123 | border-collapse: collapse; 124 | border-spacing: 0; 125 | } 126 | p { 127 | margin: 0; 128 | } 129 | -------------------------------------------------------------------------------- /app/components/Loading/styles.scss: -------------------------------------------------------------------------------- 1 | @import "@/app/assets/styles/_variables.scss"; 2 | 3 | .loading-screen { 4 | display: flex; 5 | justify-content: center; 6 | align-items: center; 7 | width: 100%; 8 | height: 100vh; 9 | font-size: 2rem; 10 | background-color: url("/assets/images/loading-background.png"); 11 | 12 | .dot { 13 | position: relative; 14 | width: 2rem; 15 | height: 2rem; 16 | margin: 0.8em; 17 | border-radius: 50%; 18 | 19 | &::before { 20 | position: absolute; 21 | content: ""; 22 | width: 100%; 23 | height: 100%; 24 | background: inherit; 25 | border-radius: inherit; 26 | animation: anime 2s ease-out infinite; 27 | } 28 | 29 | &:nth-child(1) { 30 | background-color: $color-gray-4; 31 | 32 | &::before { 33 | animation-delay: 0.2s; 34 | } 35 | } 36 | &:nth-child(2) { 37 | background-color: $color-gray-3; 38 | 39 | &::before { 40 | animation-delay: 0.4s; 41 | } 42 | } 43 | 44 | &:nth-child(3) { 45 | background-color: $color-black; 46 | 47 | &::before { 48 | animation-delay: 0.6s; 49 | } 50 | } 51 | 52 | &:nth-child(4) { 53 | background-color: $color-gray-3; 54 | 55 | &::before { 56 | animation-delay: 0.8s; 57 | } 58 | } 59 | 60 | &:nth-child(5) { 61 | background-color: $color-gray-4; 62 | 63 | &::before { 64 | animation-delay: 1s; 65 | } 66 | } 67 | } 68 | } 69 | 70 | @-webkit-keyframes anime { 71 | 50%, 72 | 75% { 73 | transform: scale(2.5); 74 | } 75 | 80%, 76 | 100% { 77 | opacity: 0; 78 | } 79 | } 80 | 81 | @keyframes anime { 82 | 50%, 83 | 75% { 84 | transform: scale(2.5); 85 | } 86 | 80%, 87 | 100% { 88 | opacity: 0; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /app/components/Search/styles.scss: -------------------------------------------------------------------------------- 1 | @import "../../../app/assets/styles/variables"; 2 | 3 | .searchbar { 4 | div { 5 | label { 6 | color: $color-black !important; 7 | font-size: 1.1rem; 8 | } 9 | 10 | div { 11 | background-color: $color-white; 12 | 13 | .MuiAutocomplete-endAdornment { 14 | border: unset; 15 | } 16 | 17 | fieldset { 18 | border: 1px solid $color-black !important; 19 | color: $color-black !important; 20 | 21 | legend { 22 | color: $color-black !important; 23 | span { 24 | color: $color-black !important; 25 | } 26 | } 27 | } 28 | 29 | input { 30 | color: $color-black; 31 | font-weight: bold; 32 | } 33 | 34 | svg { 35 | width: 2rem; 36 | height: 2rem; 37 | fill: $color-black; 38 | color: $color-black; 39 | } 40 | } 41 | } 42 | 43 | &-list { 44 | &-item { 45 | padding: 1rem; 46 | cursor: pointer; 47 | border-bottom: 1px solid $color-gray-4; 48 | min-height: 3rem; 49 | transition: 0.3s ease-in-out; 50 | 51 | &:hover { 52 | background-color: $color-gray-4; 53 | } 54 | } 55 | 56 | &-item:last-child { 57 | border: none; 58 | } 59 | } 60 | } 61 | 62 | .MuiAutocomplete-popper { 63 | div { 64 | ul { 65 | padding: 0; 66 | } 67 | } 68 | } 69 | 70 | b { 71 | font-weight: 700; 72 | } 73 | 74 | @media (max-width: $mobile-device) { 75 | .searchbar { 76 | div { 77 | label { 78 | font-size: 1.4rem; 79 | } 80 | 81 | input { 82 | font-size: 1.4rem; 83 | } 84 | 85 | div { 86 | svg { 87 | width: 2.6rem; 88 | height: 2.6rem; 89 | } 90 | } 91 | } 92 | 93 | &-list { 94 | &-item { 95 | font-size: 1.4rem; 96 | } 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /app/components/Header/HeaderDialog/styles.scss: -------------------------------------------------------------------------------- 1 | @import "../../../../app/assets/styles/variables"; 2 | 3 | .dialog { 4 | &.MuiPaper-root { 5 | min-width: 400px; 6 | } 7 | &.MuiDialogContent-root { 8 | width: 500px; 9 | } 10 | 11 | &-title { 12 | margin: 0; 13 | padding: 2rem; 14 | } 15 | 16 | &-content { 17 | width: 500px; 18 | display: flex; 19 | flex-direction: row; 20 | align-items: center; 21 | gap: 1rem; 22 | padding: 3rem; 23 | } 24 | } 25 | 26 | .content { 27 | &-info { 28 | width: 50%; 29 | display: flex; 30 | flex-direction: column; 31 | align-items: center; 32 | justify-content: center; 33 | gap: 1rem; 34 | 35 | h5 { 36 | font-size: 1.5rem; 37 | font-weight: 600; 38 | } 39 | 40 | span { 41 | font-size: 1rem; 42 | color: $color-gray-2; 43 | } 44 | 45 | &-links { 46 | display: flex; 47 | flex-direction: row; 48 | align-items: center; 49 | justify-content: center; 50 | flex-wrap: wrap; 51 | gap: 0.5rem; 52 | 53 | a { 54 | text-decoration: none; 55 | color: $color-black; 56 | transition: all 0.3s ease-in-out; 57 | 58 | &:hover { 59 | transform: scale(1.1); 60 | } 61 | } 62 | } 63 | } 64 | } 65 | 66 | .bottom-responsive { 67 | display: flex; 68 | flex-direction: column; 69 | align-items: center; 70 | justify-content: center; 71 | gap: 1.2rem; 72 | width: 100%; 73 | 74 | h4 { 75 | font-size: 2.5rem; 76 | font-weight: 600; 77 | } 78 | 79 | &-info { 80 | width: 100%; 81 | display: flex; 82 | align-items: center; 83 | justify-content: center; 84 | margin-top: 3rem; 85 | } 86 | } 87 | 88 | @media (max-width: $mobile-device) { 89 | .content { 90 | &-info { 91 | width: 100%; 92 | gap: 1.5rem; 93 | 94 | h5 { 95 | font-size: 2rem; 96 | } 97 | 98 | span { 99 | font-size: 1.5rem; 100 | } 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /app/types/index.ts: -------------------------------------------------------------------------------- 1 | export interface LocationData { 2 | chargingStations: LocationResponse[]; 3 | } 4 | 5 | export interface FilteredLocationData { 6 | chargingStations: LocationResponse[]; 7 | total: number; 8 | distance: number; 9 | distanceUnit: string; 10 | } 11 | 12 | export interface FilteredLocationResponse { 13 | data: FilteredLocationData; 14 | config: any; 15 | request: any; 16 | headers: any; 17 | status: number; 18 | } 19 | 20 | export interface FilterFormRequest { 21 | latitude: number; 22 | longitude: number; 23 | distance: number; 24 | size: number; 25 | } 26 | 27 | export interface ChargingStationData { 28 | id: string; 29 | location: Location; 30 | title?: string; 31 | address?: string; 32 | city?: string; 33 | plugs: Plugs[]; 34 | pointOfInterests?: string[]; 35 | plugsTotal?: number; 36 | provider: Providers; 37 | provideLiveStats?: boolean; 38 | } 39 | 40 | export type LocationResponse = ChargingStationData; 41 | 42 | export interface Location { 43 | lat: number; 44 | lon: number; 45 | } 46 | 47 | export type TooltipData = ChargingStationData; 48 | 49 | export interface Plugs { 50 | type: PlugType; 51 | count: number; 52 | power: string; 53 | } 54 | 55 | export type PlugType = "AC" | "DC"; 56 | 57 | export type status = "active" | "inactive" | "inuse"; 58 | 59 | export type Providers = "ESARJ" | "ZES" | "SHARZ" | "AKSAENERGY" | "BEEFULL"; 60 | 61 | export type EVENT_TYPES = "movestart" | "moveend" | "zoomstart" | "zoomend" | "ready"; 62 | 63 | export type ClusterPopupData = { 64 | count: number; 65 | baseMarker: LocationData; 66 | markers: any[]; 67 | }; 68 | 69 | export type DeviceType = "mobile" | "desktop"; 70 | 71 | export enum ProvidersEnum { 72 | ESARJ = "ESARJ", 73 | ZES = "ZES", 74 | SHARZ = "SHARZ", 75 | AKSAENERGY = "AKSAENERGY", 76 | BEEFULL = "BEEFULL" 77 | } 78 | 79 | export type SuggestionChargingStation = { 80 | id: string; 81 | location: Location; 82 | title: string; 83 | }; 84 | 85 | export type SuggestionLocation = { 86 | highlightedText: string; 87 | chargingStation: TooltipData; 88 | }; 89 | 90 | export type SuggestionSearchResponse = { 91 | total: number; 92 | suggestions: SuggestionLocation[]; 93 | }; 94 | -------------------------------------------------------------------------------- /app/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { Icon } from "@iconify-icon/react/dist/iconify.js"; 3 | import { IconButton } from "@mui/material"; 4 | import dynamic from "next/dynamic"; 5 | import { SnackbarProvider, closeSnackbar } from "notistack"; 6 | import { QueryClient, QueryClientProvider } from "react-query"; 7 | import Header from "./components/Header/Header"; 8 | import { StyledIcon, StyledNotistack } from "./utils/notistack"; 9 | 10 | import "leaflet/dist/leaflet.css"; 11 | 12 | const MapContent = dynamic(() => import("./components/Map/MapContent"), { 13 | ssr: false 14 | }); 15 | 16 | export default function Home() { 17 | const queryClient = new QueryClient(); 18 | 19 | return ( 20 | 21 | 29 | 30 | 31 | ), 32 | success: ( 33 | 34 | 35 | 36 | ), 37 | warning: ( 38 | 39 | 40 | 41 | ), 42 | error: ( 43 | 44 | 45 | 46 | ) 47 | }} 48 | Components={{ 49 | default: StyledNotistack, 50 | info: StyledNotistack, 51 | success: StyledNotistack, 52 | warning: StyledNotistack, 53 | error: StyledNotistack 54 | }} 55 | action={(snackbarId) => ( 56 | closeSnackbar(snackbarId)} sx={{ p: 0.5 }}> 57 | 58 | 59 | )}> 60 |
61 | 62 | 63 | 64 | ); 65 | } 66 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import { GoogleAnalytics } from "@next/third-parties/google"; 2 | import type { Metadata } from "next"; 3 | import { Inter } from "next/font/google"; 4 | 5 | import "@/app/assets/styles/_index.scss"; 6 | 7 | const inter = Inter({ subsets: ["latin"] }); 8 | 9 | export const metadata: Metadata = { 10 | title: "sarj.dev - Elektrikli Araç Şarj İstasyonları Haritası", 11 | description: 12 | "Elektrikli araçlar için şarj istasyonlarını bulabileceğiniz interaktif harita uygulaması. sarj.dev ile şarj istasyonlarını bulun ve kullanımınızı kolaylaştırın.", 13 | keywords: 14 | "sarj, sarj.dev, elektrikli araç, şarj, şarj istasyonu, esarj, eşarj, şarjdev, şarj.dev, harita, şarj istasyonu", 15 | themeColor: "#000000", 16 | manifest: "/manifest.json", 17 | viewport: { 18 | width: "device-width", 19 | initialScale: 1, 20 | maximumScale: 1 21 | }, 22 | icons: [ 23 | { 24 | rel: "icon", 25 | url: "/favicon.ico" 26 | } 27 | ], 28 | openGraph: { 29 | type: "website", 30 | url: "https://sarj.dev", 31 | title: "sarj.dev - Elektrikli Araç Şarj İstasyonları Haritası", 32 | description: 33 | "Elektrikli araçlar için şarj istasyonlarını bulabileceğiniz interaktif harita uygulaması. sarj.dev ile şarj istasyonlarını bulun ve kullanımınızı kolaylaştırın.", 34 | images: [ 35 | { 36 | url: "https://sarj.dev/sarjdev-logo.png", 37 | width: 1200, 38 | height: 630, 39 | alt: "sarj.dev Logo" 40 | } 41 | ] 42 | }, 43 | twitter: { 44 | card: "summary_large_image", 45 | title: "sarj.dev - Elektrikli Araç Şarj İstasyonları Haritası", 46 | description: 47 | "Elektrikli araçlar için şarj istasyonlarını bulabileceğiniz interaktif harita uygulaması. sarj.dev ile şarj istasyonlarını bulun ve kullanımınızı kolaylaştırın.", 48 | images: [ 49 | { 50 | url: "https://sarj.dev/sarjdev-logo.png", 51 | width: 1200, 52 | height: 630, 53 | alt: "sarj.dev Logo" 54 | } 55 | ] 56 | } 57 | }; 58 | 59 | export default function RootLayout({ children }: { children: React.ReactNode }) { 60 | return ( 61 | 62 | {children} 63 | 64 | 65 | ); 66 | } 67 | -------------------------------------------------------------------------------- /app/utils/general-utils.ts: -------------------------------------------------------------------------------- 1 | import { PlugType, Providers, ProvidersEnum } from "../types"; 2 | import { Plug } from "../types/common"; 3 | import { SearchDetail } from "../types/search-detail"; 4 | import { SearchNearestChargingStation } from "../types/search-nearest"; 5 | 6 | export const checkPlugsType = ( 7 | tooltipData: SearchDetail | SearchNearestChargingStation, 8 | type: PlugType 9 | ): boolean => { 10 | return tooltipData?.plugs?.some((plug) => plug.type === type) ?? false; 11 | }; 12 | 13 | export const getPlugData = ( 14 | tooltipData: SearchDetail | SearchNearestChargingStation, 15 | type: PlugType 16 | ): Plug[] => { 17 | return tooltipData?.plugs?.filter((plug) => plug.type === type) ?? []; 18 | }; 19 | 20 | export const handleClickProvider = (company: Providers): string => { 21 | switch (company) { 22 | case ProvidersEnum.ESARJ: 23 | return "https://esarj.com/"; 24 | case ProvidersEnum.SHARZ: 25 | return "https://www.sharz.net/"; 26 | case ProvidersEnum.ZES: 27 | return "https://zes.net/?utm_source=digital_media&utm_medium=footer_logo&utm_campaign=ZES_borusan&utm_id=borusan"; 28 | case ProvidersEnum.AKSAENERGY: 29 | return "https://www.aksasarj.com.tr/"; 30 | case ProvidersEnum.BEEFULL: 31 | return "https://beefull.com/Elektrikli-Arac-Sarj-Istasyonlari"; 32 | default: 33 | return "https://sarj.dev/"; 34 | } 35 | }; 36 | 37 | export const UserLocationMarker = 38 | "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAABHNCSVQICAgIfAhkiAAAAF96VFh0UmF3IHByb2ZpbGUgdHlwZSBBUFAxAABo3uNKT81LLcpMVigoyk/LzEnlUgADYxMuE0sTS6NEAwMDCwMIMDQwMDYEkkZAtjlUKNEABZgamFmaGZsZmgMxiM8FAEi2FMnxHlGkAAADqElEQVRo3t1aTWgTQRQOiuDPQfHs38GDogc1BwVtQxM9xIMexIN4EWw9iAehuQdq0zb+IYhglFovClXQU+uhIuqh3hQll3iwpyjG38Zkt5uffc4XnHaSbpLZ3dnEZOBB2H3z3jeZN+9vx+fzYPgTtCoQpdVHrtA6EH7jme+/HFFawQBu6BnWNwdGjB2BWH5P32jeb0V4B54KL5uDuW3D7Y/S2uCwvrUR4GaEuZABWS0FHhhd2O4UdN3FMJneLoRtN7Y+GMvvUw2eE2RDh3LTOnCd1vQN5XZ5BXwZMV3QqQT84TFa3zuU39sy8P8IOqHb3T8fpY1emoyMSQGDI/Bwc+0ELy6i4nLtepp2mE0jc5L3UAhMsdxut0rPJfRDN2eMY1enF8Inbmj7XbtZhunkI1rZFD/cmFMlr1PFi1/nzSdGkT5RzcAzvAOPU/kVF9s0ujqw+9mP5QgDmCbJAV7McXIeGpqS3Qg7OVs4lTfMD1Yg9QLR518mZbImFcvWC8FcyLAbsev++3YETb0tn2XAvouAvjGwd14YdCahUTCWW6QQIzzDO/CIAzKm3pf77ei23AUkVbICHr8pnDZNynMQJfYPT7wyKBzPVQG3IvCAtyTsCmRBprQpMawWnkc+q2Rbn+TK/+gmRR7qTYHXEuZkdVM0p6SdLLYqX0LItnFgBxe3v0R04b5mGzwnzIUMPiBbFkdVmhGIa5tkJ4reZvyl4Rg8p3tMBh+FEqUduVRUSTKTnieL58UDG76cc70AyMgIBxs6pMyIYV5agKT9f/ltTnJFOIhuwXOCLD6gQ/oc8AJcdtuYb09xRQN3NWULgCwhfqSk3SkaBZViRTK3EYNUSBF4Hic0Y8mM+if0HhlMlaIHbQ8Z5lszxnGuIP2zrAw8J8jkA7pkMAG79AKuPTOOcgWZeVP5AsSDjAxWegGyJoSUWAj/FBpRa0JiviSbfldMqOMPcce7UVeBLK4gkMVVBLI2phLjKlIJm8lcxMNkLuIomXOTTmc1kwYf2E+nMQdzlaTTKgoaZJWyBQ141RY0DkrK6XflAQbih1geZnhJeXu5WeEZ3mVqSkrIgCzXJaXqoh65TUuLerdtFXgQ2bYKeD1pq6hobLE86SlztXMWvaA5vPO0sYWB9p2K1iJS4ra0Fju/udsN7fWu+MDRFZ+YuuIjX1d8Zu2OD92WC9G3ub1qABktBV7vssfBMX1L7yVjZ7PLHuABb9svezS7boNDyK/b4LdX123+Au+jOmNxrkG0AAAAAElFTkSuQmCC"; 39 | -------------------------------------------------------------------------------- /app/components/Cluster/Cluster.tsx: -------------------------------------------------------------------------------- 1 | import L from "leaflet"; 2 | import { FC } from "react"; 3 | import { Marker, useMap } from "react-leaflet"; 4 | import useSupercluster from "use-supercluster"; 5 | import MarkerComponent from "../Marker/MarkerComponent"; 6 | import { findClusterData } from "./ClusterData"; 7 | 8 | import { ChargingStation } from "@/app/types/search"; 9 | import "./styles.scss"; 10 | 11 | const getIcon = (count: number) => { 12 | const data = findClusterData(count); 13 | 14 | return L.divIcon({ 15 | html: `
${count}
`, 16 | className: `leaflet-marker-icon marker-cluster leaflet-interactive` 17 | }); 18 | }; 19 | 20 | type Props = { 21 | data: ChargingStation[] | null; 22 | }; 23 | 24 | export const Cluster: FC = ({ data }) => { 25 | const map = useMap(); 26 | const bounds = map.getBounds(); 27 | 28 | const geoJSON = 29 | data 30 | ?.filter((item: ChargingStation) => item?.geoLocation?.lat && item?.geoLocation?.lon) 31 | .map((item: ChargingStation) => { 32 | return { 33 | type: "Feature", 34 | geometry: { 35 | type: "Point", 36 | coordinates: [item?.geoLocation?.lon, item?.geoLocation?.lat] 37 | }, 38 | item, 39 | properties: { cluster: false, id: item.id } 40 | }; 41 | }) ?? []; 42 | 43 | const { clusters, supercluster } = useSupercluster({ 44 | points: geoJSON, 45 | bounds: [ 46 | bounds.getSouthWest().lng, 47 | bounds.getSouthWest().lat, 48 | bounds.getNorthEast().lng, 49 | bounds.getNorthEast().lat 50 | ], 51 | zoom: map.getZoom(), 52 | options: { radius: 300, maxZoom: 13 } 53 | }); 54 | 55 | return ( 56 | <> 57 | {clusters.map((cluster) => { 58 | const [longitude, latitude] = cluster.geometry.coordinates; 59 | const { cluster: isCluster, point_count: pointCount, id } = cluster.properties; 60 | if (isCluster) { 61 | return ( 62 | { 68 | const expansionZoom = Math.min( 69 | supercluster.getClusterExpansionZoom(cluster.id), 70 | 18 71 | ); 72 | map.setView([latitude, longitude], expansionZoom, { 73 | animate: true 74 | }); 75 | } 76 | }} 77 | /> 78 | ); 79 | } 80 | 81 | return ( 82 | 88 | ); 89 | })} 90 | 91 | ); 92 | }; 93 | -------------------------------------------------------------------------------- /app/components/Header/HeaderDialog/HeaderDialog.tsx: -------------------------------------------------------------------------------- 1 | import { useResponsive } from "@/app/hooks/useResponsive"; 2 | import { Icon } from "@iconify-icon/react/dist/iconify.js"; 3 | import { Dialog, DialogContent, DialogTitle, IconButton } from "@mui/material"; 4 | import { styled } from "@mui/material/styles"; 5 | import Link from "next/link"; 6 | import { FC } from "react"; 7 | import BottomSheet from "../../BottomSheet/BottomSheet"; 8 | 9 | import "./styles.scss"; 10 | 11 | type Props = { 12 | open: boolean; 13 | handleClose: VoidFunction; 14 | }; 15 | 16 | const BootstrapDialog = styled(Dialog)(({ theme }) => ({ 17 | "& .MuiDialogContent-root": { 18 | padding: theme.spacing(2), 19 | flex: "1 0 100%" 20 | }, 21 | "& .MuiDialogActions-root": { 22 | padding: theme.spacing(1) 23 | } 24 | })); 25 | 26 | const HeaderDialog: FC = ({ open, handleClose }) => { 27 | const mdUp = useResponsive("up", "md"); 28 | 29 | const content = ( 30 | <> 31 |
32 |
Yusuf Yılmaz
33 | Back-end Developer 34 |
35 | 36 | 37 | 38 | 39 | 40 | 41 |
42 |
43 |
44 |
Mehmet Mutlu
45 | Front-end Developer 46 |
47 | 48 | 49 | 50 | 51 | 52 | 53 |
54 |
55 | 56 | ); 57 | 58 | return mdUp ? ( 59 | 64 | 65 | İletişim Bilgileri 66 | 67 | theme.palette.grey[500] 75 | }}> 76 | 77 | 78 | 79 | {content} 80 | 81 | 82 | ) : ( 83 | 84 |
85 |

İletişim Bilgileri

86 |
{content}
87 |
88 |
89 | ); 90 | }; 91 | 92 | export default HeaderDialog; 93 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sarj.dev - Electric Vehicle Charging Station Map App (Front-End) 2 | 3 |

4 | 5 |

6 | 7 | 🌿 Welcome to **[sarj.dev](https://sarj.dev/)**! This open-source project provides a back-end solution for an interactive map application focused on Electric Vehicle (EV) charging stations across Turkey. Developed using _Nextjs, Zustand, TypeScript, SCSS, MUI, React-Leaflet, React-hook-form, Yupjs, React-Query and Notistack_ this project empowers EV owners to find, track, and plan their charging needs seamlessly. 8 | 9 | ## Project Overview 10 | 11 | [sarj.dev](https://sarj.dev/) aims to enhance the electric vehicle charging experience in Turkey by offering a comprehensive map application that includes real-time charging station data, search functionalities, and nearby station recommendations. 12 | 13 | ## Features 14 | 15 | - 🗺️ **Interactive Map:** Visualize electric vehicle charging stations on an interactive map. 16 | - ⚡ **Real-time Data:** Access up-to-date information about each charging station, including socket availability, power capacity, and pricing details. 17 | - 🔍 **Advanced Search:** Utilize the search feature to find charging stations based on specific criteria. 18 | - 📍 **Nearby Stations:** Get a list of charging stations near your location for convenient access. 19 | - 🔗 **Search Suggestions:** Receive search suggestions for quicker station discovery. 20 | 21 | ## Installation 22 | 23 | 1. Clone the project: `git clone https://github.com/sarjdev/front-end.git` 24 | 2. Install required dependencies using your preferred build tool (yarn install). 25 | 3. Start the application: Run `yarn dev` in the project root. 26 | 27 | ## Usage 28 | 29 | 1. Once the application is up and running, access it through your browser at `http://localhost:3000`. 30 | 2. Explore the map to view charging stations. Click on a station to reveal more information. 31 | 3. Use the search bar to filter stations based on specific attributes. 32 | 4. To view nearby charging stations, you might need to grant location permission. 33 | 34 | ## How to Contribute 35 | 36 | - Create a new branch for your feature: `git checkout -b feature/your-feature` 37 | - Make your changes and stage them using `git add`. 38 | - Commit your changes with a meaningful message: `git commit -m "Add your message here"` ([Commit Standards](https://www.conventionalcommits.org/en/v1.0.0/)). 39 | - Push your branch to your forked repository: `git push origin feature/your-feature`. 40 | - Create a pull request in the original repository and await review. 41 | 42 | ## License 43 | 44 | This project is licensed under the [GNU General Public License v3.0](https://github.com/sarjdev/front-end/blob/main/LICENSE) 45 | 46 | ## Acknowledgements 47 | 48 | We would like to express our gratitude to the contributors of this project and the open-source community for their valuable contributions and support. 49 | 50 | ## Get in Touch 51 | 52 | For questions, suggestions, or collaborations, please contact us at [hi@sarj.dev](mailto:hi@sarj.dev) or visit our website at [https://sarj.dev](https://sarj.dev). 53 | 54 | 🚀 Let's contribute to a greener future together! 🌍 55 | 56 | --- 57 | -------------------------------------------------------------------------------- /app/components/Search/SearchBar.tsx: -------------------------------------------------------------------------------- 1 | import { useDebounce } from "@/app/hooks/useDebounce"; 2 | import { useResponsive } from "@/app/hooks/useResponsive"; 3 | import { SuggestionLocation } from "@/app/types"; 4 | import { Autocomplete, TextField } from "@mui/material"; 5 | import { useSnackbar } from "notistack"; 6 | import { FC, useEffect, useRef, useState } from "react"; 7 | import ReactHtmlParser from "react-html-parser"; 8 | import { useMap } from "react-leaflet"; 9 | import { useSearch } from "./actions"; 10 | 11 | import "./styles.scss"; 12 | 13 | const SearchBar: FC = () => { 14 | const map = useMap(); 15 | const [inputValue, setInputValue] = useState(""); 16 | const [options, setOptions] = useState([]); 17 | const mdUp = useResponsive("up", "md"); 18 | const { enqueueSnackbar } = useSnackbar(); 19 | const inputRef = useRef(null); 20 | 21 | const debouncedValue = useDebounce(inputValue); 22 | 23 | const getSearchSuggestion = useSearch({ 24 | q: debouncedValue 25 | }); 26 | 27 | useEffect(() => { 28 | const fetchData = async () => { 29 | try { 30 | const response = await getSearchSuggestion.refetch(); 31 | const searchData = response.data; 32 | setOptions(searchData?.suggestions ?? []); 33 | } catch (error) { 34 | enqueueSnackbar("Arama sonuçları çekilirken bir hata oluştu!", { variant: "error" }); 35 | } 36 | }; 37 | 38 | if (inputValue) { 39 | fetchData(); 40 | } 41 | }, [debouncedValue]); 42 | 43 | const handleClickOption = (option: SuggestionLocation) => { 44 | map.flyTo([option.chargingStation.location.lat, option.chargingStation.location.lon], 17, { 45 | animate: true 46 | }); 47 | setOptions([]); 48 | setInputValue(""); 49 | inputRef.current.blur(); 50 | inputRef.current?.querySelector("input").blur(); 51 | }; 52 | 53 | return ( 54 | <> 55 | x} 69 | options={options} 70 | noOptionsText="Kelime yazarak arama yapabilirsiniz" 71 | onChange={(event: any, newValue: SuggestionLocation | null) => { 72 | setOptions(newValue ? [newValue, ...options] : options); 73 | }} 74 | onInputChange={(event, newInputValue) => { 75 | setInputValue(newInputValue); 76 | }} 77 | renderInput={(params) => } 78 | renderOption={(props, option) => { 79 | return ( 80 |
  • handleClickOption(option)}> 85 | {ReactHtmlParser(option.highlightedText)} 86 |
  • 87 | ); 88 | }} 89 | /> 90 | 91 | ); 92 | }; 93 | 94 | export default SearchBar; 95 | -------------------------------------------------------------------------------- /app/components/Marker/MarkerComponent.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import defaultMarker from "@/app/assets/images/marker.svg"; 4 | import { useResponsive } from "@/app/hooks/useResponsive"; 5 | import { useGeneralStore } from "@/app/stores/generalStore"; 6 | import { SearchDetail } from "@/app/types/search-detail"; 7 | import Leaflet, { LatLngExpression } from "leaflet"; 8 | import { useSnackbar } from "notistack"; 9 | import { FC, useLayoutEffect, useState } from "react"; 10 | import { Marker, Popup, useMap } from "react-leaflet"; 11 | import CustomPopup from "./CustomPopup/CustomPopup"; 12 | import ErrorPopup from "./ErrorPopup/ErrorPopup"; 13 | import LoadingPopup from "./LoadingPopup/LoadingPopup"; 14 | import { renderIcon } from "./MarkerIcons"; 15 | import { useGetCertaionLocation } from "./actions"; 16 | 17 | import "./styles.scss"; 18 | 19 | interface MarkerProps { 20 | position: LatLngExpression; 21 | icon: string; 22 | chargingStationId: string; 23 | } 24 | 25 | const MarkerComponent: FC = ({ position, icon, chargingStationId }) => { 26 | const map = useMap(); 27 | const [tooltipData, setTooltipData] = useState(null); 28 | const [hasError, setHasError] = useState(false); 29 | const { enqueueSnackbar } = useSnackbar(); 30 | const mdUp = useResponsive("up", "md"); 31 | const { actions } = useGeneralStore(); 32 | 33 | const getLocationDetail = useGetCertaionLocation({ 34 | chargingStationId: chargingStationId 35 | }); 36 | 37 | useLayoutEffect(() => { 38 | if (getLocationDetail.isError) { 39 | setTooltipData(null); 40 | setHasError(true); 41 | } 42 | }, [getLocationDetail.isError]); 43 | 44 | const handleMarkerClick = async () => { 45 | setTooltipData(null); 46 | setHasError(false); 47 | 48 | const newLat = (position as Leaflet.LatLngTuple)?.[0] + 0.005; 49 | 50 | map.flyTo([newLat, (position as Leaflet.LatLngTuple)?.[1]], 15); 51 | 52 | if (!mdUp) { 53 | actions.setMarkerBottomSheetOpen(true); 54 | } 55 | 56 | try { 57 | const { data } = await getLocationDetail.refetch(); 58 | 59 | mdUp ? setTooltipData(data ?? null) : actions.setMarkerBottomSheetData(data ?? null); 60 | } catch (error) { 61 | enqueueSnackbar("Şarj istasyonu verisi çekilirken bir hata oluştu!", { variant: "error" }); 62 | } 63 | }; 64 | 65 | const markerIcon = new Leaflet.Icon({ 66 | iconUrl: renderIcon(icon) || defaultMarker.src, 67 | iconRetinaUrl: renderIcon(icon) || defaultMarker.src, 68 | iconSize: [48, 48], 69 | iconAnchor: [14, 14] 70 | }); 71 | 72 | return ( 73 | 79 | {mdUp ? ( 80 | tooltipData ? ( 81 | 82 | 83 | 84 | ) : hasError ? ( 85 | 86 | 87 | 88 | ) : ( 89 | 90 | 91 | 92 | ) 93 | ) : null} 94 | 95 | ); 96 | }; 97 | 98 | export default MarkerComponent; 99 | -------------------------------------------------------------------------------- /app/components/Filter/FilterForm/FilterForm.tsx: -------------------------------------------------------------------------------- 1 | import { FilterFormSchema } from "@/app/schema/filterFormSchema"; 2 | import { useGeneralStore } from "@/app/stores/generalStore"; 3 | import { useMapGeographyStore } from "@/app/stores/mapGeographyStore"; 4 | import { Location } from "@/app/types"; 5 | import { yupResolver } from "@hookform/resolvers/yup"; 6 | import { useSnackbar } from "notistack"; 7 | import { FC, useState } from "react"; 8 | import { useForm } from "react-hook-form"; 9 | import Button from "../../Button/Button"; 10 | import FormProvider from "../../Form/FormProvider/FormProvider"; 11 | import RangeInput from "../../Form/RangeInput/RangeInput"; 12 | import FilteredCard from "../FilteredCard/FilteredCard"; 13 | import { useGetFilteredData } from "./actions"; 14 | 15 | import "./styles.scss"; 16 | 17 | type FilteredCardType = { 18 | handleClickToCenter: (location: Location) => void; 19 | }; 20 | 21 | const FilterForm: FC = ({ handleClickToCenter }) => { 22 | const [loading, setLoading] = useState(false); 23 | const methods = useForm({ 24 | resolver: yupResolver(FilterFormSchema), 25 | defaultValues: { 26 | distance: 10, 27 | size: 10 28 | } 29 | }); 30 | const { actions } = useGeneralStore(); 31 | const { location } = useMapGeographyStore(); 32 | const { enqueueSnackbar } = useSnackbar(); 33 | 34 | const { watch, handleSubmit } = methods; 35 | 36 | const values = watch(); 37 | 38 | const filterData = useGetFilteredData(); 39 | 40 | const onSubmit = handleSubmit(async (data) => { 41 | if (location && location?.[0] && location?.[1]) { 42 | setLoading(true); 43 | try { 44 | filterData.mutate( 45 | { 46 | longitude: location?.[1] ?? 0, 47 | latitude: location?.[0] ?? 0, 48 | distance: data.distance, 49 | size: data.size 50 | }, 51 | { 52 | onSuccess: (data) => { 53 | setLoading(false); 54 | actions.setFilteredLocationData(data); 55 | }, 56 | onError: (error) => { 57 | setLoading(false); 58 | enqueueSnackbar("Konumlar filtrelenirken bir hata oluştu", { variant: "error" }); 59 | } 60 | } 61 | ); 62 | } catch (error) { 63 | setLoading(false); 64 | enqueueSnackbar("Konumlar filtrelenirken bir hata oluştu", { variant: "error" }); 65 | } 66 | } else { 67 | enqueueSnackbar("Filtreleme yapabilmek için konum erişimine izin vermeniz gerekmektedir!", { 68 | variant: "warning" 69 | }); 70 | } 71 | }); 72 | 73 | return ( 74 |
    75 | 76 |

    Filtrele

    77 |
    78 | Mesafe {`(${values.distance} km)`} 79 | 80 |
    81 |
    82 | Adet {`(${values.size})`} 83 | 84 |
    85 |
    89 | ); 90 | }; 91 | 92 | export default FilterForm; 93 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true 5 | }, 6 | "extends": [ 7 | "airbnb-typescript", 8 | "prettier", 9 | "plugin:@typescript-eslint/recommended", 10 | "plugin:import/recommended", 11 | "plugin:react/recommended", 12 | "airbnb/hooks", 13 | "next" 14 | ], 15 | "parser": "@typescript-eslint/parser", 16 | "parserOptions": { 17 | "ecmaFeatures": { 18 | "jsx": true 19 | }, 20 | "ecmaVersion": 12, 21 | "sourceType": "module", 22 | "project": ["tsconfig.json"] 23 | }, 24 | "plugins": ["react", "@typescript-eslint"], 25 | "rules": { 26 | "react/no-unstable-nested-components": "off", 27 | "react/jsx-sort-props": ["error", { "shorthandFirst": true }], 28 | "import/no-extraneous-dependencies": ["off", { "devDependencies": ["**/*.stories.tsx"] }], 29 | "@typescript-eslint/no-var-requires": "off", 30 | "react/require-default-props": [0], 31 | "@typescript-eslint/no-unused-vars": ["error", { "ignoreRestSiblings": true }], 32 | "no-unused-vars": "off", 33 | "react/prop-types": "off", 34 | "no-use-before-define": "off", 35 | "@typescript-eslint/no-use-before-define": ["off"], 36 | "react/jsx-filename-extension": [1, { "extensions": [".tsx", ".ts"] }], 37 | "import/prefer-default-export": "off", 38 | "react/jsx-key": 2, 39 | "react/jsx-props-no-spreading": "off", 40 | "react/jsx-no-bind": "off", 41 | "import/no-named-as-default-member": "off", 42 | "import/default": "off", 43 | "react/display-name": "off", 44 | "import/no-named-as-default": 0, 45 | "import/order": [ 46 | "error", 47 | { 48 | "alphabetize": { 49 | "order": "asc" 50 | }, 51 | "groups": ["builtin", "external", "internal"], 52 | "pathGroupsExcludedImportTypes": ["react"], 53 | "pathGroups": [ 54 | { 55 | "pattern": "react", 56 | "group": "external", 57 | "position": "before" 58 | }, 59 | { 60 | "pattern": "components/**", 61 | "group": "internal", 62 | "position": "before" 63 | }, 64 | { 65 | "pattern": "screens/**", 66 | "group": "internal", 67 | "position": "before" 68 | }, 69 | { 70 | "pattern": "utils/**", 71 | "group": "internal", 72 | "position": "before" 73 | }, 74 | { 75 | "pattern": "locales/**", 76 | "group": "internal", 77 | "position": "before" 78 | }, 79 | { 80 | "pattern": "{type,store,hooks,navigations}/**", 81 | "group": "internal", 82 | "position": "before" 83 | }, 84 | { 85 | "pattern": "assets/**", 86 | "group": "internal", 87 | "position": "before" 88 | }, 89 | { 90 | "pattern": "common/**", 91 | "group": "internal", 92 | "position": "before" 93 | }, 94 | { 95 | "pattern": "../**", 96 | "group": "internal", 97 | "position": "before" 98 | } 99 | ], 100 | "newlines-between": "always" 101 | } 102 | ] 103 | }, 104 | "settings": { 105 | "import/parsers": { 106 | "@typescript-eslint/parser": [".ts", ".tsx"] 107 | }, 108 | "import/resolver": { 109 | "typescript": { 110 | "alwaysTryTypes": true, 111 | "extensions": [".js", ".jsx", ".ts", ".tsx"], 112 | "project": "./" 113 | } 114 | }, 115 | "react": { 116 | "version": "detect" // Tells eslint-plugin-react to automatically detect the version of React to use 117 | } 118 | }, 119 | "ignorePatterns": ["node_modules/", "metro.config.js", "src/api/*"] 120 | } 121 | -------------------------------------------------------------------------------- /app/assets/images/mz.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | zes 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /app/assets/images/mtrugo.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | trugo 5 | 6 | 7 | 10 | 11 | 12 | 13 | 14 | 17 | 18 | 19 | 22 | 23 | 24 | 27 | 28 | 29 | 32 | 33 | 34 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /app/components/Filter/FilteredCard/FilteredCard.tsx: -------------------------------------------------------------------------------- 1 | import { useGeneralStore } from "@/app/stores/generalStore"; 2 | import { Location } from "@/app/types"; 3 | import { Icon } from "@iconify-icon/react/dist/iconify.js"; 4 | import classNames from "classnames"; 5 | import Link from "next/link"; 6 | import { FC } from "react"; 7 | import Button from "../../Button/Button"; 8 | 9 | import { checkPlugsType, getPlugData } from "@/app/utils/general-utils"; 10 | import "./styles.scss"; 11 | 12 | type FilteredCardType = { 13 | handleClickToCenter: (location: Location) => void; 14 | }; 15 | 16 | const FilteredCard: FC = ({ handleClickToCenter }) => { 17 | const { filteredLocationData } = useGeneralStore(); 18 | 19 | return ( 20 |
    21 | {filteredLocationData?.total ? ( 22 | filteredLocationData?.chargingStations?.map((item) => ( 23 |
    24 |
    25 |
    {item?.title}
    26 | 32 | {item?.operator.brand} 33 | 34 |
    35 |
    40 |

    {item?.stationActive ? "Kullanıma uygun" : "Kullanıma uygun değil"}

    41 |
    42 |
    43 | 44 |

    {item?.location?.address}

    45 |
    46 |
    47 | 48 | 49 | {item?.phone} 50 | 51 |
    52 |
    100 | )) 101 | ) : ( 102 |
    103 | {filteredLocationData?.total === undefined 104 | ? "Lütfen filtreleme yapınız!" 105 | : "Herhangi bir konum bilgisi bulunamadı!"} 106 |
    107 | )} 108 |
    109 | ); 110 | }; 111 | 112 | export default FilteredCard; 113 | -------------------------------------------------------------------------------- /app/components/Map/MapContent.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { useMapEvents } from "@/app/hooks/useMapEvents"; 3 | import useUserLocation from "@/app/hooks/useUserLocation"; 4 | import { useGeneralStore } from "@/app/stores/generalStore"; 5 | import { useMapGeographyStore } from "@/app/stores/mapGeographyStore"; 6 | import { Location } from "@/app/types"; 7 | import { UserLocationMarker } from "@/app/utils/general-utils"; 8 | import classNames from "classnames"; 9 | import Leaflet, { LatLng, LatLngBounds, LatLngTuple } from "leaflet"; 10 | import { FC, useRef } from "react"; 11 | import { MapContainer, Marker, TileLayer } from "react-leaflet"; 12 | import BottomSheet from "../BottomSheet/BottomSheet"; 13 | import { Cluster } from "../Cluster/Cluster"; 14 | import FilterForm from "../Filter/FilterForm/FilterForm"; 15 | import HelperButtonGroup from "../HelperButtons/HelperButtonGroup"; 16 | import Loading from "../Loading/Loading"; 17 | import SearchBar from "../Search/SearchBar"; 18 | import { useGetLocations } from "./actions"; 19 | 20 | import { SearchNearest } from "@/app/types/search-nearest"; 21 | import "leaflet.markercluster/dist/MarkerCluster.Default.css"; 22 | import "leaflet.markercluster/dist/MarkerCluster.css"; 23 | import "leaflet/dist/leaflet.css"; 24 | import CustomPopup from "../Marker/CustomPopup/CustomPopup"; 25 | import LoadingPopup from "../Marker/LoadingPopup/LoadingPopup"; 26 | import "./styles.scss"; 27 | 28 | const MapEvents = () => { 29 | useMapEvents(); 30 | return null; 31 | }; 32 | 33 | const MapContent: FC = () => { 34 | const { zoom } = useMapGeographyStore(); 35 | const locationData = useGetLocations(); 36 | const { location: userLocation, loading } = useUserLocation(); 37 | const { isBottomSheetOpen, isMarkerBottomSheetOpen, markerBottomSheetData, actions } = 38 | useGeneralStore(); 39 | const mapRef = useRef(null); 40 | 41 | const mapBoundaries = new LatLngBounds(new LatLng(30.0, 25.0), new LatLng(44.0, 45.0)); 42 | 43 | const additionalBounds = [ 44 | new LatLng(35.0, 32.0), 45 | new LatLng(35.0, 38.0), 46 | new LatLng(33.0, 42.0), 47 | new LatLng(40.0, 45.0), 48 | new LatLng(42.0, 45.0), 49 | new LatLng(43.0, 28.0) 50 | ]; 51 | 52 | additionalBounds.forEach((coord) => { 53 | mapBoundaries.extend(coord); 54 | }); 55 | 56 | if (locationData?.isFetching || locationData?.isRefetching || loading) { 57 | return ; 58 | } 59 | 60 | const dpr = window.devicePixelRatio; 61 | const baseMapUrl = `https://mt0.google.com/vt/scale=${dpr}&hl=en&x={x}&y={y}&z={z}`; 62 | 63 | const locationCenter: LatLngTuple = [39.9255, 32.8663]; 64 | 65 | const handleClickToCenter = (location: Location) => { 66 | if (mapRef.current) { 67 | const selectedLocationData: [number, number] | undefined = location 68 | ? [location.lat, location.lon] 69 | : undefined; 70 | 71 | actions.setBottomSheetOpen(false); 72 | mapRef.current.flyTo(selectedLocationData, 17); 73 | } 74 | }; 75 | 76 | const userLocationIcon = new Leaflet.Icon({ 77 | iconUrl: UserLocationMarker, 78 | iconRetinaUrl: UserLocationMarker, 79 | iconSize: [36, 36], 80 | iconAnchor: [14, 14] 81 | }); 82 | 83 | return ( 84 | <> 85 | 99 | 100 | 101 | 102 | {zoom >= 14 && userLocation ? ( 103 | 104 | ) : null} 105 | 106 | 107 | 108 | {!isMarkerBottomSheetOpen ? ( 109 | { 112 | actions.setBottomSheetOpen(false); 113 | actions.setFilteredLocationData({} as SearchNearest); 114 | }}> 115 | 116 | 117 | ) : null} 118 | 119 | {!isBottomSheetOpen ? ( 120 | { 124 | actions.setMarkerBottomSheetOpen(false); 125 | actions.setMarkerBottomSheetData(null); 126 | }}> 127 | {markerBottomSheetData ? ( 128 | 129 | ) : ( 130 |
    131 | 132 |
    133 | )} 134 |
    135 | ) : null} 136 | 137 | ); 138 | }; 139 | 140 | export default MapContent; 141 | -------------------------------------------------------------------------------- /app/assets/images/mvoltrun.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | voltrun 5 | 6 | 7 | 10 | 11 | 12 | 13 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /app/components/Filter/FilteredCard/styles.scss: -------------------------------------------------------------------------------- 1 | @import "../../../../app/assets/styles/variables"; 2 | 3 | .card { 4 | &-empty { 5 | width: 100%; 6 | text-align: center; 7 | display: flex; 8 | align-items: center; 9 | justify-content: center; 10 | font-size: 1.2rem; 11 | } 12 | 13 | &-container { 14 | width: 100%; 15 | display: flex; 16 | flex-direction: row; 17 | gap: 1.5rem; 18 | overflow-y: auto; 19 | margin: 0; 20 | margin-left: 1rem; 21 | 22 | &-responsive { 23 | width: 100%; 24 | height: 70%; 25 | display: flex; 26 | flex-direction: column; 27 | gap: 1rem; 28 | overflow-x: auto; 29 | margin: 0; 30 | margin-top: 3rem; 31 | } 32 | } 33 | 34 | &-item { 35 | flex: 0 0 340px; 36 | height: 380px; 37 | padding: 1rem; 38 | border-radius: 4px; 39 | border: 1px solid $color-gray-4; 40 | 41 | &-responsive { 42 | flex: 0 1 220px; 43 | } 44 | 45 | &-header { 46 | display: flex; 47 | justify-content: space-between; 48 | align-items: center; 49 | padding-bottom: 0.5rem; 50 | border-bottom: 2px solid $color-gray; 51 | 52 | &-title { 53 | font-size: 1.4rem; 54 | font-weight: 700; 55 | line-height: normal; 56 | display: -webkit-box; 57 | -webkit-box-orient: vertical; 58 | overflow: hidden; 59 | -webkit-line-clamp: 1; 60 | text-overflow: ellipsis; 61 | } 62 | 63 | &-provider { 64 | font-size: 1.3rem; 65 | text-decoration: none; 66 | font-weight: 700; 67 | line-height: normal; 68 | display: -webkit-box; 69 | -webkit-box-orient: vertical; 70 | overflow: hidden; 71 | -webkit-line-clamp: 1; 72 | text-overflow: ellipsis; 73 | } 74 | } 75 | 76 | &-suitability { 77 | font-size: 1.2rem; 78 | font-weight: 700; 79 | margin: 0.8rem 0; 80 | 81 | &-okay { 82 | color: $color-green !important; 83 | } 84 | 85 | &-notokay { 86 | color: $color-red-2 !important; 87 | } 88 | 89 | p { 90 | margin: 0; 91 | } 92 | } 93 | 94 | &-location { 95 | display: flex; 96 | gap: 0.5rem; 97 | align-items: center; 98 | margin: 0.5rem 0; 99 | 100 | &-icon { 101 | font-size: 2rem; 102 | fill: $color-gray-2; 103 | color: $color-gray-2; 104 | } 105 | 106 | &-text { 107 | font-size: 1rem; 108 | font-weight: 400; 109 | display: -webkit-box; 110 | -webkit-box-orient: vertical; 111 | overflow: hidden; 112 | -webkit-line-clamp: 2; 113 | text-overflow: ellipsis; 114 | text-decoration: none; 115 | color: $color-gray-2 !important; 116 | } 117 | } 118 | 119 | &-socket { 120 | display: flex; 121 | flex-direction: column; 122 | justify-content: center; 123 | align-items: flex-start; 124 | width: 100%; 125 | margin-top: 0.8rem; 126 | gap: 0.8rem; 127 | 128 | &-container { 129 | display: flex; 130 | align-items: center; 131 | font-size: 1.2rem; 132 | padding: 0.5rem 0; 133 | padding-left: 0.5rem; 134 | gap: 5px; 135 | background-color: $color-gray-4; 136 | width: 100%; 137 | 138 | p { 139 | margin: 0; 140 | } 141 | 142 | &-icon { 143 | background-color: $color-gray-2; 144 | color: $color-gray; 145 | padding: 0.6rem 0.4rem; 146 | border-radius: 50%; 147 | 148 | &-okay { 149 | background-color: $color-green-2 !important; 150 | } 151 | } 152 | 153 | &-distance { 154 | font-size: 2.4rem; 155 | } 156 | } 157 | } 158 | 159 | &-button { 160 | width: 100%; 161 | flex: 1 0 200px; 162 | min-height: 4rem; 163 | max-height: 4rem; 164 | font-size: 1.2rem; 165 | font-weight: bold; 166 | text-transform: unset; 167 | border-radius: 4px; 168 | padding: 0.5rem 2rem; 169 | cursor: pointer; 170 | transition: 0.3s ease-in-out; 171 | } 172 | 173 | &-sub-info { 174 | margin-top: 1rem; 175 | } 176 | } 177 | } 178 | 179 | @media (max-width: $mobile-device) { 180 | .card { 181 | &-container { 182 | width: 100%; 183 | height: 70%; 184 | display: flex; 185 | flex-direction: column; 186 | gap: 1rem; 187 | overflow-x: auto; 188 | margin: 0; 189 | margin-top: 3rem; 190 | } 191 | 192 | &-item { 193 | flex: 0 1 220px; 194 | height: auto; 195 | 196 | &-header { 197 | &-title { 198 | font-size: 1.8rem; 199 | } 200 | 201 | &-provider { 202 | font-size: 1.8rem; 203 | } 204 | } 205 | 206 | &-suitability { 207 | font-size: 1.5rem; 208 | margin-top: 1.2rem; 209 | } 210 | 211 | &-location { 212 | margin: 1.2rem 0; 213 | &-icon { 214 | font-size: 2.2rem; 215 | } 216 | 217 | &-text { 218 | font-size: 1.5rem; 219 | } 220 | } 221 | 222 | &-socket { 223 | gap: 0.4rem; 224 | 225 | &-container { 226 | font-size: 1.5rem; 227 | padding: 1rem 0 1rem 1rem; 228 | gap: 5px; 229 | margin-bottom: 1rem; 230 | } 231 | } 232 | 233 | &-button { 234 | flex: 0 1 100%; 235 | font-size: 1.5rem; 236 | min-height: 5rem; 237 | margin-bottom: 0.4rem; 238 | } 239 | 240 | &-sub-info { 241 | font-size: 1.4rem; 242 | margin-top: 0.8rem; 243 | } 244 | } 245 | 246 | &-empty { 247 | font-size: 1.8rem; 248 | } 249 | } 250 | } 251 | -------------------------------------------------------------------------------- /app/assets/images/mtesla.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | tesla 5 | 6 | 7 | 10 | 11 | 12 | 13 | 14 | 17 | 18 | 19 | 22 | 23 | 24 | 27 | 28 | 29 | 32 | 33 | 34 | 37 | 38 | 39 | 42 | 43 | 44 | 47 | 48 | 49 | 52 | 53 | 54 | 57 | 58 | 59 | 62 | 63 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /app/components/Marker/CustomPopup/style.scss: -------------------------------------------------------------------------------- 1 | @import "../../../../app/assets/styles/variables"; 2 | 3 | .custom { 4 | &-popup { 5 | bottom: 10px !important; 6 | 7 | &-container { 8 | min-width: 300px; 9 | padding: 1rem 0; 10 | } 11 | 12 | &-header { 13 | display: flex; 14 | justify-content: space-between; 15 | align-items: center; 16 | padding-bottom: 0.5rem; 17 | border-bottom: 2px solid $color-gray; 18 | 19 | &-title { 20 | width: 70%; 21 | text-align: left; 22 | font-size: 1.3rem; 23 | font-weight: 700; 24 | } 25 | 26 | &-provider { 27 | width: 30%; 28 | text-align: right; 29 | font-size: 1.3rem; 30 | text-decoration: none; 31 | font-weight: 700; 32 | } 33 | } 34 | 35 | &-suitability { 36 | font-size: 1.2rem; 37 | font-weight: 700; 38 | margin-top: 0.8rem; 39 | 40 | &-okay { 41 | color: $color-green !important; 42 | } 43 | 44 | &-notokay { 45 | color: $color-red-2 !important; 46 | } 47 | 48 | p { 49 | margin: 0; 50 | } 51 | } 52 | 53 | &-info { 54 | display: flex; 55 | gap: 0.5rem; 56 | align-items: center; 57 | margin-bottom: 0.05rem; 58 | 59 | &-icon { 60 | font-size: 1.8rem; 61 | fill: $color-gray-2; 62 | color: $color-gray-2; 63 | } 64 | 65 | &-text { 66 | font-size: 1.15rem; 67 | font-weight: 400; 68 | text-decoration: none; 69 | color: $color-gray-2 !important; 70 | } 71 | } 72 | 73 | &-socket { 74 | display: flex; 75 | flex-direction: column; 76 | justify-content: center; 77 | align-items: flex-start; 78 | width: 100%; 79 | 80 | &-container { 81 | display: flex; 82 | align-items: center; 83 | font-size: 1.2rem; 84 | gap: 5px; 85 | margin-bottom: 1rem; 86 | width: 100%; 87 | background-color: $color-gray-4; 88 | 89 | &.padding { 90 | padding: 8px; 91 | } 92 | 93 | p { 94 | margin: 0; 95 | } 96 | 97 | &-title { 98 | display: flex; 99 | align-items: center; 100 | justify-content: flex-start; 101 | gap: 8px; 102 | } 103 | 104 | &-icon { 105 | background-color: $color-gray-2; 106 | color: $color-gray; 107 | padding: 5px; 108 | border-radius: 15px; 109 | 110 | &-okay { 111 | background-color: $color-green-2 !important; 112 | } 113 | } 114 | 115 | &-clock { 116 | font-size: 2.4rem; 117 | } 118 | 119 | &-data { 120 | width: 100%; 121 | display: flex; 122 | flex-direction: column; 123 | gap: 8px; 124 | 125 | &-wrapper { 126 | width: 100%; 127 | display: flex; 128 | justify-content: space-between; 129 | gap: 8px; 130 | } 131 | 132 | &-item { 133 | display: flex; 134 | flex-direction: column; 135 | gap: 8px; 136 | 137 | h5 { 138 | font-weight: bold; 139 | font-size: 1.1rem; 140 | text-align: center; 141 | } 142 | 143 | p { 144 | font-weight: normal; 145 | font-size: 1rem; 146 | text-align: center; 147 | } 148 | } 149 | } 150 | } 151 | } 152 | 153 | &-button { 154 | &-container { 155 | width: 100%; 156 | display: flex; 157 | flex-direction: row; 158 | justify-content: center; 159 | align-items: center; 160 | gap: 0.8rem; 161 | margin: 1rem 0; 162 | 163 | a, 164 | a:active, 165 | a::after, 166 | a:hover { 167 | text-decoration: none; 168 | color: unset; 169 | } 170 | 171 | &-item { 172 | text-align: center; 173 | padding: 1rem 0; 174 | border: none; 175 | font-size: 1.2rem; 176 | cursor: pointer; 177 | } 178 | 179 | &-direction { 180 | width: calc(100% - 0.2rem); 181 | background-color: $color-black; 182 | color: $color-white !important; 183 | border: 2px solid $color-gray-3; 184 | 185 | &:hover { 186 | background-color: $color-gray-3; 187 | } 188 | } 189 | 190 | &-payment { 191 | width: calc(100% - 0.2rem); 192 | background-color: $color-white; 193 | color: $color-black !important; 194 | border: 2px solid $color-gray-3; 195 | 196 | &:hover { 197 | background-color: $color-gray-3; 198 | color: $color-white !important; 199 | } 200 | } 201 | } 202 | } 203 | } 204 | 205 | &-icon { 206 | margin-left: -22px !important; 207 | } 208 | } 209 | 210 | .ZES { 211 | color: $color-red !important; 212 | } 213 | 214 | .ESARJ { 215 | color: $color-blue !important; 216 | } 217 | 218 | .AKSAENERGY { 219 | color: $color-green !important; 220 | } 221 | 222 | .SHARZ { 223 | color: $color-yellow !important; 224 | } 225 | 226 | .BEEFULL { 227 | color: $color-orange !important; 228 | } 229 | 230 | @media (max-width: $mobile-device) { 231 | .custom { 232 | &-popup { 233 | &-header { 234 | &-title { 235 | font-size: 2rem; 236 | line-height: normal; 237 | display: -webkit-box; 238 | -webkit-box-orient: vertical; 239 | overflow: hidden; 240 | -webkit-line-clamp: 2; 241 | text-overflow: ellipsis; 242 | } 243 | 244 | &-provider { 245 | font-size: 2rem; 246 | line-height: normal; 247 | display: -webkit-box; 248 | -webkit-box-orient: vertical; 249 | overflow: hidden; 250 | -webkit-line-clamp: 2; 251 | text-overflow: ellipsis; 252 | } 253 | } 254 | 255 | &-suitability { 256 | font-size: 1.8rem; 257 | margin-top: 1.5rem; 258 | } 259 | 260 | &-info { 261 | margin: 1.5rem 0; 262 | &-icon { 263 | font-size: 2.5rem; 264 | } 265 | 266 | &-text { 267 | font-size: 1.8rem; 268 | } 269 | } 270 | 271 | &-socket { 272 | &-container { 273 | font-size: 1.8rem; 274 | gap: 5px; 275 | 276 | &-clock { 277 | font-size: 3.7rem; 278 | } 279 | 280 | &-data { 281 | &-item { 282 | h5 { 283 | font-size: 1.6rem; 284 | } 285 | 286 | p { 287 | font-size: 1.5rem; 288 | } 289 | } 290 | } 291 | } 292 | } 293 | 294 | &-button { 295 | &-container { 296 | gap: 1rem; 297 | margin-bottom: 1.5rem; 298 | 299 | &-item { 300 | padding: 1.5rem 0; 301 | font-size: 1.8rem; 302 | } 303 | } 304 | } 305 | } 306 | } 307 | } 308 | -------------------------------------------------------------------------------- /app/components/Marker/CustomPopup/CustomPopup.tsx: -------------------------------------------------------------------------------- 1 | import { Icon } from "@iconify-icon/react/dist/iconify.js"; 2 | import classNames from "classnames"; 3 | import Link from "next/link"; 4 | import { FC, useState } from "react"; 5 | 6 | import { SearchDetail } from "@/app/types/search-detail"; 7 | import { checkPlugsType, getPlugData } from "@/app/utils/general-utils"; 8 | import Accordion from "../../Accordion/Accordion"; 9 | import "./style.scss"; 10 | 11 | export interface CustomPopupType { 12 | tooltipData: SearchDetail; 13 | isForFilteredCard?: boolean; 14 | } 15 | 16 | const CustomPopup: FC = ({ tooltipData }) => { 17 | const [isACPlugOpen, setIsACPlugOpen] = useState(false); 18 | const [isDCPlugOpen, setIsDCPlugOpen] = useState(false); 19 | 20 | const DCPlugData = getPlugData(tooltipData, "DC"); 21 | const ACPlugData = getPlugData(tooltipData, "AC"); 22 | 23 | const handleAccordionToggle = (isAc: boolean) => { 24 | setIsACPlugOpen(isAc ? !isACPlugOpen : false); 25 | setIsDCPlugOpen(!isAc ? !isDCPlugOpen : false); 26 | }; 27 | 28 | return ( 29 |
    30 |
    31 |
    {tooltipData?.title}
    32 | 38 | {tooltipData?.operator?.brand} 39 | 40 |
    41 |
    46 |

    {tooltipData?.stationActive ? "Kullanıma uygun" : "Kullanıma uygun değil"}

    47 |
    48 |
    49 | 50 |

    {tooltipData?.location?.address}

    51 |
    52 |
    53 | 54 | 55 | {tooltipData?.phone} 56 | 57 |
    58 |
    59 | 63 | Yol Tarifi 64 | 65 | 69 | Rezervasyon 70 | 71 |
    72 | 73 |
    74 |
    75 | handleAccordionToggle(true)} 78 | title={ 79 |
    80 |

    84 | AC 85 |

    86 |

    87 | {getPlugData(tooltipData, "AC")?.reduce((curr, next) => curr + next.count, 0)}{" "} 88 | adet 89 |

    90 |
    91 | } 92 | content={ 93 |
    94 | {ACPlugData?.length 95 | ? ACPlugData?.map((item) => ( 96 |
    97 |
    98 |
    Tip
    99 |

    {item.subType}

    100 |
    101 |
    102 |
    Soket Nu.
    103 |

    {item.socketNumber}

    104 |
    105 |
    106 |
    Güç
    107 |

    {item.power}

    108 |
    109 |
    110 |
    Ücret
    111 |

    ₺{item.price}

    112 |
    113 |
    114 |
    Sayı
    115 |

    {item.count}

    116 |
    117 |
    118 | )) 119 | : "Soket mevcut değil!"} 120 |
    121 | } 122 | /> 123 |
    124 |
    125 | handleAccordionToggle(false)} 128 | title={ 129 |
    130 |

    134 | DC 135 |

    136 |

    137 | {getPlugData(tooltipData, "DC")?.reduce((curr, next) => curr + next.count, 0)}{" "} 138 | adet 139 |

    140 |
    141 | } 142 | content={ 143 |
    144 | {DCPlugData?.length 145 | ? DCPlugData?.map((item) => ( 146 |
    147 |
    148 |
    Tip
    149 |

    {item.subType}

    150 |
    151 |
    152 |
    Soket Nu.
    153 |

    {item.socketNumber}

    154 |
    155 |
    156 |
    Güç
    157 |

    {item.power}

    158 |
    159 |
    160 |
    Ücret
    161 |

    ₺{item.price}

    162 |
    163 |
    164 |
    Sayı
    165 |

    {item.count}

    166 |
    167 |
    168 | )) 169 | : "Soket mevcut değil!"} 170 |
    171 | } 172 | /> 173 |
    174 | 175 |
    176 | 177 |

    00:00 - 23.59

    178 |
    179 |
    180 |
    181 | ); 182 | }; 183 | 184 | export default CustomPopup; 185 | -------------------------------------------------------------------------------- /app/assets/images/me.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | esarj 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/assets/images/mastor.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | astor 5 | 6 | 7 | 10 | 11 | 12 | 13 | 14 | 17 | 18 | 19 | 22 | 23 | 24 | 27 | 28 | 29 | 32 | 33 | 34 | 37 | 38 | 39 | 42 | 43 | 44 | 47 | 48 | 49 | 52 | 53 | 54 | 57 | 58 | 59 | 62 | 63 | 64 | 67 | 68 | 69 | 72 | 73 | 74 | 77 | 78 | 79 | 82 | 83 | 84 | 87 | 88 | 89 | 92 | 93 | 94 | 97 | 98 | 99 | 102 | 103 | 104 | 107 | 108 | 109 | 110 | 111 | -------------------------------------------------------------------------------- /app/assets/images/mwatt.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | wat 5 | 6 | 7 | 10 | 11 | 12 | 13 | 14 | 15 | 18 | 21 | 24 | 25 | 26 | 29 | 32 | 35 | 38 | 40 | 43 | 46 | 49 | 50 | 51 | 52 | 55 | 58 | 61 | 64 | 67 | 68 | 71 | 72 | 73 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /app/assets/images/marker.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | marker 5 | 6 | 7 | 10 | 11 | 12 | 13 | 14 | 15 | 18 | 19 | 20 | 23 | 24 | 25 | 28 | 29 | 30 | 33 | 34 | 35 | 38 | 39 | 40 | 43 | 44 | 45 | 48 | 49 | 50 | 53 | 54 | 55 | 58 | 59 | 60 | 63 | 64 | 65 | 68 | 69 | 70 | 73 | 74 | 75 | 78 | 79 | 80 | 83 | 84 | 85 | 88 | 89 | 90 | 93 | 94 | 95 | 98 | 99 | 100 | 103 | 104 | 105 | 108 | 109 | 110 | 113 | 114 | 115 | 116 | 117 | -------------------------------------------------------------------------------- /app/assets/images/mb.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | beefull 5 | 6 | 7 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /app/data/operators.ts: -------------------------------------------------------------------------------- 1 | export const OPERATORS = [ 2 | { 3 | operatorid: "952598", 4 | operatortitle: "2M ELEKTRİKLİ ARAÇ ŞARJ AĞI VE İSTASYON İŞLETMECİLİĞİ ANONİM ŞİRKETİ" 5 | }, 6 | { operatorid: "992885", operatortitle: "360 ENERJİ ANONİM ŞİRKETİ" }, 7 | { 8 | operatorid: "1118695", 9 | operatortitle: "AKDEN GEMİ İŞLETMECİLİĞİ VE DENİZCİLİK TİCARET LİMİTED ŞİRKETİ" 10 | }, 11 | { 12 | operatorid: "929651", 13 | operatortitle: "AKKÖPRÜLÜ OTOMOTİV İNŞAAT SANAYİ VE TİCARET ANONİM ŞİRKETİ" 14 | }, 15 | { operatorid: "952581", operatortitle: "AKSA MÜŞTERİ ÇÖZÜMLERİ ANONİM ŞİRKETİ" }, 16 | { operatorid: "932089", operatortitle: "ALFAMET TEKSTİL SANAYİ DIŞ TİCARET LİMİTED ŞİRKETİ" }, 17 | { operatorid: "1108420", operatortitle: "ALTUNKAYA ENERJİ VE ŞARJ TEKNOLOJİLERİ ANONİM ŞİRKETİ" }, 18 | { 19 | operatorid: "918852", 20 | operatortitle: "ANTKEM GIDA TURİZM MAKİNA SANAYİ VE TİCARET ANONİM ŞİRKETİ" 21 | }, 22 | { operatorid: "776215", operatortitle: "ANTTECH BİLİŞİM VE AKARYAKIT ANONİM ŞİRKETİ" }, 23 | { operatorid: "1004005", operatortitle: "ARCONA TEKNOLOJİ ANONİM ŞİRKETİ" }, 24 | { operatorid: "723559", operatortitle: "ARMATEC ENERJİ VE İNŞAAT LİMİTED ŞİRKETİ" }, 25 | { operatorid: "1041329", operatortitle: "ARSİMA YENİLENEBİLİR ENERJİ ANONİM ŞİRKETİ" }, 26 | { 27 | operatorid: "1039920", 28 | operatortitle: "ARTAŞ ENERJİ YATIRIMLARI SANAYİ VE TİCARET ANONİM ŞİRKETİ" 29 | }, 30 | { 31 | operatorid: "22376", 32 | operatortitle: "ARTI KURUMSAL KART VE PETROL HİZMETLERİ TİCARET LİMİTED ŞİRKETİ" 33 | }, 34 | { 35 | operatorid: "1005158", 36 | operatortitle: "ART IN SYSTEMS BİLİŞİM TEKNOLOJİLERİ TİCARET LİMİTED ŞİRKETİ" 37 | }, 38 | { 39 | operatorid: "918850", 40 | operatortitle: "ASM DEKORASYON MOBİLYA GRANİT MERMER İNŞAAT SANAYİ TİCARET LİMİTED ŞİRKETİ" 41 | }, 42 | { operatorid: "1107656", operatortitle: "ASPLUS ŞARJ İSTASYONLARI İŞLETMELERİ ANONİM ŞİRKETİ" }, 43 | { operatorid: "919121", operatortitle: "ASTOR ENERJİ ANONİM ŞİRKETİ" }, 44 | { 45 | operatorid: "952678", 46 | operatortitle: "ATAY GÜÇ VE ENERJİ SİSTEMLERİ İMALAT SANAYİ TİCARET LİMİTED ŞİRKETİ" 47 | }, 48 | { operatorid: "776489", operatortitle: "AYDEM PLUS ENERJİ ÇÖZÜMLERİ TİCARET ANONİM ŞİRKETİ" }, 49 | { operatorid: "1040166", operatortitle: "AYHAN TEKNOLOJİ VE OTOMASYON ÇÖZÜMLERİ ANONİM ŞİRKETİ" }, 50 | { operatorid: "993402", operatortitle: "BAKIRCI E MOBİLİTY TEKNOLOJİ ANONİM ŞİRKETİ" }, 51 | { operatorid: "1108496", operatortitle: "BAŞER OTOMOTİV PETROL İNŞAAT KİRALAMA ANONİM ŞİRKETİ" }, 52 | { operatorid: "992950", operatortitle: "BEEFULL ENERJİ TEKNOLOJİLERİ ANONİM ŞİRKETİ" }, 53 | { operatorid: "1068016", operatortitle: "BEYTTÜRK ENERJİ ÜRETİM VE TİCARET ANONİM ŞİRKETİ" }, 54 | { operatorid: "1037818", operatortitle: "BLADECO ALÜMİNYUM SANAYİ VE TİCARET LİMİTED ŞİRKETİ" }, 55 | { operatorid: "313330", operatortitle: "BLUE AKARYAKIT ENERJİ SANAYİ VE TİCARET ANONİM ŞİRKETİ" }, 56 | { 57 | operatorid: "1040808", 58 | operatortitle: "BORENCO ŞARJ TEKNOLOJİ YATIRIMLARI TİCARET ANONİM ŞİRKETİ" 59 | }, 60 | { 61 | operatorid: "1037659", 62 | operatortitle: "BÖLGEM MÜHENDİSLİK İNŞAAT ELEKTRİK MAKİNE SANAYİ VE TİCARET LİMİTED ŞİRKETİ" 63 | }, 64 | { operatorid: "1079978", operatortitle: "CHARGE TEKNOLOJİ SANAYİ VE TİCARET ANONİM ŞİRKETİ" }, 65 | { operatorid: "919069", operatortitle: "CW ENERJİ MÜHENDİSLİK TİCARET VE SANAYİ ANONİM ŞİRKETİ" }, 66 | { operatorid: "60858", operatortitle: "DESA ENERJİ ELEKTRİK ÜRETİM ANONİM ŞİRKETİ" }, 67 | { operatorid: "133819", operatortitle: "DİCLE KÖK ENERJİ YATIRIM ANONİM ŞİRKETİ" }, 68 | { operatorid: "1041609", operatortitle: "DOĞUŞ DİJİTAL ENERJİ LİMİTED ŞİRKETİ" }, 69 | { 70 | operatorid: "1092971", 71 | operatortitle: "DOĞUŞ ŞARJ SİSTEMLERİ PAZARLAMA VE TİCARET ANONİM ŞİRKETİ" 72 | }, 73 | { 74 | operatorid: "1092580", 75 | operatortitle: "D RECT CHARGE ELEKTRİK ENERJİ SANAYİ VE TİCARET ANONİM ŞİRKETİ" 76 | }, 77 | { operatorid: "526427", operatortitle: "DUMANOĞLU ENERJİ YATIRIM ANONİM ŞİRKETİ" }, 78 | { operatorid: "1092488", operatortitle: "EGESARJ ŞARJ ÜNİTELERİ ELEKTRİK ÜRETİM ANONİM ŞİRKETİ" }, 79 | { 80 | operatorid: "1052075", 81 | operatortitle: "EKOS MOBİLİTE ÇÖZÜMLERİ TEKNOLOJİ TİCARET ANONİM ŞİRKETİ" 82 | }, 83 | { operatorid: "930711", operatortitle: "ELARİS ENERJİ YATIRIMLARI ANONİM ŞİRKETİ" }, 84 | { operatorid: "1091495", operatortitle: "EMEA ENERGY TEKNOLOJİ ANONİM ŞİRKETİ" }, 85 | { operatorid: "1133163", operatortitle: "ENAY ENERJİ GIDA ANONİM ŞİRKETİ" }, 86 | { operatorid: "1006144", operatortitle: "ENERGY ŞARJ İSTASYON İŞLETMECİLİĞİ ANONİM ŞİRKETİ" }, 87 | { operatorid: "1052427", operatortitle: "ENERJİTURK ENERJİ YATIRIM ANONİM ŞİRKETİ" }, 88 | { operatorid: "918383", operatortitle: "EN YAKIT ANONİM ŞİRKETİ" }, 89 | { 90 | operatorid: "919196", 91 | operatortitle: "ERC SİSTEM ELEKTRİK MAKİNA İMALAT TAAHHÜT SANAYİ VE TİCARET ANONİM ŞİRKETİ" 92 | }, 93 | { operatorid: "1004378", operatortitle: "ER ELEKTRONİK SANAYİ VE TİCARET ANONİM ŞİRKETİ" }, 94 | { 95 | operatorid: "60869", 96 | operatortitle: "ESKİŞEHİR ENDÜSTRİYEL ENERJİ ELEKTRİK ÜRETİM ANONİM ŞİRKETİ" 97 | }, 98 | { 99 | operatorid: "930848", 100 | operatortitle: "EŞARJ ELEKTRİKLİ ARAÇLAR ŞARJ SİSTEMLERİ ANONİM ŞİRKETİ." 101 | }, 102 | { operatorid: "952278", operatortitle: "EVCİL YATIRIM MÜHENDİSLİK MİMARLIK LİMİTED ŞİRKETİ" }, 103 | { operatorid: "929899", operatortitle: "EVS ELEKTRİKLİ ŞARJ SİSTEMLERİ SAN.VE TİC.A.Ş." }, 104 | { 105 | operatorid: "918325", 106 | operatortitle: "FCTR ELEKTRİKLİ ARAÇLAR ENERJİ TEKNOLOJİLERİ VE HİZMETLERİ A.Ş" 107 | }, 108 | { operatorid: "1128996", operatortitle: "FEDERAL ELEKTRİK YATIRIM VE TİCARET ANONİM ŞİRKETİ" }, 109 | { operatorid: "1052318", operatortitle: "FİLOPORT ARAÇ KİRALAMA VE SERVİS ANONİM ŞİRKETİ" }, 110 | { operatorid: "1068919", operatortitle: "FOKS ENERJİ ANONİM ŞİRKETİ" }, 111 | { operatorid: "1005136", operatortitle: "FORM ELEKTRİK İNŞAAT MÜHENDİSLİK ANONİM ŞİRKETİ" }, 112 | { operatorid: "744831", operatortitle: "FORTİS ENERJİ ELEKTRİK ÜRETİM ANONİM ŞİRKETİ" }, 113 | { operatorid: "952413", operatortitle: "FOTEC ENERJİ TEKNOLOJİLERİ ANONİM ŞİRKETİ" }, 114 | { 115 | operatorid: "929944", 116 | operatortitle: 117 | "FULL ELEKTRİKLİ ARAÇLAR ŞARJ AĞI İSTASYONLARI İŞLETMECİLİĞİ ENERJİ TİCARET VE SANAYİ ANONİM ŞİRKETİ" 118 | }, 119 | { operatorid: "1037666", operatortitle: "FZY ENERJİ ŞARJ SİSTEMLERİ ANONİM ŞİRKETİ" }, 120 | { 121 | operatorid: "1006417", 122 | operatortitle: "GERSAN ŞARJ SİSTEMLERİ SANAYİ VE TİCARET ANONİM ŞİRKETİ" 123 | }, 124 | { 125 | operatorid: "1105320", 126 | operatortitle: "GEZCAR TURİZM OTOMOTİV SANAYİ VE TİCARET LİMİTED ŞİRKETİ" 127 | }, 128 | { operatorid: "1004808", operatortitle: "GİOEV ŞARJ İSTASYONLARI İŞLETMELERİ ANONİM ŞİRKETİ" }, 129 | { operatorid: "1057131", operatortitle: "GLOBAL İNOVATİF ENERJİ TEKNOLOJİLERİ ANONİM ŞİRKETİ" }, 130 | { operatorid: "952372", operatortitle: "GOŞARJ ELECTRİCAL ENERJİ YATIRIM ANONİM ŞİRKETİ" }, 131 | { 132 | operatorid: "931324", 133 | operatortitle: "GREENLEAF YENİLENEBİLİR ENERJİ SİSTEMLERİ SANAYİ VE TİCARET LİMİTED ŞİRKETİ" 134 | }, 135 | { operatorid: "952546", operatortitle: "GREEN ŞARJ İSTASYONLARI KURULUM ANONİM ŞİRKETİ" }, 136 | { 137 | operatorid: "952578", 138 | operatortitle: "GREEN WATT ENERJİ ÜRETİM İNŞAAT SANAYİ VE TİCARET ANONİM ŞİRKETİ" 139 | }, 140 | { 141 | operatorid: "1052316", 142 | operatortitle: "GS MAR ELEKTRİK VE ELEKTRONİK CİHAZLAR SANAYİ DIŞ TİCARET ANONİM ŞİRKETİ" 143 | }, 144 | { 145 | operatorid: "201676", 146 | operatortitle: "HES GROUP ENERJİ İNŞAATVE SANAYİ TİCARET ANONİM ŞİRKETİ" 147 | }, 148 | { 149 | operatorid: "992908", 150 | operatortitle: "HUNAT ENERJİ YATIRIM VE DANIŞMANLIK SANAYİ TİCARET ANONİM ŞİRKETİ" 151 | }, 152 | { 153 | operatorid: "1036897", 154 | operatortitle: "INTERDATA VERİ MERKEZİ VE ENTEGRASYON HİZMETLERİ ANONİM ŞİRKETİ" 155 | }, 156 | { operatorid: "1038961", operatortitle: "İNCHARGE ELEKTRİKLİ ARAÇLAR HİZMETLERİ ANONİM ŞİRKETİ" }, 157 | { operatorid: "1037665", operatortitle: "İSPİRLİ ŞARJ HİZMETLERİ TİCARET ANONİM ŞİRKETİ" }, 158 | { operatorid: "1005665", operatortitle: "İSTASYON ŞARJ HİZMETLERİ ANONİM ŞİRKETİ" }, 159 | { 160 | operatorid: "1041435", 161 | operatortitle: "JETCO ELEKTRİK ELEKTRONİK YÜKSEK TEKNOLOJİ HİZMETLERİ ANONİM ŞİRKETİ" 162 | }, 163 | { 164 | operatorid: "918339", 165 | operatortitle: "KALYON ELECTRICAL VEHICLE ENERJİ YATIRIM ANONİM ŞİRKETİ" 166 | }, 167 | { 168 | operatorid: "930780", 169 | operatortitle: "KARAKUŞLAR HALI TEKSTİL SANAYİ VE TİCARET ANONİM ŞİRKETİ" 170 | }, 171 | { operatorid: "1128995", operatortitle: "KARKIN ENERJİ TİCARET VE SANAYİ LİMİTED ŞİRKETİ" }, 172 | { 173 | operatorid: "1106130", 174 | operatortitle: "KLR ENERJİ SİSTEMLERİ SANAYİ VE TİCARET ANONİM ŞİRKETİ" 175 | }, 176 | { operatorid: "1052459", operatortitle: "KONYA ELEKTRİKLİ ARAÇLAR ANONİM ŞİRKETİ" }, 177 | { 178 | operatorid: "1041630", 179 | operatortitle: 180 | "KÖFTECİ YUSUF HAZIR YEMEK TEMİZLİK CANLI HAYVAN ET MAMÜLLERİ ENTEGRE GIDA İTHALAT İHRACAT SANAYİ VE TİCARET ANONİM ŞİRKETİ" 181 | }, 182 | { 183 | operatorid: "1052442", 184 | operatortitle: "LOGO RENTAL FİLO KİRALAMA TURİZM OTO ALIM SATIM ANONİM ŞİRKETİ" 185 | }, 186 | { operatorid: "1037916", operatortitle: "LUMHOUSE ENERJİ SANAYİ VE TİCARET ANONİM ŞİRKETİ" }, 187 | { 188 | operatorid: "1051881", 189 | operatortitle: "MAGİCLİNE ENERJİ SİSTEMLERİ SANAYİ VE TİCARET LİMİTED ŞİRKETİ" 190 | }, 191 | { operatorid: "1079392", operatortitle: "MAKSEM ELEKTRO ŞARJ ANONİM ŞİRKETİ" }, 192 | { operatorid: "1005245", operatortitle: "MANAS İPLİK SANAYİ VE TİCARET ANONİM ŞİRKETİ" }, 193 | { operatorid: "787365", operatortitle: "MAS SINAİ VE TİCARİ YATIRIMLAR ANONİM ŞİRKETİ" }, 194 | { operatorid: "1005929", operatortitle: "MCZ TEKNOLOJİ VE ENERJİ ANONİM ŞİRKETİ" }, 195 | { operatorid: "1004383", operatortitle: "MEİS CHARGE TEKNOLOJİLERİ ANONİM ŞİRKETİ" }, 196 | { 197 | operatorid: "1107660", 198 | operatortitle: "MERCURY ŞARJ HİZMETLERİ SANAYİ VE TİCARET LİMİTED ŞİRKETİ" 199 | }, 200 | { operatorid: "932023", operatortitle: "META MOBİLİTE ENERJİ ANONİM ŞİRKETİ" }, 201 | { operatorid: "1052674", operatortitle: "MINUS ENERGY ENERJİ SANAYİ VE TİCARET ANONİM ŞİRKETİ" }, 202 | { 203 | operatorid: "1052446", 204 | operatortitle: "MİGEN ENERJİ VE ELEKTRİKLİ ARAÇ ŞARJ HİZMETLERİ ANONİM ŞİRKETİ" 205 | }, 206 | { operatorid: "1052460", operatortitle: "MİONTİ ENERJİ VE TEKNOLOJİ ANONİM ŞİRKETİ" }, 207 | { 208 | operatorid: "931578", 209 | operatortitle: 210 | "MİTHRA POD ELEKTRİK VE ELEKTRONİK OTOMASYON SİSTEMLERİ İMALAT SANAYİ VE TİCARET LİMİTED ŞİRKETİ" 211 | }, 212 | { operatorid: "1053961", operatortitle: "MOD MALKARLAR OTOMOTİV SANAYİ TİCARET LİMİTED ŞİRKETİ" }, 213 | { operatorid: "932087", operatortitle: "MONOKON ELEKTRİK SANAYİ VE TİCARET ANONİM ŞİRKETİ" }, 214 | { 215 | operatorid: "1108031", 216 | operatortitle: 217 | "MYWEST İNŞAAT TURİZM GIDA TARIM HAYVANCILIK REKLAM EĞİTİM TEKSTİL MEDİKAL SANAYİ VE TİCARET LTD.ŞTİ." 218 | }, 219 | { 220 | operatorid: "1094580", 221 | operatortitle: "MYZ BİLGİSAYAR OTOMOTİV İNŞAAT PETROL SANAYİ VE TİCARET LİMİTED ŞİRKETİ" 222 | }, 223 | { operatorid: "930960", operatortitle: "NEVA İÇ VE DIŞ TİCARET ANONİM ŞİRKETİ" }, 224 | { 225 | operatorid: "1081065", 226 | operatortitle: "OBİŞARJ ELEKTRİKLİ ARAÇ ŞARJ SİSTEMLERİ ANONİM ŞİRKETİ" 227 | }, 228 | { 229 | operatorid: "918972", 230 | operatortitle: "ORBİT ENERJİ HABERLEŞME TEKNOLOJİ SANAYİ VE TİCARET ANONİM ŞİRKETİ" 231 | }, 232 | { operatorid: "5533", operatortitle: "OTOJET ENERJİ SANAYİ VE TİCARET A.Ş." }, 233 | { 234 | operatorid: "1040704", 235 | operatortitle: "OTP OĞULTÜRK PANO İMALAT İTHALAT İHRACAT SANAYİ VE TİCARET LİMİTED ŞİRKETİ" 236 | }, 237 | { 238 | operatorid: "1108533", 239 | operatortitle: "OVOLT ŞARJ TEKNOLOJİLERİ SANAYİ VE TİCARET ANONİM ŞİRKETİ" 240 | }, 241 | { operatorid: "932077", operatortitle: "ÖZKA ENERJİ YATIRIMLARI ANONİM ŞİRKETİ" }, 242 | { operatorid: "3866", operatortitle: "PETROL OFİSİ ANONİM ŞİRKETİ" }, 243 | { 244 | operatorid: "1052945", 245 | operatortitle: "PGD ENERJİ YATIRIMLARI SANAYİ VE TİCARET ANONİM ŞİRKETİ" 246 | }, 247 | { 248 | operatorid: "952412", 249 | operatortitle: "PİRİM GIDA VE MEŞRUBAT SANAYİ VE TİCARET ANONİM ŞİRKETİ" 250 | }, 251 | { 252 | operatorid: "993900", 253 | operatortitle: 254 | "PLUGİNN ELEKTRİKLİ TAŞITLAR ŞARJ SİSTEMLERİ VE YAZILIM HİZMETLERİ SANAYİ TİCARET ANONİM ŞİRKETİ" 255 | }, 256 | { operatorid: "1118751", operatortitle: "PORTY SMART TECH TEKNOLOJİ ANONİM ŞİRKETİ" }, 257 | { operatorid: "1005209", operatortitle: "POWER ELEKTRONİK SANAYİ VE TİCARET ANONİM ŞİRKETİ" }, 258 | { operatorid: "1005698", operatortitle: "PROMASTER DANIŞMANLIK LİMİTED ŞİRKETİ" }, 259 | { 260 | operatorid: "1005204", 261 | operatortitle: "PROŞARJ ARAÇ ŞARJ SİSTEMLERİ ENERJİ SANAYİ VE TİCARET ANONİM ŞİRKETİ" 262 | }, 263 | { operatorid: "1118816", operatortitle: "QUICK ENERJİ ANONİM ŞİRKETİ" }, 264 | { operatorid: "1004352", operatortitle: "RDS GAYRİMENKUL DANIŞMANLIK ANONİM ŞİRKETİ" }, 265 | { operatorid: "60498", operatortitle: "RHG ENERTÜRK ENERJİ ÜRETİM VE TİCARET ANONİM ŞİRKETİ" }, 266 | { 267 | operatorid: "1052655", 268 | operatortitle: "RÖNESANS ŞARJ İSTASYON ENERJİ YATIRIMLARI ANONİM ŞİRKETİ" 269 | }, 270 | { 271 | operatorid: "992905", 272 | operatortitle: "RST TEKNOLOJİ HİZMETLERİ VE ENERJİ SANAYİ TİCARET ANONİM ŞİRKETİ" 273 | }, 274 | { operatorid: "1107200", operatortitle: "SANTA SOLAR ENERJİ LİMİTED ŞİRKETİ" }, 275 | { operatorid: "47139", operatortitle: "SHELL & TURCAS PETROL ANONİM ŞİRKETİ" }, 276 | { 277 | operatorid: "1055754", 278 | operatortitle: "SHU ARGE İNOVASYON VE BİLGİ TEKNOLOJİLERİ ANONİM ŞİRKETİ" 279 | }, 280 | { operatorid: "1040849", operatortitle: "SKY WORLD SOLAR ENERJİ ANONIM ŞİRKETİ" }, 281 | { operatorid: "1041443", operatortitle: "SMART SOLARGİZE YEŞİL MOBİLİTE ENERJİ ANONİM ŞİRKETİ" }, 282 | { operatorid: "1039122", operatortitle: "SOLAR ARAÇ ŞARJ HİZMETLERİ ANONİM ŞİRKETİ" }, 283 | { 284 | operatorid: "930788", 285 | operatortitle: 286 | "SOLARPARK DANIŞMANLIK TURİZM MÜHENDİSLİK ENERJİ ÜRETİM İNŞAAT İTHALAT İHRACAT SANAYİ VE TİCARET LİMİTED ŞİRKETİ" 287 | }, 288 | { 289 | operatorid: "1005821", 290 | operatortitle: "SOLARŞARJET ENERJİ VE ELEKTRİKLİ ARAÇ ŞARJ SİSTEMLERİ ANONİM ŞİRKETİ" 291 | }, 292 | { 293 | operatorid: "1004388", 294 | operatortitle: "SPUR ENERJİ YATIRIMLARI SANAYİ VE TİCARET ANONİM ŞİRKETİ" 295 | }, 296 | { operatorid: "919178", operatortitle: "STL SOLAR ENERJİ ANONİM ŞİRKETİ" }, 297 | { operatorid: "1091359", operatortitle: "ŞARJMATİK ENERJİ BİLİŞİM LİMİTED ŞİRKETİ" }, 298 | { 299 | operatorid: "919280", 300 | operatortitle: 301 | "ŞARJON YENİLENEBİLİR ENERJİ VE ELEKTRİKLİ ARAÇ ŞARJ SİSTEMLERİ TİCARET ANONİM ŞİRKETİ" 302 | }, 303 | { 304 | operatorid: "1037283", 305 | operatortitle: "ŞARJPLUS ELEKTRİKLİ ARAÇ ŞARJ SİSTEMLERİ LİMİTED ŞİRKETİ" 306 | }, 307 | { 308 | operatorid: "1128945", 309 | operatortitle: 310 | "ŞARJZONE ENERJİ VE ELEKTRİKLİ ARAÇ ŞARJ SİSTEMLERİ SANAYİ TİCARET ANONİM ŞİRKETİ" 311 | }, 312 | { operatorid: "1129136", operatortitle: "TAYRAN GRUP İNŞAAT ANONİM ŞİRKETİ" }, 313 | { operatorid: "1105291", operatortitle: "TCDD TEKNİK MÜHENDİSLİK VE MÜŞAVİRLİK ANONİM ŞİRKETİ" }, 314 | { operatorid: "993744", operatortitle: "TESLA MOTORLARI SATIŞ VE HİZMETLERİ LİMİTED ŞİRKETİ" }, 315 | { operatorid: "952415", operatortitle: "TORA TEKNİK HİZMETLER İŞLETME ANONİM ŞİRKETİ" }, 316 | { operatorid: "1051919", operatortitle: "TRİPY MOBİLİTY TEKNOLOJİ ANONİM ŞİRKETİ" }, 317 | { 318 | operatorid: "929964", 319 | operatortitle: "TRUGO AKILLI ŞARJ ÇÖZÜMLERİ SANAYİ VE TİCARET ANONİM ŞİRKETİ" 320 | }, 321 | { operatorid: "1052059", operatortitle: "TT VENTURES PROJE GELİŞTİRME ANONİM ŞİRKETİ" }, 322 | { operatorid: "7850", operatortitle: "TUNALAR OTOMOTİV TİCARET ANONİM ŞİRKETİ" }, 323 | { operatorid: "1006519", operatortitle: "TUNÇMATİK ŞARJ TEKNOLOJİLERİ ANONİM ŞİRKETİ" }, 324 | { 325 | operatorid: "918800", 326 | operatortitle: "TURUNCU MÜHENDİSLİK ELEKTRİK TAAH.SAN.VE DIŞ TİC. LTD. ŞTİ." 327 | }, 328 | { 329 | operatorid: "1052221", 330 | operatortitle: "TÜVTURK KUZEY TAŞIT MUAYENE İSTASYONLARI YAPIM VE İŞLETİM ANONİM ŞİRKETİ" 331 | }, 332 | { 333 | operatorid: "1005767", 334 | operatortitle: 335 | "ULTRAMETRİK ENERJİ BİLGİSAYAR İNŞAAT KUYUMCULUK GIDA SANAYİ VE DIŞ TİCARET LİMİTED ŞİRKETİ" 336 | }, 337 | { 338 | operatorid: "929414", 339 | operatortitle: "VERDEPUNTO ENERJİ SİSTEMLERİ SANAYİ VE TİCARET ANONİM ŞİRKETİ" 340 | }, 341 | { operatorid: "1004341", operatortitle: "VİTALEN ENERJİ ANONİM ŞİRKETİ" }, 342 | { operatorid: "931326", operatortitle: "VİZYONEKS BİLGİ TEKNOLOJİLERİ ANONİM ŞİRKETİ" }, 343 | { 344 | operatorid: "1037300", 345 | operatortitle: "VOLNET ENERJİ SİSTEMLERİ SANAYİ TİCARET ANONİM ŞİRKETİ" 346 | }, 347 | { operatorid: "1037081", operatortitle: "VOLTGO ŞARJ HİZMETLERİ ANONİM ŞİRKETİ" }, 348 | { operatorid: "1093161", operatortitle: "VOLTRUN ENERJİ ANONİM ŞİRKETİ" }, 349 | { 350 | operatorid: "929436", 351 | operatortitle: "WAT MOBILITE ÇOZUMLERI TEKNOLOJİ VE TİCARET ANONİM ŞİRKETİ" 352 | }, 353 | { 354 | operatorid: "931220", 355 | operatortitle: "WHITE ROSE MOTOR SANAYİ VE OTOMASYON TİCARET LİMİTED ŞİRKETİ" 356 | }, 357 | { 358 | operatorid: "929644", 359 | operatortitle: "YENİ MODEL YAPAY ZEKA VE ROBOTİK TEKNOLOJİ HİZMETLERİ ANONİM ŞİRKETİ" 360 | }, 361 | { operatorid: "202196", operatortitle: "YETAŞ YILDIRIM ENERJİ TEDARİK ANONİM ŞİRKETİ" }, 362 | { 363 | operatorid: "993552", 364 | operatortitle: "ZEPLİN TURİZM TAŞIMACILIK YATIRIM SANAYİ VE TİCARET LİMİTED ŞİRKETİ" 365 | }, 366 | { operatorid: "919287", operatortitle: "ZES DİJİTAL TİCARET ANONİM ŞİRKETİ" }, 367 | { operatorid: "931523", operatortitle: "ZEY ENERJİ SANAYİ VE TİCARET ANONİM ŞİRKETİ" } 368 | ]; 369 | --------------------------------------------------------------------------------