proje-react-panel 1.0.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.idea/modules.xml +1 -1
- package/dist/assets/output-ML2TfIjs.css +1 -0
- package/dist/{list → components/list}/List.d.ts +2 -3
- package/dist/index.cjs.js +12 -1
- package/dist/index.d.ts +6 -1
- package/dist/index.esm.js +12 -1
- package/dist/screens/ControllerCreate.d.ts +0 -1
- package/dist/{getFields.d.ts → utils/getFields.d.ts} +1 -1
- package/dist/{storeData.d.ts → utils/storeData.d.ts} +1 -1
- package/package.json +9 -5
- package/src/api/crudApi.ts +16 -0
- package/src/components/Panel.tsx +13 -0
- package/src/components/layout/Layout.tsx +18 -0
- package/src/components/layout/SideBar.tsx +23 -0
- package/src/components/list/List.tsx +75 -0
- package/src/declerations/Cell.ts +37 -0
- package/src/declerations/Crud.ts +20 -0
- package/src/index.ts +6 -0
- package/src/screens/ControllerCreate.tsx +13 -0
- package/src/screens/ControllerDetails.tsx +34 -0
- package/src/screens/ControllerEdit.tsx +31 -0
- package/src/screens/ControllerList.tsx +40 -0
- package/src/screens/Form.tsx +67 -0
- package/src/styles/form.scss +58 -0
- package/src/styles/index.scss +4 -0
- package/src/styles/layout.scss +13 -0
- package/src/styles/list.scss +39 -0
- package/src/styles/sidebar.scss +76 -0
- package/src/types/Screen.ts +4 -0
- package/src/types/ScreenCreatorData.ts +9 -0
- package/src/utils/createScreens.ts +5 -0
- package/src/utils/getFields.ts +21 -0
- package/src/utils/getScreenForRoutes.tsx +44 -0
- package/src/utils/storeData.ts +7 -0
- /package/.idea/{react-panel.iml → proje-react-panel.iml} +0 -0
- /package/dist/{Panel.d.ts → components/Panel.d.ts} +0 -0
- /package/dist/{createScreens.d.ts → utils/createScreens.d.ts} +0 -0
- /package/dist/{getScreenForRoutes.d.ts → utils/getScreenForRoutes.d.ts} +0 -0
@@ -1,2 +1,2 @@
|
|
1
|
-
import { ScreenCreatorData } from "
|
1
|
+
import { ScreenCreatorData } from "../types/ScreenCreatorData";
|
2
2
|
export declare function getFields<T>(entityClass: T): ScreenCreatorData<T>;
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "proje-react-panel",
|
3
|
-
"version": "1.0.
|
3
|
+
"version": "1.0.1",
|
4
4
|
"description": "",
|
5
5
|
"author": "SEFA DEMİR",
|
6
6
|
"license": "ISC",
|
@@ -14,17 +14,18 @@
|
|
14
14
|
"scripts": {
|
15
15
|
"test": "echo \"Error: no test specified\" && exit 1",
|
16
16
|
"build": "rollup -c ",
|
17
|
-
"lint": "eslint src --ext .ts,.tsx"
|
17
|
+
"lint": "eslint src --ext .ts,.tsx",
|
18
|
+
"generate-nestjs-entity": "node scripts/generate-nestjs-entity.js"
|
18
19
|
},
|
19
20
|
"repository": {
|
20
21
|
"type": "git",
|
21
|
-
"url": "git+https://github.com/demirsefa/react-panel.git"
|
22
|
+
"url": "git+https://github.com/demirsefa/proje-react-panel.git"
|
22
23
|
},
|
23
24
|
"keywords": [],
|
24
25
|
"bugs": {
|
25
|
-
"url": "https://github.com/demirsefa/react-panel/issues"
|
26
|
+
"url": "https://github.com/demirsefa/proje-react-panel/issues"
|
26
27
|
},
|
27
|
-
"homepage": "https://github.com/demirsefa/react-panel#readme",
|
28
|
+
"homepage": "https://github.com/demirsefa/proje-react-panel#readme",
|
28
29
|
"dependencies": {
|
29
30
|
"axios": "^1.8.3",
|
30
31
|
"react-hook-form": "^7.54.2",
|
@@ -33,6 +34,7 @@
|
|
33
34
|
"devDependencies": {
|
34
35
|
"@hookform/resolvers": "^4.1.3",
|
35
36
|
"@rollup/plugin-commonjs": "^28.0.3",
|
37
|
+
"@rollup/plugin-json": "^6.1.0",
|
36
38
|
"@rollup/plugin-node-resolve": "^16.0.1",
|
37
39
|
"@types/react": "^19.0.10",
|
38
40
|
"@types/react-dom": "^19.0.4",
|
@@ -41,11 +43,13 @@
|
|
41
43
|
"class-transformer": "^0.5.1",
|
42
44
|
"class-validator": "^0.14.1",
|
43
45
|
"eslint": "^8.57.1",
|
46
|
+
"node-sass": "^9.0.0",
|
44
47
|
"react": "^19.0.0",
|
45
48
|
"react-router-dom": "^7.3.0",
|
46
49
|
"reflect-metadata": "^0.2.2",
|
47
50
|
"rollup": "^4.35.0",
|
48
51
|
"rollup-plugin-peer-deps-external": "^2.2.4",
|
52
|
+
"rollup-plugin-scss": "^4.0.1",
|
49
53
|
"rollup-plugin-terser": "^7.0.2",
|
50
54
|
"rollup-plugin-typescript2": "^0.36.0",
|
51
55
|
"typescript": "^5.8.2"
|
@@ -0,0 +1,16 @@
|
|
1
|
+
import axios from 'axios';
|
2
|
+
|
3
|
+
export const CrudApi = {
|
4
|
+
getList: (api: string, page: number) => {
|
5
|
+
return axios.get(api, { data: { page } });
|
6
|
+
},
|
7
|
+
create: (api: string, data: any) => {
|
8
|
+
return axios.post(api, data);
|
9
|
+
},
|
10
|
+
details(api: string, id: any) {
|
11
|
+
return axios.get(api + '/' + id);
|
12
|
+
},
|
13
|
+
edit(api: string, data: any) {
|
14
|
+
return axios.put(api + '/' + data.id, data);
|
15
|
+
},
|
16
|
+
};
|
@@ -0,0 +1,18 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
import { Outlet } from 'react-router-dom';
|
3
|
+
import { SideBar } from './SideBar';
|
4
|
+
|
5
|
+
export function Layout({
|
6
|
+
children,
|
7
|
+
noSidebar = false,
|
8
|
+
}: {
|
9
|
+
children?: React.ReactNode;
|
10
|
+
noSidebar?: boolean;
|
11
|
+
}) {
|
12
|
+
return (
|
13
|
+
<div className="layout">
|
14
|
+
{!noSidebar && <SideBar />}
|
15
|
+
<main className="content">{children || <Outlet />}</main>
|
16
|
+
</div>
|
17
|
+
);
|
18
|
+
}
|
@@ -0,0 +1,23 @@
|
|
1
|
+
import React, { useState } from 'react';
|
2
|
+
import { Link } from 'react-router-dom';
|
3
|
+
|
4
|
+
|
5
|
+
export function SideBar() {
|
6
|
+
const [isOpen, setIsOpen] = useState(true);
|
7
|
+
|
8
|
+
return (
|
9
|
+
<div className={`sidebar ${isOpen ? 'open' : 'closed'}`}>
|
10
|
+
<button className='toggle-button' onClick={() => setIsOpen(!isOpen)}>
|
11
|
+
{isOpen ? '<' : '>'}
|
12
|
+
</button>
|
13
|
+
<nav className='nav-links'>
|
14
|
+
<Link to='/'>Home</Link>
|
15
|
+
<Link to='/accounts'>Accounts</Link>
|
16
|
+
<Link to='/maps'>Maps</Link>
|
17
|
+
<div className='bottom-link'>
|
18
|
+
<Link to='/settings'>Settings</Link>
|
19
|
+
</div>
|
20
|
+
</nav>
|
21
|
+
</div>
|
22
|
+
);
|
23
|
+
}
|
@@ -0,0 +1,75 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
import { CellOptions } from '../../declerations/Cell';
|
3
|
+
import { Link } from 'react-router-dom';
|
4
|
+
import { Screen } from '../../types/Screen';
|
5
|
+
|
6
|
+
interface ListProps<T> {
|
7
|
+
data: T[];
|
8
|
+
cells: CellOptions<T>[];
|
9
|
+
screen: Screen;
|
10
|
+
}
|
11
|
+
|
12
|
+
export function List<T>({ data, cells, screen }: ListProps<T>) {
|
13
|
+
if (!data || data.length === 0) {
|
14
|
+
return <div>No items available</div>;
|
15
|
+
}
|
16
|
+
|
17
|
+
return (
|
18
|
+
<div className='list-wrapper'>
|
19
|
+
<div className='header'>List</div>
|
20
|
+
<table className='list-table'>
|
21
|
+
<thead>
|
22
|
+
<tr>
|
23
|
+
{cells.map((cellOptions) => (
|
24
|
+
<th key={cellOptions.name}>{cellOptions.title ?? cellOptions.name}</th>
|
25
|
+
))}
|
26
|
+
<th />
|
27
|
+
</tr>
|
28
|
+
</thead>
|
29
|
+
<tbody>
|
30
|
+
{data.map((item, index) => (
|
31
|
+
<tr key={index}>
|
32
|
+
{cells.map((cellOptions) => {
|
33
|
+
// @ts-ignore
|
34
|
+
const value = item[cellOptions.name];
|
35
|
+
let formattedValue = value ?? '-'; // Default value if the field is undefined or null
|
36
|
+
|
37
|
+
switch (cellOptions.type) {
|
38
|
+
case 'date':
|
39
|
+
if (value) {
|
40
|
+
const date = new Date(value);
|
41
|
+
formattedValue = `${date.getDate().toString().padStart(2, '0')}/${(date.getMonth() + 1)
|
42
|
+
.toString()
|
43
|
+
.padStart(2, '0')}/${date.getFullYear()} ${date.getHours().toString().padStart(2, '0')}:${date
|
44
|
+
.getMinutes()
|
45
|
+
.toString()
|
46
|
+
.padStart(2, '0')}`;
|
47
|
+
}
|
48
|
+
break;
|
49
|
+
|
50
|
+
case 'number':
|
51
|
+
case 'string':
|
52
|
+
default:
|
53
|
+
formattedValue = value ? value.toString() : (cellOptions?.placeHolder ?? '-'); // Handles string type or default fallback
|
54
|
+
break;
|
55
|
+
}
|
56
|
+
let render = formattedValue;
|
57
|
+
if (cellOptions.linkTo) {
|
58
|
+
render = <Link to={cellOptions.linkTo(item)}>{formattedValue}</Link>;
|
59
|
+
}
|
60
|
+
return <td key={cellOptions.name}>{render}</td>;
|
61
|
+
})}
|
62
|
+
<td>
|
63
|
+
{/*@ts-ignore*/}
|
64
|
+
<Link to={'edit/' + (item?.id ?? '-')}>Edit</Link>
|
65
|
+
{/*@ts-ignore*/}
|
66
|
+
<Link to={'details/' + (item?.id ?? '-')}>Details</Link>
|
67
|
+
</td>
|
68
|
+
</tr>
|
69
|
+
))}
|
70
|
+
|
71
|
+
</tbody>
|
72
|
+
</table>
|
73
|
+
</div>
|
74
|
+
);
|
75
|
+
}
|
@@ -0,0 +1,37 @@
|
|
1
|
+
import 'reflect-metadata';
|
2
|
+
|
3
|
+
const CELL_KEY = Symbol('cell');
|
4
|
+
|
5
|
+
export interface CellOptions<T> {
|
6
|
+
name?: string;
|
7
|
+
title?: string;
|
8
|
+
type?: 'string' | 'number' | 'date';
|
9
|
+
placeHolder?: string;
|
10
|
+
linkTo?: (item: T) => string;
|
11
|
+
}
|
12
|
+
|
13
|
+
export function Cell<T>(options?: CellOptions<T>): PropertyDecorator {
|
14
|
+
return (target, propertyKey) => {
|
15
|
+
const existingCells: string[] = Reflect.getMetadata(CELL_KEY, target) || [];
|
16
|
+
Reflect.defineMetadata(CELL_KEY, [...existingCells, propertyKey.toString()], target);
|
17
|
+
|
18
|
+
if (options) {
|
19
|
+
const keyString = `${CELL_KEY.toString()}:${propertyKey.toString()}:options`;
|
20
|
+
Reflect.defineMetadata(keyString, options, target);
|
21
|
+
}
|
22
|
+
};
|
23
|
+
}
|
24
|
+
|
25
|
+
|
26
|
+
export function getCellFields<T>(entityClass: any): CellOptions<T> [] {
|
27
|
+
const prototype = entityClass.prototype;
|
28
|
+
const cellFields: string[] = Reflect.getMetadata(CELL_KEY, prototype) || [];
|
29
|
+
return cellFields.map((field) => {
|
30
|
+
const fields = (Reflect.getMetadata(`${CELL_KEY.toString()}:${field}:options`, prototype) || {});
|
31
|
+
return {
|
32
|
+
...fields,
|
33
|
+
name: fields?.name ?? field,
|
34
|
+
};
|
35
|
+
|
36
|
+
});
|
37
|
+
}
|
@@ -0,0 +1,20 @@
|
|
1
|
+
import 'reflect-metadata';
|
2
|
+
|
3
|
+
const CRUD_KEY = 'Crud'; // Changed from Symbol to string
|
4
|
+
|
5
|
+
export interface CrudOptions {
|
6
|
+
controller: string;
|
7
|
+
}
|
8
|
+
|
9
|
+
export function Crud(options?: CrudOptions): ClassDecorator {
|
10
|
+
return (target: Function) => {
|
11
|
+
if (options) {
|
12
|
+
Reflect.defineMetadata(CRUD_KEY, options, target);
|
13
|
+
}
|
14
|
+
};
|
15
|
+
}
|
16
|
+
|
17
|
+
|
18
|
+
export function getClassCrudData(entityClass: any): CrudOptions | undefined {
|
19
|
+
return Reflect.getMetadata(CRUD_KEY, entityClass);
|
20
|
+
}
|
package/src/index.ts
ADDED
@@ -0,0 +1,6 @@
|
|
1
|
+
import "./styles/index.scss"
|
2
|
+
export { createScreens } from "./utils/createScreens";
|
3
|
+
export { getScreenForRoutes } from "./utils/getScreenForRoutes";
|
4
|
+
export { Layout } from "./components/layout/Layout";
|
5
|
+
export { SideBar } from "./components/layout/SideBar";
|
6
|
+
export {Panel} from "./components/Panel"
|
@@ -0,0 +1,13 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
import { Screen } from '../types/Screen';
|
3
|
+
import { Layout } from '../components/layout/Layout';
|
4
|
+
import { Form } from './Form';
|
5
|
+
|
6
|
+
export function ControllerCreate({ screen }: { screen: Screen }) {
|
7
|
+
|
8
|
+
return (
|
9
|
+
<Layout>
|
10
|
+
<Form screen={screen} />
|
11
|
+
</Layout>
|
12
|
+
);
|
13
|
+
}
|
@@ -0,0 +1,34 @@
|
|
1
|
+
import { Layout } from '../components/layout/Layout';
|
2
|
+
import { useParams } from 'react-router-dom';
|
3
|
+
import React, { useEffect, useState } from 'react';
|
4
|
+
import { CrudApi } from '../api/crudApi';
|
5
|
+
import { Screen } from '../types/Screen';
|
6
|
+
|
7
|
+
export function ControllerDetails({ screen }: { screen: Screen }) {
|
8
|
+
const { id } = useParams();
|
9
|
+
const [data, setData] = useState<any>(null);
|
10
|
+
const [error, setError] = useState(null);
|
11
|
+
|
12
|
+
useEffect(() => {
|
13
|
+
if (screen.controller && id) {
|
14
|
+
CrudApi.details(screen.controller, id)
|
15
|
+
.then((res) => {
|
16
|
+
setData(res.data);
|
17
|
+
})
|
18
|
+
.catch((e: any) => {
|
19
|
+
setError(e);
|
20
|
+
console.error(e);
|
21
|
+
});
|
22
|
+
}
|
23
|
+
}, [id, screen]);
|
24
|
+
|
25
|
+
return (
|
26
|
+
<Layout>
|
27
|
+
<p
|
28
|
+
dangerouslySetInnerHTML={{
|
29
|
+
__html: JSON.stringify(data, null, ' ' + '<br/>'),
|
30
|
+
}}
|
31
|
+
/>
|
32
|
+
</Layout>
|
33
|
+
);
|
34
|
+
}
|
@@ -0,0 +1,31 @@
|
|
1
|
+
import { Layout } from '../components/layout/Layout';
|
2
|
+
import { Form } from './Form';
|
3
|
+
import React, { useEffect, useState } from 'react';
|
4
|
+
import { Screen } from '../types/Screen';
|
5
|
+
import { useParams } from 'react-router-dom';
|
6
|
+
import { CrudApi } from '../api/crudApi';
|
7
|
+
|
8
|
+
export function ControllerEdit({ screen }: { screen: Screen }) {
|
9
|
+
const { id } = useParams();
|
10
|
+
const [data, setData] = useState<any>(null);
|
11
|
+
const [error, setError] = useState(null);
|
12
|
+
|
13
|
+
useEffect(() => {
|
14
|
+
if (screen.controller && id) {
|
15
|
+
CrudApi.details(screen.controller, id)
|
16
|
+
.then((res) => {
|
17
|
+
setData(res.data);
|
18
|
+
})
|
19
|
+
.catch((e: any) => {
|
20
|
+
setError(e);
|
21
|
+
console.error(e);
|
22
|
+
});
|
23
|
+
}
|
24
|
+
}, [id, screen]);
|
25
|
+
|
26
|
+
return (
|
27
|
+
<Layout>
|
28
|
+
<Form data={data} screen={screen} />
|
29
|
+
</Layout>
|
30
|
+
);
|
31
|
+
}
|
@@ -0,0 +1,40 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
import { Layout } from '../components/layout/Layout';
|
3
|
+
import { Screen } from '../types/Screen';
|
4
|
+
import { useEffect, useState } from 'react';
|
5
|
+
import { CrudApi } from '../api/crudApi';
|
6
|
+
import { Link } from 'react-router-dom';
|
7
|
+
import { List } from '../components/list/List';
|
8
|
+
|
9
|
+
import { StoreData } from "../utils/storeData";
|
10
|
+
|
11
|
+
export function ControllerList({ screen }: { screen: Screen }) {
|
12
|
+
const [page, setPage] = useState(0);
|
13
|
+
const [data, setData] = useState<any>(null);
|
14
|
+
const [error, setError] = useState(null);
|
15
|
+
|
16
|
+
useEffect(() => {
|
17
|
+
if (screen.controller) {
|
18
|
+
CrudApi.getList(screen.controller, page)
|
19
|
+
.then((res) => {
|
20
|
+
setData(res.data);
|
21
|
+
})
|
22
|
+
.catch((e: any) => {
|
23
|
+
setError(e);
|
24
|
+
console.error(e);
|
25
|
+
});
|
26
|
+
}
|
27
|
+
}, [page, screen.controller]);
|
28
|
+
|
29
|
+
return (
|
30
|
+
<Layout>
|
31
|
+
<Link to={'/maps/create'}>Create</Link>
|
32
|
+
{error ? <p>Error {error}</p> : null}
|
33
|
+
<List
|
34
|
+
screen={screen}
|
35
|
+
cells={StoreData.screens[screen.key].cells}
|
36
|
+
data={data}
|
37
|
+
/>
|
38
|
+
</Layout>
|
39
|
+
);
|
40
|
+
}
|
@@ -0,0 +1,67 @@
|
|
1
|
+
import React, { useEffect } from 'react';
|
2
|
+
import { Screen } from '../types/Screen';
|
3
|
+
import { FieldErrors, useForm } from 'react-hook-form';
|
4
|
+
import { useNavigate } from 'react-router-dom';
|
5
|
+
import { CrudApi } from '../api/crudApi';
|
6
|
+
import { StoreData } from "../utils/storeData";
|
7
|
+
|
8
|
+
export function Form({ data, screen }: { data?: any; screen: Screen }) {
|
9
|
+
const {
|
10
|
+
register,
|
11
|
+
handleSubmit,
|
12
|
+
reset,
|
13
|
+
formState: { errors },
|
14
|
+
} = useForm<any>({
|
15
|
+
resolver: StoreData.screens[screen.controller].resolver,
|
16
|
+
defaultValues: data,
|
17
|
+
});
|
18
|
+
const navigate = useNavigate();
|
19
|
+
const fields = StoreData.screens[screen.controller].fields;
|
20
|
+
useEffect(() => {
|
21
|
+
reset(data);
|
22
|
+
}, [data, reset]);
|
23
|
+
return (
|
24
|
+
<div className="form-wrapper">
|
25
|
+
<form
|
26
|
+
onSubmit={handleSubmit((dataForm) => {
|
27
|
+
if (data) {
|
28
|
+
CrudApi.edit(screen.controller, dataForm).then(() => {
|
29
|
+
navigate('/' + screen.controller, {
|
30
|
+
replace: true,
|
31
|
+
});
|
32
|
+
});
|
33
|
+
} else {
|
34
|
+
CrudApi.create(screen.controller, dataForm).then(() => {
|
35
|
+
navigate('/' + screen.controller, {
|
36
|
+
replace: true,
|
37
|
+
});
|
38
|
+
});
|
39
|
+
}
|
40
|
+
})}
|
41
|
+
>
|
42
|
+
{fields.map((field) => (
|
43
|
+
<div className="form-field" key={field}>
|
44
|
+
<label htmlFor={field}>
|
45
|
+
{field.charAt(0).toUpperCase() + field.slice(1)}
|
46
|
+
</label>
|
47
|
+
<input
|
48
|
+
type="text"
|
49
|
+
{...register(field)}
|
50
|
+
placeholder={`Enter ${field}`}
|
51
|
+
id={field}
|
52
|
+
/>
|
53
|
+
{errors[field] && (
|
54
|
+
<span className="error-message">
|
55
|
+
{/*@ts-ignore*/}
|
56
|
+
{(errors[field] as FieldErrors)?.message}
|
57
|
+
</span>
|
58
|
+
)}
|
59
|
+
</div>
|
60
|
+
))}
|
61
|
+
<button type="submit" className="submit-button">
|
62
|
+
Submit
|
63
|
+
</button>
|
64
|
+
</form>
|
65
|
+
</div>
|
66
|
+
);
|
67
|
+
}
|
@@ -0,0 +1,58 @@
|
|
1
|
+
.form-wrapper {
|
2
|
+
max-width: 400px;
|
3
|
+
padding: 20px;
|
4
|
+
background-color: #f9f9f9;
|
5
|
+
border-radius: 10px;
|
6
|
+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
7
|
+
|
8
|
+
form {
|
9
|
+
display: flex;
|
10
|
+
flex-direction: column;
|
11
|
+
}
|
12
|
+
|
13
|
+
.form-field {
|
14
|
+
margin-bottom: 16px;
|
15
|
+
|
16
|
+
label {
|
17
|
+
font-weight: bold;
|
18
|
+
margin-bottom: 6px;
|
19
|
+
display: block;
|
20
|
+
color: #333;
|
21
|
+
}
|
22
|
+
|
23
|
+
input[type='text'] {
|
24
|
+
padding: 10px;
|
25
|
+
font-size: 16px;
|
26
|
+
border: 1px solid #ccc;
|
27
|
+
border-radius: 5px;
|
28
|
+
width: 100%;
|
29
|
+
transition: border-color 0.2s;
|
30
|
+
|
31
|
+
&:focus {
|
32
|
+
border-color: #007bff;
|
33
|
+
outline: none;
|
34
|
+
}
|
35
|
+
}
|
36
|
+
|
37
|
+
.error-message {
|
38
|
+
margin-top: 4px;
|
39
|
+
font-size: 14px;
|
40
|
+
color: #ff4d4f;
|
41
|
+
}
|
42
|
+
}
|
43
|
+
|
44
|
+
.submit-button {
|
45
|
+
padding: 12px;
|
46
|
+
font-size: 18px;
|
47
|
+
background-color: #007bff;
|
48
|
+
color: white;
|
49
|
+
border: none;
|
50
|
+
border-radius: 5px;
|
51
|
+
cursor: pointer;
|
52
|
+
transition: background-color 0.2s;
|
53
|
+
|
54
|
+
&:hover {
|
55
|
+
background-color: #0056b3;
|
56
|
+
}
|
57
|
+
}
|
58
|
+
}
|
@@ -0,0 +1,39 @@
|
|
1
|
+
.list-wrapper {
|
2
|
+
width: 100%;
|
3
|
+
padding: 16px;
|
4
|
+
background-color: #ffffff;
|
5
|
+
border-radius: 8px;
|
6
|
+
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
7
|
+
|
8
|
+
.header {
|
9
|
+
font-size: 24px;
|
10
|
+
font-weight: bold;
|
11
|
+
text-align: center;
|
12
|
+
margin-bottom: 20px;
|
13
|
+
}
|
14
|
+
|
15
|
+
.list-table {
|
16
|
+
width: 100%;
|
17
|
+
border-collapse: collapse;
|
18
|
+
|
19
|
+
th,
|
20
|
+
td {
|
21
|
+
padding: 12px 16px;
|
22
|
+
text-align: left;
|
23
|
+
border-bottom: 1px solid #ddd;
|
24
|
+
}
|
25
|
+
|
26
|
+
th {
|
27
|
+
background-color: #007bff;
|
28
|
+
color: white;
|
29
|
+
}
|
30
|
+
|
31
|
+
tr:nth-child(even) {
|
32
|
+
background-color: #f9f9f9;
|
33
|
+
}
|
34
|
+
|
35
|
+
tr:hover {
|
36
|
+
background-color: #e8f4ff;
|
37
|
+
}
|
38
|
+
}
|
39
|
+
}
|
@@ -0,0 +1,76 @@
|
|
1
|
+
.sidebar {
|
2
|
+
position: relative;
|
3
|
+
background-color: #f5f5f5;
|
4
|
+
height: 100vh;
|
5
|
+
transition: width 0.3s ease;
|
6
|
+
border-right: 1px solid #e0e0e0;
|
7
|
+
|
8
|
+
&.open {
|
9
|
+
width: 250px;
|
10
|
+
}
|
11
|
+
|
12
|
+
&.closed {
|
13
|
+
width: 60px;
|
14
|
+
|
15
|
+
.nav-links {
|
16
|
+
a {
|
17
|
+
span {
|
18
|
+
display: none;
|
19
|
+
}
|
20
|
+
}
|
21
|
+
}
|
22
|
+
}
|
23
|
+
|
24
|
+
.toggle-button {
|
25
|
+
position: absolute;
|
26
|
+
top: 10px;
|
27
|
+
right: -12px;
|
28
|
+
width: 24px;
|
29
|
+
height: 24px;
|
30
|
+
border-radius: 50%;
|
31
|
+
background-color: #ffffff;
|
32
|
+
border: 1px solid #e0e0e0;
|
33
|
+
display: flex;
|
34
|
+
align-items: center;
|
35
|
+
justify-content: center;
|
36
|
+
cursor: pointer;
|
37
|
+
z-index: 10;
|
38
|
+
|
39
|
+
&:hover {
|
40
|
+
background-color: #f0f0f0;
|
41
|
+
}
|
42
|
+
}
|
43
|
+
|
44
|
+
.nav-links {
|
45
|
+
display: flex;
|
46
|
+
flex-direction: column;
|
47
|
+
padding: 2rem 1rem;
|
48
|
+
height: 100%;
|
49
|
+
|
50
|
+
a {
|
51
|
+
padding: 0.75rem 1rem;
|
52
|
+
margin-bottom: 0.5rem;
|
53
|
+
color: #333;
|
54
|
+
text-decoration: none;
|
55
|
+
border-radius: 4px;
|
56
|
+
font-weight: 500;
|
57
|
+
|
58
|
+
&:hover {
|
59
|
+
background-color: rgba(0, 0, 0, 0.05);
|
60
|
+
}
|
61
|
+
|
62
|
+
&.active {
|
63
|
+
background-color: rgba(0, 0, 0, 0.1);
|
64
|
+
}
|
65
|
+
}
|
66
|
+
|
67
|
+
.bottom-link {
|
68
|
+
margin-top: auto;
|
69
|
+
|
70
|
+
a {
|
71
|
+
color: #666;
|
72
|
+
font-weight: 400;
|
73
|
+
}
|
74
|
+
}
|
75
|
+
}
|
76
|
+
}
|