diff --git a/README.md b/README.md index efc7369..f5f7c00 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ This application uses [device_tree-rs](https://github.com/platform-system-interf ## Screenshot -![grafik](https://github.com/m0veax/dtvis/assets/2205193/f15c087a-81d9-4935-9126-4d788e68459b) +![screenshot](assets/screenshot.png) ## Local Development diff --git a/app/DTNode.tsx b/app/DTNode.tsx index 5cdf221..6eddc2f 100644 --- a/app/DTNode.tsx +++ b/app/DTNode.tsx @@ -1,6 +1,7 @@ import { memo, useState } from "react"; import { Handle, NodeProps, Position } from "reactflow"; + const style = { // wordWrap: "break-word", whiteSpace: "pre-wrap" as "pre-wrap", // This is weird, TypoScripto... @@ -11,6 +12,7 @@ const style = { width: 150, fontSize: 11, fontFamily: "Fira Code", + display: "block", }; const DTNode = ({ @@ -20,12 +22,16 @@ const DTNode = ({ sourcePosition = Position.Bottom }: NodeProps) => { const [hovered, setHovered] = useState(false); + const [collapsed, setCollapsed] = useState(false); const hoverOn = () => setHovered(true); const hoverOff = () => setHovered(false); + const collapseOn = () => setCollapsed(collapsed ? false : true); const borderColor = hovered ? "#987987" : "#789789"; const borderStyle = hovered ? "dotted" : "solid"; + const collapseText = collapsed ? "[+]" : "[-]"; + return ( <> - {data?.label} + {data?.label} () + if (!storeRef.current) { + // Create the store instance the first time this renders + storeRef.current = makeStore() + storeRef.current.dispatch(initializeState(tree)) + } + + return {children} +} \ No newline at end of file diff --git a/app/features/tree/tree.tsx b/app/features/tree/tree.tsx new file mode 100644 index 0000000..b2fad95 --- /dev/null +++ b/app/features/tree/tree.tsx @@ -0,0 +1,68 @@ +import React, { useState } from 'react'; + +import { useAppSelector, useAppDispatch } from '../../hooks'; +import { + decrement, + increment, + incrementByAmount, + incrementAsync, + incrementIfOdd, + selectCount, +} from './treeSlice'; +import styles from './Counter.module.css'; + +export function Counter() { + const count = useAppSelector(selectCount); + const dispatch = useAppDispatch(); + const [incrementAmount, setIncrementAmount] = useState('2'); + + const incrementValue = Number(incrementAmount) || 0; + + return ( +
+
+ + {count} + +
+
+ setIncrementAmount(e.target.value)} + /> + + + +
+
+ ); +} \ No newline at end of file diff --git a/app/features/tree/treeApi.ts b/app/features/tree/treeApi.ts new file mode 100644 index 0000000..6bb90ca --- /dev/null +++ b/app/features/tree/treeApi.ts @@ -0,0 +1,7 @@ + +// A mock function to mimic making an async request for data +export function fetchTree(amount = 1) { + return new Promise<{ data: any }>((resolve) => + setTimeout(() => resolve({ data: amount }), 500) + ); + } \ No newline at end of file diff --git a/app/features/tree/treeSlice.ts b/app/features/tree/treeSlice.ts new file mode 100644 index 0000000..5d830ad --- /dev/null +++ b/app/features/tree/treeSlice.ts @@ -0,0 +1,85 @@ +import { createAsyncThunk, createSlice, PayloadAction } from '@reduxjs/toolkit'; +import { RootState, AppThunk } from '../../store'; +import { fetchTree } from './treeApi'; + + +export interface TreeState { + value: number; + status: 'idle' | 'loading' | 'failed'; +} + +const initialState: TreeState = { + value: 0, + status: 'idle', +}; + +// The function below is called a thunk and allows us to perform async logic. It +// can be dispatched like a regular action: `dispatch(incrementAsync(10))`. This +// will call the thunk with the `dispatch` function as the first argument. Async +// code can then be executed and other actions can be dispatched. Thunks are +// typically used to make async requests. +export const incrementAsync = createAsyncThunk( + 'tree/fetchCount', + async (amount: number) => { + const response = await fetchCount(amount); + // The value we return becomes the `fulfilled` action payload + return response.data; + } +); + +export const treeSlice = createSlice({ + name: 'tree', + initialState, + // The `reducers` field lets us define reducers and generate associated actions + reducers: { + increment: (state) => { + // Redux Toolkit allows us to write "mutating" logic in reducers. It + // doesn't actually mutate the state because it uses the Immer library, + // which detects changes to a "draft state" and produces a brand new + // immutable state based off those changes + state.value += 1; + }, + decrement: (state) => { + state.value -= 1; + }, + // Use the PayloadAction type to declare the contents of `action.payload` + incrementByAmount: (state, action: PayloadAction) => { + state.value += action.payload; + }, + }, + // The `extraReducers` field lets the slice handle actions defined elsewhere, + // including actions generated by createAsyncThunk or in other slices. + extraReducers: (builder) => { + builder + .addCase(incrementAsync.pending, (state) => { + state.status = 'loading'; + }) + .addCase(incrementAsync.fulfilled, (state, action) => { + state.status = 'idle'; + state.value += action.payload; + }) + .addCase(incrementAsync.rejected, (state) => { + state.status = 'failed'; + }); + }, +}); + +export const { increment, decrement, incrementByAmount } = treeSlice.actions; + +// The function below is called a selector and allows us to select a value from +// the state. Selectors can also be defined inline where they're used instead of +// in the slice file. For example: `useSelector((state: RootState) => state.tree.value)` +export const selectCount = (state: RootState) => state.tree.value; + +// We can also write thunks by hand, which may contain both sync and async logic. +// Here's an example of conditionally dispatching actions based on current state. +export const incrementIfOdd = + (amount: number): AppThunk => + (dispatch, getState) => { + const currentValue = selectCount(getState()); + if (currentValue % 2 === 1) { + dispatch(incrementByAmount(amount)); + } + }; + +export default treeSlice.reducer; \ No newline at end of file diff --git a/app/hooks.ts b/app/hooks.ts new file mode 100644 index 0000000..233256a --- /dev/null +++ b/app/hooks.ts @@ -0,0 +1,8 @@ +import { useDispatch, useSelector, useStore } from 'react-redux' +import type { TypedUseSelectorHook } from 'react-redux' +import type { RootState, AppDispatch, AppStore } from './store' + +// Use throughout your app instead of plain `useDispatch` and `useSelector` +export const useAppDispatch: () => AppDispatch = useDispatch +export const useAppSelector: TypedUseSelectorHook = useSelector +export const useAppStore: () => AppStore = useStore \ No newline at end of file diff --git a/app/layout.tsx b/app/layout.tsx index a0511cc..b646c0c 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -2,7 +2,6 @@ import type { Metadata } from 'next' import { Inter } from 'next/font/google' import 'reactflow/dist/style.css'; import './globals.css' - const inter = Inter({ subsets: ['latin'] }) export const metadata: Metadata = { diff --git a/app/store.ts b/app/store.ts new file mode 100644 index 0000000..d4cb7d9 --- /dev/null +++ b/app/store.ts @@ -0,0 +1,20 @@ +import { configureStore, ThunkAction, Action } from '@reduxjs/toolkit'; +import treeReducer from './features/tree/treeSlice'; + +export const makeStore = () => { + return configureStore({ + reducer: {} + }) +} + +// Infer the type of makeStore +export type AppStore = ReturnType +// Infer the `RootState` and `AppDispatch` types from the store itself +export type RootState = ReturnType +export type AppDispatch = AppStore['dispatch'] +export type AppThunk = ThunkAction< + ReturnType, + RootState, + unknown, + Action +>; \ No newline at end of file diff --git a/assets/screenshot.png b/assets/screenshot.png new file mode 100644 index 0000000..2568d77 Binary files /dev/null and b/assets/screenshot.png differ diff --git a/package-lock.json b/package-lock.json index 43e4ef1..0ecf038 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,7 @@ "name": "dtvis", "version": "0.1.0", "dependencies": { - "@reduxjs/toolkit": "^1.9.5", + "@reduxjs/toolkit": "^1.9.7", "@types/node": "20.5.9", "@types/react": "18.2.21", "@types/react-dom": "18.2.7", @@ -17,7 +17,7 @@ "next": "^14.0.0", "react": "18.2.0", "react-dom": "18.2.0", - "react-redux": "^8.1.2", + "react-redux": "^8.1.3", "reactflow": "^11.8.3", "redux": "^4.2.1", "typescript": "5.2.2", @@ -405,9 +405,9 @@ } }, "node_modules/@reduxjs/toolkit": { - "version": "1.9.5", - "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-1.9.5.tgz", - "integrity": "sha512-Rt97jHmfTeaxL4swLRNPD/zV4OxTes4la07Xc4hetpUW/vc75t5m1ANyxG6ymnEQ2FsLQsoMlYB2vV1sO3m8tQ==", + "version": "1.9.7", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-1.9.7.tgz", + "integrity": "sha512-t7v8ZPxhhKgOKtU+uyJT13lu4vL7az5aFi4IdoDs/eS548edn2M8Ik9h8fxgvMjGoAUVFSt6ZC1P5cWmQ014QQ==", "dependencies": { "immer": "^9.0.21", "redux": "^4.2.1", @@ -3299,9 +3299,9 @@ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, "node_modules/react-redux": { - "version": "8.1.2", - "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-8.1.2.tgz", - "integrity": "sha512-xJKYI189VwfsFc4CJvHqHlDrzyFTY/3vZACbE+rr/zQ34Xx1wQfB4OTOSeOSNrF6BDVe8OOdxIrAnMGXA3ggfw==", + "version": "8.1.3", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-8.1.3.tgz", + "integrity": "sha512-n0ZrutD7DaX/j9VscF+uTALI3oUPa/pO4Z3soOBIjuRn/FzVu6aehhysxZCLi6y7duMf52WNZGMl7CtuK5EnRw==", "dependencies": { "@babel/runtime": "^7.12.1", "@types/hoist-non-react-statics": "^3.3.1", diff --git a/package.json b/package.json index d70778d..7d9539a 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "lint": "next lint" }, "dependencies": { - "@reduxjs/toolkit": "^1.9.5", + "@reduxjs/toolkit": "^1.9.7", "@types/node": "20.5.9", "@types/react": "18.2.21", "@types/react-dom": "18.2.7", @@ -18,7 +18,7 @@ "next": "^14.0.0", "react": "18.2.0", "react-dom": "18.2.0", - "react-redux": "^8.1.2", + "react-redux": "^8.1.3", "reactflow": "^11.8.3", "redux": "^4.2.1", "typescript": "5.2.2",