Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fahim #1

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .eslintrc.cjs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint-disable no-undef */
module.exports = {
env: { browser: true, es2020: true },
extends: [
@@ -10,5 +11,7 @@ module.exports = {
plugins: ['react-refresh'],
rules: {
'react-refresh/only-export-components': 'warn',

'@typescript-eslint/no-non-null-assertion': 'off',
},
}
};
698 changes: 548 additions & 150 deletions package-lock.json

Large diffs are not rendered by default.

34 changes: 23 additions & 11 deletions src/components/Cart.tsx
Original file line number Diff line number Diff line change
@@ -12,15 +12,17 @@ import {
HiOutlineTrash,
} from 'react-icons/hi';
import { Button } from './ui/button';
import { IProduct } from '@/types/globalTypes';
import { useAppDispatch, useAppSelector } from '@/redux/hook';
import {
addtoCart,
removeFromCart,
removeOne,
} from '@/redux/feature/cart/CartSlice/cartSlice';

export default function Cart() {
//! Dummy data

const products: IProduct[] = [];
const total = 0;

//! **
//!! redux query
const { products, total } = useAppSelector((state) => state.Cart);
const dispatch = useAppDispatch();

return (
<Sheet>
@@ -47,18 +49,28 @@ export default function Cart() {
<h1 className="text-2xl self-center">{product?.name}</h1>
<p>Quantity: {product.quantity}</p>
<p className="text-xl">
Total Price: {(product.price * product.quantity!).toFixed(2)}{' '}
$
Total Price: {(product.price * product.quantity!).toFixed(2)}$
</p>
</div>
<div className="border-l pl-5 flex flex-col justify-between">
<Button>
<Button
onClick={() => {
dispatch(addtoCart(product));
}}
>
<HiOutlinePlus size="20" />
</Button>
<Button>
<Button
onClick={() => {
dispatch(removeOne(product));
}}
>
<HiMinus size="20" />
</Button>
<Button
onClick={() => {
dispatch(removeFromCart(product));
}}
variant="destructive"
className="bg-red-500 hover:bg-red-400"
>
5 changes: 5 additions & 0 deletions src/components/ProductCard.tsx
Original file line number Diff line number Diff line change
@@ -2,13 +2,18 @@ import { IProduct } from '@/types/globalTypes';
import { toast } from './ui/use-toast';
import { Button } from './ui/button';
import { Link } from 'react-router-dom';
import { useAppDispatch } from '@/redux/hook';
import { addtoCart } from '@/redux/feature/cart/CartSlice/cartSlice';

interface IProps {
product: IProduct;
}

export default function ProductCard({ product }: IProps) {
const dispatch = useAppDispatch();
const handleAddProduct = (product: IProduct) => {
// use dispatch to send data into redux store
dispatch(addtoCart(product));
toast({
description: 'Product Added',
});
70 changes: 58 additions & 12 deletions src/components/ProductReview.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,72 @@
import { ChangeEvent, FormEvent, useState } from 'react';
import { Avatar, AvatarFallback, AvatarImage } from './ui/avatar';
import { Button } from './ui/button';
import { Textarea } from './ui/textarea';
import { FiSend } from 'react-icons/fi';
import {
useGetCommentQuery,
usePostCommentMutation,
} from '@/redux/feature/product/productApi/productApi';

const dummyComments = [
'Bhalo na',
'Ki shob ghori egula??',
'Eta kono product holo ??',
'200 taka dibo, hobe ??',
];
// const dummyComments = [
// 'Bhalo na',
// 'Ki shob ghori egula??',
// 'Eta kono product holo ??',
// '200 taka dibo, hobe ??',
// ];

interface IProps {
id: string;
}

export default function ProductReview({ id }: IProps) {
// !! get query
const { data } = useGetCommentQuery(id, {
refetchOnMountOrArgChange: true,
pollingInterval: 30000,
});

//!! post mutation
const [postComment, { isError, isLoading, isSuccess }] =
usePostCommentMutation();
console.log(`${isLoading} ${isError} ${isSuccess}`);

const [inputValue, setInputValue] = useState<string>('');
console.log(inputValue);
const handleSubmit = (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();

// ! post comment using Mutation
const options = {
id: id,
comment: { comment: inputValue },
};
postComment(options);

setInputValue('');
};

const handleChange = (event: ChangeEvent<HTMLTextAreaElement>) => {
setInputValue(event.target.value);
};

export default function ProductReview() {
return (
<div className="max-w-7xl mx-auto mt-5">
<div className="flex gap-5 items-center">
<Textarea className="min-h-[30px]" />
<Button className="rounded-full h-10 w-10 p-2 text-[25px]">
<form className="flex gap-5 items-center" onSubmit={handleSubmit}>
<Textarea
className="min-h-[30px]"
onChange={handleChange}
value={inputValue}
/>
<Button
type="submit"
className="rounded-full h-10 w-10 p-2 text-[25px]"
>
<FiSend />
</Button>
</div>
</form>
<div className="mt-10">
{dummyComments.map((comment, index) => (
{data?.comment?.map((comment: string, index: number) => (
<div key={index} className="flex gap-3 items-center mb-5">
<Avatar>
<AvatarImage src="https://github.com/shadcn.png" />
15 changes: 15 additions & 0 deletions src/lib/firebase.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { initializeApp } from 'firebas/app';
import { getAuth } from 'firebase/auth';

const firebaseConfig = {
apiKey: 'AIzaSyDiXjaqDcUBVWR440_aZk5DbLpgXsRSsqQ',
authDomain: 'simple-ema-john-8bc50.firebaseapp.com',
projectId: 'simple-ema-john-8bc50',
storageBucket: 'simple-ema-john-8bc50.appspot.com',
messagingSenderId: '1080330401067',
appId: '1:1080330401067:web:bc3f1b9675b3d45cf0816e',
};

const app = initializeApp(firebaseConfig);

export const auth = getAuth(app);
6 changes: 5 additions & 1 deletion src/main.tsx
Original file line number Diff line number Diff line change
@@ -3,9 +3,13 @@ import ReactDOM from 'react-dom/client';
import './index.css';
import { RouterProvider } from 'react-router-dom';
import routes from './routes/routes.tsx';
import { Provider } from 'react-redux';
import { store } from './redux/store.ts';

ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
<React.StrictMode>
<RouterProvider router={routes} />
<Provider store={store}>
<RouterProvider router={routes} />
</Provider>
</React.StrictMode>
);
6 changes: 2 additions & 4 deletions src/pages/Checkout.tsx
Original file line number Diff line number Diff line change
@@ -5,7 +5,7 @@ import { Label } from '@/components/ui/label';
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
import { Switch } from '@/components/ui/switch';
import { Textarea } from '@/components/ui/textarea';
import { IProduct } from '@/types/globalTypes';
import { useAppSelector } from '@/redux/hook';

import { useState } from 'react';

@@ -14,9 +14,7 @@ export default function Checkout() {

//! Dummy Data

const products: IProduct[] = [];

//! **
const { products } = useAppSelector((state) => state.Cart);

return (
<div className="flex justify-center items-center h-[calc(100vh-80px)] gap-10 text-primary">
19 changes: 4 additions & 15 deletions src/pages/ProductDetails.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,12 @@
import ProductReview from '@/components/ProductReview';
import { Button } from '@/components/ui/button';
import { IProduct } from '@/types/globalTypes';
import { useEffect, useState } from 'react';
import { useSingleProductQuery } from '@/redux/feature/product/productApi/productApi';
import { useParams } from 'react-router-dom';

export default function ProductDetails() {
const { id } = useParams();

//! Temporary code, should be replaced with redux
const [data, setData] = useState<IProduct[]>([]);
useEffect(() => {
fetch('../../public/data.json')
.then((res) => res.json())
.then((data) => setData(data));
}, []);

const product = data?.find((item) => item._id === Number(id));

//! Temporary code ends here
const { data: product } = useSingleProductQuery(id);

return (
<>
@@ -29,14 +18,14 @@ export default function ProductDetails() {
<h1 className="text-3xl font-semibold">{product?.name}</h1>
<p className="text-xl">Rating: {product?.rating}</p>
<ul className="space-y-1 text-lg">
{product?.features?.map((feature) => (
{product?.features?.map((feature: string) => (
<li key={feature}>{feature}</li>
))}
</ul>
<Button>Add to cart</Button>
</div>
</div>
<ProductReview />
<ProductReview id={id!} />
</>
);
}
43 changes: 23 additions & 20 deletions src/pages/Products.tsx
Original file line number Diff line number Diff line change
@@ -3,48 +3,51 @@ import { Label } from '@/components/ui/label';
import { Slider } from '@/components/ui/slider';
import { Switch } from '@/components/ui/switch';
import { useToast } from '@/components/ui/use-toast';
import { useGetProductsQuery } from '@/redux/feature/product/productApi/productApi';
import {
setPriceRange,
toggleStatus,
} from '@/redux/feature/product/productSlice/productSlice';
import { useAppDispatch, useAppSelector } from '@/redux/hook';
import { IProduct } from '@/types/globalTypes';
import { useEffect, useState } from 'react';

export default function Products() {
const [data, setData] = useState<IProduct[]>([]);
useEffect(() => {
fetch('./data.json')
.then((res) => res.json())
.then((data) => setData(data));
}, []);
const { data, isLoading, error } = useGetProductsQuery(undefined);
console.log(isLoading, error);

const { toast } = useToast();

//! Dummy Data

const status = true;
const priceRange = 100;

//! **
const { priceRange, status } = useAppSelector((state) => state.Product);
const dispatch = useAppDispatch();

const handleSlider = (value: number[]) => {
console.log(value);
dispatch(setPriceRange(value[0]));
};

let productsData;

if (status) {
productsData = data.filter(
(item) => item.status === true && item.price < priceRange
productsData = data?.data?.filter(
(item: { status: boolean; price: number }) =>
item.status === true && item.price < priceRange
);
} else if (priceRange > 0) {
productsData = data.filter((item) => item.price < priceRange);
productsData = data?.data?.filter(
(item: { price: number }) => item.price < priceRange
);
} else {
productsData = data;
productsData = data?.data;
}

return (
<div className="grid grid-cols-12 max-w-7xl mx-auto relative ">
<div className="col-span-3 z mr-10 space-y-5 border rounded-2xl border-gray-200/80 p-5 self-start sticky top-16 h-[calc(100vh-80px)]">
<div>
<h1 className="text-2xl uppercase">Availability</h1>
<div className="flex items-center space-x-2 mt-3">
<div
onClick={() => dispatch(toggleStatus())}
className="flex items-center space-x-2 mt-3"
>
<Switch id="in-stock" />
<Label htmlFor="in-stock">In stock</Label>
</div>
@@ -64,7 +67,7 @@ export default function Products() {
</div>
</div>
<div className="col-span-9 grid grid-cols-3 gap-10 pb-20">
{productsData?.map((product) => (
{productsData?.map((product: IProduct) => (
<ProductCard product={product} />
))}
</div>
10 changes: 10 additions & 0 deletions src/redux/api/apiSlice.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';

export const api = createApi({
reducerPath: 'api',
baseQuery: fetchBaseQuery({ baseUrl: 'http://localhost:5000' }),
tagTypes: ['comments'],
endpoints: () => ({}),
});

export default api;
48 changes: 48 additions & 0 deletions src/redux/feature/cart/CartSlice/cartSlice.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { IProduct } from '@/types/globalTypes';
import { PayloadAction, createSlice } from '@reduxjs/toolkit';

interface ICartProduct {
products: IProduct[];
total: number;
}
const initialState: ICartProduct = {
products: [],
total: 0,
};

export const cartSlice = createSlice({
name: 'add to cart',
initialState,
reducers: {
addtoCart: (state, action: PayloadAction<IProduct>) => {
const existing = state.products.find(
(product) => product._id === action.payload._id
);
if (existing) {
existing.quantity = existing.quantity! + 1;
} else {
state.products.push({ ...action.payload, quantity: 1 });
}
state.total += action.payload.price;
},
removeOne: (state, action: PayloadAction<IProduct>) => {
const existing = state.products.find(
(product) => product._id === action.payload._id
);
if (existing && existing.quantity! > 1) {
existing.quantity = existing.quantity! - 1;

state.total -= action.payload.price;
}
},
removeFromCart: (state, action: PayloadAction<IProduct>) => {
state.products = state.products.filter(
(product) => product._id !== action.payload._id
);
state.total -= action.payload.price * action.payload.quantity!;
},
},
});

export const { addtoCart, removeOne, removeFromCart } = cartSlice.actions;
export default cartSlice.reducer;
33 changes: 33 additions & 0 deletions src/redux/feature/product/productApi/productApi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { api } from '@/redux/api/apiSlice';

const productApi = api.injectEndpoints({
endpoints: (builder) => ({
getProducts: builder.query({
query: () => '/products',
}),
singleProduct: builder.query({
query: (id) => `/product/${id}`,
}),
postComment: builder.mutation({
query: ({ id, data }) => ({
url: `/comment/${id}`,
method: 'POST',
body: data,
}),
invalidatesTags: ['comments'],
}),

getComment: builder.query({
query: (id) => `/product/${id}`,

providesTags: ['comments'],
}),
}),
});

export const {
useGetProductsQuery,
useSingleProductQuery,
usePostCommentMutation,
useGetCommentQuery,
} = productApi;
27 changes: 27 additions & 0 deletions src/redux/feature/product/productSlice/productSlice.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { PayloadAction, createSlice } from '@reduxjs/toolkit';

interface IProductCart {
status: boolean;
priceRange: number;
}

const initialState: IProductCart = {
status: false,
priceRange: 150,
};

export const productSlice = createSlice({
name: 'product',
initialState,
reducers: {
toggleStatus: (state) => {
state.status = !state.status;
},
setPriceRange: (state, action: PayloadAction<number>) => {
state.priceRange = action.payload;
},
},
});

export const { toggleStatus, setPriceRange } = productSlice.actions;
export default productSlice.reducer;
99 changes: 99 additions & 0 deletions src/redux/feature/user/userSlice.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { auth } from '@/lib/firebase';
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
import type { PayloadAction } from '@reduxjs/toolkit';
import {
createUserWithEmailAndPassword,
signInWithEmailAndPassword,
} from 'firebase/auth';

// firebase authetication
interface IUserState {
user: {
email: string | null;
};
isLoading: boolean;
isError: boolean;
error: string | null;
}

interface ICredential {
email: string;
password: string;
}

const initialState: IUserState = {
user: {
email: null,
},
isLoading: false,
isError: false,
error: null,
};

export const createUser = createAsyncThunk(
'user/createUser',
async ({ email, password }: ICredential) => {
const data = await createUserWithEmailAndPassword(auth, email, password);

return data.user.email;
}
);

export const loginUser = createAsyncThunk(
'user/loginUser',
async ({ email, password }: ICredential) => {
const data = await signInWithEmailAndPassword(auth, email, password);

return data.user.email;
}
);

const userSlice = createSlice({
name: 'user ',
initialState,
reducers: {
setUser: (state, action: PayloadAction<string | null>) => {
state.user.email = action.payload;
},
setLoading: (state, action: PayloadAction<boolean>) => {
state.isLoading = action.payload;
},
},
extraReducers: (builder) => {
builder
.addCase(createUser.pending, (state) => {
state.isLoading = true;
state.isError = false;
state.error = null;
})
.addCase(createUser.fulfilled, (state, action) => {
state.user.email = action.payload;
state.isLoading = false;
})
.addCase(createUser.rejected, (state, action) => {
state.user.email = null;
state.isLoading = false;
state.isError = true;
state.error = action.error.message!;
})
.addCase(loginUser.pending, (state) => {
state.isLoading = true;
state.isError = false;
state.error = null;
})
.addCase(loginUser.fulfilled, (state, action) => {
state.user.email = action.payload;
state.isLoading = false;
})
.addCase(loginUser.rejected, (state, action) => {
state.user.email = null;
state.isLoading = false;
state.isError = true;
state.error = action.error.message!;
});
},
});

export const { setUser, setLoading } = userSlice.actions;

export default userSlice.reducer;
6 changes: 6 additions & 0 deletions src/redux/hook.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { useDispatch, useSelector } from 'react-redux';
import type { TypedUseSelectorHook } from 'react-redux';
import type { RootState, AppDispatch } from './store';

export const useAppDispatch: () => AppDispatch = useDispatch;
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
20 changes: 20 additions & 0 deletions src/redux/store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { configureStore } from '@reduxjs/toolkit';
import cartReducer from './feature/cart/CartSlice/cartSlice';
import productReducer from './feature/product/productSlice/productSlice';
import { api } from './api/apiSlice';
import userReducer from './feature/user/userSlice';

export const store = configureStore({
reducer: {
Cart: cartReducer,
Product: productReducer,
User: userReducer,

[api.reducerPath]: api.reducer,
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(api.middleware),
});

export type RootState = ReturnType<typeof store.getState>;
export type useAppDispatch = typeof store.dispatch;