diff --git a/.eslintrc.js b/.eslintrc.js index e9c55eec0..9d508f78c 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,4 +1,16 @@ module.exports = { + env: { + browser: true, // Enables browser global variables + es2021: true, // Enables ES2021 global variables + node: true, // Enables Node.js global variables + jest: true, // Enables Jest global variables + }, + extends: [ + 'eslint:recommended', // Uses the recommended rules from ESLint + 'plugin:react/recommended', // Uses the recommended rules from @eslint-plugin-react + 'plugin:prettier/recommended', // Enables eslint-plugin-prettier and eslint-config-prettier. This will display prettier errors as ESLint errors. Make sure this is always the last configuration in the extends array. + 'plugin:react-hooks/recommended', // Enforces the Rules of Hooks + ], // parser: '@typescript-eslint/parser', // Specifies the ESLint parser parserOptions: { ecmaVersion: 2020, // Allows for the parsing of modern ECMAScript features @@ -7,17 +19,15 @@ module.exports = { jsx: true, // Allows for the parsing of JSX }, }, + plugins: ['react', 'react-hooks'], settings: { react: { version: 'detect', // Tells eslint-plugin-react to automatically detect the version of React to use }, }, - extends: [ - 'plugin:react/recommended', // Uses the recommended rules from @eslint-plugin-react - 'plugin:prettier/recommended', // Enables eslint-plugin-prettier and eslint-config-prettier. This will display prettier errors as ESLint errors. Make sure this is always the last configuration in the extends array. - ], - plugins: ['react', 'react-hooks'], rules: { + console: 'off', // Disables the no-console rule + 'no-unused-vars': 'warn', // Warns about unused variables // Place to specify ESLint rules. Can be used to overwrite rules specified from the extended configs // e.g. "@typescript-eslint/explicit-function-return-type": "off", }, diff --git a/index.html b/index.html index ca596acc7..d8cf93a43 100644 --- a/index.html +++ b/index.html @@ -1,20 +1,13 @@ - - + - CoreUI Free React.js Admin Template + TakeIt diff --git a/package.json b/package.json index fde78d803..941cd9ff1 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { - "name": "@coreui/coreui-free-react-admin-template", - "version": "5.2.0", - "description": "CoreUI Free React Admin Template", + "name": "taketit", + "version": "0.0.0", + "description": "Projet PFE 2025", "homepage": ".", "bugs": { "url": "https://github.com/coreui/coreui-free-react-admin-template/issues" @@ -14,7 +14,8 @@ "author": "The CoreUI Team (https://github.com/orgs/coreui/people)", "scripts": { "build": "vite build", - "lint": "eslint \"src/**/*.js\"", + "lint": "eslint src/**/*.{js,jsx}", + "lint:fix": "eslint src/**/*.{js,jsx} --fix", "serve": "vite preview", "start": "vite" }, @@ -26,29 +27,48 @@ "@coreui/react": "^5.4.0", "@coreui/react-chartjs": "^3.0.0", "@coreui/utils": "^2.0.2", + "@emotion/react": "^11.14.0", + "@emotion/styled": "^11.14.0", + "@mui/icons-material": "^7.1.0", + "@mui/material": "^7.1.0", + "@mui/styled-engine-sc": "^7.1.0", + "@mui/x-date-pickers": "^8.3.1", "@popperjs/core": "^2.11.8", + "@tinymce/miniature": "^6.0.0", + "@tinymce/tinymce-react": "^6.1.0", + "@toolpad/core": "^0.15.0", + "axios": "^1.7.7", + "bootstrap": "^5.3.3", + "bootstrap-icons": "^1.11.3", "chart.js": "^4.4.4", "classnames": "^2.5.1", "core-js": "^3.38.1", + "date-fns": "^4.1.0", + "jspdf": "^3.0.1", + "jspdf-autotable": "^5.0.2", "prop-types": "^15.8.1", "react": "^18.3.1", "react-dom": "^18.3.1", "react-redux": "^9.1.2", - "react-router-dom": "^6.26.2", + "react-router-dom": "^7.5.2", + "react-toastify": "^11.0.5", "redux": "5.0.1", - "simplebar-react": "^3.2.6" + "redux-devtools-extension": "^2.13.9", + "redux-thunk": "^3.1.0", + "simplebar-react": "^3.2.6", + "styled-components": "^6.1.18" }, "devDependencies": { "@vitejs/plugin-react": "^4.3.1", "autoprefixer": "^10.4.20", - "eslint": "^8.57.0", + "eslint": "^8.57.1", "eslint-config-prettier": "^9.1.0", "eslint-plugin-prettier": "^5.2.1", - "eslint-plugin-react": "^7.36.1", + "eslint-plugin-react": "^7.37.5", "eslint-plugin-react-hooks": "^4.6.2", "postcss": "^8.4.47", "prettier": "3.3.3", "sass": "^1.79.3", - "vite": "^5.4.8" + "vite": "^6.2.0" } } diff --git a/src/App.js b/src/App.js index 7f8e6d7ea..5bf5e5ccb 100644 --- a/src/App.js +++ b/src/App.js @@ -1,55 +1,66 @@ -import React, { Suspense, useEffect } from 'react' -import { HashRouter, Route, Routes } from 'react-router-dom' -import { useSelector } from 'react-redux' - -import { CSpinner, useColorModes } from '@coreui/react' +import React, { Suspense, useCallback, useEffect, useRef, useState } from 'react' +import { BrowserRouter, Route, Routes } from 'react-router-dom' +import { useDispatch } from 'react-redux' import './scss/style.scss' +import PrivateRoute from './PrivateRute' +import { checkAuthentication } from './actions/authActions' +import { CSpinner } from '@coreui/react' // Containers const DefaultLayout = React.lazy(() => import('./layout/DefaultLayout')) // Pages const Login = React.lazy(() => import('./views/pages/login/Login')) -const Register = React.lazy(() => import('./views/pages/register/Register')) -const Page404 = React.lazy(() => import('./views/pages/page404/Page404')) -const Page500 = React.lazy(() => import('./views/pages/page500/Page500')) const App = () => { - const { isColorModeSet, setColorMode } = useColorModes('coreui-free-react-admin-template-theme') - const storedTheme = useSelector((state) => state.theme) + const dispatch = useDispatch() + const [isChecking, setIsChecking] = useState(true) - useEffect(() => { - const urlParams = new URLSearchParams(window.location.href.split('?')[1]) - const theme = urlParams.get('theme') && urlParams.get('theme').match(/^[A-Za-z0-9\s]+/)[0] - if (theme) { - setColorMode(theme) + const isFirstRender = useRef(true) + + const checkAuth = useCallback(async () => { + try { + await dispatch(checkAuthentication()) + } catch (error) { + console.error('Authentication check failed:', error) + } finally { + setIsChecking(false) } + }, [dispatch]) - if (isColorModeSet()) { - return + useEffect(() => { + if (isFirstRender.current) { + checkAuth() + isFirstRender.current = false } + }, [checkAuth]) - setColorMode(storedTheme) - }, []) // eslint-disable-line react-hooks/exhaustive-deps + if (isChecking) { + return ( +
+ +
+ ) + } return ( - + - + } > - } /> - } /> - } /> - } /> - } /> + } /> + } exact> + } /> + } /> + - + ) } diff --git a/src/PrivateRute.js b/src/PrivateRute.js new file mode 100644 index 000000000..f080f6150 --- /dev/null +++ b/src/PrivateRute.js @@ -0,0 +1,11 @@ +/* eslint-disable prettier/prettier */ +import React from 'react' +import { Navigate, Outlet } from 'react-router-dom' +import { useSelector } from 'react-redux' + +const PrivateRoute = () => { + const isAuthenticated = useSelector((state) => state.auth.isAuthenticated) + return isAuthenticated ? : +} + +export default PrivateRoute diff --git a/src/_nav.js b/src/_nav.js index db78c49bd..4a96965b5 100644 --- a/src/_nav.js +++ b/src/_nav.js @@ -268,43 +268,6 @@ const _nav = [ text: 'NEW', }, }, - { - component: CNavTitle, - name: 'Extras', - }, - { - component: CNavGroup, - name: 'Pages', - icon: , - items: [ - { - component: CNavItem, - name: 'Login', - to: '/login', - }, - { - component: CNavItem, - name: 'Register', - to: '/register', - }, - { - component: CNavItem, - name: 'Error 404', - to: '/404', - }, - { - component: CNavItem, - name: 'Error 500', - to: '/500', - }, - ], - }, - { - component: CNavItem, - name: 'Docs', - href: 'https://coreui.io/react/docs/templates/installation/', - icon: , - }, ] export default _nav diff --git a/src/actions/appActions.js b/src/actions/appActions.js new file mode 100644 index 000000000..ee333eb36 --- /dev/null +++ b/src/actions/appActions.js @@ -0,0 +1,9 @@ +/* eslint-disable prettier/prettier */ +export const toggleUnfoldable = () => ({ + type: 'TOGGLE_UNFOLDABLE', +}) + +export const switchThemeMode = (theme) => ({ + type: 'CHANGE_THEME', + payload: theme +}) diff --git a/src/actions/authActions.js b/src/actions/authActions.js new file mode 100644 index 000000000..ddbe8757b --- /dev/null +++ b/src/actions/authActions.js @@ -0,0 +1,98 @@ +import React from 'react' +import authService from '../services/authService' + +export const loginRequest = () => ({ + type: 'LOGIN_REQUEST', +}) + +export const loginSuccess = (user, role) => ({ + type: 'LOGIN_SUCCESS', + payload: { user, role }, +}) + +export const loginFailure = (error) => ({ + type: 'LOGIN_FAILURE', + payload: error, +}) + +export const logoutRequest = () => ({ + type: 'LOGOUT_REQUEST', +}) + +export const logoutSuccess = (user) => ({ + type: 'LOGOUT_SUCCESS', + payload: user, +}) + +export const logoutFailure = (error) => ({ + type: 'LOGOUT_FAILURE', + payload: error, +}) + +export const login = (username, password) => (dispatch) => { + dispatch(loginRequest()) + return authService + .login(username, password) + .then((response) => { + if (response.error) { + dispatch(loginFailure(response.error)) + throw new Error(response.error) + } else { + const user = response.user + if (user.IsEmployee && user.IsManager === false) { + dispatch(loginSuccess(response, 'employee')) + } + if (user.IsManager && user.IsEmployee === false) { + dispatch(loginSuccess(response, 'manager')) + } + return response + } + }) + .catch((error) => { + dispatch(loginFailure(error)) + throw new Error(error) + }) +} + +export const logout = () => async (dispatch) => { + dispatch(logoutRequest()) + try { + const user = await authService.logout() + dispatch(logoutSuccess(user)) + } catch (error) { + dispatch(logoutFailure(error)) + console.error('Error logging out:', error) + } +} + +export const checkAuthRequest = () => ({ + type: 'AUTH_CHECK_REQUEST', +}) + +export const checkAuthSuccess = (data) => ({ + type: 'AUTH_CHECK_SUCCESS', + payload: data, +}) + +export const checkAuthFailure = () => ({ + type: 'AUTH_CHECK_FAILURE', +}) + +export const checkAuthentication = () => (dispatch) => { + dispatch(checkAuthRequest()) + return authService + .checkAuth() + .then((response) => { + if (response.data.error) { + dispatch(checkAuthFailure()) + throw new Error(response.error) + } else { + dispatch(checkAuthSuccess(response.data)) + return response + } + }) + .catch((error) => { + dispatch(checkAuthFailure()) + throw new Error(error) + }) +} diff --git a/src/actions/jiraActions.js b/src/actions/jiraActions.js new file mode 100644 index 000000000..110fa2f97 --- /dev/null +++ b/src/actions/jiraActions.js @@ -0,0 +1,164 @@ +import jiraService from '../services/jiraService' + +export const GetAllConfigJiraRequest = () => ({ + type: 'GET_ALL_CONFIG_JIRA_REQUEST', +}) + +export const GET_ALL_CONFIG_JIRA_SUCCESS = (user) => ({ + type: 'GET_ALL_CONFIG_JIRA_SUCCESS', + payload: user, +}) + +export const GET_ALL_CONFIG_JIRA_FAILURE = (error) => ({ + type: 'GET_ALL_CONFIG_JIRA_FAILURE', + payload: error, +}) + +export const CHECK_CONNECTION_JIRA_API_REQUEST = () => ({ + type: 'CHECK_CONNECTION_JIRA_API_REQUEST', +}) + +export const CHECK_CONNECTION_JIRA_API_SUCCESS = () => ({ + type: 'CHECK_CONNECTION_JIRA_API_SUCCESS', +}) + +export const CHECK_CONNECTION_JIRA_API_FAILURE = (error) => ({ + type: 'CHECK_CONNECTION_JIRA_API_FAILURE', + payload: error, +}) + +export const ADD_NEW_CONFIG_JIRA_API_REQUEST = () => ({ + type: 'ADD_NEW_CONFIG_JIRA_API_REQUEST', +}) + +export const ADD_NEW_CONFIG_JIRA_API_SUCCESS = () => ({ + type: 'ADD_NEW_CONFIG_JIRA_API_SUCCESS', +}) + +export const ADD_NEW_CONFIG_JIRA_API_FAILURE = (error) => ({ + type: 'ADD_NEW_CONFIG_JIRA_API_FAILURE', + payload: error, +}) + +export const DELETE_CONFIG_JIRA_API_REQUEST = () => ({ + type: 'DELETE_CONFIG_JIRA_API_REQUEST', +}) + +export const DELETE_CONFIG_JIRA_API_SUCCESS = () => ({ + type: 'DELETE_CONFIG_JIRA_API_SUCCESS', +}) + +export const DELETE_CONFIG_JIRA_API_FAILURE = (error) => ({ + type: 'DELETE_CONFIG_JIRA_API_FAILURE', + payload: error, +}) + +export const toggleEditConfigJiraModalOpen = (configId) => ({ + type: 'TOGGLE_EDIT_CONFIG_JIRA_MODAL_OPEN', + payload: configId, +}) + +export const toggleEditConfigJiraModalClose = () => ({ + type: 'TOGGLE_EDIT_CONFIG_JIRA_MODAL_CLOSE', +}) + +export const EDIT_CONFIG_JIRA_API_REQUEST = () => ({ + type: 'EDIT_CONFIG_JIRA_API_REQUEST', +}) +export const EDIT_CONFIG_JIRA_API_SUCCESS = () => ({ + type: 'EDIT_CONFIG_JIRA_API_SUCCESS', +}) +export const EDIT_CONFIG_JIRA_API_FAILURE = (error) => ({ + type: 'EDIT_CONFIG_JIRA_API_FAILURE', + payload: error, +}) + +export const getAllConfigJiraAPI = () => (dispatch) => { + dispatch(GetAllConfigJiraRequest()) + return jiraService + .getAllConfigJira() + .then((response) => { + if (response.error) { + dispatch(GET_ALL_CONFIG_JIRA_FAILURE(response.error)) + throw new Error(response.error) + } else { + dispatch(GET_ALL_CONFIG_JIRA_SUCCESS(response.data.data)) + return response + } + }) + .catch((error) => { + dispatch(GET_ALL_CONFIG_JIRA_FAILURE(error)) + throw new Error(error) + }) +} + +export const checkConnectionJiraAPI = + (protocol, host, username, password, apiVersion, strictSSL) => (dispatch) => { + dispatch(CHECK_CONNECTION_JIRA_API_REQUEST()) + return jiraService + .checkConnectionJiraApi(protocol, host, username, password, apiVersion, strictSSL) + .then((response) => { + dispatch(CHECK_CONNECTION_JIRA_API_SUCCESS()) + return response + }) + .catch((error) => { + dispatch(CHECK_CONNECTION_JIRA_API_FAILURE(error)) + throw new Error(error) + }) + } + +export const addNewConfigJiraAPI = + (protocol, host, username, password, apiVersion, strictSSL) => (dispatch) => { + dispatch(CHECK_CONNECTION_JIRA_API_REQUEST()) + return jiraService + .addNewConfigJiraAPI(protocol, host, username, password, apiVersion, strictSSL) + .then((response) => { + console.log(response) + dispatch(CHECK_CONNECTION_JIRA_API_SUCCESS()) + return response + }) + .catch((error) => { + dispatch(CHECK_CONNECTION_JIRA_API_FAILURE(error)) + throw new Error(error) + }) + } + +export const deleteConfigJiraAPI = (id) => (dispatch) => { + dispatch(DELETE_CONFIG_JIRA_API_REQUEST()) + return jiraService + .deleteConfigJiraAPI(id) + .then((response) => { + console.log(response) + dispatch(DELETE_CONFIG_JIRA_API_SUCCESS()) + return response + }) + .catch((error) => { + dispatch(DELETE_CONFIG_JIRA_API_FAILURE(error)) + throw new Error(error) + }) +} + +export const editConfigJiraAPI = + (id, protocol, host, username, password, apiVersion, strictSSL, enableConfig) => (dispatch) => { + dispatch(EDIT_CONFIG_JIRA_API_REQUEST()) + return jiraService + .editConfigJiraAPI( + id, + protocol, + host, + username, + password, + apiVersion, + strictSSL, + enableConfig, + ) + .then((response) => { + console.log(response) + dispatch(EDIT_CONFIG_JIRA_API_SUCCESS()) + return response + }) + .catch((error) => { + dispatch(EDIT_CONFIG_JIRA_API_FAILURE(error)) + throw new Error(error) + }) + } diff --git a/src/actions/projectActions.js b/src/actions/projectActions.js new file mode 100644 index 000000000..bcf55d8c2 --- /dev/null +++ b/src/actions/projectActions.js @@ -0,0 +1,143 @@ +import projectService from '../services/projectService' + +export const toggleEditProjectModalOpen = (projectId) => ({ + type: 'TOGGLE_EDIT_PROJECT_MODAL_OPEN', + payload: projectId, +}) + +export const toggleEditProjectModalClose = () => ({ + type: 'TOGGLE_EDIT_PROJECT_MODAL_CLOSE', +}) + +export const GET_ALL_PROJECTS_REQUEST = () => ({ + type: 'GET_ALL_PROJECTS_REQUEST', +}) + +export const GET_ALL_PROJECTS_SUCCESS = (projects) => ({ + type: 'GET_ALL_PROJECTS_SUCCESS', + payload: projects, +}) + +export const GET_ALL_PROJECTS_FAILURE = (error) => ({ + type: 'GET_ALL_PROJECTS_FAILURE', + payload: error, +}) + +export const getAllProjectAPI = () => (dispatch) => { + dispatch(GET_ALL_PROJECTS_REQUEST()) + projectService + .getAllProjects() + .then((response) => { + if (response.error) { + dispatch(GET_ALL_PROJECTS_FAILURE(response.error)) + throw new Error(response.error) + } else { + dispatch(GET_ALL_PROJECTS_SUCCESS(response.data.data)) + return response + } + }) + .catch((error) => { + dispatch(GET_ALL_PROJECTS_FAILURE(error)) + throw new Error(error) + }) +} + +export const ADD_NEW_PROJECT_REQUEST = () => ({ + type: 'ADD_NEW_PROJECT_REQUEST', +}) + +export const ADD_NEW_PROJECT_SUCCESS = (project) => ({ + type: 'ADD_NEW_PROJECT_SUCCESS', + payload: project, +}) + +export const ADD_NEW_PROJECT_FAILURE = (error) => ({ + type: 'ADD_NEW_PROJECT_FAILURE', + payload: error, +}) + +export const addNewProjectAPI = (projectData) => (dispatch) => { + dispatch(ADD_NEW_PROJECT_REQUEST()) + projectService + .addNewProject(projectData, dispatch) + .then((response) => { + if (response.error) { + dispatch(ADD_NEW_PROJECT_FAILURE(response.error)) + throw new Error(response.error) + } else { + dispatch(ADD_NEW_PROJECT_SUCCESS(response.data.data)) + return response + } + }) + .catch((error) => { + dispatch(ADD_NEW_PROJECT_FAILURE(error)) + throw new Error(error) + }) +} + +export const DELETE_PROJECT_REQUEST = () => ({ + type: 'DELETE_PROJECT_REQUEST', +}) + +export const DELETE_PROJECT_SUCCESS = (projectId) => ({ + type: 'DELETE_PROJECT_SUCCESS', + payload: projectId, +}) + +export const DELETE_PROJECT_FAILURE = (error) => ({ + type: 'DELETE_PROJECT_FAILURE', + payload: error, +}) + +export const deleteProjectAPI = (projectId) => (dispatch) => { + dispatch(DELETE_PROJECT_REQUEST()) + projectService + .deleteProject(projectId, dispatch) + .then((response) => { + if (response.error) { + dispatch(DELETE_PROJECT_FAILURE(response.error)) + throw new Error(response.error) + } else { + dispatch(DELETE_PROJECT_SUCCESS(projectId)) + return response + } + }) + .catch((error) => { + dispatch(DELETE_PROJECT_FAILURE(error)) + throw new Error(error) + }) +} + +export const EDIT_PROJECT_REQUEST = () => ({ + type: 'EDIT_PROJECT_REQUEST', +}) + +export const EDIT_PROJECT_SUCCESS = (projectId) => ({ + type: 'EDIT_PROJECT_SUCCESS', + payload: projectId, +}) + +export const EDIT_PROJECT_FAILURE = (error) => ({ + type: 'EDIT_PROJECT_FAILURE', + payload: error, +}) + +export const editProjectAPI = (projectId, projectData) => (dispatch) => { + dispatch(EDIT_PROJECT_REQUEST()) + projectService + .editProject(projectId, projectData, dispatch) + .then((response) => { + if (response.error) { + dispatch(EDIT_PROJECT_FAILURE(response.error)) + throw new Error(response.error) + } else { + dispatch(EDIT_PROJECT_SUCCESS(projectId)) + dispatch(toggleEditProjectModalClose()) + return response + } + }) + .catch((error) => { + dispatch(EDIT_PROJECT_FAILURE(error)) + throw new Error(error) + }) +} diff --git a/src/actions/ticketActions.js b/src/actions/ticketActions.js new file mode 100644 index 000000000..27b460c67 --- /dev/null +++ b/src/actions/ticketActions.js @@ -0,0 +1,74 @@ +import ticketService from '../services/ticketService' + +export const toggleCreateTicketModalOpen = () => ({ + type: 'TOGGLE_CREATE_TICKET_MODAL_OPEN', +}) + +export const toggleCreateTicketModalClose = () => ({ + type: 'TOGGLE_CREATE_TICKET_MODAL_CLOSE', +}) + +export const GET_ALL_TICKETS_REQUEST = () => ({ + type: 'GET_ALL_TICKETS_REQUEST', +}) + +export const GET_ALL_TICKETS_SUCCESS = (tickets) => ({ + type: 'GET_ALL_TICKETS_SUCCESS', + payload: tickets, +}) + +export const GET_ALL_TICKETS_FAILURE = (error) => ({ + type: 'GET_ALL_TICKETS_FAILURE', + payload: error, +}) + +export const ADD_NEW_TICKET_REQUEST = () => ({ + type: 'ADD_NEW_TICKET_REQUEST', +}) + +export const ADD_NEW_TICKET_SUCCESS = () => ({ + type: 'ADD_NEW_TICKET_SUCCESS', +}) + +export const ADD_NEW_TICKET_FAILURE = (error) => ({ + type: 'ADD_NEW_TICKET_FAILURE', + payload: error, +}) + +export const getAllTicketAPI = () => (dispatch) => { + dispatch(GET_ALL_TICKETS_REQUEST()) + ticketService + .getAllTickets() + .then((response) => { + if (response.error) { + dispatch(GET_ALL_TICKETS_FAILURE(response.error)) + throw new Error(response.error) + } else { + dispatch(GET_ALL_TICKETS_SUCCESS(response.data.results)) + return response + } + }) + .catch((error) => { + dispatch(GET_ALL_TICKETS_FAILURE(error)) + throw new Error(error) + }) +} + +export const addNewTicketAPI = (ticketData) => (dispatch) => { + dispatch(ADD_NEW_TICKET_REQUEST()) + return ticketService + .addNewTicket(ticketData) + .then((response) => { + if (response.error) { + dispatch(ADD_NEW_TICKET_FAILURE(response.error)) + throw new Error(response.error) + } else { + dispatch(ADD_NEW_TICKET_SUCCESS()) + return response + } + }) + .catch((error) => { + dispatch(ADD_NEW_TICKET_FAILURE(error)) + throw new Error(error) + }) +} diff --git a/src/actions/userActions.js b/src/actions/userActions.js new file mode 100644 index 000000000..d492116dd --- /dev/null +++ b/src/actions/userActions.js @@ -0,0 +1,34 @@ +import ticketService from '../services/userService' + +export const GET_ALL_USERS_REQUEST = () => ({ + type: 'GET_ALL_USERS_REQUEST', +}) + +export const GET_ALL_USERS_SUCCESS = (users) => ({ + type: 'GET_ALL_USERS_SUCCESS', + payload: users, +}) + +export const GET_ALL_USERS_FAILURE = (error) => ({ + type: 'GET_ALL_USERS_FAILURE', + payload: error, +}) + +export const getAllUsersAPI = () => (dispatch) => { + dispatch(GET_ALL_USERS_REQUEST()) + ticketService + .getAllUsers() + .then((response) => { + if (response.error) { + dispatch(GET_ALL_USERS_FAILURE(response.error)) + throw new Error(response.error) + } else { + dispatch(GET_ALL_USERS_SUCCESS(response.data.results)) + return response + } + }) + .catch((error) => { + dispatch(GET_ALL_USERS_FAILURE(error)) + throw new Error(error) + }) +} diff --git a/src/assets/images/issuetype/10303.svg b/src/assets/images/issuetype/10303.svg new file mode 100644 index 000000000..3b3ea8732 --- /dev/null +++ b/src/assets/images/issuetype/10303.svg @@ -0,0 +1,15 @@ + + + + bug + Created with Sketch. + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/images/issuetype/10307.svg b/src/assets/images/issuetype/10307.svg new file mode 100644 index 000000000..8d3949b40 --- /dev/null +++ b/src/assets/images/issuetype/10307.svg @@ -0,0 +1,17 @@ + + + + epic + Created with Sketch. + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/images/issuetype/10315.svg b/src/assets/images/issuetype/10315.svg new file mode 100644 index 000000000..c1516b2ae --- /dev/null +++ b/src/assets/images/issuetype/10315.svg @@ -0,0 +1,15 @@ + + + + story + Created with Sketch. + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/images/issuetype/10318.svg b/src/assets/images/issuetype/10318.svg new file mode 100644 index 000000000..e2759677c --- /dev/null +++ b/src/assets/images/issuetype/10318.svg @@ -0,0 +1,18 @@ + + + + task + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/images/logo.png b/src/assets/images/logo.png new file mode 100644 index 000000000..4260d2f29 Binary files /dev/null and b/src/assets/images/logo.png differ diff --git a/src/assets/images/priorities/high.svg b/src/assets/images/priorities/high.svg new file mode 100644 index 000000000..3607057e0 --- /dev/null +++ b/src/assets/images/priorities/high.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/assets/images/priorities/highest.svg b/src/assets/images/priorities/highest.svg new file mode 100644 index 000000000..dd3a70cbe --- /dev/null +++ b/src/assets/images/priorities/highest.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/assets/images/priorities/low.svg b/src/assets/images/priorities/low.svg new file mode 100644 index 000000000..04260f447 --- /dev/null +++ b/src/assets/images/priorities/low.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/assets/images/priorities/lowest.svg b/src/assets/images/priorities/lowest.svg new file mode 100644 index 000000000..275df1ba2 --- /dev/null +++ b/src/assets/images/priorities/lowest.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/assets/images/priorities/medium.svg b/src/assets/images/priorities/medium.svg new file mode 100644 index 000000000..0b532817a --- /dev/null +++ b/src/assets/images/priorities/medium.svg @@ -0,0 +1,16 @@ + + + + +icon/16px/medium-priority +Created with Sketch. + + + + + + diff --git a/src/components/AppBreadcrumb.js b/src/components/AppBreadcrumb.js index d37de8ce9..3ab19aa21 100644 --- a/src/components/AppBreadcrumb.js +++ b/src/components/AppBreadcrumb.js @@ -1,31 +1,41 @@ import React from 'react' -import { useLocation } from 'react-router-dom' - +import { useLocation, matchPath } from 'react-router-dom' import routes from '../routes' - import { CBreadcrumb, CBreadcrumbItem } from '@coreui/react' const AppBreadcrumb = () => { - const currentLocation = useLocation().pathname + const location = useLocation() + const currentLocation = location.pathname const getRouteName = (pathname, routes) => { - const currentRoute = routes.find((route) => route.path === pathname) - return currentRoute ? currentRoute.name : false + for (const route of routes) { + const match = matchPath({ path: route.path, end: true }, pathname) + if (match) { + return route.name + } + } + return null } const getBreadcrumbs = (location) => { const breadcrumbs = [] - location.split('/').reduce((prev, curr, index, array) => { - const currentPathname = `${prev}/${curr}` - const routeName = getRouteName(currentPathname, routes) - routeName && + const pathnames = location.split('/').filter(Boolean) + + pathnames.reduce((prevPath, currSegment, index) => { + const currentPath = `${prevPath}/${currSegment}`.replace(/\/+/g, '/') + const routeName = getRouteName(currentPath, routes) + + if (routeName) { breadcrumbs.push({ - pathname: currentPathname, + pathname: currentPath, name: routeName, - active: index + 1 === array.length ? true : false, + active: index === pathnames.length - 1, }) - return currentPathname - }) + } + + return currentPath + }, '') + return breadcrumbs } @@ -34,16 +44,14 @@ const AppBreadcrumb = () => { return ( Home - {breadcrumbs.map((breadcrumb, index) => { - return ( - - {breadcrumb.name} - - ) - })} + {breadcrumbs.map((breadcrumb, index) => ( + + {breadcrumb.name} + + ))} ) } diff --git a/src/components/AppContent.js b/src/components/AppContent.js index b9a39ef50..180b55b2a 100644 --- a/src/components/AppContent.js +++ b/src/components/AppContent.js @@ -1,14 +1,14 @@ import React, { Suspense } from 'react' import { Navigate, Route, Routes } from 'react-router-dom' -import { CContainer, CSpinner } from '@coreui/react' +import { CircularProgress } from '@mui/material' // routes config import routes from '../routes' const AppContent = () => { return ( - - }> +
+ }> {routes.map((route, idx) => { return ( @@ -26,7 +26,7 @@ const AppContent = () => { } /> - +
) } diff --git a/src/components/AppFooter.js b/src/components/AppFooter.js index fd126f460..590cd82be 100644 --- a/src/components/AppFooter.js +++ b/src/components/AppFooter.js @@ -5,15 +5,15 @@ const AppFooter = () => { return (
- - CoreUI + + PFE - © 2024 creativeLabs. + © 2025
diff --git a/src/components/AppHeader.js b/src/components/AppHeader.js index b10bd7e12..6be404a0a 100644 --- a/src/components/AppHeader.js +++ b/src/components/AppHeader.js @@ -13,27 +13,22 @@ import { CNavLink, CNavItem, useColorModes, + CButton, } from '@coreui/react' import CIcon from '@coreui/icons-react' -import { - cilBell, - cilContrast, - cilEnvelopeOpen, - cilList, - cilMenu, - cilMoon, - cilSun, -} from '@coreui/icons' +import { cilBell, cilEnvelopeOpen, cilList, cilMenu, cilMoon, cilSun } from '@coreui/icons' import { AppBreadcrumb } from './index' -import { AppHeaderDropdown } from './header/index' +import { AppHeaderDropdown, AppHeaderDropdownManager } from './header/index' +import { switchThemeMode } from '../actions/appActions' +import { toggleCreateTicketModalOpen } from '../actions/ticketActions' const AppHeader = () => { const headerRef = useRef() const { colorMode, setColorMode } = useColorModes('coreui-free-react-admin-template-theme') - const dispatch = useDispatch() - const sidebarShow = useSelector((state) => state.sidebarShow) + const { theme } = useSelector((state) => state.data) + const { user } = useSelector((state) => state.auth) useEffect(() => { document.addEventListener('scroll', () => { @@ -42,15 +37,17 @@ const AppHeader = () => { }) }, []) + const switchColorMode = (color) => { + dispatch(switchThemeMode(color)) + } + + useEffect(() => { + setColorMode(theme) + }, [theme, setColorMode]) + return ( - dispatch({ type: 'set', sidebarShow: !sidebarShow })} - style={{ marginInlineStart: '-14px' }} - > - - @@ -58,13 +55,29 @@ const AppHeader = () => { - Users + + Employees + - Settings + + Projects + + + + + Tickets + + + dispatch(toggleCreateTicketModalOpen())}> + + Créer + + + @@ -85,50 +98,12 @@ const AppHeader = () => {
  • - - - {colorMode === 'dark' ? ( - - ) : colorMode === 'auto' ? ( - - ) : ( - - )} - - - setColorMode('light')} - > - Light - - setColorMode('dark')} - > - Dark - - setColorMode('auto')} - > - Auto - - - -
  • -
    -
  • - + {user !== null && user.user.IsEmployee && !user.user.IsManager ? ( + + ) : user !== null && !user.user.IsEmployee && user.user.IsManager ? ( + + ) : null} + {/* */}
    diff --git a/src/components/AppSidebar.js b/src/components/AppSidebar.js deleted file mode 100644 index 021cb52c3..000000000 --- a/src/components/AppSidebar.js +++ /dev/null @@ -1,59 +0,0 @@ -import React from 'react' -import { useSelector, useDispatch } from 'react-redux' - -import { - CCloseButton, - CSidebar, - CSidebarBrand, - CSidebarFooter, - CSidebarHeader, - CSidebarToggler, -} from '@coreui/react' -import CIcon from '@coreui/icons-react' - -import { AppSidebarNav } from './AppSidebarNav' - -import { logo } from 'src/assets/brand/logo' -import { sygnet } from 'src/assets/brand/sygnet' - -// sidebar nav config -import navigation from '../_nav' - -const AppSidebar = () => { - const dispatch = useDispatch() - const unfoldable = useSelector((state) => state.sidebarUnfoldable) - const sidebarShow = useSelector((state) => state.sidebarShow) - - return ( - { - dispatch({ type: 'set', sidebarShow: visible }) - }} - > - - - - - - dispatch({ type: 'set', sidebarShow: false })} - /> - - - - dispatch({ type: 'set', sidebarUnfoldable: !unfoldable })} - /> - - - ) -} - -export default React.memo(AppSidebar) diff --git a/src/components/Modal/ModalBody/BugIssueForm.js b/src/components/Modal/ModalBody/BugIssueForm.js new file mode 100644 index 000000000..a1484d6b1 --- /dev/null +++ b/src/components/Modal/ModalBody/BugIssueForm.js @@ -0,0 +1,186 @@ +import React from 'react' +import PropTypes from 'prop-types' +import { CCol, CFormInput, CFormLabel, CFormSelect, CRow } from '@coreui/react' +import { Editor } from '@tinymce/tinymce-react' +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider' +import { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFns' +import { DatePicker } from '@mui/x-date-pickers/DatePicker' + +import { Prioritys } from '../../../utils/TicketsConsts' + +const BugIssueForm = ({ newIssue, setNewIssue }) => { + const [Priority, setPriority] = React.useState(Prioritys[0].value) + const [startDate, setStartDate] = React.useState(null) + const [endDate, setEndDate] = React.useState(null) + + const handleEditorChange = (content) => { + setNewIssue({ + ...newIssue, + fields: { + ...newIssue.fields, + description: content, + }, + }) + } + + const handleChangeSummary = (event) => { + setNewIssue({ + ...newIssue, + fields: { + ...newIssue.fields, + summary: event.target.value, + }, + }) + } + + const handlePriorityChange = (event) => { + const selectedPriority = event.target.value + setPriority(selectedPriority) + + // Trouve l'objet priority complet basé sur la valeur sélectionnée + const priorityObj = Prioritys.find((p) => p.value === selectedPriority) + + setNewIssue({ + ...newIssue, + fields: { + ...newIssue.fields, + priority: priorityObj, + }, + }) + } + + const handleStartDateChange = (date) => { + setStartDate(date) + setNewIssue({ + ...newIssue, + fields: { + ...newIssue.fields, + customfield_10015: date ? date.toISOString() : null, // Date de début + }, + }) + } + + const handleEndDateChange = (date) => { + setEndDate(date) + setNewIssue({ + ...newIssue, + fields: { + ...newIssue.fields, + customfield_10016: date ? date.toISOString() : null, // Date de fin + }, + }) + } + + return ( +
    + + + Summary* + + + + + + + + + Description* + + + + + + + + + Priorité + + + + {Prioritys.map((option) => ( + + ))} + + + Veuillez sélectionner la priorité du bug + + + + + + + Date début + + + + + + + + + + + Date fin + + + + + + + +
    + ) +} + +BugIssueForm.propTypes = { + newIssue: PropTypes.object.isRequired, + setNewIssue: PropTypes.func.isRequired, +} + +export default BugIssueForm diff --git a/src/components/Modal/ModalBody/StoryIssueForm.js b/src/components/Modal/ModalBody/StoryIssueForm.js new file mode 100644 index 000000000..ff506e67e --- /dev/null +++ b/src/components/Modal/ModalBody/StoryIssueForm.js @@ -0,0 +1,224 @@ +import React from 'react' +import PropTypes from 'prop-types' +import { CCol, CFormInput, CFormLabel, CFormSelect, CRow } from '@coreui/react' +import { Editor } from '@tinymce/tinymce-react' +import { Prioritys } from '../../../utils/TicketsConsts' +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider' +import { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFns' +import { DatePicker } from '@mui/x-date-pickers/DatePicker' + +const StoryIssueForm = ({ newIssue, setNewIssue }) => { + const [Priority, setPriority] = React.useState(Prioritys[0].value) + const [startDate, setStartDate] = React.useState(null) + const [endDate, setEndDate] = React.useState(null) + const [storyPoints, setStoryPoints] = React.useState('') + + const handleEditorChange = (content) => { + setNewIssue({ + ...newIssue, + fields: { + ...newIssue.fields, + description: content, + }, + }) + } + + const handleChangeSummary = (event) => { + setNewIssue({ + ...newIssue, + fields: { + ...newIssue.fields, + summary: event.target.value, + }, + }) + } + + const handlePriorityChange = (event) => { + const selectedPriority = event.target.value + setPriority(selectedPriority) + + // Trouve l'objet priority complet basé sur la valeur sélectionnée + const priorityObj = Prioritys.find((p) => p.value === selectedPriority) + + setNewIssue({ + ...newIssue, + fields: { + ...newIssue.fields, + priority: priorityObj, + }, + }) + } + + const handleStoryPointsChange = (event) => { + const value = event.target.value + setStoryPoints(value) + + setNewIssue({ + ...newIssue, + fields: { + ...newIssue.fields, + customfield_10028: value ? parseInt(value, 10) : null, // Story Points + }, + }) + } + + const handleStartDateChange = (date) => { + setStartDate(date) + setNewIssue({ + ...newIssue, + fields: { + ...newIssue.fields, + customfield_10015: date ? date.toISOString() : null, // Date de début + }, + }) + } + + const handleEndDateChange = (date) => { + setEndDate(date) + setNewIssue({ + ...newIssue, + fields: { + ...newIssue.fields, + customfield_10016: date ? date.toISOString() : null, // Date de fin + }, + }) + } + + return ( +
    + + + Summary* + + + + + + + + + Description* + + + + + + + + + Story Points + + + + + + + + + + + + + + Estimation de l'effort en points de story + + + + + + + Priorité + + + + {Prioritys.map((option) => ( + + ))} + + + Veuillez sélectionner la priorité de la story + + + + + + + Date début + + + + + + + + + + + Date fin + + + + + + + +
    + ) +} + +StoryIssueForm.propTypes = { + newIssue: PropTypes.object.isRequired, + setNewIssue: PropTypes.func.isRequired, +} + +export default StoryIssueForm diff --git a/src/components/Modal/ModalBody/TaskIssueForm.js b/src/components/Modal/ModalBody/TaskIssueForm.js new file mode 100644 index 000000000..6399cb46f --- /dev/null +++ b/src/components/Modal/ModalBody/TaskIssueForm.js @@ -0,0 +1,247 @@ +import React from 'react' +import PropTypes from 'prop-types' +import { CCol, CFormInput, CFormLabel, CFormSelect, CRow } from '@coreui/react' +import { Editor } from '@tinymce/tinymce-react' +import { Prioritys } from '../../../utils/TicketsConsts' +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider' +import { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFns' +import { DatePicker } from '@mui/x-date-pickers/DatePicker' + +const TaskIssueForm = ({ newIssue, setNewIssue }) => { + const [Priority, setPriority] = React.useState(Prioritys[0].value) + const [startDate, setStartDate] = React.useState(null) + const [endDate, setEndDate] = React.useState(null) + const [assignee, setAssignee] = React.useState('') + const [estimation, setEstimation] = React.useState('') + + const handleEditorChange = (content) => { + setNewIssue({ + ...newIssue, + fields: { + ...newIssue.fields, + description: content, + }, + }) + } + + const handleChangeSummary = (event) => { + setNewIssue({ + ...newIssue, + fields: { + ...newIssue.fields, + summary: event.target.value, + }, + }) + } + + const handlePriorityChange = (event) => { + const selectedPriority = event.target.value + setPriority(selectedPriority) + + // Trouve l'objet priority complet basé sur la valeur sélectionnée + const priorityObj = Prioritys.find((p) => p.value === selectedPriority) + + setNewIssue({ + ...newIssue, + fields: { + ...newIssue.fields, + priority: priorityObj, + }, + }) + } + + const handleAssigneeChange = (event) => { + const value = event.target.value + setAssignee(value) + + setNewIssue({ + ...newIssue, + fields: { + ...newIssue.fields, + assignee: value ? { displayName: value, emailAddress: value } : null, + }, + }) + } + + const handleEstimationChange = (event) => { + const value = event.target.value + setEstimation(value) + + setNewIssue({ + ...newIssue, + fields: { + ...newIssue.fields, + timeoriginalestimate: value ? parseInt(value, 10) * 3600 : null, // Convertir heures en secondes + }, + }) + } + + const handleStartDateChange = (date) => { + setStartDate(date) + setNewIssue({ + ...newIssue, + fields: { + ...newIssue.fields, + customfield_10015: date ? date.toISOString() : null, // Date de début + }, + }) + } + + const handleEndDateChange = (date) => { + setEndDate(date) + setNewIssue({ + ...newIssue, + fields: { + ...newIssue.fields, + duedate: date ? date.toISOString().split('T')[0] : null, // Date d'échéance + }, + }) + } + + return ( +
    + + + Summary* + + + + + + + + + Description* + + + + + + + + + Assigné à + + + + + Adresse email de la personne assignée à cette tâche + + + + + + + Estimation (heures) + + + + Estimation du temps nécessaire en heures + + + + + + Priorité + + + + {Prioritys.map((option) => ( + + ))} + + + Veuillez sélectionner la priorité de la tâche + + + + + + + Date début + + + + + + + + + + + Date d'échéance + + + + + + + +
    + ) +} + +TaskIssueForm.propTypes = { + newIssue: PropTypes.object.isRequired, + setNewIssue: PropTypes.func.isRequired, +} + +export default TaskIssueForm diff --git a/src/components/Modal/ModalCreateTicket.js b/src/components/Modal/ModalCreateTicket.js new file mode 100644 index 000000000..e8c754b1f --- /dev/null +++ b/src/components/Modal/ModalCreateTicket.js @@ -0,0 +1,301 @@ +import React, { useEffect, useState } from 'react' +import { useDispatch, useSelector } from 'react-redux' +import { useLocation } from 'react-router-dom' +import { + CButton, + CCol, + CForm, + CFormLabel, + CFormSelect, + CModal, + CModalBody, + CModalFooter, + CModalHeader, + CModalTitle, + CRow, + CCallout, +} from '@coreui/react' +import { addNewTicketAPI, toggleCreateTicketModalClose } from '../../actions/ticketActions' +import BugIssueForm from './ModalBody/BugIssueForm' +import TaskIssueForm from './ModalBody/TaskIssueForm' +import StoryIssueForm from './ModalBody/StoryIssueForm' +// import EpicIssueForm from './ModalBody/EpicIssueForm' // À créer si nécessaire +import { projects, issueTypes } from '../../utils/TicketsConsts' +import { emptyIssue } from '../../utils/emptyIssue' + +const ModalCreateTicket = () => { + const { isCreateTicketModalOpen } = useSelector((state) => state.ticket) + const location = useLocation() + const [project, setProject] = useState(projects[0].value) + const [issueType, setIssueType] = useState(issueTypes[0].value) + const [newIssue, setNewIssue] = useState(emptyIssue) + const [isSubmitting, setIsSubmitting] = useState(false) + const dispatch = useDispatch() + + // Extraire le code du projet depuis le pathname + const getProjectFromPath = () => { + const pathSegments = location.pathname.split('/') + // Supposons que le format soit /project/{projectCode} ou similar + const projectCodeFromPath = pathSegments.find((segment) => + projects.some( + (p) => p.value === segment || p.label.toLowerCase().includes(segment.toLowerCase()), + ), + ) + + if (projectCodeFromPath) { + const foundProject = projects.find( + (p) => + p.value === projectCodeFromPath || + p.label.toLowerCase().includes(projectCodeFromPath.toLowerCase()), + ) + return foundProject ? foundProject.value : projects[0].value + } + + return projects[0].value + } + + const handleClose = () => { + // Reset du formulaire lors de la fermeture + setNewIssue(emptyIssue) + setProject(projects[0].value) + setIssueType(issueTypes[0].value) + setIsSubmitting(false) + dispatch(toggleCreateTicketModalClose()) + } + + const handleSubmitTicket = async () => { + // Validation basique + if (!newIssue.fields?.summary?.trim()) { + alert('Le résumé est obligatoire') + return + } + + setIsSubmitting(true) + try { + await dispatch(addNewTicketAPI(newIssue)) + handleClose() // Fermer après succès + } catch (error) { + console.error('Erreur lors de la création du ticket:', error) + alert('Erreur lors de la création du ticket') + } finally { + setIsSubmitting(false) + } + } + + const generateId = () => { + let id = '' + for (let i = 0; i < 8; i++) { + id += Math.floor(Math.random() * 10) + } + return id + } + + // Définir le projet basé sur le pathname lors de l'ouverture du modal + useEffect(() => { + if (isCreateTicketModalOpen) { + const projectFromPath = getProjectFromPath() + setProject(projectFromPath) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isCreateTicketModalOpen, location.pathname]) + + useEffect(() => { + const now = new Date().toISOString() + setNewIssue((prevIssue) => ({ + ...prevIssue, + id: generateId(), + fields: { + ...prevIssue.fields, + created: now, + lastViewed: now, + updated: now, + statuscategorychangedate: now, + project: { + key: project, + name: projects.find((p) => p.value === project)?.label || project, + }, + }, + })) + }, [isCreateTicketModalOpen, project]) + + useEffect(() => { + setNewIssue((prevIssue) => { + let updatedIssue = { ...prevIssue } + + switch (issueType) { + case 'Bug': + updatedIssue = { + ...prevIssue, + fields: { + ...prevIssue.fields, + issuetype: { + id: '10002', + hierarchyLevel: 0, + iconUrl: + 'https://sesame-team-pfe.atlassian.net/rest/api/2/universal_avatar/view/type/issuetype/avatar/10303?size=medium', + avatarId: 10303, + subtask: false, + description: 'Un problème ou une erreur.', + entityId: 'b6942a7a-0278-49e3-89d3-85295176d3e8', + name: 'Bug', + self: 'https://sesame-team-pfe.atlassian.net/rest/api/2/issuetype/10002', + }, + }, + } + break + + case 'Task': + updatedIssue = { + ...prevIssue, + fields: { + ...prevIssue.fields, + issuetype: { + self: 'https://sesame-team-pfe.atlassian.net/rest/api/2/issuetype/10001', + name: 'Tâche', + description: 'Une tâche distincte.', + id: '10001', + entityId: 'ca1798d2-e4c6-4758-bfaf-7e38a85cd0ea', + iconUrl: + 'https://sesame-team-pfe.atlassian.net/rest/api/2/universal_avatar/view/type/issuetype/avatar/10318?size=medium', + avatarId: 10318, + subtask: false, + hierarchyLevel: 0, + }, + }, + } + break + + case 'Story': + updatedIssue = { + ...prevIssue, + fields: { + ...prevIssue.fields, + issuetype: { + hierarchyLevel: 0, + subtask: false, + description: "Une fonctionnalité exprimée sous la forme d'un objectif utilisateur.", + iconUrl: + 'https://sesame-team-pfe.atlassian.net/rest/api/2/universal_avatar/view/type/issuetype/avatar/10315?size=medium', + avatarId: 10315, + entityId: '6b7e8da8-f84c-41ac-80ac-ba881764a634', + name: 'Story', + id: '10003', + self: 'https://sesame-team-pfe.atlassian.net/rest/api/2/issuetype/10003', + }, + }, + } + break + + case 'Epic': + updatedIssue = { + ...prevIssue, + fields: { + ...prevIssue.fields, + issuetype: { + self: 'https://sesame-team-pfe.atlassian.net/rest/api/2/issuetype/10004', + id: '10004', + description: 'Une collection de bugs, stories et tâches connexes.', + iconUrl: + 'https://sesame-team-pfe.atlassian.net/rest/api/2/universal_avatar/view/type/issuetype/avatar/10307?size=medium', + name: 'Epic', + subtask: false, + avatarId: 10307, + entityId: 'd1c66b55-cd69-4aba-b239-665a2e2f6af3', + hierarchyLevel: 1, + }, + }, + } + break + + default: + break + } + + return updatedIssue + }) + }, [issueType]) + + return ( + + + Créer un nouveau ticket + + + + Les champs obligatoires sont marqués d'un astérisque * + + + + + + Projet* + + + setProject(event.target.value)} + aria-describedby="project-help" + > + {projects.map((option) => ( + + ))} + + + Veuillez sélectionner votre projet + + + + + + + Type d'issue* + + + setIssueType(event.target.value)} + aria-describedby="issue-type-help" + > + {issueTypes.map((option) => ( + + ))} + + + Veuillez sélectionner le type d'issue + + + + + +
    + + {issueType === 'Bug' && } + {issueType === 'Task' && } + {issueType === 'Story' && } + {issueType === 'Epic' && ( +
    Formulaire Epic à implémenter (EpicIssueForm)
    + )} +
    + + + Annuler + + + {isSubmitting ? 'Création...' : 'Créer'} + + +
    + ) +} + +export default ModalCreateTicket diff --git a/src/components/Modal/ModalEditConfigJira.js b/src/components/Modal/ModalEditConfigJira.js new file mode 100644 index 000000000..e2d27c64b --- /dev/null +++ b/src/components/Modal/ModalEditConfigJira.js @@ -0,0 +1,220 @@ +import React, { useState, useEffect } from 'react' +import { + CButton, + CModal, + CModalBody, + CModalFooter, + CModalHeader, + CForm, + CFormInput, + CFormCheck, + CCallout, +} from '@coreui/react' +import { + editConfigJiraAPI, + checkConnectionJiraAPI, + getAllConfigJiraAPI, + toggleEditConfigJiraModalClose, +} from '../../actions/jiraActions' +import { useDispatch, useSelector } from 'react-redux' +import { toast } from 'react-toastify' + +const ModalEditConfigJira = () => { + const dispatch = useDispatch() + const { configCanbeAdded, isEditConfigJiraModalOpen, configIdToEdit, jiraConfigList } = + useSelector((state) => state.jira) + + const [FormControlInputHostURL, setFormControlInputHostURL] = useState('') + const [RadioOptionProtocol, setRadioOptionProtocol] = useState('https') + const [FormControlInputUsername, setFormControlInputUsername] = useState('') + const [FormControlInputPassword, setFormControlInputPassword] = useState('') + const [FormControlInputAPIVersion, setFormControlInputAPIVersion] = useState(2) + const [CheckStrictSSL, setCheckStrictSSL] = useState(true) + const [enableConfig, setEnableConfig] = useState(true) + + useEffect(() => { + if (configIdToEdit !== null) { + const configToEdit = jiraConfigList.find((config) => config.id === configIdToEdit) + if (configToEdit) { + console.log(configToEdit) + setFormControlInputHostURL(configToEdit.host) + setRadioOptionProtocol(configToEdit.protocol) + setFormControlInputUsername(configToEdit.username) + setFormControlInputPassword(configToEdit.password) + setFormControlInputAPIVersion(configToEdit.apiVersion) + setCheckStrictSSL(configToEdit.strictSSL) + setEnableConfig(configToEdit.enableConfig) + } + } + }, [configIdToEdit, jiraConfigList]) + + const checkConnection = (e) => { + e.preventDefault() + dispatch( + checkConnectionJiraAPI( + RadioOptionProtocol, + FormControlInputHostURL, + FormControlInputUsername, + FormControlInputPassword, + FormControlInputAPIVersion, + CheckStrictSSL, + ), + ) + .then((response) => { + if (response) { + if (response.data.error) { + toast.error('Connection failed') + } else { + toast.success('Connection successful') + } + } + }) + .catch((error) => { + console.error('Error checking connection:', error) + toast.error('Connection failed') + }) + } + + const handleFormSubmit = (e) => { + e.preventDefault() + if (configCanbeAdded) { + dispatch( + editConfigJiraAPI( + configIdToEdit, + RadioOptionProtocol, + FormControlInputHostURL, + FormControlInputUsername, + FormControlInputPassword, + FormControlInputAPIVersion, + CheckStrictSSL, + enableConfig, + ), + ) + .then((response) => { + if (response) { + console.log(response) + if (response.data.error) { + toast.error('edited failed') + } else { + toast.success('successful edited') + dispatch(toggleEditConfigJiraModalClose()) + } + } + }) + .then(() => { + dispatch(getAllConfigJiraAPI()) + }) + .catch((error) => { + console.error('Error checking connection:', error) + toast.error('Connection failed') + }) + } else { + toast.error('please check the connection before adding a configuration') + } + } + return ( + dispatch(toggleEditConfigJiraModalClose())} + backdrop="static" + aria-labelledby="ScrollingLongContentExampleLabel LiveDemoExampleLabel" + scrollable + alignment="center" + > + dispatch(toggleEditConfigJiraModalClose())}> + Modifier configuration Jira + + + + setFormControlInputHostURL(e.target.value)} + value={FormControlInputHostURL} + /> + setRadioOptionProtocol(e.target.value)} + /> + setRadioOptionProtocol(e.target.value)} + /> +
    + setFormControlInputUsername(e.target.value)} + value={FormControlInputUsername} + /> + setFormControlInputPassword(e.target.value)} + value={FormControlInputPassword} + /> + setFormControlInputAPIVersion(e.target.value)} + value={FormControlInputAPIVersion} + /> + setCheckStrictSSL(e.target.checked)} + checked={CheckStrictSSL} + /> + + before editing a configuration, please make sure that the host url is reachable and the + username and password are correct.
    + Note: please check the Connection before editing a configuration. +
    +
    +
    + + checkConnection(e)}> + Test Connection + + handleFormSubmit(e)}> + Edit Configuration + + +
    + ) +} + +export default ModalEditConfigJira diff --git a/src/components/Modal/ModalEditProject.js b/src/components/Modal/ModalEditProject.js new file mode 100644 index 000000000..59bac1953 --- /dev/null +++ b/src/components/Modal/ModalEditProject.js @@ -0,0 +1,145 @@ +import { + CButton, + CCallout, + CCol, + CForm, + CFormInput, + CFormSelect, + CModal, + CModalBody, + CModalFooter, + CModalHeader, + CRow, +} from '@coreui/react' +import React, { useEffect, useState } from 'react' +import { useDispatch, useSelector } from 'react-redux' +import { toggleEditProjectModalClose, editProjectAPI } from '../../actions/projectActions' +import { toast } from 'react-toastify' + +const ModalEditProject = () => { + const { user } = useSelector((state) => state.auth.user) + + const dispatch = useDispatch() + const { projectIdToEdit, projectList, isEditProjectModalOpen } = useSelector( + (state) => state.project, + ) + + const [key, setKey] = useState('') + const [projectName, setProjectName] = useState('') + const [projectType, setProjectType] = useState('') + + useEffect(() => { + if (projectIdToEdit !== null) { + const projectToEdit = projectList.find((config) => config.id === projectIdToEdit) + if (projectToEdit) { + setKey(projectToEdit.key) + setProjectName(projectToEdit.projectName) + setProjectType(projectToEdit.projectType) + } + } + }, [projectIdToEdit, projectList]) + + const handleFormSubmit = (e) => { + e.preventDefault() + if (!projectName || !projectType) { + toast.error('Please fill in all required fields') + return + } + dispatch( + editProjectAPI(projectIdToEdit, { + projectName, + key, + projectType, + projectCategory: 'No category', + projectLead: user.uid, + }), + ) + } + return ( + dispatch(toggleEditProjectModalClose())} + backdrop="static" + aria-labelledby="ScrollingLongContentExampleLabel LiveDemoExampleLabel" + scrollable + alignment="center" + > + dispatch(toggleEditProjectModalClose())}> + Modifier configuration Jira + + + + + + setProjectName(e.target.value)} + value={projectName} + /> + + + + + + + + e.target.value && setProjectType(e.target.value)} + value={projectType} + /> + + + + + + + + + + handleFormSubmit(e)}> + Edit Project + + + + ) +} + +export default ModalEditProject diff --git a/src/components/header/AppHeaderDropdown.js b/src/components/header/AppHeaderDropdown.js index 30c0df82b..fdfedd295 100644 --- a/src/components/header/AppHeaderDropdown.js +++ b/src/components/header/AppHeaderDropdown.js @@ -1,35 +1,53 @@ import React from 'react' import { - CAvatar, - CBadge, - CDropdown, CDropdownDivider, + CDropdownToggle, CDropdownHeader, - CDropdownItem, CDropdownMenu, - CDropdownToggle, + CDropdownItem, + CDropdown, + CAvatar, + CBadge, + CCol, } from '@coreui/react' import { - cilBell, - cilCreditCard, cilCommentSquare, + cilAccountLogout, cilEnvelopeOpen, - cilFile, - cilLockLocked, + cilCreditCard, cilSettings, + cilBell, + cilFile, cilTask, cilUser, } from '@coreui/icons' import CIcon from '@coreui/icons-react' import avatar8 from './../../assets/images/avatars/8.jpg' +import { useDispatch, useSelector } from 'react-redux' +import { useNavigate } from 'react-router-dom' +import { checkAuthentication, logout } from '../../actions/authActions' const AppHeaderDropdown = () => { + const dispatch = useDispatch() + const navigate = useNavigate() + const { user } = useSelector((state) => state.auth) + + const handleLogout = async (e) => { + e.preventDefault() + await dispatch(logout()) + // await dispatch(checkAuthentication()) + } return ( - - - + +
    + {user.user.FirstName} {user.user.LastName} +
    + + + +
    Account @@ -69,24 +87,24 @@ const AppHeaderDropdown = () => { Settings - + {/* Payments 42 - - + */} + {/* Projects 42 - + */} - - - Lock Account + + + Logout
    diff --git a/src/components/header/AppHeaderDropdownManager.js b/src/components/header/AppHeaderDropdownManager.js new file mode 100644 index 000000000..9436d0be4 --- /dev/null +++ b/src/components/header/AppHeaderDropdownManager.js @@ -0,0 +1,96 @@ +import React from 'react' +import { + CDropdownDivider, + CDropdownToggle, + CDropdownHeader, + CDropdownMenu, + CDropdownItem, + CDropdown, + CAvatar, + CBadge, + CCol, +} from '@coreui/react' +import { + cilCommentSquare, + cilAccountLogout, + cilEnvelopeOpen, + cilCreditCard, + cilSettings, + cilBell, + cilFile, + cilTask, + cilUser, +} from '@coreui/icons' +import CIcon from '@coreui/icons-react' + +import avatar8 from './../../assets/images/avatars/8.jpg' +import { useDispatch, useSelector } from 'react-redux' +import { useNavigate } from 'react-router-dom' +import { checkAuthentication, logout } from '../../actions/authActions' + +const AppHeaderDropdownManager = () => { + const dispatch = useDispatch() + const navigate = useNavigate() + const { user } = useSelector((state) => state.auth) + + const handleLogout = async (e) => { + e.preventDefault() + await dispatch(logout()) + // await dispatch(checkAuthentication()) + } + return ( + + +
    + {user.user.FirstName} {user.user.LastName} +
    + + + +
    + + Account + + + Updates + + 42 + + + + + Messages + + 42 + + + + + Tasks + + 42 + + + + + Comments + + 42 + + + Settings + Profile + Settings + configuration jira API + Projects + + + + Logout + + +
    + ) +} + +export default AppHeaderDropdownManager diff --git a/src/components/header/index.js b/src/components/header/index.js index bf8af6c1c..5e5673927 100644 --- a/src/components/header/index.js +++ b/src/components/header/index.js @@ -1,3 +1,4 @@ import AppHeaderDropdown from './AppHeaderDropdown' +import AppHeaderDropdownManager from './AppHeaderDropdownManager' -export { AppHeaderDropdown } +export { AppHeaderDropdown, AppHeaderDropdownManager } diff --git a/src/components/index.js b/src/components/index.js index 6cdf33563..571e49a58 100644 --- a/src/components/index.js +++ b/src/components/index.js @@ -3,7 +3,6 @@ import AppContent from './AppContent' import AppFooter from './AppFooter' import AppHeader from './AppHeader' import AppHeaderDropdown from './header/AppHeaderDropdown' -import AppSidebar from './AppSidebar' import DocsCallout from './DocsCallout' import DocsLink from './DocsLink' import DocsExample from './DocsExample' @@ -14,7 +13,6 @@ export { AppFooter, AppHeader, AppHeaderDropdown, - AppSidebar, DocsCallout, DocsLink, DocsExample, diff --git a/src/index.js b/src/index.js index 11d6e8658..1d0b1b66f 100644 --- a/src/index.js +++ b/src/index.js @@ -1,13 +1,28 @@ import React from 'react' +import ReactDOM from 'react-dom' +import StoreProvider from './store' +import App from './App' import { createRoot } from 'react-dom/client' -import { Provider } from 'react-redux' -import 'core-js' +import { ToastContainer } from 'react-toastify' +import { StyledEngineProvider } from '@mui/material/styles' -import App from './App' -import store from './store' +const container = document.getElementById('root') +const root = createRoot(container) -createRoot(document.getElementById('root')).render( - - - , +root.render( + + + + + + + + , ) diff --git a/src/layout/DefaultLayout.js b/src/layout/DefaultLayout.js index 19fbf225f..8fc39096e 100644 --- a/src/layout/DefaultLayout.js +++ b/src/layout/DefaultLayout.js @@ -1,14 +1,19 @@ import React from 'react' -import { AppContent, AppSidebar, AppFooter, AppHeader } from '../components/index' +import { AppContent, AppFooter, AppHeader } from '../components/index' +import ModalCreateTicket from '../components/Modal/ModalCreateTicket' +import ModalEditConfigJira from '../components/Modal/ModalEditConfigJira' +import ModalEditProject from '../components/Modal/ModalEditProject' const DefaultLayout = () => { return (
    -
    + + +
    diff --git a/src/reducers/appReducer.js b/src/reducers/appReducer.js new file mode 100644 index 000000000..f88185550 --- /dev/null +++ b/src/reducers/appReducer.js @@ -0,0 +1,18 @@ +/* eslint-disable prettier/prettier */ +const initialState = { + sidebarUnfoldable: false, +} + +const appReducer = (state = initialState, action) => { + switch (action.type) { + case 'TOGGLE_UNFOLDABLE': + return { + ...state, + sidebarUnfoldable: !state.sidebarUnfoldable, + } + default: + return state + } +} + +export default appReducer diff --git a/src/reducers/authReducer.js b/src/reducers/authReducer.js new file mode 100644 index 000000000..6f1b114ce --- /dev/null +++ b/src/reducers/authReducer.js @@ -0,0 +1,72 @@ +const initialState = { + isAuthenticated: false, + user: null, + loading: false, + error: null, + role: null, +} + +const authReducer = (state = initialState, action) => { + switch (action.type) { + case 'LOGIN_REQUEST': + return { + ...state, + loading: true, + } + case 'LOGOUT_REQUEST': + return { + ...state, + loading: true, + } + case 'LOGOUT_SUCCESS': + return { + ...state, + isAuthenticated: false, + user: null, + loading: false, + } + case 'LOGOUT_FAILURE': + return { + ...state, + loading: false, + error: action.payload, + } + case 'AUTH_CHECK_REQUEST': + return { + ...state, + loading: true, + } + case 'LOGIN_SUCCESS': + return { + ...state, + isAuthenticated: true, + user: action.payload.user, + role: action.payload.role, + loading: false, + } + case 'LOGIN_FAILURE': + case 'AUTH_CHECK_FAILURE': + return { + ...state, + isAuthenticated: false, + loading: false, + } + case 'LOGOUT': + return { + ...state, + isAuthenticated: false, + user: null, + } + case 'AUTH_CHECK_SUCCESS': + return { + ...state, + isAuthenticated: true, + user: action.payload, + loading: false, + } + default: + return state + } +} + +export default authReducer diff --git a/src/reducers/index.js b/src/reducers/index.js new file mode 100644 index 000000000..67855ee80 --- /dev/null +++ b/src/reducers/index.js @@ -0,0 +1,15 @@ +/* eslint-disable prettier/prettier */ +import { combineReducers } from 'redux' +import authReducer from './authReducer' +import appReducer from './appReducer' +import jiraReducer from './jiraReducer' +import ticketReducer from './ticketReducer' + +const rootReducer = combineReducers({ + auth: authReducer, + app: appReducer, + jira: jiraReducer, + ticket: ticketReducer, +}) + +export default rootReducer diff --git a/src/reducers/jiraReducer.js b/src/reducers/jiraReducer.js new file mode 100644 index 000000000..b1a67d7e6 --- /dev/null +++ b/src/reducers/jiraReducer.js @@ -0,0 +1,111 @@ +const initialState = { + configCanbeAdded: false, + jiraConfigList: [], + loading: false, + error: null, + isEditConfigJiraModalOpen: false, + configIdToEdit: null, +} + +const jiraReducer = (state = initialState, action) => { + switch (action.type) { + case 'GET_ALL_CONFIG_JIRA_REQUEST': + return { + ...state, + loading: true, + } + case 'GET_ALL_CONFIG_JIRA_SUCCESS': + return { + ...state, + jiraConfigList: action.payload, + loading: false, + } + case 'GET_ALL_CONFIG_JIRA_FAILURE': + return { + ...state, + loading: false, + error: action.payload, + } + case 'CHECK_CONNECTION_JIRA_API_REQUEST': + return { + ...state, + loading: true, + } + case 'CHECK_CONNECTION_JIRA_API_SUCCESS': + return { + ...state, + configCanbeAdded: true, + loading: false, + } + case 'CHECK_CONNECTION_JIRA_API_FAILURE': + return { + ...state, + loading: false, + error: action.payload, + configCanbeAdded: false, + } + case 'ADD_NEW_CONFIG_JIRA_API_REQUEST': + return { + ...state, + loading: true, + } + case 'ADD_NEW_CONFIG_JIRA_API_SUCCESS': + return { + ...state, + loading: false, + } + case 'ADD_NEW_CONFIG_JIRA_API_FAILURE': + return { + ...state, + loading: false, + error: action.payload, + } + case 'DELETE_CONFIG_JIRA_API_REQUEST': + return { + ...state, + loading: true, + } + case 'DELETE_CONFIG_JIRA_API_SUCCESS': + return { + ...state, + loading: false, + } + case 'DELETE_CONFIG_JIRA_API_FAILURE': + return { + ...state, + loading: false, + error: action.payload, + } + case 'TOGGLE_EDIT_CONFIG_JIRA_MODAL_OPEN': + return { + ...state, + isEditConfigJiraModalOpen: true, + configIdToEdit: action.payload, + } + case 'TOGGLE_EDIT_CONFIG_JIRA_MODAL_CLOSE': + return { + ...state, + isEditConfigJiraModalOpen: false, + } + case 'EDIT_CONFIG_JIRA_API_REQUEST': + return { + ...state, + loading: true, + } + case 'EDIT_CONFIG_JIRA_API_SUCCESS': + return { + ...state, + loading: false, + } + case 'EDIT_CONFIG_JIRA_API_FAILURE': + return { + ...state, + loading: false, + error: action.payload, + } + default: + return state + } +} + +export default jiraReducer diff --git a/src/reducers/projectReducer.js b/src/reducers/projectReducer.js new file mode 100644 index 000000000..a76ba24c4 --- /dev/null +++ b/src/reducers/projectReducer.js @@ -0,0 +1,93 @@ +const initialState = { + projectList: [], + loading: false, + error: null, + isEditProjectModalOpen: false, + projectIdToEdit: null, +} + +const projectReducer = (state = initialState, action) => { + switch (action.type) { + case 'TOGGLE_EDIT_PROJECT_MODAL_OPEN': + return { + ...state, + isEditProjectModalOpen: true, + projectIdToEdit: action.payload, + } + case 'TOGGLE_EDIT_PROJECT_MODAL_CLOSE': + return { + ...state, + isEditProjectModalOpen: false, + projectIdToEdit: null, + } + case 'GET_ALL_PROJECTS_REQUEST': + return { + ...state, + loading: true, + } + case 'GET_ALL_PROJECTS_SUCCESS': + return { + ...state, + loading: false, + projectList: action.payload, + } + case 'GET_ALL_PROJECTS_FAILURE': + return { + ...state, + loading: false, + error: action.payload, + } + case 'ADD_NEW_PROJECT_REQUEST': + return { + ...state, + loading: true, + } + case 'ADD_NEW_PROJECT_SUCCESS': + return { + ...state, + loading: false, + } + case 'ADD_NEW_PROJECT_FAILURE': + return { + ...state, + loading: false, + error: action.payload, + } + case 'DELETE_PROJECT_REQUEST': + return { + ...state, + loading: true, + } + case 'DELETE_PROJECT_SUCCESS': + return { + ...state, + loading: false, + } + case 'DELETE_PROJECT_FAILURE': + return { + ...state, + loading: false, + error: action.payload, + } + case 'EDIT_PROJECT_REQUEST': + return { + ...state, + loading: true, + } + case 'EDIT_PROJECT_SUCCESS': + return { + ...state, + loading: false, + } + case 'EDIT_PROJECT_FAILURE': + return { + ...state, + loading: false, + error: action.payload, + } + default: + return state + } +} + +export default projectReducer diff --git a/src/reducers/ticketReducer.js b/src/reducers/ticketReducer.js new file mode 100644 index 000000000..c376ba575 --- /dev/null +++ b/src/reducers/ticketReducer.js @@ -0,0 +1,58 @@ +const initialState = { + ticketList: [], + loading: false, + error: null, + isCreateTicketModalOpen: false, +} + +const ticketReducer = (state = initialState, action) => { + switch (action.type) { + case 'TOGGLE_CREATE_TICKET_MODAL_OPEN': + return { + ...state, + isCreateTicketModalOpen: true, + } + case 'TOGGLE_CREATE_TICKET_MODAL_CLOSE': + return { + ...state, + isCreateTicketModalOpen: false, + } + case 'GET_ALL_TICKETS_REQUEST': + return { + ...state, + loading: true, + } + case 'GET_ALL_TICKETS_SUCCESS': + return { + ...state, + loading: false, + ticketList: action.payload, + } + case 'GET_ALL_TICKETS_FAILURE': + return { + ...state, + loading: false, + error: action.payload, + } + case 'ADD_NEW_TICKET_REQUEST': + return { + ...state, + loading: true, + } + case 'ADD_NEW_TICKET_SUCCESS': + return { + ...state, + loading: false, + } + case 'ADD_NEW_TICKET_FAILURE': + return { + ...state, + loading: false, + error: action.payload, + } + default: + return state + } +} + +export default ticketReducer diff --git a/src/reducers/userReducer.js b/src/reducers/userReducer.js new file mode 100644 index 000000000..3db17d963 --- /dev/null +++ b/src/reducers/userReducer.js @@ -0,0 +1,31 @@ +const initialState = { + usersList: [], + loading: false, + error: null, +} + +const userReducer = (state = initialState, action) => { + switch (action.type) { + case 'GET_ALL_USERS_REQUEST': + return { + ...state, + loading: true, + } + case 'GET_ALL_USERS_SUCCESS': + return { + ...state, + loading: false, + usersList: action.payload, + } + case 'GET_ALL_USERS_FAILURE': + return { + ...state, + loading: false, + error: action.payload, + } + default: + return state + } +} + +export default userReducer diff --git a/src/routes.js b/src/routes.js index d2e9d6479..cae6dd4a0 100644 --- a/src/routes.js +++ b/src/routes.js @@ -1,6 +1,10 @@ import React from 'react' const Dashboard = React.lazy(() => import('./views/dashboard/Dashboard')) + +const Tickets = React.lazy(() => import('./views/pages/Tickets/TicketsHome')) +const TicketView = React.lazy(() => import('./views/pages/Tickets/TicketView')) + const Colors = React.lazy(() => import('./views/theme/colors/Colors')) const Typography = React.lazy(() => import('./views/theme/typography/Typography')) @@ -51,9 +55,24 @@ const Toasts = React.lazy(() => import('./views/notifications/toasts/Toasts')) const Widgets = React.lazy(() => import('./views/widgets/Widgets')) +const ConfigJiraApi = React.lazy(() => import('./views/pages/jira/ConfigJiraApi')) + +const Projet = React.lazy(() => import('./views/pages/projet/Projet')) + const routes = [ { path: '/', exact: true, name: 'Home' }, { path: '/dashboard', name: 'Dashboard', element: Dashboard }, + + { path: '/tickets', name: 'Tickets', element: Tickets, exact: true }, + { path: '/tickets/list', name: 'liste des Tickets', element: Tickets }, + { path: '/ticket/:code', name: 'Ticket View', element: TicketView }, + + { path: '/jira', name: 'Jira', element: ConfigJiraApi, exact: true }, + { path: '/jira/config-jira-api', name: 'Configuration Jira API', element: ConfigJiraApi }, + + { path: '/projet', element: Projet, name: 'Projet', exact: true }, + { path: '/projet/list', name: 'liste des projets', element: Projet }, + { path: '/theme', name: 'Theme', element: Colors, exact: true }, { path: '/theme/colors', name: 'Colors', element: Colors }, { path: '/theme/typography', name: 'Typography', element: Typography }, diff --git a/src/scss/_mixins.scss b/src/scss/_mixins.scss new file mode 100644 index 000000000..0ab7c900f --- /dev/null +++ b/src/scss/_mixins.scss @@ -0,0 +1,23 @@ +@mixin ltr-rtl($property, $value) { + #{$property}: $value; + [dir="rtl"] & { + #{$property}-right: $value; + #{$property}-left: auto; + } +} + +@mixin transition($transition...) { + transition: $transition; +} + +@mixin color-mode($mode) { + @if $mode == dark { + @media (prefers-color-scheme: dark) { + @content; + } + } @else if $mode == light { + @media (prefers-color-scheme: light) { + @content; + } + } +} diff --git a/src/scss/_theme.scss b/src/scss/_theme.scss index 49e1c79e6..940a7bba9 100644 --- a/src/scss/_theme.scss +++ b/src/scss/_theme.scss @@ -1,13 +1,16 @@ +@use 'variables' as *; +@use 'mixins'; + body { background-color: var(--cui-tertiary-bg); } .wrapper { width: 100%; - @include ltr-rtl("padding-left", var(--cui-sidebar-occupy-start, 0)); - @include ltr-rtl("padding-right", var(--cui-sidebar-occupy-end, 0)); + @include mixins.ltr-rtl("padding-left", var(--cui-sidebar-occupy-start, 0)); + @include mixins.ltr-rtl("padding-right", var(--cui-sidebar-occupy-end, 0)); will-change: auto; - @include transition(padding .15s); + @include mixins.transition(padding .15s); } .header > .container-fluid, @@ -33,13 +36,13 @@ body { } .sidebar-toggler { - @include ltr-rtl("margin-left", auto); + @include mixins.ltr-rtl("margin-left", auto); } .sidebar-narrow, .sidebar-narrow-unfoldable:not(:hover) { .sidebar-toggler { - @include ltr-rtl("margin-right", auto); + @include mixins.ltr-rtl("margin-right", auto); } } @@ -52,7 +55,7 @@ body { } @if $enable-dark-mode { - @include color-mode(dark) { + @include mixins.color-mode(dark) { body { background-color: var(--cui-dark-bg-subtle); } diff --git a/src/scss/_variables.scss b/src/scss/_variables.scss index b0f8a52a3..6b4b1671a 100644 --- a/src/scss/_variables.scss +++ b/src/scss/_variables.scss @@ -3,3 +3,4 @@ // If you want to customize your project please add your variables below. $enable-deprecation-messages: false !default; +$enable-dark-mode: true; diff --git a/src/scss/scss/_accordion.import.scss b/src/scss/scss/_accordion.import.scss new file mode 100644 index 000000000..65bc6f97a --- /dev/null +++ b/src/scss/scss/_accordion.import.scss @@ -0,0 +1 @@ +@forward "accordion"; diff --git a/src/scss/scss/_accordion.scss b/src/scss/scss/_accordion.scss new file mode 100644 index 000000000..28bc11efc --- /dev/null +++ b/src/scss/scss/_accordion.scss @@ -0,0 +1,161 @@ +@use "functions/escape-svg" as *; +@use "mixins/border-radius" as *; +@use "mixins/color-mode" as *; +@use "mixins/transition" as *; +@use "vendor/rfs" as *; +@use "variables" as *; +@use "variables-dark" as *; + +// +// Base styles +// + +.accordion { + // scss-docs-start accordion-css-vars + --#{$prefix}accordion-color: #{$accordion-color}; + --#{$prefix}accordion-bg: #{$accordion-bg}; + --#{$prefix}accordion-transition: #{$accordion-transition}; + --#{$prefix}accordion-border-color: #{$accordion-border-color}; + --#{$prefix}accordion-border-width: #{$accordion-border-width}; + --#{$prefix}accordion-border-radius: #{$accordion-border-radius}; + --#{$prefix}accordion-inner-border-radius: #{$accordion-inner-border-radius}; + --#{$prefix}accordion-btn-padding-x: #{$accordion-button-padding-x}; + --#{$prefix}accordion-btn-padding-y: #{$accordion-button-padding-y}; + --#{$prefix}accordion-btn-color: #{$accordion-button-color}; + --#{$prefix}accordion-btn-bg: #{$accordion-button-bg}; + --#{$prefix}accordion-btn-icon: #{escape-svg($accordion-button-icon)}; + --#{$prefix}accordion-btn-icon-width: #{$accordion-icon-width}; + --#{$prefix}accordion-btn-icon-transform: #{$accordion-icon-transform}; + --#{$prefix}accordion-btn-icon-transition: #{$accordion-icon-transition}; + --#{$prefix}accordion-btn-active-icon: #{escape-svg($accordion-button-active-icon)}; + --#{$prefix}accordion-btn-focus-box-shadow: #{$accordion-button-focus-box-shadow}; + --#{$prefix}accordion-body-padding-x: #{$accordion-body-padding-x}; + --#{$prefix}accordion-body-padding-y: #{$accordion-body-padding-y}; + --#{$prefix}accordion-active-color: #{$accordion-button-active-color}; + --#{$prefix}accordion-active-bg: #{$accordion-button-active-bg}; + // scss-docs-end accordion-css-vars +} + +.accordion-button { + position: relative; + display: flex; + align-items: center; + width: 100%; + padding: var(--#{$prefix}accordion-btn-padding-y) var(--#{$prefix}accordion-btn-padding-x); + @include font-size($font-size-base); + color: var(--#{$prefix}accordion-btn-color); + text-align: start; // Reset button style + background-color: var(--#{$prefix}accordion-btn-bg); + border: 0; + @include border-radius(0); + overflow-anchor: none; + @include transition(var(--#{$prefix}accordion-transition)); + + &:not(.collapsed) { + color: var(--#{$prefix}accordion-active-color); + background-color: var(--#{$prefix}accordion-active-bg); + box-shadow: inset 0 calc(-1 * var(--#{$prefix}accordion-border-width)) 0 var(--#{$prefix}accordion-border-color); // stylelint-disable-line function-disallowed-list + + &::after { + background-image: var(--#{$prefix}accordion-btn-active-icon); + transform: var(--#{$prefix}accordion-btn-icon-transform); + } + } + + // Accordion icon + &::after { + flex-shrink: 0; + width: var(--#{$prefix}accordion-btn-icon-width); + height: var(--#{$prefix}accordion-btn-icon-width); + margin-inline-start: auto; + content: ""; + background-image: var(--#{$prefix}accordion-btn-icon); + background-repeat: no-repeat; + background-size: var(--#{$prefix}accordion-btn-icon-width); + @include transition(var(--#{$prefix}accordion-btn-icon-transition)); + } + + &:hover { + z-index: 2; + } + + &:focus { + z-index: 3; + outline: 0; + box-shadow: var(--#{$prefix}accordion-btn-focus-box-shadow); + } +} + +.accordion-header { + margin-bottom: 0; +} + +.accordion-item { + color: var(--#{$prefix}accordion-color); + background-color: var(--#{$prefix}accordion-bg); + border: var(--#{$prefix}accordion-border-width) solid var(--#{$prefix}accordion-border-color); + + &:first-of-type { + @include border-top-radius(var(--#{$prefix}accordion-border-radius)); + + > .accordion-header .accordion-button { + @include border-top-radius(var(--#{$prefix}accordion-inner-border-radius)); + } + } + + &:not(:first-of-type) { + border-top: 0; + } + + // Only set a border-radius on the last item if the accordion is collapsed + &:last-of-type { + @include border-bottom-radius(var(--#{$prefix}accordion-border-radius)); + + > .accordion-header .accordion-button { + &.collapsed { + @include border-bottom-radius(var(--#{$prefix}accordion-inner-border-radius)); + } + } + + > .accordion-collapse { + @include border-bottom-radius(var(--#{$prefix}accordion-border-radius)); + } + } +} + +.accordion-body { + padding: var(--#{$prefix}accordion-body-padding-y) var(--#{$prefix}accordion-body-padding-x); +} + + +// Flush accordion items +// +// Remove borders and border-radius to keep accordion items edge-to-edge. + +.accordion-flush { + > .accordion-item { + border-right: 0; + border-left: 0; + @include border-radius(0); + + &:first-child { border-top: 0; } + &:last-child { border-bottom: 0; } + + // stylelint-disable selector-max-class + > .accordion-collapse, + > .accordion-header .accordion-button, + > .accordion-header .accordion-button.collapsed { + @include border-radius(0); + } + // stylelint-enable selector-max-class + } +} + +@if $enable-dark-mode { + @include color-mode(dark) { + .accordion-button::after { + --#{$prefix}accordion-btn-icon: #{escape-svg($accordion-button-icon-dark)}; + --#{$prefix}accordion-btn-active-icon: #{escape-svg($accordion-button-active-icon-dark)}; + } + } +} diff --git a/src/scss/scss/_alert.import.scss b/src/scss/scss/_alert.import.scss new file mode 100644 index 000000000..faaf45081 --- /dev/null +++ b/src/scss/scss/_alert.import.scss @@ -0,0 +1 @@ +@forward "alert"; diff --git a/src/scss/scss/_alert.scss b/src/scss/scss/_alert.scss new file mode 100644 index 000000000..0bfe341c3 --- /dev/null +++ b/src/scss/scss/_alert.scss @@ -0,0 +1,72 @@ +@use "sass:map"; +@use "mixins/border-radius" as *; +@use "variables" as *; + +// +// Base styles +// + +.alert { + // scss-docs-start alert-css-vars + --#{$prefix}alert-bg: transparent; + --#{$prefix}alert-padding-x: #{$alert-padding-x}; + --#{$prefix}alert-padding-y: #{$alert-padding-y}; + --#{$prefix}alert-margin-bottom: #{$alert-margin-bottom}; + --#{$prefix}alert-color: inherit; + --#{$prefix}alert-border-color: transparent; + --#{$prefix}alert-border: #{$alert-border-width} solid var(--#{$prefix}alert-border-color); + --#{$prefix}alert-border-radius: #{$alert-border-radius}; + --#{$prefix}alert-link-color: inherit; + // scss-docs-end alert-css-vars + + position: relative; + padding: var(--#{$prefix}alert-padding-y) var(--#{$prefix}alert-padding-x); + margin-bottom: var(--#{$prefix}alert-margin-bottom); + color: var(--#{$prefix}alert-color); + background-color: var(--#{$prefix}alert-bg); + border: var(--#{$prefix}alert-border); + @include border-radius(var(--#{$prefix}alert-border-radius)); +} + +// Headings for larger alerts +.alert-heading { + // Specified to prevent conflicts of changing $headings-color + color: inherit; +} + +// Provide class for links that match alerts +.alert-link { + font-weight: $alert-link-font-weight; + color: var(--#{$prefix}alert-link-color); +} + + +// Dismissible alerts +// +// Expand the right padding and account for the close button's positioning. + +.alert-dismissible { + padding-inline-end: $alert-dismissible-padding-r; + + // Adjust close link position + .btn-close { + position: absolute; + inset-inline-end: 0; + top: 0; + z-index: $stretched-link-z-index + 1; + padding: $alert-padding-y * 1.25 $alert-padding-x; + } +} + + +// scss-docs-start alert-modifiers +// Generate contextual modifier classes for colorizing the alert. +@each $state in map.keys($theme-colors) { + .alert-#{$state} { + --#{$prefix}alert-color: var(--#{$prefix}#{$state}-text-emphasis); + --#{$prefix}alert-bg: var(--#{$prefix}#{$state}-bg-subtle); + --#{$prefix}alert-border-color: var(--#{$prefix}#{$state}-border-subtle); + --#{$prefix}alert-link-color: var(--#{$prefix}#{$state}-text-emphasis); + } +} +// scss-docs-end alert-modifiers diff --git a/src/scss/scss/_avatar.import.scss b/src/scss/scss/_avatar.import.scss new file mode 100644 index 000000000..347d5ad64 --- /dev/null +++ b/src/scss/scss/_avatar.import.scss @@ -0,0 +1 @@ +@forward "avatar"; diff --git a/src/scss/scss/_avatar.scss b/src/scss/scss/_avatar.scss new file mode 100644 index 000000000..d2ce21f6e --- /dev/null +++ b/src/scss/scss/_avatar.scss @@ -0,0 +1,66 @@ +@use "sass:map"; +@use "mixins/border-radius" as *; +@use "mixins/transition" as *; +@use "variables" as *; + +.avatar { + // scss-docs-start avatar-css-vars + --#{$prefix}avatar-width: #{$avatar-width}; + --#{$prefix}avatar-height: #{$avatar-height}; + --#{$prefix}avatar-font-size: #{$avatar-font-size}; + --#{$prefix}avatar-border-radius: #{$avatar-border-radius}; + --#{$prefix}avatar-status-width: #{$avatar-status-width}; + --#{$prefix}avatar-status-height: #{$avatar-status-height}; + --#{$prefix}avatar-status-border-radius: #{$avatar-status-border-radius}; + // scss-docs-end avatar-css-vars + + position: relative; + display: inline-flex; + align-items: center; + justify-content: center; + width: var(--#{$prefix}avatar-width); + height: var(--#{$prefix}avatar-height); + font-size: var(--#{$prefix}avatar-font-size); + vertical-align: middle; + @include border-radius(var(--#{$prefix}avatar-border-radius)); + @include transition($avatar-transition); +} + +.avatar-img { + width: 100%; + height: auto; + @include border-radius(var(--#{$prefix}avatar-border-radius)); +} + +.avatar-status { + position: absolute; + inset-inline-end: 0; + bottom: 0; + display: block; + width: var(--#{$prefix}avatar-status-width); + height: var(--#{$prefix}avatar-status-height); + border: 1px solid $white; + @include border-radius(var(--#{$prefix}avatar-status-border-radius)); +} + +@each $size, $map in $avatar-sizes { + .avatar-#{$size} { + --#{$prefix}avatar-width: #{map.get($map, "width")}; + --#{$prefix}avatar-height: #{map.get($map, "height")}; + --#{$prefix}avatar-font-size: #{map.get($map, "font-size")}; + --#{$prefix}avatar-status-width: #{map.get($map, "status-width")}; + --#{$prefix}avatar-status-height: #{map.get($map, "status-height")}; + } +} + +.avatars-stack { + display: flex; + + .avatar { + margin-inline-end: calc(-.4 * var(--#{$prefix}avatar-width)); // stylelint-disable-line function-disallowed-list + + &:hover { + margin-inline-end: 0; + } + } +} diff --git a/src/scss/scss/_badge.import.scss b/src/scss/scss/_badge.import.scss new file mode 100644 index 000000000..97e7f4439 --- /dev/null +++ b/src/scss/scss/_badge.import.scss @@ -0,0 +1 @@ +@forward "badge"; diff --git a/src/scss/scss/_badge.scss b/src/scss/scss/_badge.scss new file mode 100644 index 000000000..f205fc5d9 --- /dev/null +++ b/src/scss/scss/_badge.scss @@ -0,0 +1,52 @@ +@use "mixins/gradients" as *; +@use "vendor/rfs" as *; +@use "variables" as *; + +// Base class +// +// Requires one of the contextual, color modifier classes for `color` and +// `background-color`. + +.badge { + // scss-docs-start badge-css-vars + --#{$prefix}badge-padding-x: #{$badge-padding-x}; + --#{$prefix}badge-padding-y: #{$badge-padding-y}; + @include rfs($badge-font-size, --#{$prefix}badge-font-size); + --#{$prefix}badge-font-weight: #{$badge-font-weight}; + --#{$prefix}badge-color: #{$badge-color}; + --#{$prefix}badge-border-radius: #{$badge-border-radius}; + // scss-docs-end badge-css-vars + + display: inline-block; + padding: var(--#{$prefix}badge-padding-y) var(--#{$prefix}badge-padding-x); + @include font-size(var(--#{$prefix}badge-font-size)); + font-weight: var(--#{$prefix}badge-font-weight); + line-height: 1; + color: var(--#{$prefix}badge-color); + text-align: center; + white-space: nowrap; + vertical-align: baseline; + border-radius: var(--#{$prefix}badge-border-radius, 0); // stylelint-disable-line property-disallowed-list + @include gradient-bg(); + + // Empty badges collapse automatically + &:empty { + display: none; + } +} + +// Quick fix for badges in buttons +.btn .badge { + position: relative; + top: -1px; +} + +// +// Badge Sizes +// + +.badge-sm { + --#{$prefix}badge-padding-x: #{$badge-padding-x-sm}; + --#{$prefix}badge-padding-y: #{$badge-padding-y-sm}; + @include font-size($badge-font-size-sm); +} diff --git a/src/scss/scss/_banner.scss b/src/scss/scss/_banner.scss new file mode 100644 index 000000000..ded260d4d --- /dev/null +++ b/src/scss/scss/_banner.scss @@ -0,0 +1,7 @@ +$file: "" !default; + +/*! + * CoreUI #{$file} v5.3.2 (https://coreui.io) + * Copyright (c) 2025 creativeLabs Łukasz Holeczek + * Licensed under MIT (https://github.com/coreui/coreui/blob/main/LICENSE) + */ diff --git a/src/scss/scss/_breadcrumb.import.scss b/src/scss/scss/_breadcrumb.import.scss new file mode 100644 index 000000000..39683620c --- /dev/null +++ b/src/scss/scss/_breadcrumb.import.scss @@ -0,0 +1 @@ +@forward "breadcrumb"; diff --git a/src/scss/scss/_breadcrumb.scss b/src/scss/scss/_breadcrumb.scss new file mode 100644 index 000000000..e4d23d2a0 --- /dev/null +++ b/src/scss/scss/_breadcrumb.scss @@ -0,0 +1,51 @@ +@use "functions/escape-svg" as *; +@use "mixins/border-radius" as *; +@use "mixins/ltr-rtl" as *; +@use "vendor/rfs" as *; +@use "variables" as *; + +.breadcrumb { + // scss-docs-start breadcrumb-css-vars + --#{$prefix}breadcrumb-padding-x: #{$breadcrumb-padding-x}; + --#{$prefix}breadcrumb-padding-y: #{$breadcrumb-padding-y}; + --#{$prefix}breadcrumb-margin-bottom: #{$breadcrumb-margin-bottom}; + @include rfs($breadcrumb-font-size, --#{$prefix}breadcrumb-font-size); + --#{$prefix}breadcrumb-bg: #{$breadcrumb-bg}; + --#{$prefix}breadcrumb-border-radius: #{$breadcrumb-border-radius}; + --#{$prefix}breadcrumb-divider-color: #{$breadcrumb-divider-color}; + --#{$prefix}breadcrumb-item-padding-x: #{$breadcrumb-item-padding-x}; + --#{$prefix}breadcrumb-item-active-color: #{$breadcrumb-active-color}; + // scss-docs-end breadcrumb-css-vars + + display: flex; + flex-wrap: wrap; + padding: var(--#{$prefix}breadcrumb-padding-y) var(--#{$prefix}breadcrumb-padding-x); + margin-bottom: var(--#{$prefix}breadcrumb-margin-bottom); + @include font-size(var(--#{$prefix}breadcrumb-font-size)); + list-style: none; + background-color: var(--#{$prefix}breadcrumb-bg); + @include border-radius(var(--#{$prefix}breadcrumb-border-radius)); +} + +.breadcrumb-item { + // The separator between breadcrumbs (by default, a forward-slash: "/") + + .breadcrumb-item { + padding-inline-start: var(--#{$prefix}breadcrumb-item-padding-x); + + &::before { + float: inline-start; // Suppress inline spacings and underlining of the separator + padding-inline-end: var(--#{$prefix}breadcrumb-item-padding-x); + color: var(--#{$prefix}breadcrumb-divider-color); + @include ltr-rtl( + "content", + var(--#{$prefix}breadcrumb-divider, escape-svg($breadcrumb-divider)), + null, + var(--#{$prefix}breadcrumb-divider-flipped, escape-svg($breadcrumb-divider-flipped)) + ); + } + } + + &.active { + color: var(--#{$prefix}breadcrumb-item-active-color); + } +} diff --git a/src/scss/scss/_button-group.import.scss b/src/scss/scss/_button-group.import.scss new file mode 100644 index 000000000..9e34c905a --- /dev/null +++ b/src/scss/scss/_button-group.import.scss @@ -0,0 +1 @@ +@forward "button-group"; diff --git a/src/scss/scss/_button-group.scss b/src/scss/scss/_button-group.scss new file mode 100644 index 000000000..0efee627f --- /dev/null +++ b/src/scss/scss/_button-group.scss @@ -0,0 +1,152 @@ +@use "mixins/border-radius" as *; +@use "mixins/box-shadow" as *; +@use "buttons" as *; +@use "variables" as *; + +// Make the div behave like a button +.btn-group, +.btn-group-vertical { + position: relative; + display: inline-flex; + vertical-align: middle; // match .btn alignment given font-size hack above + + > .btn { + position: relative; + flex: 1 1 auto; + } + + // Bring the hover, focused, and "active" buttons to the front to overlay + // the borders properly + > .btn-check:checked + .btn, + > .btn-check:focus + .btn, + > .btn:hover, + > .btn:focus, + > .btn:active, + > .btn.active { + z-index: 1; + } +} + +// Optional: Group multiple button groups together for a toolbar +.btn-toolbar { + display: flex; + flex-wrap: wrap; + justify-content: flex-start; + + .input-group { + width: auto; + } +} + +.btn-group { + @include border-radius($btn-border-radius); + + // Prevent double borders when buttons are next to each other + > :not(.btn-check:first-child) + .btn, + > .btn-group:not(:first-child) { + margin-inline-start: calc(-1 * #{$btn-border-width}); // stylelint-disable-line function-disallowed-list + } + + // Reset rounded corners + > .btn:not(:last-child):not(.dropdown-toggle), + > .btn.dropdown-toggle-split:first-child, + > .btn-group:not(:last-child) > .btn { + @include border-end-radius(0); + } + + // The left radius should be 0 if the button is: + // - the "third or more" child + // - the second child and the previous element isn't `.btn-check` (making it the first child visually) + // - part of a btn-group which isn't the first child + > .btn:nth-child(n + 3), + > :not(.btn-check) + .btn, + > .btn-group:not(:first-child) > .btn { + @include border-start-radius(0); + } +} + +// Sizing +// +// Remix the default button sizing classes into new ones for easier manipulation. + +.btn-group-sm > .btn { @extend .btn-sm; } +.btn-group-lg > .btn { @extend .btn-lg; } + + +// +// Split button dropdowns +// + +.dropdown-toggle-split { + padding-right: $btn-padding-x * .75; + padding-left: $btn-padding-x * .75; + + &::after, + .dropup &::after, + .dropend &::after { + margin-inline-start: 0; + } + + .dropstart &::before { + margin-inline-end: 0; + } +} + +.btn-sm + .dropdown-toggle-split { + padding-right: $btn-padding-x-sm * .75; + padding-left: $btn-padding-x-sm * .75; +} + +.btn-lg + .dropdown-toggle-split { + padding-right: $btn-padding-x-lg * .75; + padding-left: $btn-padding-x-lg * .75; +} + + +// The clickable button for toggling the menu +// Set the same inset shadow as the :active state +.btn-group.show .dropdown-toggle { + @include box-shadow($btn-active-box-shadow); + + // Show no shadow for `.btn-link` since it has no other button styles. + &.btn-link { + @include box-shadow(none); + } +} + + +// +// Vertical button groups +// + +.btn-group-vertical { + flex-direction: column; + align-items: flex-start; + justify-content: center; + + > .btn, + > .btn-group { + width: 100%; + } + + > .btn:not(:first-child), + > .btn-group:not(:first-child) { + margin-top: calc(-1 * #{$btn-border-width}); // stylelint-disable-line function-disallowed-list + } + + // Reset rounded corners + > .btn:not(:last-child):not(.dropdown-toggle), + > .btn-group:not(:last-child) > .btn { + @include border-bottom-radius(0); + } + + // The top radius should be 0 if the button is: + // - the "third or more" child + // - the second child and the previous element isn't `.btn-check` (making it the first child visually) + // - part of a btn-group which isn't the first child + > .btn:nth-child(n + 3), + > :not(.btn-check) + .btn, + > .btn-group:not(:first-child) > .btn { + @include border-top-radius(0); + } +} diff --git a/src/scss/scss/_buttons.import.scss b/src/scss/scss/_buttons.import.scss new file mode 100644 index 000000000..73468922f --- /dev/null +++ b/src/scss/scss/_buttons.import.scss @@ -0,0 +1 @@ +@forward "buttons"; diff --git a/src/scss/scss/_buttons.scss b/src/scss/scss/_buttons.scss new file mode 100644 index 000000000..10c9343da --- /dev/null +++ b/src/scss/scss/_buttons.scss @@ -0,0 +1,256 @@ +@use "functions/color" as *; +@use "mixins/border-radius" as *; +@use "mixins/box-shadow" as *; +@use "mixins/buttons" as *; +@use "mixins/color-mode" as *; +@use "mixins/gradients" as *; +@use "mixins/transition" as *; +@use "vendor/rfs" as *; +@use "variables" as *; +@use "variables-dark" as *; + +// +// Base styles +// + +.btn { + // scss-docs-start btn-css-vars + --#{$prefix}btn-padding-x: #{$btn-padding-x}; + --#{$prefix}btn-padding-y: #{$btn-padding-y}; + --#{$prefix}btn-font-family: #{$btn-font-family}; + @include rfs($btn-font-size, --#{$prefix}btn-font-size); + --#{$prefix}btn-font-weight: #{$btn-font-weight}; + --#{$prefix}btn-line-height: #{$btn-line-height}; + --#{$prefix}btn-color: #{$btn-color}; + --#{$prefix}btn-bg: transparent; + --#{$prefix}btn-border-width: #{$btn-border-width}; + --#{$prefix}btn-border-color: transparent; + --#{$prefix}btn-border-radius: #{$btn-border-radius}; + --#{$prefix}btn-hover-border-color: transparent; + --#{$prefix}btn-box-shadow: #{$btn-box-shadow}; + --#{$prefix}btn-disabled-opacity: #{$btn-disabled-opacity}; + --#{$prefix}btn-focus-box-shadow: 0 0 0 #{$btn-focus-width} rgba(var(--#{$prefix}btn-focus-shadow-rgb), .5); + // scss-docs-end btn-css-vars + + display: inline-block; + padding: var(--#{$prefix}btn-padding-y) var(--#{$prefix}btn-padding-x); + font-family: var(--#{$prefix}btn-font-family); + @include font-size(var(--#{$prefix}btn-font-size)); + font-weight: var(--#{$prefix}btn-font-weight); + line-height: var(--#{$prefix}btn-line-height); + color: var(--#{$prefix}btn-color); + text-align: center; + text-decoration: if($link-decoration == none, null, none); + white-space: $btn-white-space; + vertical-align: middle; + cursor: if($enable-button-pointers, pointer, null); + user-select: none; + border: var(--#{$prefix}btn-border-width) solid var(--#{$prefix}btn-border-color); + @include border-radius(var(--#{$prefix}btn-border-radius)); + @include gradient-bg(var(--#{$prefix}btn-bg)); + @include box-shadow(var(--#{$prefix}btn-box-shadow)); + @include transition($btn-transition); + + &:hover { + color: var(--#{$prefix}btn-hover-color); + text-decoration: if($link-hover-decoration == underline, none, null); + background-color: var(--#{$prefix}btn-hover-bg); + border-color: var(--#{$prefix}btn-hover-border-color); + } + + .btn-check + &:hover { + // override for the checkbox/radio buttons + color: var(--#{$prefix}btn-color); + background-color: var(--#{$prefix}btn-bg); + border-color: var(--#{$prefix}btn-border-color); + } + + &:focus-visible { + color: var(--#{$prefix}btn-hover-color); + @include gradient-bg(var(--#{$prefix}btn-hover-bg)); + border-color: var(--#{$prefix}btn-hover-border-color); + outline: 0; + // Avoid using mixin so we can pass custom focus shadow properly + @if $enable-shadows { + box-shadow: var(--#{$prefix}btn-box-shadow), var(--#{$prefix}btn-focus-box-shadow); + } @else { + box-shadow: var(--#{$prefix}btn-focus-box-shadow); + } + } + + .btn-check:focus-visible + & { + border-color: var(--#{$prefix}btn-hover-border-color); + outline: 0; + // Avoid using mixin so we can pass custom focus shadow properly + @if $enable-shadows { + box-shadow: var(--#{$prefix}btn-box-shadow), var(--#{$prefix}btn-focus-box-shadow); + } @else { + box-shadow: var(--#{$prefix}btn-focus-box-shadow); + } + } + + .btn-check:checked + &, + :not(.btn-check) + &:active, + &:first-child:active, + &.active, + &.show { + color: var(--#{$prefix}btn-active-color); + background-color: var(--#{$prefix}btn-active-bg); + // Remove CSS gradients if they're enabled + background-image: if($enable-gradients, none, null); + border-color: var(--#{$prefix}btn-active-border-color); + @include box-shadow(var(--#{$prefix}btn-active-shadow)); + + &:focus-visible { + // Avoid using mixin so we can pass custom focus shadow properly + @if $enable-shadows { + box-shadow: var(--#{$prefix}btn-active-shadow), var(--#{$prefix}btn-focus-box-shadow); + } @else { + box-shadow: var(--#{$prefix}btn-focus-box-shadow); + } + } + } + + .btn-check:checked:focus-visible + & { + // Avoid using mixin so we can pass custom focus shadow properly + @if $enable-shadows { + box-shadow: var(--#{$prefix}btn-active-shadow), var(--#{$prefix}btn-focus-box-shadow); + } @else { + box-shadow: var(--#{$prefix}btn-focus-box-shadow); + } + } + + &:disabled, + &.disabled, + fieldset:disabled & { + color: var(--#{$prefix}btn-disabled-color); + pointer-events: none; + background-color: var(--#{$prefix}btn-disabled-bg); + background-image: if($enable-gradients, none, null); + border-color: var(--#{$prefix}btn-disabled-border-color); + opacity: var(--#{$prefix}btn-disabled-opacity); + @include box-shadow(none); + } +} + + +// +// Alternate buttons +// + +.btn-transparent { + --#{$prefix}btn-active-border-color: transparent; + --#{$prefix}btn-disabled-border-color: transparent; + --#{$prefix}btn-hover-border-color: transparent; +} + +// scss-docs-start btn-variant-loops +@each $color, $value in $theme-colors { + .btn-#{$color} { + @if $color == "light" { + @include button-variant( + $value, + $value, + $hover-background: shade-color($value, $btn-hover-bg-shade-amount), + $hover-border: shade-color($value, $btn-hover-border-shade-amount), + $active-background: shade-color($value, $btn-active-bg-shade-amount), + $active-border: shade-color($value, $btn-active-border-shade-amount) + ); + } @else if $color == "dark" { + @include button-variant( + $value, + $value, + $hover-background: tint-color($value, $btn-hover-bg-tint-amount), + $hover-border: tint-color($value, $btn-hover-border-tint-amount), + $active-background: tint-color($value, $btn-active-bg-tint-amount), + $active-border: tint-color($value, $btn-active-border-tint-amount) + ); + } @else { + @include button-variant($value, $value); + } + } +} + +@each $color, $value in $theme-colors { + .btn-outline-#{$color} { + @include button-outline-variant($value); + } +} + +@each $color, $value in $theme-colors { + .btn-ghost-#{$color} { + @include button-ghost-variant($value); + } +} +// scss-docs-end btn-variant-loops + + +// +// Link buttons +// + +// Make a button look and behave like a link +.btn-link { + --#{$prefix}btn-font-weight: #{$font-weight-normal}; + --#{$prefix}btn-color: #{$btn-link-color}; + --#{$prefix}btn-bg: transparent; + --#{$prefix}btn-border-color: transparent; + --#{$prefix}btn-hover-color: #{$btn-link-hover-color}; + --#{$prefix}btn-hover-border-color: transparent; + --#{$prefix}btn-active-border-color: transparent; + --#{$prefix}btn-disabled-color: #{$btn-link-disabled-color}; + --#{$prefix}btn-disabled-border-color: transparent; + --#{$prefix}btn-box-shadow: none; + --#{$prefix}btn-focus-shadow-rgb: #{$btn-link-focus-shadow-rgb}; + + text-decoration: $link-decoration; + @if $enable-gradients { + background-image: none; + } + + &:hover, + &:focus-visible { + text-decoration: $link-hover-decoration; + } + + &:focus-visible { + color: var(--#{$prefix}btn-color); + } + + // No need for an active state here +} + + +// +// Button Sizes +// + +.btn-lg { + @include button-size($btn-padding-y-lg, $btn-padding-x-lg, $btn-font-size-lg, $btn-border-radius-lg); +} + +.btn-sm { + @include button-size($btn-padding-y-sm, $btn-padding-x-sm, $btn-font-size-sm, $btn-border-radius-sm); +} + +@if $enable-dark-mode { + @include color-mode(dark) { + @each $color, $value in $theme-colors-dark { + .btn-#{$color} { + @include button-variant($value, $value); + } + } + + @each $color, $value in $theme-colors-dark { + .btn-outline-#{$color} { + @include button-outline-variant($value); + } + } + + @each $color, $value in $theme-colors-dark { + .btn-ghost-#{$color} { + @include button-ghost-variant($value); + } + } + } +} diff --git a/src/scss/scss/_callout.import.scss b/src/scss/scss/_callout.import.scss new file mode 100644 index 000000000..975f078be --- /dev/null +++ b/src/scss/scss/_callout.import.scss @@ -0,0 +1 @@ +@use "callout"; diff --git a/src/scss/scss/_callout.scss b/src/scss/scss/_callout.scss new file mode 100644 index 000000000..9f5630c5a --- /dev/null +++ b/src/scss/scss/_callout.scss @@ -0,0 +1,31 @@ +@use "mixins/border-radius" as *; +@use "variables" as *; + +.callout { + // scss-docs-start callout-css-vars + --#{$prefix}callout-padding-x: #{$callout-padding-x}; + --#{$prefix}callout-padding-y: #{$callout-padding-y}; + --#{$prefix}callout-margin-x: #{$callout-margin-x}; + --#{$prefix}callout-margin-y: #{$callout-margin-y}; + --#{$prefix}callout-border-width: #{$callout-border-width}; + --#{$prefix}callout-border-color: #{$callout-border-color}; + --#{$prefix}callout-border-left-width: #{$callout-border-left-width}; + --#{$prefix}callout-border-radius: #{$callout-border-radius}; + // scss-docs-end callout-css-vars + + padding: var(--#{$prefix}callout-padding-y) var(--#{$prefix}callout-padding-x); + margin: var(--#{$prefix}callout-margin-y) var(--#{$prefix}callout-margin-x); + border: var(--#{$prefix}callout-border-width) solid var(--#{$prefix}callout-border-color); + border-inline-start-color: var(--#{$prefix}callout-border-left-color); + border-inline-start-width: var(--#{$prefix}callout-border-left-width); + @include border-radius(var(--#{$prefix}callout-border-radius)); +} + +// scss-docs-start callout-modifiers +// Generate contextual modifier classes for colorizing the collor. +@each $state, $value in $callout-variants { + .callout-#{$state} { + --#{$prefix}callout-border-left-color: #{$value}; + } +} +// scss-docs-end callout-modifiers diff --git a/src/scss/scss/_card.import.scss b/src/scss/scss/_card.import.scss new file mode 100644 index 000000000..00085495f --- /dev/null +++ b/src/scss/scss/_card.import.scss @@ -0,0 +1 @@ +@forward "card"; diff --git a/src/scss/scss/_card.scss b/src/scss/scss/_card.scss new file mode 100644 index 000000000..02df3a401 --- /dev/null +++ b/src/scss/scss/_card.scss @@ -0,0 +1,243 @@ +@use "mixins/border-radius" as *; +@use "mixins/box-shadow" as *; +@use "mixins/breakpoints" as *; +@use "variables" as *; + +// +// Base styles +// + +.card { + // scss-docs-start card-css-vars + --#{$prefix}card-spacer-y: #{$card-spacer-y}; + --#{$prefix}card-spacer-x: #{$card-spacer-x}; + --#{$prefix}card-title-spacer-y: #{$card-title-spacer-y}; + --#{$prefix}card-title-color: #{$card-title-color}; + --#{$prefix}card-subtitle-color: #{$card-subtitle-color}; + --#{$prefix}card-border-width: #{$card-border-width}; + --#{$prefix}card-border-color: #{$card-border-color}; + --#{$prefix}card-border-radius: #{$card-border-radius}; + --#{$prefix}card-box-shadow: #{$card-box-shadow}; + --#{$prefix}card-inner-border-radius: #{$card-inner-border-radius}; + --#{$prefix}card-cap-padding-y: #{$card-cap-padding-y}; + --#{$prefix}card-cap-padding-x: #{$card-cap-padding-x}; + --#{$prefix}card-cap-bg: #{$card-cap-bg}; + --#{$prefix}card-cap-color: #{$card-cap-color}; + --#{$prefix}card-height: #{$card-height}; + --#{$prefix}card-color: #{$card-color}; + --#{$prefix}card-bg: #{$card-bg}; + --#{$prefix}card-img-overlay-padding: #{$card-img-overlay-padding}; + --#{$prefix}card-group-margin: #{$card-group-margin}; + // scss-docs-end card-css-vars + + position: relative; + display: flex; + flex-direction: column; + min-width: 0; // See https://github.com/twbs/bootstrap/pull/22740#issuecomment-305868106 + height: var(--#{$prefix}card-height); + color: var(--#{$prefix}body-color); + word-wrap: break-word; + background-color: var(--#{$prefix}card-bg); + background-clip: border-box; + border: var(--#{$prefix}card-border-width) solid var(--#{$prefix}card-border-color); + @include border-radius(var(--#{$prefix}card-border-radius)); + @include box-shadow(var(--#{$prefix}card-box-shadow)); + + > hr { + margin-right: 0; + margin-left: 0; + } + + > .list-group { + border-top: inherit; + border-bottom: inherit; + + &:first-child { + border-top-width: 0; + @include border-top-radius(var(--#{$prefix}card-inner-border-radius)); + } + + &:last-child { + border-bottom-width: 0; + @include border-bottom-radius(var(--#{$prefix}card-inner-border-radius)); + } + } + + // Due to specificity of the above selector (`.card > .list-group`), we must + // use a child selector here to prevent double borders. + > .card-header + .list-group, + > .list-group + .card-footer { + border-top: 0; + } +} + +.card-body { + // Enable `flex-grow: 1` for decks and groups so that card blocks take up + // as much space as possible, ensuring footers are aligned to the bottom. + flex: 1 1 auto; + padding: var(--#{$prefix}card-spacer-y) var(--#{$prefix}card-spacer-x); + color: var(--#{$prefix}card-color); +} + +.card-title { + margin-bottom: var(--#{$prefix}card-title-spacer-y); + color: var(--#{$prefix}card-title-color); +} + +.card-subtitle { + margin-top: calc(-.5 * var(--#{$prefix}card-title-spacer-y)); // stylelint-disable-line function-disallowed-list + margin-bottom: 0; + color: var(--#{$prefix}card-subtitle-color); +} + +.card-text:last-child { + margin-bottom: 0; +} + +.card-link { + &:hover { + text-decoration: if($link-hover-decoration == underline, none, null); + } + + + .card-link { + margin-inline-start: var(--#{$prefix}card-spacer-x); + } +} + +// +// Optional textual caps +// + +.card-header { + padding: var(--#{$prefix}card-cap-padding-y) var(--#{$prefix}card-cap-padding-x); + margin-bottom: 0; // Removes the default margin-bottom of + color: var(--#{$prefix}card-cap-color); + background-color: var(--#{$prefix}card-cap-bg); + border-bottom: var(--#{$prefix}card-border-width) solid var(--#{$prefix}card-border-color); + + &:first-child { + @include border-radius(var(--#{$prefix}card-inner-border-radius) var(--#{$prefix}card-inner-border-radius) 0 0); + } +} + +.card-footer { + padding: var(--#{$prefix}card-cap-padding-y) var(--#{$prefix}card-cap-padding-x); + color: var(--#{$prefix}card-cap-color); + background-color: var(--#{$prefix}card-cap-bg); + border-top: var(--#{$prefix}card-border-width) solid var(--#{$prefix}card-border-color); + + &:last-child { + @include border-radius(0 0 var(--#{$prefix}card-inner-border-radius) var(--#{$prefix}card-inner-border-radius)); + } +} + + +// +// Header navs +// + +.card-header-tabs { + margin-right: calc(-.5 * var(--#{$prefix}card-cap-padding-x)); // stylelint-disable-line function-disallowed-list + margin-bottom: calc(-1 * var(--#{$prefix}card-cap-padding-y)); // stylelint-disable-line function-disallowed-list + margin-left: calc(-.5 * var(--#{$prefix}card-cap-padding-x)); // stylelint-disable-line function-disallowed-list + border-bottom: 0; + + .nav-link.active { + background-color: var(--#{$prefix}card-bg); + border-bottom-color: var(--#{$prefix}card-bg); + } +} + +.card-header-pills { + margin-right: calc(-.5 * var(--#{$prefix}card-cap-padding-x)); // stylelint-disable-line function-disallowed-list + margin-left: calc(-.5 * var(--#{$prefix}card-cap-padding-x)); // stylelint-disable-line function-disallowed-list +} + +// Card image +.card-img-overlay { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + padding: var(--#{$prefix}card-img-overlay-padding); + @include border-radius(var(--#{$prefix}card-inner-border-radius)); +} + +.card-img, +.card-img-top, +.card-img-bottom { + width: 100%; // Required because we use flexbox and this inherently applies align-self: stretch +} + +.card-img, +.card-img-top { + @include border-top-radius(var(--#{$prefix}card-inner-border-radius)); +} + +.card-img, +.card-img-bottom { + @include border-bottom-radius(var(--#{$prefix}card-inner-border-radius)); +} + + +// +// Card groups +// + +.card-group { + // The child selector allows nested `.card` within `.card-group` + // to display properly. + > .card { + margin-bottom: var(--#{$prefix}card-group-margin); + } + + @include media-breakpoint-up(sm) { + display: flex; + flex-flow: row wrap; + // The child selector allows nested `.card` within `.card-group` + // to display properly. + > .card { + flex: 1 0 0; + margin-bottom: 0; + + + .card { + margin-inline-start: 0; + border-inline-start: 0; + } + + // Handle rounded corners + @if $enable-rounded { + &:not(:last-child) { + @include border-end-radius(0); + + .card-img-top, + .card-header { + // stylelint-disable-next-line property-disallowed-list + border-top-right-radius: 0; + } + .card-img-bottom, + .card-footer { + // stylelint-disable-next-line property-disallowed-list + border-bottom-right-radius: 0; + } + } + + &:not(:first-child) { + @include border-start-radius(0); + + .card-img-top, + .card-header { + // stylelint-disable-next-line property-disallowed-list + border-top-left-radius: 0; + } + .card-img-bottom, + .card-footer { + // stylelint-disable-next-line property-disallowed-list + border-bottom-left-radius: 0; + } + } + } + } + } +} diff --git a/src/scss/scss/_carousel.import.scss b/src/scss/scss/_carousel.import.scss new file mode 100644 index 000000000..6ee740934 --- /dev/null +++ b/src/scss/scss/_carousel.import.scss @@ -0,0 +1 @@ +@forward "carousel"; diff --git a/src/scss/scss/_carousel.scss b/src/scss/scss/_carousel.scss new file mode 100644 index 000000000..32e659a17 --- /dev/null +++ b/src/scss/scss/_carousel.scss @@ -0,0 +1,234 @@ +@use "functions/escape-svg" as *; +@use "mixins/clearfix" as *; +@use "mixins/color-mode" as *; +@use "mixins/ltr-rtl" as *; +@use "mixins/transition" as *; +@use "variables" as *; +@use "variables-dark" as *; + +// Notes on the classes: +// +// 1. .carousel.pointer-event should ideally be pan-y (to allow for users to scroll vertically) +// even when their scroll action started on a carousel, but for compatibility (with Firefox) +// we're preventing all actions instead +// 2. The .carousel-item-start and .carousel-item-end is used to indicate where +// the active slide is heading. +// 3. .active.carousel-item is the current slide. +// 4. .active.carousel-item-start and .active.carousel-item-end is the current +// slide in its in-transition state. Only one of these occurs at a time. +// 5. .carousel-item-next.carousel-item-start and .carousel-item-prev.carousel-item-end +// is the upcoming slide in transition. + +.carousel { + position: relative; +} + +.carousel.pointer-event { + touch-action: pan-y; +} + +.carousel-inner { + position: relative; + width: 100%; + overflow: hidden; + @include clearfix(); +} + +.carousel-item { + position: relative; + display: none; + float: left; + width: 100%; + margin-right: -100%; + backface-visibility: hidden; + @include transition($carousel-transition); +} + +.carousel-item.active, +.carousel-item-next, +.carousel-item-prev { + display: block; +} + +.carousel-item-next:not(.carousel-item-start), +.active.carousel-item-end { + transform: translateX(100%); +} + +.carousel-item-prev:not(.carousel-item-end), +.active.carousel-item-start { + transform: translateX(-100%); +} + + +// +// Alternate transitions +// + +.carousel-fade { + .carousel-item { + opacity: 0; + transition-property: opacity; + transform: none; + } + + .carousel-item.active, + .carousel-item-next.carousel-item-start, + .carousel-item-prev.carousel-item-end { + z-index: 1; + opacity: 1; + } + + .active.carousel-item-start, + .active.carousel-item-end { + z-index: 0; + opacity: 0; + @include transition(opacity 0s $carousel-transition-duration); + } +} + + +// +// Left/right controls for nav +// + +.carousel-control-prev, +.carousel-control-next { + position: absolute; + top: 0; + bottom: 0; + z-index: 1; + // Use flex for alignment (1-3) + display: flex; // 1. allow flex styles + align-items: center; // 2. vertically center contents + justify-content: center; // 3. horizontally center contents + width: $carousel-control-width; + padding: 0; + color: $carousel-control-color; + text-align: center; + background: none; + filter: var(--#{$prefix}carousel-control-icon-filter); + border: 0; + opacity: $carousel-control-opacity; + @include transition($carousel-control-transition); + + // Hover/focus state + &:hover, + &:focus { + color: $carousel-control-color; + text-decoration: none; + outline: 0; + opacity: $carousel-control-hover-opacity; + } +} +.carousel-control-prev { + left: 0; + background-image: if($enable-gradients, linear-gradient(90deg, rgba($black, .25), rgba($black, .001)), null); +} +.carousel-control-next { + right: 0; + background-image: if($enable-gradients, linear-gradient(270deg, rgba($black, .25), rgba($black, .001)), null); +} + +// Icons for within +.carousel-control-prev-icon, +.carousel-control-next-icon { + display: inline-block; + width: $carousel-control-icon-width; + height: $carousel-control-icon-width; + background-repeat: no-repeat; + background-position: 50%; + background-size: 100% 100%; +} + +.carousel-control-prev-icon { + @include ltr-rtl-value-only("background-image", escape-svg($carousel-control-prev-icon-bg), escape-svg($carousel-control-next-icon-bg)); +} +.carousel-control-next-icon { + @include ltr-rtl-value-only("background-image", escape-svg($carousel-control-next-icon-bg), escape-svg($carousel-control-prev-icon-bg)); +} + +// Optional indicator pips/controls +// +// Add a container (such as a list) with the following class and add an item (ideally a focusable control, +// like a button) with data#{$data-infix}target for each slide your carousel holds. + +.carousel-indicators { + position: absolute; + right: 0; + bottom: 0; + left: 0; + z-index: 2; + display: flex; + justify-content: center; + padding: 0; + // Use the .carousel-control's width as margin so we don't overlay those + margin-right: $carousel-control-width; + margin-bottom: 1rem; + margin-left: $carousel-control-width; + + [data#{$data-infix}target] { + box-sizing: content-box; + flex: 0 1 auto; + width: $carousel-indicator-width; + height: $carousel-indicator-height; + padding: 0; + margin-right: $carousel-indicator-spacer; + margin-left: $carousel-indicator-spacer; + text-indent: -999px; + cursor: pointer; + background-color: $carousel-indicator-active-bg; + background-clip: padding-box; + border: 0; + // Use transparent borders to increase the hit area by 10px on top and bottom. + border-top: $carousel-indicator-hit-area-height solid transparent; + border-bottom: $carousel-indicator-hit-area-height solid transparent; + opacity: $carousel-indicator-opacity; + @include transition($carousel-indicator-transition); + } + + .active { + opacity: $carousel-indicator-active-opacity; + } +} + + +// Optional captions +// +// + +.carousel-caption { + position: absolute; + right: (100% - $carousel-caption-width) * .5; + bottom: $carousel-caption-spacer; + left: (100% - $carousel-caption-width) * .5; + padding-top: $carousel-caption-padding-y; + padding-bottom: $carousel-caption-padding-y; + color: var(--#{$prefix}carousel-caption-color); + text-align: center; +} + +// Dark mode carousel + +@mixin carousel-dark() { + --#{$prefix}carousel-indicator-active-bg: #{$carousel-indicator-active-bg-dark}; + --#{$prefix}carousel-caption-color: #{$carousel-caption-color-dark}; + --#{$prefix}carousel-control-icon-filter: #{$carousel-control-icon-filter-dark}; +} + +.carousel-dark { + @include carousel-dark(); +} + +:root, +[data#{$data-infix}theme="light"] { + --#{$prefix}carousel-indicator-active-bg: #{$carousel-indicator-active-bg}; + --#{$prefix}carousel-caption-color: #{$carousel-caption-color}; + --#{$prefix}carousel-control-icon-filter: #{$carousel-control-icon-filter}; +} + +@if $enable-dark-mode { + @include color-mode(dark, true) { + @include carousel-dark(); + } +} diff --git a/src/scss/scss/_close.import.scss b/src/scss/scss/_close.import.scss new file mode 100644 index 000000000..13ede5120 --- /dev/null +++ b/src/scss/scss/_close.import.scss @@ -0,0 +1 @@ +@forward "close"; diff --git a/src/scss/scss/_close.scss b/src/scss/scss/_close.scss new file mode 100644 index 000000000..15ec7e7ac --- /dev/null +++ b/src/scss/scss/_close.scss @@ -0,0 +1,72 @@ +@use "functions/escape-svg" as *; +@use "mixins/border-radius" as *; +@use "mixins/color-mode" as *; +@use "variables" as *; +@use "variables-dark" as *; + +// Transparent background and border properties included for button version. +// iOS requires the button element instead of an anchor tag. +// If you want the anchor version, it requires `href="#"`. +// See https://developer.mozilla.org/en-US/docs/Web/Events/click#Safari_Mobile + +.btn-close { + // scss-docs-start close-css-vars + --#{$prefix}btn-close-color: #{$btn-close-color}; + --#{$prefix}btn-close-bg: #{ escape-svg($btn-close-bg) }; + --#{$prefix}btn-close-opacity: #{$btn-close-opacity}; + --#{$prefix}btn-close-hover-opacity: #{$btn-close-hover-opacity}; + --#{$prefix}btn-close-focus-shadow: #{$btn-close-focus-shadow}; + --#{$prefix}btn-close-focus-opacity: #{$btn-close-focus-opacity}; + --#{$prefix}btn-close-disabled-opacity: #{$btn-close-disabled-opacity}; + // scss-docs-end close-css-vars + + box-sizing: content-box; + width: $btn-close-width; + height: $btn-close-height; + padding: $btn-close-padding-y $btn-close-padding-x; + color: var(--#{$prefix}btn-close-color); + background: transparent var(--#{$prefix}btn-close-bg) center / $btn-close-width auto no-repeat; // include transparent for button elements + filter: var(--#{$prefix}btn-close-filter); + border: 0; // for button elements + @include border-radius(); + opacity: var(--#{$prefix}btn-close-opacity); + + // Override 's hover style + &:hover { + color: var(--#{$prefix}btn-close-color); + text-decoration: none; + opacity: var(--#{$prefix}btn-close-hover-opacity); + } + + &:focus { + outline: 0; + box-shadow: var(--#{$prefix}btn-close-focus-shadow); + opacity: var(--#{$prefix}btn-close-focus-opacity); + } + + &:disabled, + &.disabled { + pointer-events: none; + user-select: none; + opacity: var(--#{$prefix}btn-close-disabled-opacity); + } +} + +@mixin btn-close-white() { + --#{$prefix}btn-close-filter: #{$btn-close-filter-dark}; +} + +.btn-close-white { + @include btn-close-white(); +} + +:root, +[data#{$data-infix}theme="light"] { + --#{$prefix}btn-close-filter: #{$btn-close-filter}; +} + +@if $enable-dark-mode { + @include color-mode(dark, true) { + @include btn-close-white(); + } +} diff --git a/src/scss/scss/_containers.import.scss b/src/scss/scss/_containers.import.scss new file mode 100644 index 000000000..724ca90c0 --- /dev/null +++ b/src/scss/scss/_containers.import.scss @@ -0,0 +1 @@ +@forward "containers"; diff --git a/src/scss/scss/_containers.scss b/src/scss/scss/_containers.scss new file mode 100644 index 000000000..b2e7760d8 --- /dev/null +++ b/src/scss/scss/_containers.scss @@ -0,0 +1,45 @@ +@use "mixins/breakpoints" as *; +@use "mixins/container" as *; +@use "variables" as *; + +// Container widths +// +// Set the container width, and override it for fixed navbars in media queries. + +@if $enable-container-classes { + // Single container class with breakpoint max-widths + .container, + // 100% wide container at all breakpoints + .container-fluid { + @include make-container(); + } + + // Responsive containers that are 100% wide until a breakpoint + @each $breakpoint, $container-max-width in $container-max-widths { + .container-#{$breakpoint} { + @extend .container-fluid; + } + + @include media-breakpoint-up($breakpoint, $grid-breakpoints) { + %responsive-container-#{$breakpoint} { + max-width: $container-max-width; + } + + // Extend each breakpoint which is smaller or equal to the current breakpoint + $extend-breakpoint: true; + + @each $name, $width in $grid-breakpoints { + @if ($extend-breakpoint) { + .container#{breakpoint-infix($name, $grid-breakpoints)} { + @extend %responsive-container-#{$breakpoint}; + } + + // Once the current breakpoint is reached, stop extending + @if ($breakpoint == $name) { + $extend-breakpoint: false; + } + } + } + } + } +} diff --git a/src/scss/scss/_dropdown.import.scss b/src/scss/scss/_dropdown.import.scss new file mode 100644 index 000000000..1fe96c2f8 --- /dev/null +++ b/src/scss/scss/_dropdown.import.scss @@ -0,0 +1 @@ +@forward "dropdown"; diff --git a/src/scss/scss/_dropdown.scss b/src/scss/scss/_dropdown.scss new file mode 100644 index 000000000..be187725e --- /dev/null +++ b/src/scss/scss/_dropdown.scss @@ -0,0 +1,259 @@ +@use "sass:map"; +@use "mixins/border-radius" as *; +@use "mixins/box-shadow" as *; +@use "mixins/breakpoints" as *; +@use "mixins/caret" as *; +@use "mixins/gradients" as *; +@use "vendor/rfs" as *; +@use "variables" as *; + +// The dropdown wrapper (`
    `) +.dropup, +.dropend, +.dropdown, +.dropstart, +.dropup-center, +.dropdown-center { + position: relative; +} + +.dropdown-toggle { + white-space: nowrap; + + // Generate the caret automatically + @include caret(); +} + +// The dropdown menu +.dropdown-menu { + // scss-docs-start dropdown-css-vars + --#{$prefix}dropdown-zindex: #{$zindex-dropdown}; + --#{$prefix}dropdown-min-width: #{$dropdown-min-width}; + --#{$prefix}dropdown-padding-x: #{$dropdown-padding-x}; + --#{$prefix}dropdown-padding-y: #{$dropdown-padding-y}; + --#{$prefix}dropdown-spacer: #{$dropdown-spacer}; + @include rfs($dropdown-font-size, --#{$prefix}dropdown-font-size); + --#{$prefix}dropdown-color: #{$dropdown-color}; + --#{$prefix}dropdown-bg: #{$dropdown-bg}; + --#{$prefix}dropdown-border-color: #{$dropdown-border-color}; + --#{$prefix}dropdown-border-radius: #{$dropdown-border-radius}; + --#{$prefix}dropdown-border-width: #{$dropdown-border-width}; + --#{$prefix}dropdown-inner-border-radius: #{$dropdown-inner-border-radius}; + --#{$prefix}dropdown-divider-bg: #{$dropdown-divider-bg}; + --#{$prefix}dropdown-divider-margin-y: #{$dropdown-divider-margin-y}; + --#{$prefix}dropdown-box-shadow: #{$dropdown-box-shadow}; + --#{$prefix}dropdown-link-color: #{$dropdown-link-color}; + --#{$prefix}dropdown-link-hover-color: #{$dropdown-link-hover-color}; + --#{$prefix}dropdown-link-hover-bg: #{$dropdown-link-hover-bg}; + --#{$prefix}dropdown-link-active-color: #{$dropdown-link-active-color}; + --#{$prefix}dropdown-link-active-bg: #{$dropdown-link-active-bg}; + --#{$prefix}dropdown-link-disabled-color: #{$dropdown-link-disabled-color}; + --#{$prefix}dropdown-item-padding-x: #{$dropdown-item-padding-x}; + --#{$prefix}dropdown-item-padding-y: #{$dropdown-item-padding-y}; + --#{$prefix}dropdown-header-color: #{$dropdown-header-color}; + --#{$prefix}dropdown-header-padding-x: #{$dropdown-header-padding-x}; + --#{$prefix}dropdown-header-padding-y: #{$dropdown-header-padding-y}; + // scss-docs-end dropdown-css-vars + + position: absolute; + z-index: var(--#{$prefix}dropdown-zindex); + display: none; // none by default, but block on "open" of the menu + min-width: var(--#{$prefix}dropdown-min-width); + padding: var(--#{$prefix}dropdown-padding-y) var(--#{$prefix}dropdown-padding-x); + margin: 0; // Override default margin of ul + @include font-size(var(--#{$prefix}dropdown-font-size)); + color: var(--#{$prefix}dropdown-color); + text-align: start; // Ensures proper alignment if parent has it changed (e.g., modal footer) + list-style: none; + background-color: var(--#{$prefix}dropdown-bg); + background-clip: padding-box; + border: var(--#{$prefix}dropdown-border-width) solid var(--#{$prefix}dropdown-border-color); + @include border-radius(var(--#{$prefix}dropdown-border-radius)); + @include box-shadow(var(--#{$prefix}dropdown-box-shadow)); + + &[data#{$data-infix}popper] { + inset-inline-start: 0; + top: 100%; + margin-top: var(--#{$prefix}dropdown-spacer); + } + + @if $dropdown-padding-y == 0 { + > .dropdown-item:first-child, + > li:first-child .dropdown-item { + @include border-top-radius(var(--#{$prefix}dropdown-inner-border-radius)); + } + > .dropdown-item:last-child, + > li:last-child .dropdown-item { + @include border-bottom-radius(var(--#{$prefix}dropdown-inner-border-radius)); + } + + } +} + +// scss-docs-start responsive-breakpoints +// We deliberately hardcode the `cui-` prefix because we check +// this custom property in JS to determine Popper's positioning + +@each $breakpoint in map.keys($grid-breakpoints) { + @include media-breakpoint-up($breakpoint) { + $infix: breakpoint-infix($breakpoint, $grid-breakpoints); + + .dropdown-menu#{$infix}-start { + --#{$prefix}position: start; + + &[data#{$data-infix}popper] { + inset-inline-start: 0; + inset-inline-end: auto; + } + } + + .dropdown-menu#{$infix}-end { + --#{$prefix}position: end; + + &[data#{$data-infix}popper] { + inset-inline-start: auto; + inset-inline-end: 0; + } + } + } +} +// scss-docs-end responsive-breakpoints + +// Allow for dropdowns to go bottom up (aka, dropup-menu) +// Just add .dropup after the standard .dropdown class and you're set. +.dropup { + .dropdown-menu[data#{$data-infix}popper] { + top: auto; + bottom: 100%; + margin-top: 0; + margin-bottom: var(--#{$prefix}dropdown-spacer); + } + + .dropdown-toggle { + @include caret(up); + } +} + +.dropend { + .dropdown-menu[data#{$data-infix}popper] { + inset-inline-start: 100%; + inset-inline-end: auto; + top: 0; + margin-inline-start: var(--#{$prefix}dropdown-spacer); + margin-top: 0; + } + + .dropdown-toggle { + @include caret(end); + &::after { + vertical-align: 0; + } + } +} + +.dropstart { + .dropdown-menu[data#{$data-infix}popper] { + inset-inline-start: auto; + inset-inline-end: 100%; + top: 0; + margin-inline-end: var(--#{$prefix}dropdown-spacer); + margin-top: 0; + } + + .dropdown-toggle { + @include caret(start); + &::before { + vertical-align: 0; + } + } +} + + +// Dividers (basically an `
    `) within the dropdown +.dropdown-divider { + height: 0; + margin: var(--#{$prefix}dropdown-divider-margin-y) 0; + overflow: hidden; + border-top: 1px solid var(--#{$prefix}dropdown-divider-bg); + opacity: 1; // Revisit in v6 to de-dupe styles that conflict with
    element +} + +// Links, buttons, and more within the dropdown menu +// +// `