├── .eslintrc.json
├── .github
└── FUNDING.yml
├── .gitignore
├── .prettierrc
├── LICENSE
├── README.md
├── app
├── assets
│ ├── images
│ │ ├── ma.svg
│ │ ├── marker.png
│ │ ├── marker.svg
│ │ ├── mastor.svg
│ │ ├── mb.svg
│ │ ├── mcw.svg
│ │ ├── mdcharge.svg
│ │ ├── me.svg
│ │ ├── mpowersarj.svg
│ │ ├── ms.svg
│ │ ├── mtesla.svg
│ │ ├── mtrugo.svg
│ │ ├── mtunc.svg
│ │ ├── mvoltrun.svg
│ │ ├── mwatt.svg
│ │ ├── mz.svg
│ │ ├── sarjdev-logo.png
│ │ ├── sarjdev.png
│ │ └── sarjdev_logo.png
│ └── styles
│ │ ├── _index.scss
│ │ ├── _reset.scss
│ │ └── _variables.scss
├── components
│ ├── Accordion
│ │ ├── Accordion.tsx
│ │ └── styles.scss
│ ├── BottomSheet
│ │ ├── BottomSheet.tsx
│ │ └── styles.scss
│ ├── Button
│ │ ├── Button.tsx
│ │ └── styles.scss
│ ├── Cluster
│ │ ├── Cluster.tsx
│ │ ├── ClusterData.ts
│ │ └── styles.scss
│ ├── Filter
│ │ ├── FilterForm
│ │ │ ├── FilterForm.tsx
│ │ │ ├── actions.ts
│ │ │ └── styles.scss
│ │ └── FilteredCard
│ │ │ ├── FilteredCard.tsx
│ │ │ └── styles.scss
│ ├── Form
│ │ ├── FormProvider
│ │ │ └── FormProvider.tsx
│ │ └── RangeInput
│ │ │ ├── RangeInput.tsx
│ │ │ └── styles.scss
│ ├── Header
│ │ ├── Header.tsx
│ │ ├── HeaderDialog
│ │ │ ├── HeaderDialog.tsx
│ │ │ └── styles.scss
│ │ └── styles.scss
│ ├── HelperButtons
│ │ ├── FilterButton
│ │ │ └── FilterButton.tsx
│ │ ├── HelperButtonGroup.tsx
│ │ ├── LocationButton
│ │ │ └── LocationButton.tsx
│ │ └── styles.scss
│ ├── Loading
│ │ ├── Loading.tsx
│ │ └── styles.scss
│ ├── Map
│ │ ├── MapContent.tsx
│ │ ├── actions.ts
│ │ └── styles.scss
│ ├── Marker
│ │ ├── CustomPopup
│ │ │ ├── CustomPopup.tsx
│ │ │ └── style.scss
│ │ ├── ErrorPopup
│ │ │ ├── ErrorPopup.tsx
│ │ │ └── style.scss
│ │ ├── LoadingPopup
│ │ │ ├── LoadingPopup.tsx
│ │ │ └── style.scss
│ │ ├── MarkerComponent.tsx
│ │ ├── MarkerIcons.ts
│ │ ├── actions.ts
│ │ └── styles.scss
│ └── Search
│ │ ├── SearchBar.tsx
│ │ ├── actions.ts
│ │ └── styles.scss
├── data
│ └── operators.ts
├── favicon.ico
├── hooks
│ ├── useDebounce.ts
│ ├── useMapEvents.ts
│ ├── useResponsive.ts
│ └── useUserLocation.ts
├── layout.tsx
├── page.tsx
├── schema
│ └── filterFormSchema.ts
├── services
│ └── axiosInstance.ts
├── stores
│ ├── generalStore.ts
│ └── mapGeographyStore.ts
├── types
│ ├── common.ts
│ ├── index.ts
│ ├── search-detail.ts
│ ├── search-nearest.ts
│ ├── search.ts
│ └── types.d.ts
└── utils
│ ├── general-utils.ts
│ ├── notistack.ts
│ └── zustand.ts
├── next.config.js
├── package-lock.json
├── package.json
├── postcss.config.js
├── public
├── android-icon-144x144.png
├── android-icon-192x192.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
├── apple-icon-57x57.png
├── apple-icon-60x60.png
├── apple-icon-72x72.png
├── apple-icon-76x76.png
├── apple-icon-precomposed.png
├── apple-icon.png
├── assets
│ └── images
│ │ └── loading-background.png
├── browserconfig.xml
├── favicon-16x16.png
├── favicon-32x32.png
├── favicon-96x96.png
├── favicon.ico
├── manifest.json
├── ms-icon-144x144.png
├── ms-icon-150x150.png
├── ms-icon-310x310.png
├── ms-icon-70x70.png
├── next.svg
├── robots.txt
├── sarjdev-logo.png
├── sitemap.xml
└── vercel.svg
├── tsconfig.json
└── yarn.lock
/.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 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: sarjdev
2 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/.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 | }
--------------------------------------------------------------------------------
/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/assets/images/marker.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sarjdev/front-end/425016e9ce2c4bc0ed17f06212d3e6ea11eb8bc8/app/assets/images/marker.png
--------------------------------------------------------------------------------
/app/assets/images/marker.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/assets/images/mastor.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/assets/images/mb.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/assets/images/me.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/assets/images/mtesla.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/assets/images/mtrugo.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/assets/images/mvoltrun.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/assets/images/mwatt.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/assets/images/mz.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/assets/images/sarjdev-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sarjdev/front-end/425016e9ce2c4bc0ed17f06212d3e6ea11eb8bc8/app/assets/images/sarjdev-logo.png
--------------------------------------------------------------------------------
/app/assets/images/sarjdev.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sarjdev/front-end/425016e9ce2c4bc0ed17f06212d3e6ea11eb8bc8/app/assets/images/sarjdev.png
--------------------------------------------------------------------------------
/app/assets/images/sarjdev_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sarjdev/front-end/425016e9ce2c4bc0ed17f06212d3e6ea11eb8bc8/app/assets/images/sarjdev_logo.png
--------------------------------------------------------------------------------
/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/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/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/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 |
30 | )}
31 |
32 | );
33 | };
34 |
35 | export default Accordion;
36 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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/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/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/Cluster/ClusterData.ts:
--------------------------------------------------------------------------------
1 | export type ClusterDataType = {
2 | id: number;
3 | intensity: string;
4 | minClus: number;
5 | maxClus?: number;
6 | };
7 |
8 | export interface IClusterData {
9 | [key: string]: ClusterDataType;
10 | }
11 |
12 | export const ClusterData: IClusterData = {
13 | safe: {
14 | id: 1,
15 | intensity: "safe",
16 | minClus: 0,
17 | maxClus: 0
18 | },
19 | low: {
20 | id: 2,
21 | intensity: "low",
22 | minClus: 1,
23 | maxClus: 15
24 | },
25 | "mid-low": {
26 | id: 3,
27 | intensity: "mid-low",
28 | minClus: 16,
29 | maxClus: 35
30 | },
31 | mid: {
32 | id: 4,
33 | intensity: "mid",
34 | minClus: 36,
35 | maxClus: 65
36 | },
37 | "mid-high": {
38 | id: 5,
39 | intensity: "mid-high",
40 | minClus: 66,
41 | maxClus: 85
42 | },
43 | high: {
44 | id: 6,
45 | intensity: "high",
46 | minClus: 86
47 | }
48 | };
49 |
50 | export function findClusterData(clusterCount: number): ClusterDataType {
51 | const data = Object.values(ClusterData).find(
52 | (item) =>
53 | clusterCount >= item.minClus && clusterCount <= (item.maxClus ?? Number.MAX_SAFE_INTEGER)
54 | );
55 |
56 | return data || ClusterData.safe;
57 | }
58 |
--------------------------------------------------------------------------------
/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/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 |
86 |
87 |
88 |
89 | );
90 | };
91 |
92 | export default FilterForm;
93 |
--------------------------------------------------------------------------------
/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/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/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/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/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 |
18 |
19 | );
20 | };
21 |
22 | export default FormProvider;
23 |
--------------------------------------------------------------------------------
/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/Form/RangeInput/styles.scss:
--------------------------------------------------------------------------------
1 | @import "../../../../app/assets/styles/variables";
2 |
3 | .range-input {
4 | input[type="range"] {
5 | -webkit-appearance: none;
6 | appearance: none;
7 | width: 100%;
8 | cursor: pointer;
9 | outline: none;
10 | overflow: hidden;
11 | border-radius: 16px;
12 |
13 | &::-webkit-slider-runnable-track {
14 | height: 15px;
15 | background: $color-gray-4;
16 | border-radius: 16px;
17 | }
18 |
19 | &::-moz-range-track {
20 | height: 15px;
21 | background: $color-gray-4;
22 | border-radius: 16px;
23 | }
24 |
25 | &::-webkit-slider-thumb {
26 | -webkit-appearance: none;
27 | appearance: none;
28 | height: 15px;
29 | width: 15px;
30 | background-color: $color-white;
31 | border-radius: 50%;
32 | border: 2px solid $color-black;
33 | box-shadow: -407px 0 0 400px $color-black;
34 | }
35 |
36 | &::-moz-range-thumb {
37 | height: 15px;
38 | width: 15px;
39 | background-color: $color-white;
40 | border-radius: 50%;
41 | border: 1px solid $color-black;
42 | box-shadow: -407px 0 0 400px $color-black;
43 | }
44 | }
45 |
46 | &-error {
47 | font-size: 1rem;
48 | color: $color-red;
49 | padding: 0.2rem;
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/app/components/Header/Header.tsx:
--------------------------------------------------------------------------------
1 | import Logo from "@/app/assets/images/sarjdev_logo.png";
2 | import { useResponsive } from "@/app/hooks/useResponsive";
3 | import Image from "next/image";
4 | import Link from "next/link";
5 | import { FC, useState } from "react";
6 | import HeaderDialog from "./HeaderDialog/HeaderDialog";
7 |
8 | import "./styles.scss";
9 |
10 | const Header: FC = () => {
11 | const mdUp = useResponsive("up", "md");
12 | const [open, setOpen] = useState(false);
13 |
14 | const handleOpen = () => {
15 | setOpen(true);
16 | };
17 |
18 | const handleClose = () => {
19 | setOpen(false);
20 | };
21 |
22 | return (
23 | <>
24 |
38 |
39 | >
40 | );
41 | };
42 |
43 | export default Header;
44 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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 | }
26 | onClick={handleClickFilterButton}
27 | />
28 | );
29 | };
30 |
31 | export default FilterButton;
32 |
--------------------------------------------------------------------------------
/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/LocationButton/LocationButton.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 { useMap } from "react-leaflet";
7 | import Button from "../../Button/Button";
8 |
9 | const LocationButton: FC = () => {
10 | const { actions } = useGeneralStore();
11 | const { location } = useMapGeographyStore();
12 | const map = useMap();
13 | const { enqueueSnackbar } = useSnackbar();
14 |
15 | const handleClickToCenter = () => {
16 | if (location) {
17 | const selectedLocationData: [number, number] = [location?.[0], location?.[1]];
18 |
19 | actions.setBottomSheetOpen(false);
20 | map.flyTo(selectedLocationData, 17);
21 | } else {
22 | enqueueSnackbar("Şu an konumunuza ulaşamıyoruz. Lütfen konum bilgisi için izin veriniz!", {
23 | variant: "warning"
24 | });
25 | }
26 | };
27 |
28 | return (
29 | }
32 | onClick={handleClickToCenter}
33 | />
34 | );
35 | };
36 |
37 | export default LocationButton;
38 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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/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/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/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/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/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/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/Marker/LoadingPopup/LoadingPopup.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import "./style.scss";
4 |
5 | const LoadingPopup = () => {
6 | return (
7 |
13 | );
14 | };
15 |
16 | export default LoadingPopup;
17 |
--------------------------------------------------------------------------------
/app/components/Marker/LoadingPopup/style.scss:
--------------------------------------------------------------------------------
1 | @import "../../../../app/assets/styles/variables";
2 |
3 | .lds-ellipsis {
4 | display: inline-block;
5 | position: relative;
6 | width: 80px;
7 | height: 80px;
8 |
9 | div {
10 | position: absolute;
11 | top: 33px;
12 | width: 13px;
13 | height: 13px;
14 | border-radius: 50%;
15 | background: $color-mid;
16 | animation-timing-function: cubic-bezier(0, 1, 1, 0);
17 | }
18 | }
19 |
20 | .lds-ellipsis div:nth-child(1) {
21 | left: 8px;
22 | animation: lds-ellipsis1 0.6s infinite;
23 | }
24 |
25 | .lds-ellipsis div:nth-child(2) {
26 | left: 8px;
27 | animation: lds-ellipsis2 0.6s infinite;
28 | }
29 |
30 | .lds-ellipsis div:nth-child(3) {
31 | left: 32px;
32 | animation: lds-ellipsis2 0.6s infinite;
33 | }
34 |
35 | .lds-ellipsis div:nth-child(4) {
36 | left: 56px;
37 | animation: lds-ellipsis3 0.6s infinite;
38 | }
39 |
40 | @keyframes lds-ellipsis1 {
41 | 0% {
42 | transform: scale(0);
43 | }
44 | 100% {
45 | transform: scale(1);
46 | }
47 | }
48 | @keyframes lds-ellipsis3 {
49 | 0% {
50 | transform: scale(1);
51 | }
52 | 100% {
53 | transform: scale(0);
54 | }
55 | }
56 | @keyframes lds-ellipsis2 {
57 | 0% {
58 | transform: translate(0, 0);
59 | }
60 | 100% {
61 | transform: translate(24px, 0);
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/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/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/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/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 |
--------------------------------------------------------------------------------
/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/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/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/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 |
--------------------------------------------------------------------------------
/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sarjdev/front-end/425016e9ce2c4bc0ed17f06212d3e6ea11eb8bc8/app/favicon.ico
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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/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/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/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 |
--------------------------------------------------------------------------------
/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/stores/generalStore.ts:
--------------------------------------------------------------------------------
1 | import { create } from "zustand";
2 | import { SearchDetail } from "../types/search-detail";
3 | import { SearchNearest } from "../types/search-nearest";
4 |
5 | interface State {
6 | isBottomSheetOpen: boolean;
7 | isMarkerBottomSheetOpen: boolean;
8 | filteredLocationData: SearchNearest;
9 | markerBottomSheetData: SearchDetail | null;
10 | actions: {
11 | setBottomSheetOpen: (isBottomSheetOpen: boolean) => void;
12 | setMarkerBottomSheetOpen: (isMarkerBottomSheetOpen: boolean) => void;
13 | setFilteredLocationData: (filteredLocationData: SearchNearest) => void;
14 | setMarkerBottomSheetData: (markerBottomSheetData: SearchDetail | null) => void;
15 | };
16 | }
17 |
18 | export const useGeneralStore = create((set) => ({
19 | isBottomSheetOpen: false,
20 | isMarkerBottomSheetOpen: false,
21 | filteredLocationData: {} as SearchNearest,
22 | markerBottomSheetData: null,
23 | actions: {
24 | setBottomSheetOpen: (isBottomSheetOpen) => set(() => ({ isBottomSheetOpen })),
25 | setMarkerBottomSheetOpen: (isMarkerBottomSheetOpen) => set(() => ({ isMarkerBottomSheetOpen })),
26 | setFilteredLocationData: (filteredLocationData) => set(() => ({ filteredLocationData })),
27 | setMarkerBottomSheetData: (markerBottomSheetData) => set(() => ({ markerBottomSheetData }))
28 | }
29 | }));
30 |
--------------------------------------------------------------------------------
/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/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/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/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/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/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 |
--------------------------------------------------------------------------------
/app/types/types.d.ts:
--------------------------------------------------------------------------------
1 | declare module 'leaflet.markercluster';
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 | });
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | autoprefixer: {},
4 | },
5 | }
6 |
--------------------------------------------------------------------------------
/public/android-icon-144x144.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sarjdev/front-end/425016e9ce2c4bc0ed17f06212d3e6ea11eb8bc8/public/android-icon-144x144.png
--------------------------------------------------------------------------------
/public/android-icon-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sarjdev/front-end/425016e9ce2c4bc0ed17f06212d3e6ea11eb8bc8/public/android-icon-192x192.png
--------------------------------------------------------------------------------
/public/android-icon-36x36.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sarjdev/front-end/425016e9ce2c4bc0ed17f06212d3e6ea11eb8bc8/public/android-icon-36x36.png
--------------------------------------------------------------------------------
/public/android-icon-48x48.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sarjdev/front-end/425016e9ce2c4bc0ed17f06212d3e6ea11eb8bc8/public/android-icon-48x48.png
--------------------------------------------------------------------------------
/public/android-icon-72x72.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sarjdev/front-end/425016e9ce2c4bc0ed17f06212d3e6ea11eb8bc8/public/android-icon-72x72.png
--------------------------------------------------------------------------------
/public/android-icon-96x96.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sarjdev/front-end/425016e9ce2c4bc0ed17f06212d3e6ea11eb8bc8/public/android-icon-96x96.png
--------------------------------------------------------------------------------
/public/apple-icon-114x114.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sarjdev/front-end/425016e9ce2c4bc0ed17f06212d3e6ea11eb8bc8/public/apple-icon-114x114.png
--------------------------------------------------------------------------------
/public/apple-icon-120x120.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sarjdev/front-end/425016e9ce2c4bc0ed17f06212d3e6ea11eb8bc8/public/apple-icon-120x120.png
--------------------------------------------------------------------------------
/public/apple-icon-144x144.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sarjdev/front-end/425016e9ce2c4bc0ed17f06212d3e6ea11eb8bc8/public/apple-icon-144x144.png
--------------------------------------------------------------------------------
/public/apple-icon-152x152.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sarjdev/front-end/425016e9ce2c4bc0ed17f06212d3e6ea11eb8bc8/public/apple-icon-152x152.png
--------------------------------------------------------------------------------
/public/apple-icon-180x180.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sarjdev/front-end/425016e9ce2c4bc0ed17f06212d3e6ea11eb8bc8/public/apple-icon-180x180.png
--------------------------------------------------------------------------------
/public/apple-icon-57x57.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sarjdev/front-end/425016e9ce2c4bc0ed17f06212d3e6ea11eb8bc8/public/apple-icon-57x57.png
--------------------------------------------------------------------------------
/public/apple-icon-60x60.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sarjdev/front-end/425016e9ce2c4bc0ed17f06212d3e6ea11eb8bc8/public/apple-icon-60x60.png
--------------------------------------------------------------------------------
/public/apple-icon-72x72.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sarjdev/front-end/425016e9ce2c4bc0ed17f06212d3e6ea11eb8bc8/public/apple-icon-72x72.png
--------------------------------------------------------------------------------
/public/apple-icon-76x76.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sarjdev/front-end/425016e9ce2c4bc0ed17f06212d3e6ea11eb8bc8/public/apple-icon-76x76.png
--------------------------------------------------------------------------------
/public/apple-icon-precomposed.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sarjdev/front-end/425016e9ce2c4bc0ed17f06212d3e6ea11eb8bc8/public/apple-icon-precomposed.png
--------------------------------------------------------------------------------
/public/apple-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sarjdev/front-end/425016e9ce2c4bc0ed17f06212d3e6ea11eb8bc8/public/apple-icon.png
--------------------------------------------------------------------------------
/public/assets/images/loading-background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sarjdev/front-end/425016e9ce2c4bc0ed17f06212d3e6ea11eb8bc8/public/assets/images/loading-background.png
--------------------------------------------------------------------------------
/public/browserconfig.xml:
--------------------------------------------------------------------------------
1 |
2 | #ffffff
--------------------------------------------------------------------------------
/public/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sarjdev/front-end/425016e9ce2c4bc0ed17f06212d3e6ea11eb8bc8/public/favicon-16x16.png
--------------------------------------------------------------------------------
/public/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sarjdev/front-end/425016e9ce2c4bc0ed17f06212d3e6ea11eb8bc8/public/favicon-32x32.png
--------------------------------------------------------------------------------
/public/favicon-96x96.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sarjdev/front-end/425016e9ce2c4bc0ed17f06212d3e6ea11eb8bc8/public/favicon-96x96.png
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sarjdev/front-end/425016e9ce2c4bc0ed17f06212d3e6ea11eb8bc8/public/favicon.ico
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Sarj.dev",
3 | "short_name": "Sarj.dev",
4 | "display": "standalone",
5 | "start_url": "/",
6 | "theme_color": "#000000",
7 | "background_color": "#ffffff",
8 | "icons": [
9 | {
10 | "src": "/android-icon-36x36.png",
11 | "sizes": "36x36",
12 | "type": "image/png",
13 | "density": "0.75"
14 | },
15 | {
16 | "src": "/android-icon-48x48.png",
17 | "sizes": "48x48",
18 | "type": "image/png",
19 | "density": "1.0"
20 | },
21 | {
22 | "src": "/android-icon-72x72.png",
23 | "sizes": "72x72",
24 | "type": "image/png",
25 | "density": "1.5"
26 | },
27 | {
28 | "src": "/android-icon-96x96.png",
29 | "sizes": "96x96",
30 | "type": "image/png",
31 | "density": "2.0"
32 | },
33 | {
34 | "src": "/android-icon-144x144.png",
35 | "sizes": "144x144",
36 | "type": "image/png",
37 | "density": "3.0"
38 | },
39 | {
40 | "src": "/android-icon-192x192.png",
41 | "sizes": "192x192",
42 | "type": "image/png",
43 | "density": "4.0"
44 | }
45 | ]
46 | }
47 |
--------------------------------------------------------------------------------
/public/ms-icon-144x144.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sarjdev/front-end/425016e9ce2c4bc0ed17f06212d3e6ea11eb8bc8/public/ms-icon-144x144.png
--------------------------------------------------------------------------------
/public/ms-icon-150x150.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sarjdev/front-end/425016e9ce2c4bc0ed17f06212d3e6ea11eb8bc8/public/ms-icon-150x150.png
--------------------------------------------------------------------------------
/public/ms-icon-310x310.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sarjdev/front-end/425016e9ce2c4bc0ed17f06212d3e6ea11eb8bc8/public/ms-icon-310x310.png
--------------------------------------------------------------------------------
/public/ms-icon-70x70.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sarjdev/front-end/425016e9ce2c4bc0ed17f06212d3e6ea11eb8bc8/public/ms-icon-70x70.png
--------------------------------------------------------------------------------
/public/next.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/public/sarjdev-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sarjdev/front-end/425016e9ce2c4bc0ed17f06212d3e6ea11eb8bc8/public/sarjdev-logo.png
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------