Upload 10 files
Browse files- frontend-project/Button.css +26 -0
- frontend-project/Button.stories.tsx +36 -0
- frontend-project/Button.test.tsx +23 -0
- frontend-project/Button.tsx +23 -0
- frontend-project/userSlice.test.ts +51 -0
- frontend-project/userSlice.ts +46 -0
- frontend-project/userSlice.types.ts +26 -0
- frontend-project/webpack.config.js +25 -0
- frontend-project/webpack.dev.js +12 -0
- frontend-project/webpack.prod.js +13 -0
frontend-project/Button.css
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.btn {
|
| 2 |
+
padding: 10px 20px;
|
| 3 |
+
border: none;
|
| 4 |
+
border-radius: 4px;
|
| 5 |
+
font-size: 16px;
|
| 6 |
+
cursor: pointer;
|
| 7 |
+
transition: background-color 0.3s ease;
|
| 8 |
+
}
|
| 9 |
+
|
| 10 |
+
.btn-primary {
|
| 11 |
+
background-color: #007bff;
|
| 12 |
+
color: white;
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
.btn-primary:hover {
|
| 16 |
+
background-color: #0056b3;
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
.btn-secondary {
|
| 20 |
+
background-color: #6c757d;
|
| 21 |
+
color: white;
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
.btn-secondary:hover {
|
| 25 |
+
background-color: #545b62;
|
| 26 |
+
}
|
frontend-project/Button.stories.tsx
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React from 'react';
|
| 2 |
+
import { Meta, StoryObj } from '@storybook/react';
|
| 3 |
+
import { Button } from './Button';
|
| 4 |
+
|
| 5 |
+
const meta: Meta<typeof Button> = {
|
| 6 |
+
title: 'Components/Button',
|
| 7 |
+
component: Button,
|
| 8 |
+
parameters: {
|
| 9 |
+
layout: 'centered',
|
| 10 |
+
},
|
| 11 |
+
tags: ['autodocs'],
|
| 12 |
+
};
|
| 13 |
+
|
| 14 |
+
export default meta;
|
| 15 |
+
type Story = StoryObj<typeof Button>;
|
| 16 |
+
|
| 17 |
+
export const Primary: Story = {
|
| 18 |
+
args: {
|
| 19 |
+
label: 'Primary Button',
|
| 20 |
+
variant: 'primary',
|
| 21 |
+
},
|
| 22 |
+
};
|
| 23 |
+
|
| 24 |
+
export const Secondary: Story = {
|
| 25 |
+
args: {
|
| 26 |
+
label: 'Secondary Button',
|
| 27 |
+
variant: 'secondary',
|
| 28 |
+
},
|
| 29 |
+
};
|
| 30 |
+
|
| 31 |
+
export const WithClickHandler: Story = {
|
| 32 |
+
args: {
|
| 33 |
+
label: 'Click Me',
|
| 34 |
+
onClick: () => alert('Button clicked!'),
|
| 35 |
+
},
|
| 36 |
+
};
|
frontend-project/Button.test.tsx
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React from 'react';
|
| 2 |
+
import { render, screen, fireEvent } from '@testing-library/react';
|
| 3 |
+
import { Button } from './Button';
|
| 4 |
+
|
| 5 |
+
describe('Button Component', () => {
|
| 6 |
+
it('renders with label', () => {
|
| 7 |
+
render(<Button label="Click me" />);
|
| 8 |
+
expect(screen.getByText('Click me')).toBeInTheDocument();
|
| 9 |
+
});
|
| 10 |
+
|
| 11 |
+
it('calls onClick when clicked', () => {
|
| 12 |
+
const handleClick = jest.fn();
|
| 13 |
+
render(<Button label="Click me" onClick={handleClick} />);
|
| 14 |
+
fireEvent.click(screen.getByText('Click me'));
|
| 15 |
+
expect(handleClick).toHaveBeenCalledTimes(1);
|
| 16 |
+
});
|
| 17 |
+
|
| 18 |
+
it('applies variant class', () => {
|
| 19 |
+
render(<Button label="Secondary" variant="secondary" />);
|
| 20 |
+
const button = screen.getByText('Secondary');
|
| 21 |
+
expect(button).toHaveClass('btn-secondary');
|
| 22 |
+
});
|
| 23 |
+
});
|
frontend-project/Button.tsx
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React from 'react';
|
| 2 |
+
import './Button.css';
|
| 3 |
+
|
| 4 |
+
interface ButtonProps {
|
| 5 |
+
label: string;
|
| 6 |
+
onClick?: () => void;
|
| 7 |
+
variant?: 'primary' | 'secondary';
|
| 8 |
+
}
|
| 9 |
+
|
| 10 |
+
export const Button: React.FC<ButtonProps> = ({
|
| 11 |
+
label,
|
| 12 |
+
onClick,
|
| 13 |
+
variant = 'primary'
|
| 14 |
+
}) => {
|
| 15 |
+
return (
|
| 16 |
+
<button
|
| 17 |
+
className={`btn btn-${variant}`}
|
| 18 |
+
onClick={onClick}
|
| 19 |
+
>
|
| 20 |
+
{label}
|
| 21 |
+
</button>
|
| 22 |
+
);
|
| 23 |
+
};
|
frontend-project/userSlice.test.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import userReducer, { setCurrentUser, addUser, removeUser, setLoading, setError } from './userSlice';
|
| 2 |
+
import { User } from './userSlice';
|
| 3 |
+
|
| 4 |
+
describe('userSlice', () => {
|
| 5 |
+
const mockUser: User = {
|
| 6 |
+
id: '1',
|
| 7 |
+
name: 'John Doe',
|
| 8 |
+
email: 'john@example.com',
|
| 9 |
+
};
|
| 10 |
+
|
| 11 |
+
it('should return the initial state', () => {
|
| 12 |
+
expect(userReducer(undefined, { type: 'unknown' })).toEqual({
|
| 13 |
+
currentUser: null,
|
| 14 |
+
users: [],
|
| 15 |
+
loading: false,
|
| 16 |
+
error: null,
|
| 17 |
+
});
|
| 18 |
+
});
|
| 19 |
+
|
| 20 |
+
it('should handle setCurrentUser', () => {
|
| 21 |
+
const actual = userReducer(undefined, setCurrentUser(mockUser));
|
| 22 |
+
expect(actual.currentUser).toEqual(mockUser);
|
| 23 |
+
});
|
| 24 |
+
|
| 25 |
+
it('should handle addUser', () => {
|
| 26 |
+
const actual = userReducer(undefined, addUser(mockUser));
|
| 27 |
+
expect(actual.users).toContainEqual(mockUser);
|
| 28 |
+
});
|
| 29 |
+
|
| 30 |
+
it('should handle removeUser', () => {
|
| 31 |
+
const state = {
|
| 32 |
+
currentUser: null,
|
| 33 |
+
users: [mockUser],
|
| 34 |
+
loading: false,
|
| 35 |
+
error: null,
|
| 36 |
+
};
|
| 37 |
+
const actual = userReducer(state, removeUser('1'));
|
| 38 |
+
expect(actual.users).not.toContainEqual(mockUser);
|
| 39 |
+
});
|
| 40 |
+
|
| 41 |
+
it('should handle setLoading', () => {
|
| 42 |
+
const actual = userReducer(undefined, setLoading(true));
|
| 43 |
+
expect(actual.loading).toBe(true);
|
| 44 |
+
});
|
| 45 |
+
|
| 46 |
+
it('should handle setError', () => {
|
| 47 |
+
const errorMessage = 'An error occurred';
|
| 48 |
+
const actual = userReducer(undefined, setError(errorMessage));
|
| 49 |
+
expect(actual.error).toBe(errorMessage);
|
| 50 |
+
});
|
| 51 |
+
});
|
frontend-project/userSlice.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
| 2 |
+
|
| 3 |
+
export interface User {
|
| 4 |
+
id: string;
|
| 5 |
+
name: string;
|
| 6 |
+
email: string;
|
| 7 |
+
}
|
| 8 |
+
|
| 9 |
+
interface UserState {
|
| 10 |
+
currentUser: User | null;
|
| 11 |
+
users: User[];
|
| 12 |
+
loading: boolean;
|
| 13 |
+
error: string | null;
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
const initialState: UserState = {
|
| 17 |
+
currentUser: null,
|
| 18 |
+
users: [],
|
| 19 |
+
loading: false,
|
| 20 |
+
error: null,
|
| 21 |
+
};
|
| 22 |
+
|
| 23 |
+
const userSlice = createSlice({
|
| 24 |
+
name: 'user',
|
| 25 |
+
initialState,
|
| 26 |
+
reducers: {
|
| 27 |
+
setCurrentUser: (state, action: PayloadAction<User>) => {
|
| 28 |
+
state.currentUser = action.payload;
|
| 29 |
+
},
|
| 30 |
+
addUser: (state, action: PayloadAction<User>) => {
|
| 31 |
+
state.users.push(action.payload);
|
| 32 |
+
},
|
| 33 |
+
removeUser: (state, action: PayloadAction<string>) => {
|
| 34 |
+
state.users = state.users.filter(user => user.id !== action.payload);
|
| 35 |
+
},
|
| 36 |
+
setLoading: (state, action: PayloadAction<boolean>) => {
|
| 37 |
+
state.loading = action.payload;
|
| 38 |
+
},
|
| 39 |
+
setError: (state, action: PayloadAction<string | null>) => {
|
| 40 |
+
state.error = action.payload;
|
| 41 |
+
},
|
| 42 |
+
},
|
| 43 |
+
});
|
| 44 |
+
|
| 45 |
+
export const { setCurrentUser, addUser, removeUser, setLoading, setError } = userSlice.actions;
|
| 46 |
+
export default userSlice.reducer;
|
frontend-project/userSlice.types.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { User } from './userSlice';
|
| 2 |
+
|
| 3 |
+
export interface UserApiResponse {
|
| 4 |
+
success: boolean;
|
| 5 |
+
data: User | User[];
|
| 6 |
+
message?: string;
|
| 7 |
+
}
|
| 8 |
+
|
| 9 |
+
export interface UserFormData {
|
| 10 |
+
name: string;
|
| 11 |
+
email: string;
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
export interface UserFilters {
|
| 15 |
+
search?: string;
|
| 16 |
+
role?: string;
|
| 17 |
+
status?: 'active' | 'inactive';
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
export type UserSortField = 'name' | 'email' | 'createdAt';
|
| 21 |
+
export type UserSortOrder = 'asc' | 'desc';
|
| 22 |
+
|
| 23 |
+
export interface UserSortOptions {
|
| 24 |
+
field: UserSortField;
|
| 25 |
+
order: UserSortOrder;
|
| 26 |
+
}
|
frontend-project/webpack.config.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const path = require('path');
|
| 2 |
+
|
| 3 |
+
module.exports = {
|
| 4 |
+
entry: './src/index.js',
|
| 5 |
+
output: {
|
| 6 |
+
path: path.resolve(__dirname, 'dist'),
|
| 7 |
+
filename: 'bundle.js',
|
| 8 |
+
},
|
| 9 |
+
module: {
|
| 10 |
+
rules: [
|
| 11 |
+
{
|
| 12 |
+
test: /\.tsx?$/,
|
| 13 |
+
use: 'ts-loader',
|
| 14 |
+
exclude: /node_modules/,
|
| 15 |
+
},
|
| 16 |
+
{
|
| 17 |
+
test: /\.css$/,
|
| 18 |
+
use: ['style-loader', 'css-loader'],
|
| 19 |
+
},
|
| 20 |
+
],
|
| 21 |
+
},
|
| 22 |
+
resolve: {
|
| 23 |
+
extensions: ['.tsx', '.ts', '.js'],
|
| 24 |
+
},
|
| 25 |
+
};
|
frontend-project/webpack.dev.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const { merge } = require('webpack-merge');
|
| 2 |
+
const common = require('./webpack.config.js');
|
| 3 |
+
|
| 4 |
+
module.exports = merge(common, {
|
| 5 |
+
mode: 'development',
|
| 6 |
+
devtool: 'inline-source-map',
|
| 7 |
+
devServer: {
|
| 8 |
+
contentBase: './dist',
|
| 9 |
+
hot: true,
|
| 10 |
+
port: 3000,
|
| 11 |
+
},
|
| 12 |
+
});
|
frontend-project/webpack.prod.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const { merge } = require('webpack-merge');
|
| 2 |
+
const common = require('./webpack.config.js');
|
| 3 |
+
|
| 4 |
+
module.exports = merge(common, {
|
| 5 |
+
mode: 'production',
|
| 6 |
+
devtool: 'source-map',
|
| 7 |
+
optimization: {
|
| 8 |
+
minimize: true,
|
| 9 |
+
splitChunks: {
|
| 10 |
+
chunks: 'all',
|
| 11 |
+
},
|
| 12 |
+
},
|
| 13 |
+
});
|