diff --git a/.env b/.env new file mode 100644 index 0000000..4b0f9f5 --- /dev/null +++ b/.env @@ -0,0 +1 @@ +VITE_FLATTASTIC_API_KEY="Lrq8tXOeK4aA1563zvS1soZZElKoomz2" \ No newline at end of file diff --git a/bun.lock b/bun.lock index dcec529..e5adb16 100644 --- a/bun.lock +++ b/bun.lock @@ -6,10 +6,12 @@ "dependencies": { "@reduxjs/toolkit": "^2.8.2", "@types/lodash": "^4.17.20", + "@types/node": "^24.1.0", "lodash": "^4.17.21", "react": "^19.1.0", "react-dom": "^19.1.0", "react-redux": "^9.2.0", + "redux-persist": "^6.0.0", }, "devDependencies": { "@eslint/js": "^9.31.0", @@ -204,6 +206,8 @@ "@types/lodash": ["@types/lodash@4.17.20", "", {}, "sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA=="], + "@types/node": ["@types/node@24.1.0", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-ut5FthK5moxFKH2T1CUOC6ctR67rQRvvHdFLCD2Ql6KXmMuCrjsSsRI9UsLCm9M18BMwClv4pn327UvB7eeO1w=="], + "@types/react": ["@types/react@19.1.8", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g=="], "@types/react-dom": ["@types/react-dom@19.1.6", "", { "peerDependencies": { "@types/react": "^19.0.0" } }, "sha512-4hOiT/dwO8Ko0gV1m/TJZYk3y0KBnY9vzDh7W+DH17b2HFSOGgdj33dhihPeuy3l0q23+4e+hoXHV6hCC4dCXw=="], @@ -616,6 +620,8 @@ "redux": ["redux@5.0.1", "", {}, "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w=="], + "redux-persist": ["redux-persist@6.0.0", "", { "peerDependencies": { "redux": ">4.0.0" } }, "sha512-71LLMbUq2r02ng2We9S215LtPu3fY0KgaGE0k8WRgl6RkqxtGfl7HUozz1Dftwsb0D/5mZ8dwAaPbtnzfvbEwQ=="], + "redux-thunk": ["redux-thunk@3.1.0", "", { "peerDependencies": { "redux": "^5.0.0" } }, "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw=="], "reflect.getprototypeof": ["reflect.getprototypeof@1.0.10", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.7", "get-proto": "^1.0.1", "which-builtin-type": "^1.2.1" } }, "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw=="], @@ -712,6 +718,8 @@ "unbox-primitive": ["unbox-primitive@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "has-bigints": "^1.0.2", "has-symbols": "^1.1.0", "which-boxed-primitive": "^1.1.1" } }, "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw=="], + "undici-types": ["undici-types@7.8.0", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="], + "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], "use-sync-external-store": ["use-sync-external-store@1.5.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A=="], diff --git a/package.json b/package.json index bfd2802..7b5f256 100644 --- a/package.json +++ b/package.json @@ -12,10 +12,12 @@ "dependencies": { "@reduxjs/toolkit": "^2.8.2", "@types/lodash": "^4.17.20", + "@types/node": "^24.1.0", "lodash": "^4.17.21", "react": "^19.1.0", "react-dom": "^19.1.0", - "react-redux": "^9.2.0" + "react-redux": "^9.2.0", + "redux-persist": "^6.0.0" }, "devDependencies": { "@eslint/js": "^9.31.0", diff --git a/src/App.tsx b/src/App.tsx index 3530ebf..5f62f17 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -4,16 +4,20 @@ import { useDispatch } from "react-redux"; import { fetchTimetable } from "./store/index"; import { useEffect } from "react"; import { type AppDispatch } from "./store/index"; +import Flatastic from "./components/Flatastic/Flatastic"; +import fetchFlatasticChores from "./store/thunks/fetchFlatasticChores"; function App() { const dispatch = useDispatch(); // Fetch the timetable data when the app loads useEffect(() => { dispatch(fetchTimetable()); + dispatch(fetchFlatasticChores()); }, [dispatch]); return ( <> + ); } diff --git a/src/api/api.ts b/src/api/api.ts index 8f3f270..4a4aa98 100644 --- a/src/api/api.ts +++ b/src/api/api.ts @@ -1,9 +1,13 @@ -function callApi(stopId: number) { - const API_URL = `https://projekte.kvv-efa.de/sl3-alone/XSLT_DM_REQUEST?outputFormat=JSON&coordOutputFormat=WGS84[dd.ddddd]&depType=stopEvents&locationServerActive=1&mode=direct&name_dm=${stopId}&type_dm=stop&useOnlyStops=1&useRealtime=1&limit=6&line=kvv%253A22301%253AE%253AH%253As25&line=kvv%253A21012%253AE%253AH%253As25&line=kvv%253A21012%253AE%253AR%253As25&line=kvv%253A22305%253AE%253AH%253As25`; +async function callApi(stopId: number) { + const API_URL = `https://projekte.kvv-efa.de/sl3-alone/XSLT_DM_REQUEST?outputFormat=JSON&coordOutputFormat=WGS84[dd.ddddd]&depType=stopEvents&locationServerActive=1&mode=direct&name_dm=${stopId}&type_dm=stop&useOnlyStops=1&useRealtime=1&limit=6&line=kvv:22301:E:H:s25&line=kvv:21012:E:H:s25&line=kvv:21012:E:R:s25&line=kvv:22305:E:H:s25`; - return fetch(API_URL, { + const data = await fetch(API_URL, { method: "GET", }); + if (!data.ok) { + throw new Error(`HTTP error! status: ${data.status}`); + } + return data; } export { callApi }; diff --git a/src/api/flatastic.ts b/src/api/flatastic.ts new file mode 100644 index 0000000..3a22691 --- /dev/null +++ b/src/api/flatastic.ts @@ -0,0 +1,55 @@ + +class Flatastic { + private apikey: string; + + constructor(apikey: string) { + this.apikey = apikey; + } + + async request(url: string, option: any, cb: (info: any) => void) { + const headers = { + "accept": "application/json, text/plain, */*", + "accept-language": "de-CH,de;q=0.9,en-US;q=0.8,en-CH;q=0.7,en;q=0.6,ar-JO;q=0.5,ar;q=0.4,de-DE;q=0.3", + // "cache-control": "no-cache", + // "pragma": "no-cache", + // "sec-fetch-dest": "empty", + // "sec-fetch-mode": "cors", + // "sec-fetch-site": "same-site", + "x-api-key": this.apikey, + "x-api-version": "2.0.0", + "x-client-version": "2.3.20" + }; + try { + const response = await fetch(url, { + method: 'GET', + headers: headers + }); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + const info = await response.json(); + cb(info); + } catch (error) { + let message = 'Unknown error'; + if (error instanceof Error) { + message = error.message; + } + cb({ error: message }); + } + } + + getShoppingList(callback: (info: any) => void) { + this.request('https://api.flatastic-app.com/index.php/api/shoppinglist', {}, callback); + } + + getTaskList(callback: (info: any) => void) { + this.request('https://api.flatastic-app.com/index.php/api/chores', {}, callback); + } + + getInformation(callback: (info: any) => void) { + this.request('https://api.flatastic-app.com/index.php/api/wg', {}, callback); + } +} + +export { Flatastic }; +export default Flatastic; \ No newline at end of file diff --git a/src/components/Flatastic/Flatastic.tsx b/src/components/Flatastic/Flatastic.tsx new file mode 100644 index 0000000..8346352 --- /dev/null +++ b/src/components/Flatastic/Flatastic.tsx @@ -0,0 +1,21 @@ +import fetchFlatasticChores from "@/store/thunks/fetchFlatasticChores"; +import { useEffect } from "react"; +import { useDispatch } from "react-redux"; +import { type AppDispatch } from "@/store/index"; + +export default function Flatastic() { + const dispatch = useDispatch(); + useEffect(() => { + const intervalID = setInterval(() => { + dispatch(fetchFlatasticChores()); + }, 60000); // Fetch every 60 seconds + + return () => clearInterval(intervalID); + }, []); + + return ( +

+ Flatastic API Key: {import.meta.env.VITE_FLATTASTIC_API_KEY || "Not set"} +

+ ) +} \ No newline at end of file diff --git a/src/components/Timetable/Timetable.tsx b/src/components/Timetable/Timetable.tsx index 52d8b41..17473b4 100644 --- a/src/components/Timetable/Timetable.tsx +++ b/src/components/Timetable/Timetable.tsx @@ -1,23 +1,19 @@ import { useEffect } from "react"; import { useDispatch, useSelector } from "react-redux"; -import { fetchTimetable, type AppDispatch, type AppState } from "@/store/index"; -import { type DepartureList } from "@/types/types"; +import { + fetchTimetable, + type AppDispatch, + type AppState, +} from "../../store/index"; +import { type DepartureList } from "../../types/types"; +import TimetableRow from "../TimetableRow/TimetableRow"; import _ from "lodash"; function parseTimetableData(data: DepartureList[]) { const result = data.map((item) => { - const { dateTime } = item; - const hour = _.padStart(_.toString(dateTime.hour), 2, "0"); - const minute = _.padStart(_.toString(dateTime.minute), 2, "0"); - const dateTimeString = `${hour}:${minute}`; return { - dateTimeString, - servingLine: { - number: item.servingLine.number, - name: item.servingLine.name, - direction: item.servingLine.direction, - }, + ...item, }; }); @@ -47,31 +43,21 @@ export default function Timetable() {

Timetable

H-Street Departures

-
    + + {hStreetData.map((departure, index) => ( -
  • - {departure.dateTimeString} -{" "} - {departure.servingLine?.name || "Unknown Line"}{" "} - {departure.servingLine?.number || "Unknown Number"} ( - {departure.servingLine?.direction || - "Unknown Direction"} - ) -
  • + ))} - + +

    P-Street Departures

    -
      + + {pStreetData.map((departure, index) => ( -
    • - {departure.dateTimeString} -{" "} - {departure.servingLine?.name || "Unknown Line"}{" "} - {departure.servingLine?.number || "Unknown Number"} ( - {departure.servingLine?.direction || - "Unknown Direction"} - ) -
    • + ))} - + +
); } diff --git a/src/components/TimetableRow/TimetableRow.tsx b/src/components/TimetableRow/TimetableRow.tsx new file mode 100644 index 0000000..445bb6f --- /dev/null +++ b/src/components/TimetableRow/TimetableRow.tsx @@ -0,0 +1,23 @@ +import { type DepartureList } from "../../types/types"; + +import styles from "./style.module.css"; + +export default function TimetableRow({ + departure, +}: { + departure: DepartureList; +}) { + const hour = String(departure.dateTime.hour).padStart(2, "0"); + const minute = String(departure.dateTime.minute).padStart(2, "0"); + const dateTimeString = `${hour}:${minute}`; + return ( + <> + + {dateTimeString} + {departure.servingLine.name} + {departure.servingLine.number} + ({departure.servingLine.direction}) + + + ); +} diff --git a/src/components/TimetableRow/style.module.css b/src/components/TimetableRow/style.module.css new file mode 100644 index 0000000..a6f418f --- /dev/null +++ b/src/components/TimetableRow/style.module.css @@ -0,0 +1,3 @@ +.timetableRow { + text-align: left; +} diff --git a/src/constants/timetable.ts b/src/constants/timetable.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/main.tsx b/src/main.tsx index 0905455..f2d2ee4 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,14 +1,21 @@ import { StrictMode } from "react"; import { createRoot } from "react-dom/client"; import { Provider } from "react-redux"; +import { PersistGate } from 'redux-persist/integration/react' +import { persistStore } from 'redux-persist' + import "./index.css"; import App from "./App.tsx"; import store from "./store/index.ts"; +let persistor = persistStore(store) + createRoot(document.getElementById("root")!).render( - + + + , ); diff --git a/src/store/index.ts b/src/store/index.ts index 0190f07..57d7b30 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -1,48 +1,38 @@ import { configureStore } from "@reduxjs/toolkit"; -import { createSlice } from "@reduxjs/toolkit"; -import { createAsyncThunk } from "@reduxjs/toolkit"; -import { callApi } from "@/api/api"; -import { type DepartureList } from "@/types/types"; +import { persistReducer } from "redux-persist"; +import timetableSlice from "./slices/timetableSlice"; +import flatasticChoresSlice from "./slices/flatastiucChoresSlice"; +import fetchTimetable from "./thunks/fetchTimetable"; +import persistConfig from "./persist/persistConfig"; -const fetchTimetable = createAsyncThunk("timetable/phillipStreet", async () => { - const hStreetStopId = 7000044; - const pStreetStopId = 7000045; +const persistedTimetableReducer = persistReducer( + persistConfig, + timetableSlice.reducer +); - const hStreetData = await callApi(hStreetStopId); - const pStreetData = await callApi(pStreetStopId); - - const hStreetJson = await hStreetData.json(); - const pStreetJson = await pStreetData.json(); - - return { - hStreet: hStreetJson, - pStreet: pStreetJson, - }; -}); - -const timetableSlice = createSlice({ - name: "timetable", - initialState: { - hStreet: { - departureList: [] as DepartureList[], - }, - pStreet: { - departureList: [] as DepartureList[], - }, - }, - reducers: {}, - extraReducers: (builder) => { - builder.addCase(fetchTimetable.fulfilled, (state, action) => { - state.hStreet = action.payload.hStreet; - state.pStreet = action.payload.pStreet; - }); - }, -}); +const persistedFlatasticChoresReducer = persistReducer( + persistConfig, + flatasticChoresSlice.reducer +); const store = configureStore({ reducer: { - timetable: timetableSlice.reducer, + timetable: persistedTimetableReducer, + flatasticChores: persistedFlatasticChoresReducer }, + middleware: (getDefaultMiddleware) => + getDefaultMiddleware({ + serializableCheck: { + ignoredActions: [ + "persist/PERSIST", + "persist/REHYDRATE", + "persist/PAUSE", + "persist/FLUSH", + "persist/REGISTER", + "persist/PURGE", + ], + }, + }), }); export { store, fetchTimetable }; @@ -51,4 +41,3 @@ export type RootState = ReturnType; export type AppState = ReturnType; export type AppDispatch = typeof store.dispatch; export type ThunkDispatch = typeof store.dispatch; -export type fetchTimetableType = typeof fetchTimetable; diff --git a/src/store/persist/persistConfig.ts b/src/store/persist/persistConfig.ts new file mode 100644 index 0000000..7e166a5 --- /dev/null +++ b/src/store/persist/persistConfig.ts @@ -0,0 +1,8 @@ +import storage from 'redux-persist/lib/storage'; + +const persistConfig = { + key: 'root', + storage, +}; + +export default persistConfig; diff --git a/src/store/slices/flatastiucChoresSlice.ts b/src/store/slices/flatastiucChoresSlice.ts new file mode 100644 index 0000000..2933778 --- /dev/null +++ b/src/store/slices/flatastiucChoresSlice.ts @@ -0,0 +1,20 @@ +import { createSlice } from "@reduxjs/toolkit"; +import fetchFlatasticChores from "../thunks/fetchFlatasticChores"; + +const timetableSlice = createSlice({ + name: "chores", + initialState: { + chores: [] as any[], + }, + reducers: {}, + extraReducers: (builder) => { + builder.addCase(fetchFlatasticChores.fulfilled, (state, action) => { + // Filter out timetable-related entries + state.chores = Array.isArray(action.payload) + ? action.payload.filter((item) => item.name !== "hstreeet" && item.name !== "pstreet") + : []; + }); + }, +}); + +export default timetableSlice; diff --git a/src/store/slices/timetableSlice.ts b/src/store/slices/timetableSlice.ts new file mode 100644 index 0000000..00abce3 --- /dev/null +++ b/src/store/slices/timetableSlice.ts @@ -0,0 +1,24 @@ +import { createSlice } from "@reduxjs/toolkit"; +import fetchTimetable from "@thunks/fetchTimetable"; +import { type DepartureList } from "@/types/types"; + +const timetableSlice = createSlice({ + name: "timetable", + initialState: { + hStreet: { + departureList: [] as DepartureList[], + }, + pStreet: { + departureList: [] as DepartureList[], + }, + }, + reducers: {}, + extraReducers: (builder) => { + builder.addCase(fetchTimetable.fulfilled, (state, action) => { + state.hStreet = action.payload.hStreet; + state.pStreet = action.payload.pStreet; + }); + }, +}); + +export default timetableSlice; diff --git a/src/store/thunks/fetchFlatasticChores.ts b/src/store/thunks/fetchFlatasticChores.ts new file mode 100644 index 0000000..75a6698 --- /dev/null +++ b/src/store/thunks/fetchFlatasticChores.ts @@ -0,0 +1,22 @@ +import { createAsyncThunk } from "@reduxjs/toolkit"; +import { Flatastic } from "@/api/flatastic"; + +const fetchFlatasticChores = createAsyncThunk("flatastic/chores", async () => { + if (!import.meta.env.VITE_FLATTASTIC_API_KEY) { + throw new Error("Flatastic API Key is not set"); + } + const flatastic = new Flatastic(import.meta.env.VITE_FLATTASTIC_API_KEY); + const data = await new Promise((resolve, reject) => { + flatastic.getTaskList((info) => { + if (info.error) { + reject(new Error(info.error)); + } else { + resolve(info); + } + }); + }); + + return data; +}); + +export default fetchFlatasticChores; diff --git a/src/store/thunks/fetchTimetable.ts b/src/store/thunks/fetchTimetable.ts new file mode 100644 index 0000000..0a5985d --- /dev/null +++ b/src/store/thunks/fetchTimetable.ts @@ -0,0 +1,18 @@ +import { createAsyncThunk } from "@reduxjs/toolkit"; +import { callApi } from "@/api/api"; + +const fetchTimetable = createAsyncThunk("timetable/fetchTimeTable", async () => { + const hStreetStopId = 7000044; + const pStreetStopId = 7000045; + const hStreetData = await callApi(hStreetStopId); + const pStreetData = await callApi(pStreetStopId); + const hStreetJson = await hStreetData.json(); + const pStreetJson = await pStreetData.json(); + + return { + hStreet: hStreetJson, + pStreet: pStreetJson, + }; +}); + +export default fetchTimetable; diff --git a/tsconfig.app.json b/tsconfig.app.json index b7a4132..e1f695d 100644 --- a/tsconfig.app.json +++ b/tsconfig.app.json @@ -20,16 +20,14 @@ "@components/*": ["src/components/*"], "@store/*": ["src/store/*"], "@api/*": ["src/api/*"], - "@types/*": ["src/types/*"] + "@types/*": ["src/types/*"], + "@thunks/*": ["src/store/thunks/*"], + "@slices/*": ["src/store/slices/*"], }, /* Linting */ "strict": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "erasableSyntaxOnly": true, - "noFallthroughCasesInSwitch": true, - "noUncheckedSideEffectImports": true + "noUnusedLocals": true }, "include": ["src"] } diff --git a/tsconfig.json b/tsconfig.json index 456ffbd..d13f7ec 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,7 +11,9 @@ "@components/*": ["src/components/*"], "@store/*": ["src/store/*"], "@api/*": ["src/api/*"], - "@types/*": ["src/types/*"] + "@types/*": ["src/types/*"], + "@thunks/*": ["src/store/thunks/*"], + "@slices/*": ["src/store/slices/*"] }, "module": "esnext", "moduleResolution": "node", diff --git a/vite.config.ts b/vite.config.ts index b17502b..9568ffa 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,7 +1,19 @@ import { defineConfig } from "vite"; import react from "@vitejs/plugin-react-swc"; +import path from "path"; // https://vite.dev/config/ export default defineConfig({ plugins: [react()], + resolve: { + alias: { + "@": path.resolve(__dirname, "src"), + "@components": path.resolve(__dirname, "src/components"), + "@store": path.resolve(__dirname, "src/store"), + "@api": path.resolve(__dirname, "src/api"), + "@types": path.resolve(__dirname, "src/types"), + "@thunks": path.resolve(__dirname, "src/store/thunks"), + "@slices": path.resolve(__dirname, "src/store/slices"), + }, + }, });