diff --git a/.eslintrc.json b/.eslintrc.json index bffb357..580f3c9 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,3 +1,7 @@ { - "extends": "next/core-web-vitals" + "extends": "next/core-web-vitals", + "rules": { + "react/no-unescaped-entities": "off", + "@next/next/no-img-element": "off" + } } diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..8df415c --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Metehan Alp Saral + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index c403366..1b6359b 100644 --- a/README.md +++ b/README.md @@ -1,36 +1,20 @@ -This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). +# Istanbulkart -## Getting Started +Kişiye Özel Istanbulkart Uygulaması; kendi bilgisayarınızda kullanabileceğiniz ve özelleştirebileceğiniz bir arayüz sağlamaktadır. Tarayıcı tarafından takip edilmediğiniz sürece kişisel verileriniz iştahlı gözlerden uzak bir şekilde Istanbulkart'ınıza girebilirsiniz, en son nerede kullandığınızı görebilirsiniz. -First, run the development server: +## Gereksinimler -```bash -npm run dev -# or -yarn dev -# or -pnpm dev -# or -bun dev -``` - -Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. - -You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. - -This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. - -## Learn More +* [Node.js](https://nodejs.org/) +* bir İstanbulkart -To learn more about Next.js, take a look at the following resources: +## Kurulum -- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. -- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. - -You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! - -## Deploy on Vercel +```bash +npm install +``` -The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. +## Geliştirme -Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. +```bash +npm run dev +``` diff --git a/app/_variables.scss b/app/_variables.scss new file mode 100644 index 0000000..3f8237a --- /dev/null +++ b/app/_variables.scss @@ -0,0 +1,3 @@ +$bg-color: #1C1D2B; +$box-color: #0D0E17; +$theme-color: #1581be; diff --git a/app/api/getCardList/route.ts b/app/api/getCardList/route.ts new file mode 100644 index 0000000..64ccd8e --- /dev/null +++ b/app/api/getCardList/route.ts @@ -0,0 +1,37 @@ +import getRequest from "@/helpers/getRequest"; +import { cookies } from "next/headers"; +import { NextRequest, NextResponse } from "next/server"; + +export async function POST(req: NextRequest) { + const cookieStore = cookies(); + const token = String(cookieStore.get("IstKart_Token")?.value); + const customerNumber = String(cookieStore.get("IstKart_ID")?.value); + const sessionToken = String(cookieStore.get("IstKart_SToken")?.value); + + try { + const getCardList = await getRequest({ + token, + command: "RI.OnGetCardList", + data: { + CustomerNumber: customerNumber, + SessionToken: sessionToken, + }, + }); + + const cardListData = await getCardList.json(); + + if (cardListData.data.ResponseCode === "0") { + return NextResponse.json({ + status: true, + list: cardListData.data.CardList, + }); + } + } catch (error) { + console.error("Error fetching card list:", error); + } + + return NextResponse.json({ + status: false, + list: [], + }); +} diff --git a/app/api/getCardTransactionList/route.ts b/app/api/getCardTransactionList/route.ts new file mode 100644 index 0000000..552c136 --- /dev/null +++ b/app/api/getCardTransactionList/route.ts @@ -0,0 +1,53 @@ +import getRequest from "@/helpers/getRequest"; +import getThreeMonthsAgoDate from "@/utils/getThreeMonthsAgoDate"; +import { cookies } from "next/headers"; +import { NextRequest, NextResponse } from "next/server"; + +export async function POST(req: NextRequest) { + const cookieStore = cookies(); + const body: CardTransaction = await req.json(); + + if (!body.cardId) { + return NextResponse.json({ + status: false, + list: [], + }); + } + + const token = String(cookieStore.get("IstKart_Token")?.value); + const customerNumber = Number(String(cookieStore.get("IstKart_ID")?.value)); + const sessionToken = String(cookieStore.get("IstKart_SToken")?.value); + + const getTransactionList = await getRequest({ + token, + command: "RI.OnGetCardTransactionList", + data: { + CustomerNumber: customerNumber, + SessionToken: sessionToken, + CardId: body.cardId, + TrnType: 0, + PageIndex: 1, + PageSize: 30, + StartDate: getThreeMonthsAgoDate(), + EndDate: new Date().toISOString().slice(0, 10), + }, + }); + + try { + const transactionListData = await getTransactionList.json(); + + if (transactionListData.data.ResponseCode === "0") { + return NextResponse.json({ + status: true, + list: transactionListData.data.TransactionList, + }); + } + } catch (error) { + console.error("Error fetching transaction list:", error); + } + + return NextResponse.json({ + status: false, + list: [], + }); +} diff --git a/app/api/getCardTransactionList/types.d.ts b/app/api/getCardTransactionList/types.d.ts new file mode 100644 index 0000000..ae58a78 --- /dev/null +++ b/app/api/getCardTransactionList/types.d.ts @@ -0,0 +1,3 @@ +interface CardTransaction { + cardId: number +} diff --git a/app/api/login/route.ts b/app/api/login/route.ts new file mode 100644 index 0000000..9c0ca7e --- /dev/null +++ b/app/api/login/route.ts @@ -0,0 +1,84 @@ +import getRequest from "@/helpers/getRequest"; +import getToken from "@/helpers/getToken"; +import { NextRequest, NextResponse } from "next/server"; + +export async function POST(req: NextRequest) { + const body: LoginBody = await req.json(); + + if (body.type === "SEND_SMS" && body.phone_number && body.password) { + try { + const tokenResponse = await getToken(); + const token = await tokenResponse.json(); + + const loginResponse = await getRequest({ + token: token.token, + command: "RI.OnLogin", + data: { + CountryCode: "90", + CellPhone: String(body.phone_number), + Password: String(body.password), + DeviceId: "1.1.1.1", + OSType: 10, + IPAdress: "1.1.1.1", + Port: "", + }, + }); + + const loginData = await loginResponse.json(); + + if (loginData.data.ResponseCode === "0") { + return NextResponse.json({ + status: true, + message: "SMS_SENT", + token, + login: loginData, + }); + } else { + return NextResponse.json({ + status: false, + message: "ERROR", + }); + } + } catch (error) { + console.error("Error during login process:", error); + return NextResponse.json({ + status: false, + message: "ERROR", + }); + } + } else if (body.type === "SMS_CONTROL") { + const smsControlResponse = await getRequest({ + token: body.token, + command: "RI.OnConfirmLoginSms", + data: { + CountryCode: "90", + CellPhone: body.phone_number, + SmsToken: body.sms_code, + OSType: 10, + AppVersion: "0.1.0", + IPAdress: "", + Port: "", + }, + }); + + const smsData = await smsControlResponse.json(); + + if (smsData.data.ResponseCode === "1020") { + return NextResponse.json({ + status: false, + message: "WRONG_CODE", + }); + } else if (smsData.data.ResponseCode === "0") { + return NextResponse.json({ + status: true, + message: "OK", + data: smsData.data, + }); + } + } + + return NextResponse.json({ + status: false, + message: "WTF", + }); +} diff --git a/app/api/login/types.d.ts b/app/api/login/types.d.ts new file mode 100644 index 0000000..c82d7a8 --- /dev/null +++ b/app/api/login/types.d.ts @@ -0,0 +1,7 @@ +interface LoginBody { + phone_number: string + password: string + sms_code: string + token: string + type: "SEND_SMS" | "SMS_CONTROL" +} diff --git a/app/favicon.ico b/app/favicon.ico deleted file mode 100644 index 718d6fe..0000000 Binary files a/app/favicon.ico and /dev/null differ diff --git a/app/globals.css b/app/globals.css deleted file mode 100644 index d4f491e..0000000 --- a/app/globals.css +++ /dev/null @@ -1,107 +0,0 @@ -:root { - --max-width: 1100px; - --border-radius: 12px; - --font-mono: ui-monospace, Menlo, Monaco, 'Cascadia Mono', 'Segoe UI Mono', - 'Roboto Mono', 'Oxygen Mono', 'Ubuntu Monospace', 'Source Code Pro', - 'Fira Mono', 'Droid Sans Mono', 'Courier New', monospace; - - --foreground-rgb: 0, 0, 0; - --background-start-rgb: 214, 219, 220; - --background-end-rgb: 255, 255, 255; - - --primary-glow: conic-gradient( - from 180deg at 50% 50%, - #16abff33 0deg, - #0885ff33 55deg, - #54d6ff33 120deg, - #0071ff33 160deg, - transparent 360deg - ); - --secondary-glow: radial-gradient( - rgba(255, 255, 255, 1), - rgba(255, 255, 255, 0) - ); - - --tile-start-rgb: 239, 245, 249; - --tile-end-rgb: 228, 232, 233; - --tile-border: conic-gradient( - #00000080, - #00000040, - #00000030, - #00000020, - #00000010, - #00000010, - #00000080 - ); - - --callout-rgb: 238, 240, 241; - --callout-border-rgb: 172, 175, 176; - --card-rgb: 180, 185, 188; - --card-border-rgb: 131, 134, 135; -} - -@media (prefers-color-scheme: dark) { - :root { - --foreground-rgb: 255, 255, 255; - --background-start-rgb: 0, 0, 0; - --background-end-rgb: 0, 0, 0; - - --primary-glow: radial-gradient(rgba(1, 65, 255, 0.4), rgba(1, 65, 255, 0)); - --secondary-glow: linear-gradient( - to bottom right, - rgba(1, 65, 255, 0), - rgba(1, 65, 255, 0), - rgba(1, 65, 255, 0.3) - ); - - --tile-start-rgb: 2, 13, 46; - --tile-end-rgb: 2, 5, 19; - --tile-border: conic-gradient( - #ffffff80, - #ffffff40, - #ffffff30, - #ffffff20, - #ffffff10, - #ffffff10, - #ffffff80 - ); - - --callout-rgb: 20, 20, 20; - --callout-border-rgb: 108, 108, 108; - --card-rgb: 100, 100, 100; - --card-border-rgb: 200, 200, 200; - } -} - -* { - box-sizing: border-box; - padding: 0; - margin: 0; -} - -html, -body { - max-width: 100vw; - overflow-x: hidden; -} - -body { - color: rgb(var(--foreground-rgb)); - background: linear-gradient( - to bottom, - transparent, - rgb(var(--background-end-rgb)) - ) - rgb(var(--background-start-rgb)); -} - -a { - color: inherit; - text-decoration: none; -} - -@media (prefers-color-scheme: dark) { - html { - color-scheme: dark; - } -} diff --git a/app/globals.scss b/app/globals.scss new file mode 100644 index 0000000..d2a7c17 --- /dev/null +++ b/app/globals.scss @@ -0,0 +1,16 @@ +* { + box-sizing: border-box; + padding: 0; + margin: 0; +} + +html, +body { + max-width: 100vw; + overflow-x: hidden; +} + +a { + color: inherit; + text-decoration: none; +} diff --git a/app/layout.tsx b/app/layout.tsx index 40e027f..c6c9c31 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,21 +1,17 @@ -import type { Metadata } from 'next' -import { Inter } from 'next/font/google' -import './globals.css' +import type { Metadata } from "next" +import { Inter } from "next/font/google" +import "./globals.scss" -const inter = Inter({ subsets: ['latin'] }) +const inter = Inter({ subsets: ["latin"] }) export const metadata: Metadata = { - title: 'Create Next App', - description: 'Generated by create next app', + title: "Istanbulkart", + description: "Kişiye Özel Istanbulkart Uygulaması; kendi bilgisayarınızda kullanabileceğiniz ve özelleştirebileceğiniz bir arayüz sağlamaktadır.", } -export default function RootLayout({ - children, -}: { - children: React.ReactNode -}) { +export default function RootLayout({ children }: { children: React.ReactNode }) { return ( - + {children} ) diff --git a/app/login/page.module.scss b/app/login/page.module.scss new file mode 100644 index 0000000..523a01e --- /dev/null +++ b/app/login/page.module.scss @@ -0,0 +1,118 @@ +@import "../_variables.scss"; + +.container { + height: 100vh; + width: 100vw; + background-color: $bg-color; + overflow-y: auto; + + .center_content { + margin: 0 auto; + position: relative; + max-width: 500px; + width: 100%; + height: 100vh; + display: flex; + align-items: center; + + .login_form { + padding: 30px; + background-color: $box-color; + width: 100%; + border-radius: 15px; + gap: 30px; + display: flex; + flex-direction: column; + + .form_title { + display: flex; + flex-direction: column; + + span { + color: #A0A0A0; + user-select: none; + font-size: 20px; + + &:first-child { + color: #fff; + font-weight: bold; + font-size: 28px; + } + } + } + + .form_inputs { + display: flex; + flex-direction: column; + gap: 15px; + border: 2px solid #282934; + padding: 20px; + border-radius: 15px; + + &[data-active="true"] { + border-color: $theme-color; + background-color: rgba($theme-color, 0.03); + + .form_input { + span { + color: lighten($theme-color, 20); + } + + input { + background-color: rgba($theme-color, 0.05); + border-color: $theme-color !important; + color: lighten($theme-color, 30); + } + } + + .send_button { + cursor: pointer; + background-color: $theme-color; + color: lighten($theme-color, 55); + + &:hover { + background-color: rgba($theme-color, 0.8); + } + } + } + + .form_input { + display: flex; + flex-direction: column; + + span { + color: #CCCCCC; + font-size: 17px; + font-weight: bold; + } + + input { + margin-top: 10px; + border-radius: 15px; + color: #fff; + background-color: rgba($bg-color, 0.5); + outline: none; + border: 2px solid #282934; + padding: 15px; + font-size: 17px; + + &:focus { + border-color: #64656B; + } + } + } + + .send_button { + margin-top: 5px; + background-color: rgba($bg-color, 0.5); + padding: 15px; + border-radius: 15px; + font-weight: bold; + color: #D3D3D3; + text-align: center; + cursor: not-allowed; + } + } + } + } +} diff --git a/app/login/page.tsx b/app/login/page.tsx new file mode 100644 index 0000000..aaa8f8b --- /dev/null +++ b/app/login/page.tsx @@ -0,0 +1,160 @@ +"use client" + +import { useState } from "react" +import styles from "./page.module.scss" +import apiRequest from "@/helpers/apiRequest" +import { useRouter } from "next/navigation" +import { setCookie, getCookie } from "cookies-next" + +function Login() { + const router = useRouter(); + + const [token, setToken] = useState(""); + const [phone, setPhone] = useState(""); + const [password, setPassword] = useState(""); + const [page, setPage] = useState(0); + const [smsCode, setSmsCode] = useState(""); + + const sendSms = async () => { + if (phone.length === 0 || password.length === 0 || smsCode.length === 0) return; + + try { + const newRequest = await apiRequest({ + service: "/login", + body: { + phone_number: phone, + password: password, + sms_code: smsCode, + token: token, + type: "SMS_CONTROL", + }, + }); + + const data = await newRequest.json(); + + if (data.status) { + setCookies(data); + router.push("/profile"); + } else { + alert("Invalid code, please try again!"); + } + } catch (error) { + console.error("Error during login process:", error); + alert("An error occurred, please try again."); + } + }; + + const getSms = async () => { + if (phone.length === 0 || password.length === 0) return; + + try { + const newRequest = await apiRequest({ + service: "/login", + body: { + phone_number: phone, + password: password, + type: "SEND_SMS", + }, + }); + + const data = await newRequest.json(); + + if (data.status) { + setToken(data.token.token); + setPage(1); + } else { + resetPageAndInputs(); + alert("An error occurred while sending SMS, please try again."); + } + } catch (error) { + console.error("Error during login process:", error); + resetPageAndInputs(); + alert("An error occurred, please try again."); + } + }; + + const setCookies = (data: any) => { + setCookie("IstKart_ID", `${data.data.CustomerNumber}`); + setCookie("IstKart_Name", `${data.data.Name} ${data.data.Surname}`); + setCookie("IstKart_Email", `${data.data.Mail}`); + setCookie("IstKart_BirthDate", `${data.data.BirthDate}`); + setCookie("IstKart_SToken", `${data.data.SessionToken}`); + setCookie("IstKart_Token", `${token}`); + }; + + const resetPageAndInputs = () => { + setPage(0); + setPhone(""); + setPassword(""); + }; + + return ( +
+
+ {page === 0 && ( +
+
+ Giriş Yap + IstanbulKart bilgilerinle giriş yap +
+
+
+ Telefon Numarası + setPhone(e.target.value)} + /> +
+
+ Şifre + setPassword(e.target.value)} + /> +
+
+ Giriş Yap +
+
+
+ )} + {page === 1 && ( +
+
+ Bilgileri Doğrulama + {phone} telefon numarasına gönderilen kodu giriniz +
+
+
+ Doğrulama Kodu + setSmsCode(e.target.value)} + /> +
+
+ Doğrula +
+
+
+ )} +
+
+ ) +} + +export default Login diff --git a/app/page.module.css b/app/page.module.css deleted file mode 100644 index 6676d2c..0000000 --- a/app/page.module.css +++ /dev/null @@ -1,229 +0,0 @@ -.main { - display: flex; - flex-direction: column; - justify-content: space-between; - align-items: center; - padding: 6rem; - min-height: 100vh; -} - -.description { - display: inherit; - justify-content: inherit; - align-items: inherit; - font-size: 0.85rem; - max-width: var(--max-width); - width: 100%; - z-index: 2; - font-family: var(--font-mono); -} - -.description a { - display: flex; - justify-content: center; - align-items: center; - gap: 0.5rem; -} - -.description p { - position: relative; - margin: 0; - padding: 1rem; - background-color: rgba(var(--callout-rgb), 0.5); - border: 1px solid rgba(var(--callout-border-rgb), 0.3); - border-radius: var(--border-radius); -} - -.code { - font-weight: 700; - font-family: var(--font-mono); -} - -.grid { - display: grid; - grid-template-columns: repeat(4, minmax(25%, auto)); - max-width: 100%; - width: var(--max-width); -} - -.card { - padding: 1rem 1.2rem; - border-radius: var(--border-radius); - background: rgba(var(--card-rgb), 0); - border: 1px solid rgba(var(--card-border-rgb), 0); - transition: background 200ms, border 200ms; -} - -.card span { - display: inline-block; - transition: transform 200ms; -} - -.card h2 { - font-weight: 600; - margin-bottom: 0.7rem; -} - -.card p { - margin: 0; - opacity: 0.6; - font-size: 0.9rem; - line-height: 1.5; - max-width: 30ch; -} - -.center { - display: flex; - justify-content: center; - align-items: center; - position: relative; - padding: 4rem 0; -} - -.center::before { - background: var(--secondary-glow); - border-radius: 50%; - width: 480px; - height: 360px; - margin-left: -400px; -} - -.center::after { - background: var(--primary-glow); - width: 240px; - height: 180px; - z-index: -1; -} - -.center::before, -.center::after { - content: ''; - left: 50%; - position: absolute; - filter: blur(45px); - transform: translateZ(0); -} - -.logo { - position: relative; -} -/* Enable hover only on non-touch devices */ -@media (hover: hover) and (pointer: fine) { - .card:hover { - background: rgba(var(--card-rgb), 0.1); - border: 1px solid rgba(var(--card-border-rgb), 0.15); - } - - .card:hover span { - transform: translateX(4px); - } -} - -@media (prefers-reduced-motion) { - .card:hover span { - transform: none; - } -} - -/* Mobile */ -@media (max-width: 700px) { - .content { - padding: 4rem; - } - - .grid { - grid-template-columns: 1fr; - margin-bottom: 120px; - max-width: 320px; - text-align: center; - } - - .card { - padding: 1rem 2.5rem; - } - - .card h2 { - margin-bottom: 0.5rem; - } - - .center { - padding: 8rem 0 6rem; - } - - .center::before { - transform: none; - height: 300px; - } - - .description { - font-size: 0.8rem; - } - - .description a { - padding: 1rem; - } - - .description p, - .description div { - display: flex; - justify-content: center; - position: fixed; - width: 100%; - } - - .description p { - align-items: center; - inset: 0 0 auto; - padding: 2rem 1rem 1.4rem; - border-radius: 0; - border: none; - border-bottom: 1px solid rgba(var(--callout-border-rgb), 0.25); - background: linear-gradient( - to bottom, - rgba(var(--background-start-rgb), 1), - rgba(var(--callout-rgb), 0.5) - ); - background-clip: padding-box; - backdrop-filter: blur(24px); - } - - .description div { - align-items: flex-end; - pointer-events: none; - inset: auto 0 0; - padding: 2rem; - height: 200px; - background: linear-gradient( - to bottom, - transparent 0%, - rgb(var(--background-end-rgb)) 40% - ); - z-index: 1; - } -} - -/* Tablet and Smaller Desktop */ -@media (min-width: 701px) and (max-width: 1120px) { - .grid { - grid-template-columns: repeat(2, 50%); - } -} - -@media (prefers-color-scheme: dark) { - .vercelLogo { - filter: invert(1); - } - - .logo { - filter: invert(1) drop-shadow(0 0 0.3rem #ffffff70); - } -} - -@keyframes rotate { - from { - transform: rotate(360deg); - } - to { - transform: rotate(0deg); - } -} diff --git a/app/page.module.scss b/app/page.module.scss new file mode 100644 index 0000000..ed68e02 --- /dev/null +++ b/app/page.module.scss @@ -0,0 +1,13 @@ +@import "./variables"; + +.main { + background-color: $bg-color; + + .loader { + height: 100vh; + width: 100vw; + display: flex; + align-items: center; + justify-content: center; + } +} diff --git a/app/page.tsx b/app/page.tsx index 41d4d97..c036525 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,94 +1,32 @@ -import Image from 'next/image' -import styles from './page.module.css' +"use client" -export default function Home() { - return ( -
-
-

- Get started by editing  - app/page.tsx -

- -
+import { useEffect } from "react" +import styles from "./page.module.scss" +import { getCookie } from "cookies-next" +import { useRouter } from "next/navigation" +import { Bars } from "react-loader-spinner" -
- Next.js Logo -
- -
- -

- Docs -> -

-

Find in-depth information about Next.js features and API.

-
+export default function Home() { + const router = useRouter() - -

- Learn -> -

-

Learn about Next.js in an interactive course with quizzes!

-
+ useEffect(() => { + const token = getCookie("IstKart_Token") - -

- Templates -> -

-

Explore starter templates for Next.js.

-
+ if (typeof token !== "string") { + router.push("/login") + } else { + router.push("/profile") + } + }, [router]) - -

- Deploy -> -

-

- Instantly deploy your Next.js site to a shareable URL with Vercel. -

-
+ return ( +
+
+
) diff --git a/app/profile/page.module.scss b/app/profile/page.module.scss new file mode 100644 index 0000000..1e1ad71 --- /dev/null +++ b/app/profile/page.module.scss @@ -0,0 +1,175 @@ +@import "../_variables.scss"; + +.loader { + padding: 30px; + width: 100%; + display: flex; + justify-content: center; + align-items: center; +} + +.container { + height: 100vh; + width: 100vw; + background-color: $bg-color; + overflow-y: auto; + + .profile { + margin: 0 auto; + position: relative; + max-width: 800px; + width: 100%; + padding: 30px; + display: flex; + flex-direction: column; + gap: 15px; + + .header { + background-color: $box-color; + width: 100%; + border-radius: 15px; + + .banner { + img { + object-fit: cover; + height: 200px; + width: 100%; + border-radius: 15px 15px 0 0; + } + } + + .profile_content { + display: flex; + align-items: center; + padding: 30px; + } + + .profile_picture { + background: linear-gradient(45deg, rgb(50 119 239), lab(75.26 -21.65 -38.17)); + height: 80px; + width: 80px; + border-radius: 100%; + display: flex; + align-items: center; + justify-content: center; + } + + .profile_details { + display: flex; + flex-direction: column; + margin-left: 15px; + span { + font-size: 18px; + color: #777777; + + &:first-child { + font-size: 24px; + font-weight: bold; + color: #fff; + } + } + } + } + + .cards { + padding: 30px; + background-color: $box-color; + width: 100%; + border-radius: 15px; + display: flex; + flex-direction: column; + gap: 20px; + + .title { + font-size: 24px; + font-weight: bold; + color: #fff; + user-select: none; + margin-bottom: 10px; + } + + .item { + background-color: rgba($bg-color, 0.5); + padding: 15px; + border: 2px solid #282934; + border-radius: 15px; + color: #fff; + user-select: none; + + display: flex; + align-items: center; + + .item_left { + display: flex; + flex-direction: column; + } + } + + .card { + background-color: rgba($bg-color, 0.5); + padding: 15px; + border: 2px solid #282934; + border-radius: 15px; + color: #fff; + display: flex; + align-items: center; + justify-content: space-between; + cursor: pointer; + user-select: none; + + &:hover { + opacity: 0.8; + } + + .card_left { + display: flex; + align-items: center; + gap: 15px; + } + + .card_right { + display: flex; + align-items: center; + font-size: 20px; + font-weight: bold; + color: #fff; + } + + .card_icon { + background: linear-gradient(45deg, rgb(50 119 239), lab(75.26 -21.65 -38.17)); + height: 60px; + width: 40px; + border-radius: 4px; + } + + .metro_icon { + height: 40px; + width: 40px; + border-radius: 100%; + background: linear-gradient(45deg, rgb(50 119 239), lab(75.26 -21.65 -38.17));; + font-weight: bold; + display: flex; + align-items: center; + justify-content: center; + + } + + .card_details { + display: flex; + flex-direction: column; + + .card_nickname { + font-weight: bold; + font-size: 18px; + } + + .card_description { + font-size: 16px; + margin-top: 3px; + color: #B5B5B5; + } + } + } + } + } +} diff --git a/app/profile/page.tsx b/app/profile/page.tsx new file mode 100644 index 0000000..1da0266 --- /dev/null +++ b/app/profile/page.tsx @@ -0,0 +1,167 @@ +"use client" + +import { useEffect, useState } from "react" +import styles from "./page.module.scss" +import { setCookie, getCookie } from "cookies-next" +import getRequest from "@/helpers/getRequest" +import { useRouter } from "next/navigation" +import apiRequest from "@/helpers/apiRequest" +import { FaUser } from "react-icons/fa" +import capitalizeWords from "@/utils/capitalizeWords" +import { Bars } from "react-loader-spinner" +import { THEME_COLOR } from "@/constant" +import extractLastWord from "@/utils/extractLastWord" + +function Profile() { + const router = useRouter(); + const [token, setToken] = useState(""); + const [customerNumber, setCustomerNumber] = useState(""); + const [cards, setCards] = useState([]); + const [selectedCard, setSelectedCard] = useState(null); + const [transList, setTransList] = useState([]); + + const getCard = async (card: any) => { + setSelectedCard(card); + setTransList([]); + + try { + const getCardRequest = await apiRequest({ + service: "/getCardTransactionList", + body: { + cardId: card.CardId, + }, + }); + + const cardData = await getCardRequest.json(); + + if (cardData.status) { + setTransList(cardData.list); + } else { + setSelectedCard(null); + alert("No transactions found, try another card."); + } + } catch (error) { + console.error("Error fetching card transactions:", error); + alert("An error occurred, please try again."); + } + }; + + const getCards = async () => { + try { + const getCardListRequest = await apiRequest({ service: "/getCardList" }); + const cardList = await getCardListRequest.json(); + + if (cardList.status) { + setCards(cardList.list); + } + } catch (error) { + console.error("Error fetching card list:", error); + } + }; + + useEffect(() => { + const istKartToken = getCookie("IstKart_Token"); + const istKartId = getCookie("IstKart_ID"); + + if (typeof istKartToken === "string" && typeof istKartId === "string") { + setToken(istKartToken); + setCustomerNumber(istKartId); + + getCards(); + } else { + alert("An error occurred, please log in again."); + router.push("/login"); + } + }, [router]); + + return ( +
+
+
+
+ Istanbul +
+
+
+ +
+
+ {String(getCookie("IstKart_Name")) !== "undefined" && capitalizeWords(String(getCookie("IstKart_Name")))} + {getCookie("IstKart_Email")} +
+
+
+ +
+
+ Kartlar +
+ + {cards.length === 0 && ( +
+ +
+ )} + + {cards.length !== 0 && cards.map((card: any, index) => { + return ( +
getCard(card)} className={styles.card} key={index}> +
+
+
+ {card.NickName ? card.NickName : "İstanbulkart"} + {card.ProductCodeDesc} +
+
+
+ {card.CardBalance}₺ +
+
+ ) + })} +
+ + + {String(selectedCard) !== "null" && ( +
+
+ {selectedCard.NickName ? selectedCard.NickName : "İstanbulkart"} İşlemleri +
+ {transList.length === 0 && ( +
+ +
+ )} + {transList.length !== 0 && transList.map((item: any, index) => { + const line = extractLastWord(item.TrnDescription) + return ( +
+
+
{line === "MARMARAY" ? "B1" : line.startsWith("M") ? line : ""}
+
+ {item.TrnCodeDescription} {line.startsWith("M")} + {item.TrnDescription} +
+
+
+ {item.TrnAmount}₺ +
+
+ ) + })} +
+ )} +
+
+ ) +} + +export default Profile diff --git a/constant/index.ts b/constant/index.ts new file mode 100644 index 0000000..e49c12c --- /dev/null +++ b/constant/index.ts @@ -0,0 +1,5 @@ +export const THEME_COLOR = "#1581be" + +export const ISTANBULKART_GATEWAY = "https://apigateway.belbim.istanbul:8080/belbim/getRequest" +export const ISTANBULKART_REFERRER = "https://bireysel.istanbulkart.istanbul/" +export const ISTANBULKART_TOKEN = "NTE2ZDU2NGQ1ODMxNjQ0NjUxNmMzODc5NGQ0NDQ5Nzg1NTMwNjQ1NDU4MzE1NTc2NGMzMDY3NzQ1NjQ4NDI3MjUyNmMzODMwNTU1NjVhNDY1YTdhNTk3OTUyNTM0Njc5NWE2YjY0NmM1MjZhNjg2YjRiMzA3NDZlNGE1NzRhNjY2NTZiNjg2Yw==" diff --git a/helpers/apiRequest/index.ts b/helpers/apiRequest/index.ts new file mode 100644 index 0000000..5424d28 --- /dev/null +++ b/helpers/apiRequest/index.ts @@ -0,0 +1,13 @@ +async function apiRequest({ service, body = {}, method = "POST" }: ApiRequest) { + return await fetch("/api" + service, { + "headers": { + "accept": "application/json, text/plain, */*", + "access-control-allow-origin": "*", + "content-type": "application/json;charset=UTF-8" + }, + "body": JSON.stringify(body), + "method": method + }); +} + +export default apiRequest diff --git a/helpers/apiRequest/types.d.ts b/helpers/apiRequest/types.d.ts new file mode 100644 index 0000000..8058d84 --- /dev/null +++ b/helpers/apiRequest/types.d.ts @@ -0,0 +1,5 @@ +interface ApiRequest { + service: string + body?: any + method?: "POST" | "GET" +} diff --git a/helpers/getRequest/index.ts b/helpers/getRequest/index.ts new file mode 100644 index 0000000..78d97d4 --- /dev/null +++ b/helpers/getRequest/index.ts @@ -0,0 +1,21 @@ +import { ISTANBULKART_GATEWAY, ISTANBULKART_REFERRER } from "@/constant"; + +async function getRequest({ token, command, data, method = "POST" }: GetRequest) { + return await fetch(ISTANBULKART_GATEWAY, { + "headers": { + "accept": "application/json, text/plain, */*", + "access-control-allow-origin": "*", + "content-type": "application/json;charset=UTF-8" + }, + "referrer": ISTANBULKART_REFERRER, + "referrerPolicy": "strict-origin-when-cross-origin", + "body": JSON.stringify({ + "command": command, + "id": token, + "data": data + }), + "method": method + }); +} + +export default getRequest diff --git a/helpers/getRequest/types.d.ts b/helpers/getRequest/types.d.ts new file mode 100644 index 0000000..dd051c2 --- /dev/null +++ b/helpers/getRequest/types.d.ts @@ -0,0 +1,6 @@ +interface GetRequest { + token: string + command: string + data: any, + method?: "POST" | "GET" +} diff --git a/helpers/getToken/index.ts b/helpers/getToken/index.ts new file mode 100644 index 0000000..eca45e6 --- /dev/null +++ b/helpers/getToken/index.ts @@ -0,0 +1,30 @@ +import { ISTANBULKART_GATEWAY, ISTANBULKART_REFERRER, ISTANBULKART_TOKEN } from "@/constant"; + +async function getToken() { + const token = await fetch(ISTANBULKART_GATEWAY, { + "headers": { + "accept": "application/json, text/plain, */*", + "accept-language": "en-US,en;q=0.9", + "access-control-allow-origin": "*", + "content-type": "application/json;charset=UTF-8", + "password": "", + "sec-ch-ua": "\"Not_A Brand\";v=\"8\", \"Chromium\";v=\"120\", \"Google Chrome\";v=\"120\"", + "sec-ch-ua-mobile": "?0", + "sec-ch-ua-platform": "\"macOS\"", + "sec-fetch-dest": "empty", + "sec-fetch-mode": "cors", + "sec-fetch-site": "cross-site", + "username": "", + "Referer": ISTANBULKART_REFERRER, + "Referrer-Policy": "strict-origin-when-cross-origin" + }, + "body": JSON.stringify({ + res: ISTANBULKART_TOKEN + }), + "method": "POST" + }); + + return token +} + +export default getToken diff --git a/package-lock.json b/package-lock.json index 7867a82..32967d4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,9 +8,13 @@ "name": "istanbulkart-nextjs", "version": "0.1.0", "dependencies": { + "cookies-next": "^4.1.0", "next": "14.0.4", "react": "^18", - "react-dom": "^18" + "react-dom": "^18", + "react-icons": "^4.12.0", + "react-loader-spinner": "^6.1.6", + "sass": "^1.69.7" }, "devDependencies": { "@types/node": "^20", @@ -42,6 +46,24 @@ "node": ">=6.9.0" } }, + "node_modules/@emotion/is-prop-valid": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.1.tgz", + "integrity": "sha512-61Mf7Ufx4aDxx1xlDeOm8aFFigGHE4z+0sKCa+IHCeZKiyP9RLD0Mmx7m8b9/Cf37f7NAvQOOJAbQQGVr5uERw==", + "dependencies": { + "@emotion/memoize": "^0.8.1" + } + }, + "node_modules/@emotion/memoize": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz", + "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==" + }, + "node_modules/@emotion/unitless": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.0.tgz", + "integrity": "sha512-VINS5vEYAscRl2ZUDiT3uMPlrFQupiKgHz5AA4bCH1miKBg4qtwkim1qPmJj/4WG6TreYMY111rEFsjupcOKHw==" + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", @@ -329,6 +351,11 @@ "tslib": "^2.4.0" } }, + "node_modules/@types/cookie": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==" + }, "node_modules/@types/json5": { "version": "0.0.29", "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", @@ -376,6 +403,11 @@ "integrity": "sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==", "dev": true }, + "node_modules/@types/stylis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@types/stylis/-/stylis-4.2.0.tgz", + "integrity": "sha512-n4sx2bqL0mW1tvDf/loQ+aMX7GQD3lc3fkCMC55VFNDu/vBOabO+LTIeXKM14xK0ppk5TUGcWRjiSpIlUpghKw==" + }, "node_modules/@typescript-eslint/parser": { "version": "6.18.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.18.0.tgz", @@ -570,6 +602,18 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -766,6 +810,14 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, + "node_modules/binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "engines": { + "node": ">=8" + } + }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -780,7 +832,6 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, "dependencies": { "fill-range": "^7.0.1" }, @@ -822,6 +873,14 @@ "node": ">=6" } }, + "node_modules/camelize": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz", + "integrity": "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/caniuse-lite": { "version": "1.0.30001575", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001575.tgz", @@ -857,6 +916,43 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/client-only": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", @@ -886,6 +982,29 @@ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true }, + "node_modules/cookie": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", + "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookies-next": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cookies-next/-/cookies-next-4.1.0.tgz", + "integrity": "sha512-BREVc4TJT4NwXfyKjdjnYFXM6iRns+MYpCd34ClXuYqeisXnkPkbq7Ok9xaqi9mHmV6H2rwPE+p3EpMz4pF/kQ==", + "dependencies": { + "@types/cookie": "^0.4.1", + "@types/node": "^16.10.2", + "cookie": "^0.4.0" + } + }, + "node_modules/cookies-next/node_modules/@types/node": { + "version": "16.18.70", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.70.tgz", + "integrity": "sha512-8eIk20G5VVVQNZNouHjLA2b8utE2NvGybLjMaF4lyhA9uhGwnmXF8o+icdXKGSQSNANJewXva/sFUoZLwAaYAg==" + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -900,6 +1019,24 @@ "node": ">= 8" } }, + "node_modules/css-color-keywords": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz", + "integrity": "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==", + "engines": { + "node": ">=4" + } + }, + "node_modules/css-to-react-native": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.2.0.tgz", + "integrity": "sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==", + "dependencies": { + "camelize": "^1.0.0", + "css-color-keywords": "^1.0.0", + "postcss-value-parser": "^4.0.2" + } + }, "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", @@ -1626,7 +1763,6 @@ "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, "dependencies": { "to-regex-range": "^5.0.1" }, @@ -1685,6 +1821,19 @@ "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", "dev": true }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -1964,6 +2113,11 @@ "node": ">= 4" } }, + "node_modules/immutable": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.4.tgz", + "integrity": "sha512-fsXeu4J4i6WNWSikpI88v/PcVflZz+6kMhUfIwc5SY+poQRPnaf5V7qds6SUyUN3cVxEzuCab7QIoLOQ+DQ1wA==" + }, "node_modules/import-fresh": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", @@ -2060,6 +2214,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/is-boolean-object": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", @@ -2119,7 +2284,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -2155,7 +2319,6 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, "dependencies": { "is-extglob": "^2.1.1" }, @@ -2188,7 +2351,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, "engines": { "node": ">=0.12.0" } @@ -2622,6 +2784,14 @@ } } }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -2859,7 +3029,6 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, "engines": { "node": ">=8.6" }, @@ -2894,6 +3063,11 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -2966,12 +3140,52 @@ "react": "^18.2.0" } }, + "node_modules/react-icons": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-4.12.0.tgz", + "integrity": "sha512-IBaDuHiShdZqmfc/TwHu6+d6k2ltNCf3AszxNmjJc1KUfXdEeRJOKyNvLmAHaarhzGmTSVygNdyu8/opXv2gaw==", + "peerDependencies": { + "react": "*" + } + }, "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "dev": true }, + "node_modules/react-loader-spinner": { + "version": "6.1.6", + "resolved": "https://registry.npmjs.org/react-loader-spinner/-/react-loader-spinner-6.1.6.tgz", + "integrity": "sha512-x5h1Jcit7Qn03MuKlrWcMG9o12cp9SNDVHVJTNRi9TgtGPKcjKiXkou4NRfLAtXaFB3+Z8yZsVzONmPzhv2ErA==", + "dependencies": { + "react-is": "^18.2.0", + "styled-components": "^6.1.2" + }, + "engines": { + "node": ">= 12" + }, + "peerDependencies": { + "react": "^16.0.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/react-loader-spinner/node_modules/react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.4.tgz", @@ -3130,6 +3344,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/sass": { + "version": "1.69.7", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.69.7.tgz", + "integrity": "sha512-rzj2soDeZ8wtE2egyLXgOOHQvaC2iosZrkF6v3EUG+tBwEvhqUCzm0VP3k9gHF9LXbSrRhT5SksoI56Iw8NPnQ==", + "dependencies": { + "chokidar": ">=3.0.0 <4.0.0", + "immutable": "^4.0.0", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/scheduler": { "version": "0.23.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", @@ -3182,6 +3412,11 @@ "node": ">= 0.4" } }, + "node_modules/shallowequal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz", + "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -3340,6 +3575,43 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/styled-components": { + "version": "6.1.6", + "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-6.1.6.tgz", + "integrity": "sha512-DgTLULSC29xpabJ24bbn1+hulU6vvGFQf4RPwBOJrm8WJFnN42yXpo5voBt3jDSJBa5tBd1L6PqswJjQ0wRKdg==", + "dependencies": { + "@emotion/is-prop-valid": "1.2.1", + "@emotion/unitless": "0.8.0", + "@types/stylis": "4.2.0", + "css-to-react-native": "3.2.0", + "csstype": "3.1.2", + "postcss": "8.4.31", + "shallowequal": "1.1.0", + "stylis": "4.3.1", + "tslib": "2.5.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/styled-components" + }, + "peerDependencies": { + "react": ">= 16.8.0", + "react-dom": ">= 16.8.0" + } + }, + "node_modules/styled-components/node_modules/csstype": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", + "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==" + }, + "node_modules/styled-components/node_modules/tslib": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", + "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==" + }, "node_modules/styled-jsx": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.1.tgz", @@ -3362,6 +3634,11 @@ } } }, + "node_modules/stylis": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.1.tgz", + "integrity": "sha512-EQepAV+wMsIaGVGX1RECzgrcqRRU/0sYOHkeLsZ3fzHaHXZy4DaOOX0vOlGQdlsjkh3mFHAIlVimpwAs4dslyQ==" + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -3405,7 +3682,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, "dependencies": { "is-number": "^7.0.0" }, diff --git a/package.json b/package.json index 4727445..c37c865 100644 --- a/package.json +++ b/package.json @@ -9,16 +9,20 @@ "lint": "next lint" }, "dependencies": { + "cookies-next": "^4.1.0", + "next": "14.0.4", "react": "^18", "react-dom": "^18", - "next": "14.0.4" + "react-icons": "^4.12.0", + "react-loader-spinner": "^6.1.6", + "sass": "^1.69.7" }, "devDependencies": { - "typescript": "^5", "@types/node": "^20", "@types/react": "^18", "@types/react-dom": "^18", "eslint": "^8", - "eslint-config-next": "14.0.4" + "eslint-config-next": "14.0.4", + "typescript": "^5" } } diff --git a/public/anna-berdnik-0n0AHB1fgTQ-unsplash.jpg b/public/anna-berdnik-0n0AHB1fgTQ-unsplash.jpg new file mode 100644 index 0000000..fd99bc1 Binary files /dev/null and b/public/anna-berdnik-0n0AHB1fgTQ-unsplash.jpg differ diff --git a/public/next.svg b/public/next.svg deleted file mode 100644 index 5174b28..0000000 --- a/public/next.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/public/vercel.svg b/public/vercel.svg deleted file mode 100644 index d2f8422..0000000 --- a/public/vercel.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/utils/capitalizeWords.ts b/utils/capitalizeWords.ts new file mode 100644 index 0000000..ba79822 --- /dev/null +++ b/utils/capitalizeWords.ts @@ -0,0 +1,16 @@ +/** + * Capitalizes the first letter of each word in a given text. + * + * @param {string} text - The input text to be processed. + * @return {string} - The modified text with the first letter of each word capitalized. + */ +function capitalizeWords(text: string) { + const capitalizedWords = + String(text).trim().split(" ") + .map((word: string) => + word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()) + + return capitalizedWords.join(" ") +} + +export default capitalizeWords diff --git a/utils/extractLastWord.ts b/utils/extractLastWord.ts new file mode 100644 index 0000000..fa0b3a2 --- /dev/null +++ b/utils/extractLastWord.ts @@ -0,0 +1,6 @@ +function extractLastWord(text: string) { + const splitted_text = text.split("-") + return splitted_text[splitted_text.length - 1]; +} + +export default extractLastWord diff --git a/utils/getThreeMonthsAgoDate.ts b/utils/getThreeMonthsAgoDate.ts new file mode 100644 index 0000000..609aa7f --- /dev/null +++ b/utils/getThreeMonthsAgoDate.ts @@ -0,0 +1,20 @@ +/** + * Calculates the date exactly 3 months ago from today and returns it in "YYYY-MM-DD" format. + * @returns {string} The date three months ago in "YYYY-MM-DD" format. + */ +function getThreeMonthsAgoDate() { + // Get today's date + let today = new Date(); + + // Calculate the date three months ago + let threeMonthsAgo = new Date(today); + threeMonthsAgo.setMonth(today.getMonth() - 3); + + // Convert the date to the "YYYY-MM-DD" format + let formattedDate = threeMonthsAgo.toISOString().slice(0, 10); + + // Return the result + return formattedDate; +} + +export default getThreeMonthsAgoDate