├── .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 | 4 | marker 5 | 6 | 7 | 10 | 11 | 12 | 13 | 14 | 15 | 18 | 19 | 20 | 23 | 24 | 25 | 28 | 29 | 30 | 33 | 34 | 35 | 38 | 39 | 40 | 43 | 44 | 45 | 48 | 49 | 50 | 53 | 54 | 55 | 58 | 59 | 60 | 63 | 64 | 65 | 68 | 69 | 70 | 73 | 74 | 75 | 78 | 79 | 80 | 83 | 84 | 85 | 88 | 89 | 90 | 93 | 94 | 95 | 98 | 99 | 100 | 103 | 104 | 105 | 108 | 109 | 110 | 113 | 114 | 115 | 116 | 117 | -------------------------------------------------------------------------------- /app/assets/images/mastor.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | astor 5 | 6 | 7 | 10 | 11 | 12 | 13 | 14 | 17 | 18 | 19 | 22 | 23 | 24 | 27 | 28 | 29 | 32 | 33 | 34 | 37 | 38 | 39 | 42 | 43 | 44 | 47 | 48 | 49 | 52 | 53 | 54 | 57 | 58 | 59 | 62 | 63 | 64 | 67 | 68 | 69 | 72 | 73 | 74 | 77 | 78 | 79 | 82 | 83 | 84 | 87 | 88 | 89 | 92 | 93 | 94 | 97 | 98 | 99 | 102 | 103 | 104 | 107 | 108 | 109 | 110 | 111 | -------------------------------------------------------------------------------- /app/assets/images/mb.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | beefull 5 | 6 | 7 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /app/assets/images/me.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | esarj 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/assets/images/mtesla.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | tesla 5 | 6 | 7 | 10 | 11 | 12 | 13 | 14 | 17 | 18 | 19 | 22 | 23 | 24 | 27 | 28 | 29 | 32 | 33 | 34 | 37 | 38 | 39 | 42 | 43 | 44 | 47 | 48 | 49 | 52 | 53 | 54 | 57 | 58 | 59 | 62 | 63 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /app/assets/images/mtrugo.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | trugo 5 | 6 | 7 | 10 | 11 | 12 | 13 | 14 | 17 | 18 | 19 | 22 | 23 | 24 | 27 | 28 | 29 | 32 | 33 | 34 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /app/assets/images/mvoltrun.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | voltrun 5 | 6 | 7 | 10 | 11 | 12 | 13 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /app/assets/images/mwatt.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | wat 5 | 6 | 7 | 10 | 11 | 12 | 13 | 14 | 15 | 18 | 21 | 24 | 25 | 26 | 29 | 32 | 35 | 38 | 40 | 43 | 46 | 49 | 50 | 51 | 52 | 55 | 58 | 61 | 64 | 67 | 68 | 71 | 72 | 73 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /app/assets/images/mz.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | zes 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /app/assets/images/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 |
28 |

{content}

29 |
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 |
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 |
15 | 16 | {children} 17 |
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 |