diff --git a/src/components/AddToCampaign.jsx b/src/components/AddToCampaign.jsx index b4facb1..8c48d88 100644 --- a/src/components/AddToCampaign.jsx +++ b/src/components/AddToCampaign.jsx @@ -4,7 +4,6 @@ import { useDispatch, useSelector } from 'react-redux'; import { fetchCampaigns } from '../store/slices/brandsSlice'; import SpinningComponent from './Spinning'; import { addCreatorsToCampaign } from '../store/slices/creatorsSlice'; -import { setNotificationBarMessage } from '../store/slices/notificationBarSlice'; export default function AddToCampaign({ show, onHide }) { const dispatch = useDispatch(); diff --git a/src/components/DatabaseFilter.jsx b/src/components/DatabaseFilter.jsx index 409803a..115e7bf 100644 --- a/src/components/DatabaseFilter.jsx +++ b/src/components/DatabaseFilter.jsx @@ -47,6 +47,9 @@ export default function DatabaseFilter({ path, pageType = 'database' }) { // 预定义的离散点值 const discreteValues = [0, 100, 1000, 10000, 100000, 250000, 500000]; const discretePricingValues = [0, 200, 400, 600, 800, 1000, 3000]; + const marks = ['0', '100', '1k', '10k', '100k', '250k', '500k+']; + const PricingMarks = ['0', '200', '400', '600', '800', '1000', '3000']; + // 找到最接近的离散值索引 const findClosestDiscreteIndex = (value) => { let closestIndex = 0; @@ -319,8 +322,7 @@ export default function DatabaseFilter({ path, pageType = 'database' }) {
Views
@@ -359,8 +361,7 @@ export default function DatabaseFilter({ path, pageType = 'database' }) {
Pricing
diff --git a/src/components/NotificationBar.jsx b/src/components/NotificationBar.jsx index e0b9350..33e6255 100644 --- a/src/components/NotificationBar.jsx +++ b/src/components/NotificationBar.jsx @@ -1,7 +1,8 @@ -import { Check, CircleAlert, Info, X } from 'lucide-react'; +import { Check, CircleAlert, Info, ShieldAlert, X } from 'lucide-react'; import { useDispatch, useSelector } from 'react-redux'; import { resetNotificationBar } from '../store/slices/notificationBarSlice'; import { useEffect } from 'react'; +import { Alert } from 'react-bootstrap'; export default function NotificationBar() { const { message, type, show } = useSelector((state) => state.notificationBar); @@ -24,18 +25,17 @@ export default function NotificationBar() { return ( show && ( -
{type === 'success' && } {type === 'warning' && } - {type === 'error' && } + {type === 'error' && } {type === 'info' && }
{message}
- -
+ ) ); } diff --git a/src/components/ProductsList.jsx b/src/components/ProductsList.jsx index a7470c8..d65c7cc 100644 --- a/src/components/ProductsList.jsx +++ b/src/components/ProductsList.jsx @@ -1,19 +1,15 @@ import React, { useEffect, useState } from 'react'; import { Table, Form } from 'react-bootstrap'; import { useSelector, useDispatch } from 'react-redux'; -import { useParams } from 'react-router-dom'; import '../styles/Products.scss'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import Spinning from './Spinning'; -export default function ProductsList({ onShowProductDetail }) { - const { brandId } = useParams(); +export default function ProductsList({ products, setSelectedProduct, onShowProductDetail }) { const { selectedBrand } = useSelector((state) => state.brands); - const [products, setProducts] = useState([]); - const [selectedProducts, setSelectedProducts] = useState([]); const [sortField, setSortField] = useState(null); const [sortDirection, setSortDirection] = useState('asc'); - + const [selectedProducts, setSelectedProducts] = useState([]); const handleSort = (field) => { return; @@ -60,7 +56,7 @@ export default function ProductsList({ onShowProductDetail }) { 0} + checked={selectedProducts.length === products?.length && products?.length > 0} onChange={handleSelectAll} /> @@ -94,14 +90,14 @@ export default function ProductsList({ onShowProductDetail }) { - {selectedBrand?.products?.length === 0 ? ( + {products?.length === 0 ? ( No products found for this brand. ) : ( - selectedBrand?.products?.map((product) => ( + products?.map((product) => (
onShowProductDetail(product)} style={{cursor: 'pointer'}}> -
{product.name.slice(0, 1)}
+ {product.name} + {/*
{product.name.slice(0, 1)}
*/}
{product.name}
diff --git a/src/components/RangeSlider.jsx b/src/components/RangeSlider.jsx index b1fbdf8..0657d8f 100644 --- a/src/components/RangeSlider.jsx +++ b/src/components/RangeSlider.jsx @@ -2,9 +2,9 @@ import React, { useState, useEffect, useRef } from 'react'; import '../styles/RangeSlider.scss'; import debounce from 'lodash/debounce'; -export default function RangeSlider({ min = 0, max = 100, value, onChange }) { +export default function RangeSlider({ value, onChange, discreteValues }) { // 预定义的离散值点 - const discreteValues = [0, 100, 1000, 10000, 100000, 250000, 500000]; + // const discreteValues = [0, 100, 1000, 10000, 100000, 250000, 500000]; const marks = ['0', '100', '1k', '10k', '100k', '250k', '500k+']; // 使用索引位置作为滑块的实际值,以实现等分 @@ -170,8 +170,8 @@ export default function RangeSlider({ min = 0, max = 100, value, onChange }) {
- {marks.map((mark, index) => ( - {mark} + {discreteValues.map((mark, index) => ( + {formatValue(mark)} ))}
{/* 显示当前选中的值 */} diff --git a/src/lib/constant.js b/src/lib/constant.js new file mode 100644 index 0000000..55ec385 --- /dev/null +++ b/src/lib/constant.js @@ -0,0 +1,153 @@ +export const BRAND_SOURCES = [ + { + value: 'TKS Official', + name: 'TKS Official', + }, + { + value: 'Third-party Agency', + name: 'Third-party Agency', + }, + { + value: 'Offline Event', + name: 'Offline Event', + }, + { + value: 'Social Media', + name: 'Social Media', + }, +]; + +export const CAMPAIGN_SERVICES = [ + { + value: 'fufei', + name: '达人短视频(付费)', + }, + { + value: 'chunyong', + name: '达人短视频(纯佣)', + }, + { + value: 'dai', + name: '直播(代播)', + }, + { + value: 'dabao', + name: '直播(达播)', + }, + { + value: 'chun', + name: '纯素材短视频', + }, +]; + +export const CREATOR_TYPES = [ + { + value: 'dai', + name: '带货类达人', + }, + { + value: 'exposure', + name: '曝光类达人', + }, +]; + +export const GMV_RANGES = [ + { + value: '0-5k', + name: '$0 - $5K', + }, + { + value: '5k-25k', + name: '$5K - $25K', + }, + { + value: '25k-60k', + name: '$25K - $60K', + }, + { + value: '60k-150k', + name: '$60K - $150K', + }, + { + value: '150k-400k', + name: '$150K - $400K', + }, + { + value: '400k-1500k', + name: '$400K - $1500K', + }, + { + value: '1500k+', + name: '$1500K+', + }, +]; + +export const CREATOR_LEVELS = [ + { + value: 'L1', + name: 'L1', + }, + { + value: 'L2', + name: 'L2', + }, + { + value: 'L3', + name: 'L3', + }, + { + value: 'L4', + name: 'L4', + }, + { + value: 'L5', + name: 'L5', + }, + { + value: 'L6', + name: 'L6', + }, + { + value: 'L7', + name: 'L7', + }, +]; + +export const CREATOR_CATEGORIES = [ + { + value: 'Phones & Electronics', + name: 'Phones & Electronics', + }, + { + value: 'Womenswear & Underwear', + name: 'Womenswear & Underwear', + }, + { + value: 'Sports & Outdoor', + name: 'Sports & Outdoor', + }, + { + value: 'Food & Beverage', + name: 'Food & Beverage', + }, + { + value: 'Health', + name: 'Health', + }, + { + value: 'Kitchenware', + name: 'Kitchenware', + }, + { + value: 'Household Appliances', + name: 'Household Appliances', + }, + { + value: 'Womensware & Underwear', + name: 'Womensware & Underwear', + }, + { + value: 'Other', + name: 'Other', + }, +]; diff --git a/src/pages/Brands.jsx b/src/pages/Brands.jsx index 2d7314a..ef7f5da 100644 --- a/src/pages/Brands.jsx +++ b/src/pages/Brands.jsx @@ -8,6 +8,7 @@ import { Plus } from 'lucide-react'; import { useDispatch, useSelector } from 'react-redux'; import { createBrandThunk, fetchBrands, selectBrand } from '../store/slices/brandsSlice'; import SpinningComponent from '../components/Spinning'; +import { BRAND_SOURCES } from '../lib/constant'; export default function Brands() { const navigate = useNavigate(); @@ -48,13 +49,6 @@ function AddBrandModal({ show, onHide }) { const dispatch = useDispatch(); const { status, error } = useSelector((state) => state.brands); - const sourceOptions = [ - { value: '1', label: 'TKS Official' }, - { value: '2', label: 'Third Party Agency' }, - { value: '3', label: 'Official Event' }, - { value: '4', label: 'Social Media' }, - ]; - const handleSubmit = async (e) => { const form = document.getElementById('brandForm'); if (form.checkValidity() === false) { @@ -112,7 +106,7 @@ function AddBrandModal({ show, onHide }) { Source setBrandSource(e.target.value)} required> - {sourceOptions.map((option) => ( + {BRAND_SOURCES.map((option) => ( diff --git a/src/pages/BrandsDetail.jsx b/src/pages/BrandsDetail.jsx index ac359dd..ba1f642 100644 --- a/src/pages/BrandsDetail.jsx +++ b/src/pages/BrandsDetail.jsx @@ -1,14 +1,23 @@ import React, { useEffect, useState } from 'react'; import SearchBar from '../components/SearchBar'; -import { Button } from 'react-bootstrap'; +import { Alert, Button, Form, Modal } from 'react-bootstrap'; import { Folders, Hash, LinkIcon, Plus, Users } from 'lucide-react'; import { useDispatch, useSelector } from 'react-redux'; import { Link, useParams } from 'react-router-dom'; import CampaignList from '../components/CampaignList'; import ProductsList from '../components/ProductsList'; -import { fetchBrandDetail, fetchBrandCampaigns, fetchBrandProducts, setSelectedProduct } from '../store/slices/brandsSlice'; +import { + fetchBrandDetail, + fetchBrandCampaigns, + fetchBrandProducts, + setSelectedProduct, + createCampaignThunk, +} from '../store/slices/brandsSlice'; import SlidePanel from '../components/SlidePanel'; import ProductDetail from '../components/ProductDetail'; +import { CAMPAIGN_SERVICES, CREATOR_CATEGORIES, CREATOR_LEVELS, CREATOR_TYPES, GMV_RANGES } from '../lib/constant'; +import RangeSlider from '../components/RangeSlider'; +import SpinningComponent from '../components/Spinning'; export default function BrandsDetail() { const { id } = useParams(); @@ -16,6 +25,7 @@ export default function BrandsDetail() { const [activeTab, setActiveTab] = useState('campaigns'); const { selectedBrand } = useSelector((state) => state.brands); const [showProductDetail, setShowProductDetail] = useState(false); + const [showAddCampaignModal, setShowAddCampaignModal] = useState(false); useEffect(() => { if (id) { @@ -30,14 +40,13 @@ export default function BrandsDetail() { setShowProductDetail(true); }; - return ( selectedBrand?.id && (
{activeTab === 'campaigns' && ( - @@ -125,7 +134,10 @@ export default function BrandsDetail() { {activeTab === 'campaigns' && } {activeTab === 'products' && ( <> - + setShowProductDetail(false)} @@ -136,7 +148,223 @@ export default function BrandsDetail() { )} + setShowAddCampaignModal(false)} /> ) ); } + +export function AddCampaignModal({ show, onHide }) { + const dispatch = useDispatch(); + const [formData, setFormData] = useState({ + name: '', + description: '', + service: '', + link_product: '', + creator_type: '', + creator_level: '', + creator_count: 0, + budget: [0, 200], + creator_category: '', + followers: [0, 100], + gmv: '', + views: [0, 100000], + }); + const [validated, setValidated] = useState(false); + const budgetDiscreteValues = [0, 200, 400, 600, 800, 1000, 3000]; + const followersDiscreteValues = [0, 100, 1000, 10000, 250000, 500000]; + const viewsDiscreteValues = [0, 50000, 100000, 300000, 500000, 1000000, 5000000]; + const [errors, setErrors] = useState({}); + const { status } = useSelector((state) => state.brands); + + const handleChange = (e) => { + setFormData({ ...formData, [e.target.name]: e.target.value }); + }; + + const handleSubmit = (e) => { + e.preventDefault(); + const form = document.getElementById('addCampaignForm'); + if (form.checkValidity() === false) { + e.preventDefault(); + e.stopPropagation(); + setValidated(true); + return; + } + console.log(formData); + dispatch(createCampaignThunk(formData)); + }; + + const handleViewsChange = (views) => { + console.log(views); + }; + + const handleClose = () => { + setFormData({ + name: '', + description: '', + service: '', + link_product: '', + creator_type: '', + creator_level: '', + creator_count: 0, + budget: [200, 600], + creator_category: '', + followers: [0, 100], + gmv: '', + views: [0, 100000], + }); + setValidated(false); + onHide(); + }; + + return ( + + + Add Campaign + + +
+ + Campaign Name * + + Please enter campaign name. + + + Campaign Description * + + Please enter campaign description. + + + Service * + + + {CAMPAIGN_SERVICES.map((service) => ( + + ))} + + Please select a service. + + + Link Product * + + + + Creator Type * + + + {CREATOR_TYPES.map((creatorType) => ( + + ))} + + Please select a creator type. + + + Creator Level * + + + {CREATOR_LEVELS.map((creatorLevel) => ( + + ))} + + + + # Creators * + + + Please enter the number of creators. + + + + Budget * + + + + Creator Category + + + {CREATOR_CATEGORIES.map((creatorCategory) => ( + + ))} + + + + Followers + + + + GMV + + + {GMV_RANGES.map((gmv) => ( + + ))} + + + + Avg. Video Views + + +
+
+ + + + +
+ ); +} diff --git a/src/pages/CampaignDetail.jsx b/src/pages/CampaignDetail.jsx index a81b881..aeaa623 100644 --- a/src/pages/CampaignDetail.jsx +++ b/src/pages/CampaignDetail.jsx @@ -3,12 +3,13 @@ import { Link, useParams } from 'react-router-dom'; import SearchBar from '../components/SearchBar'; import { Button, Form, Modal } from 'react-bootstrap'; import { useSelector, useDispatch } from 'react-redux'; -import { fetchBrands, fetchBrandDetail, fetchCampaignDetail, setSelectedProduct } from '../store/slices/brandsSlice'; +import { fetchBrands, fetchBrandDetail, fetchCampaignDetail, setSelectedProduct } from '../store/slices/brandsSlice'; import CampaignInfo from '../components/CampaignInfo'; import { ChevronRight, Send, Plus } from 'lucide-react'; import ProductsList from '../components/ProductsList'; import SlidePanel from '../components/SlidePanel'; import CampaignScript from './CampaignScript'; +import { addProductToCampaign, fetchProducts } from '../store/slices/productSlice'; export default function CampaignDetail() { const { brandId, campaignId } = useParams(); @@ -29,6 +30,7 @@ export default function CampaignDetail() { dispatch(fetchCampaignDetail(campaignId)); } } + dispatch(fetchProducts()); }, [dispatch, brandId, campaignId]); const handleShowProductDetail = (product) => { @@ -132,16 +134,16 @@ export default function CampaignDetail() { Add Product - + setShowProductDetail(false)} title='Product Detail' size='xxl' > - + - setShowAddProductModal(false)} /> + setShowAddProductModal(false)} /> )}
@@ -149,9 +151,26 @@ export default function CampaignDetail() { ); } -function AddProductModal({ show, onHide }) { +function AddProductModal({ campaignId, show, onHide }) { + const [selectedProduct, setSelectedProduct] = useState(null); + const { products } = useSelector((state) => state.products); + const dispatch = useDispatch(); + + const handleCancel = () => { + onHide(); + setSelectedProduct(null); + }; + + const handleSubmit = async () => { + if (!selectedProduct) return; + console.log(selectedProduct); + await dispatch(addProductToCampaign({ campaignId, productId: selectedProduct })).unwrap(); + dispatch(fetchCampaignDetail(campaignId)); + handleCancel(); + }; + return ( - + Add Product @@ -159,18 +178,26 @@ function AddProductModal({ show, onHide }) {
Product PID - + setSelectedProduct(e.target.value)} + > - - - + {products?.length > 0 && products.map((product) => ( + + ))}
- - +
diff --git a/src/pages/InboxTemplate.jsx b/src/pages/InboxTemplate.jsx index c84550a..8861481 100644 --- a/src/pages/InboxTemplate.jsx +++ b/src/pages/InboxTemplate.jsx @@ -5,6 +5,7 @@ import React, { useEffect, useState } from 'react'; import { useDispatch } from 'react-redux'; import { addTemplateApi, editTemplateApi, fetchTemplates } from '../store/slices/inboxSlice'; import TemplateList from '../components/TemplateList'; +import { CAMPAIGN_SERVICES } from '../lib/constant'; export default function InboxTemplate() { const [activeTab, setActiveTab] = useState('all'); @@ -168,11 +169,9 @@ function AddTemplateModal({ show, formData, setFormData, handleClose, type = 'ad - - - - - + {CAMPAIGN_SERVICES.map((service) => ( + + ))}
diff --git a/src/store/index.js b/src/store/index.js index c033483..6302509 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -8,6 +8,7 @@ import inboxReducer from './slices/inboxSlice'; import authReducer from './slices/authSlice'; import discoveryReducer from './slices/discoverySlice'; import notificationBarReducer from './slices/notificationBarSlice'; +import productReducer from './slices/productSlice'; const authPersistConfig = { key: 'auth', @@ -22,6 +23,7 @@ const rootReducer = combineReducers({ discovery: discoveryReducer, auth: persistReducer(authPersistConfig, authReducer), notificationBar: notificationBarReducer, + products: productReducer, }); const store = configureStore({ diff --git a/src/store/slices/brandsSlice.js b/src/store/slices/brandsSlice.js index b2c402b..5bbf098 100644 --- a/src/store/slices/brandsSlice.js +++ b/src/store/slices/brandsSlice.js @@ -1,5 +1,6 @@ import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'; import api from '@/services/api'; +import { setNotificationBarMessage } from './notificationBarSlice'; const mockProducts = [ { id: 1, @@ -191,6 +192,20 @@ export const createBrandThunk = createAsyncThunk('brands/createBrand', async (br } }); +export const createCampaignThunk = createAsyncThunk('brands/createCampaign', async (campaign, { rejectWithValue, dispatch }) => { + try { + const response = await api.post('/campaigns/', campaign); + console.log(response); + if (response.code !== 201 && response.code !== 200) { + throw new Error(response.message); + } + } catch (error) { + console.log(error); + dispatch(setNotificationBarMessage({ message: error.message, type: 'error' })); + return rejectWithValue(error.message); + } +}); + const initialState = { brands: [], campaigns: [], @@ -299,6 +314,16 @@ const brandsSlice = createSlice({ .addCase(fetchCampaignDetail.rejected, (state, action) => { state.status = 'failed'; state.error = action.error.message; + }) + .addCase(createCampaignThunk.pending, (state) => { + state.status = 'loading'; + }) + .addCase(createCampaignThunk.fulfilled, (state, action) => { + state.status = 'succeeded'; + }) + .addCase(createCampaignThunk.rejected, (state, action) => { + state.status = 'failed'; + state.error = action.error.message; }); }, }); diff --git a/src/store/slices/productSlice.js b/src/store/slices/productSlice.js new file mode 100644 index 0000000..0fe4c14 --- /dev/null +++ b/src/store/slices/productSlice.js @@ -0,0 +1,69 @@ +import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'; +import api from '@/services/api'; +import { setNotificationBarMessage } from './notificationBarSlice'; + +export const fetchProducts = createAsyncThunk('products/fetchProducts', async (_, { rejectWithValue, dispatch }) => { + try { + const response = await api.get('/products/'); + if (response.code !== 200) { + throw new Error(response.message); + } + return response.data; + } catch (error) { + return rejectWithValue(error.message); + } +}); + +export const addProductToCampaign = createAsyncThunk('products/addProductToCampaign', async (formData, { rejectWithValue, dispatch }) => { + try { + const { campaignId, productId } = formData; + const response = await api.post(`/campaigns/${campaignId}/add_product/`, { product_id:productId }); + if (response.code !== 201 && response.code !== 200) { + throw new Error(response.message); + } + console.log(response); + + return response.data; + } catch (error) { + dispatch(setNotificationBarMessage({ message: error.message, type: 'error' })); + return rejectWithValue(error.message); + } +}); + +const initialState = { + products: [], + loading: false, + error: null, +}; + +const productSlice = createSlice({ + name: 'products', + initialState, + reducers: {}, + extraReducers: (builder) => { + builder.addCase(fetchProducts.pending, (state) => { + state.loading = true; + }); + builder.addCase(fetchProducts.fulfilled, (state, action) => { + state.products = action.payload; + state.loading = false; + }) + builder.addCase(fetchProducts.rejected, (state, action) => { + state.error = action.payload; + state.loading = false; + }) + builder.addCase(addProductToCampaign.pending, (state) => { + state.loading = true; + }) + builder.addCase(addProductToCampaign.fulfilled, (state, action) => { + state.products = action.payload; + state.loading = false; + }) + builder.addCase(addProductToCampaign.rejected, (state, action) => { + state.error = action.payload; + state.loading = false; + }) + }, +}); + +export default productSlice.reducer; diff --git a/src/styles/Brands.scss b/src/styles/Brands.scss index 5276729..0eaf72b 100644 --- a/src/styles/Brands.scss +++ b/src/styles/Brands.scss @@ -217,7 +217,7 @@ .campaign-title { font-size: 1.25rem; font-weight: 800; - margin-bottom: .5rem; + margin-bottom: 0.5rem; color: $primary; cursor: pointer; } @@ -268,29 +268,26 @@ flex-flow: row wrap; gap: 1rem; justify-content: space-between; - height: 100%; - overflow-y: auto; - + .campaign-detail-info { width: 100%; background-color: #fff; padding: 1.5rem; border-radius: 0.375rem; - + .campaign-info-top { position: relative; display: flex; flex-flow: column nowrap; align-items: flex-start; gap: 0.25rem; - + .campaign-name { font-size: 1.25rem; font-weight: 800; color: $primary; } .campaign-descp { - } .campaign-edit { position: absolute; @@ -303,7 +300,7 @@ gap: 0.25rem; } } - + .campaign-info-bottom { display: flex; flex-flow: row wrap; @@ -311,7 +308,7 @@ align-items: center; gap: 0.5rem; width: 100%; - + .campaign-info-item { width: 30%; flex-shrink: 0; @@ -320,7 +317,7 @@ align-items: center; gap: 0.5rem; font-size: 0.875rem; - + .campaign-info-item-label { color: $neutral-700; width: 8.5rem; @@ -328,11 +325,10 @@ align-items: center; gap: 0.25rem; } - } } } - + .campaign-requirements { width: 30%; max-width: 380px; @@ -358,7 +354,7 @@ } } } - + .campaign-progress { flex: 1; background-color: #fff; @@ -375,7 +371,7 @@ gap: 0.375rem; flex: 1; border-bottom: 4px solid transparent; - padding: .375rem 0; + padding: 0.375rem 0; .campaign-progress-item-index { width: 1.75rem; @@ -402,3 +398,16 @@ } } } + +#addCampaignForm { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 1rem; + + div { + flex: 1; + .range-slider { + width: 90%; + } + } +} diff --git a/src/styles/DatabaseList.scss b/src/styles/DatabaseList.scss index b30be13..5db0533 100644 --- a/src/styles/DatabaseList.scss +++ b/src/styles/DatabaseList.scss @@ -147,8 +147,6 @@ .table-container { position: relative; - max-height: calc(100% - 455px); // Adjust this value based on your layout - overflow-y: auto; .sticky-header { position: sticky; diff --git a/src/styles/custom-theme.scss b/src/styles/custom-theme.scss index 599300c..d8ec45d 100644 --- a/src/styles/custom-theme.scss +++ b/src/styles/custom-theme.scss @@ -18,7 +18,7 @@ select:focus { box-shadow: 0 0 0 0.2rem rgba(99, 102, 241, 0.25) !important; outline: none; } -input:focus { +.form-control:valid:focus { box-shadow: 0 0 0 0.2rem rgba(99, 102, 241, 0.25) !important; outline: none; } @@ -134,3 +134,8 @@ a { box-shadow: 0px 0px 1px #171a1f12, 0px 0px 2px #171a1f1f; /* shadow-xs */ padding: 1rem; } + +.modal-content { + width: max-content; + max-width: 60vw; +} \ No newline at end of file diff --git a/src/styles/sidebar.scss b/src/styles/sidebar.scss index 7283c26..1d0f019 100644 --- a/src/styles/sidebar.scss +++ b/src/styles/sidebar.scss @@ -151,7 +151,7 @@ transition: all 0.3s ease; background: #f8f9fa; border-radius: 8px; - overflow: hidden; + overflow-y: auto; } // Collapsed sidebar adjustments