Easy Sell
Easy Sell
I want to create simple onlineshopping web application using JHipster with Spring Boot and
React.
• Functions/Roles:
• createAccount()
• createProduct() / registerProduct()
2. Products
• Fields:
• pName
• pDescription
• pPrice
• pImage
• sLocation
• sPhonenumber
• sName
• sId (added: foreign key to relate product to the seller)
Entity Relationship:
• A Customer/Seller can have many Products
• A Product belongs to one Customer/Seller
Relationship type:
• OneToMany from Customer/Seller to Products
• On the header:
• A search panel for buyers to search any products they want
• Below the header:
• An animation toggle for new products like in Amazon
• Body section:
• This component displays all products so that buyers can see and buy
2. <SELLERREGISTER/>
• This component should display a form with all required fields for customer/seller
registration
• After seller registration, it should navigate to the login component
• Validate phone, email, password strength
• Check if username/email already exists
3. <SELLERLOGIN/>
• This component should have a form containing seller username and password
• After seller login is successful, it should navigate the seller to the product creation
component
4. <SELLERPRODUCTCREATION/>
• This component should contain a form with all product fields for the logged-in seller to
create/register a product
• After the product is created, it should navigate to the home component which will display
the ad and all products
• Image upload preview
• Limit product name length
• Save seller reference (seller ID) to each product
application {
config {
baseName onlineshopping
applicationType monolith
packageName com.onlineshopping
authenticationType jwt
prodDatabaseType postgresql
buildTool maven
clientFramework react
reactive false
}
entity Customer {
sName String required
sUsername String required
sPassword String required minlength(6)
sLocation String
sPhonenumber String pattern(/^\+255[0-9]{8}$/)
sEmail String required pattern(/^[^@\s]+@[^@\s]+\.[^@\s]+$/)
sRole Role required
sProfileImage byte[] contentType(image/*)
}
enum Role {
BUYER, SELLER
}
entity Product {
pName String required maxlength(100)
pDescription String required
pPrice BigDecimal required
pImage byte[] required contentType(image/*)
sLocation String
sPhonenumber String
sName String
}
relationship OneToMany {
Customer{products} to Product{owner(name)}
}
dto Customer, Product with mapstruct
service Customer, Product with serviceClass
paginate Customer, Product with pagination
useEffect(() => {
fetchProducts();
}, []);
<Container className="mt-4">
<Carousel>
{products.slice(0, 5).map((product) => (
<Carousel.Item key={product.id}>
<img
className="d-block w-100"
src={product.pImage || 'https://via.placeholder.com/800x300'}
alt={product.pName}
style={{ height: '300px', objectFit: 'cover' }}
/>
<Carousel.Caption>
<h3>{product.pName}</h3>
<p>${product.pPrice}</p>
</Carousel.Caption>
</Carousel.Item>
))}
</Carousel>
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.message || 'Registration failed');
}
navigate('/login');
} catch (error) {
setApiError(error.message);
} finally {
setSubmitting(false);
}
};
return (
<Container className="mt-5">
<Card className="p-4" style={{ maxWidth: '600px', margin: '0 auto' }}>
<h2 className="text-center mb-4">Seller Registration</h2>
{apiError && <Alert variant="danger">{apiError}</Alert>}
<Formik
initialValues={{
sName: '',
sUsername: '',
sEmail: '',
sPassword: '',
sConfirmPassword: '',
sLocation: '',
sPhonenumber: '',
sRole: 'SELLER',
}}
validationSchema={validationSchema}
onSubmit={handleSubmit}
>
{({ handleSubmit, handleChange, values, touched, errors,
isSubmitting }) => (
<Form noValidate onSubmit={handleSubmit}>
<Form.Group className="mb-3">
<Form.Label>Full Name</Form.Label>
<Form.Control
type="text"
name="sName"
value={values.sName}
onChange={handleChange}
isInvalid={touched.sName && !!errors.sName}
/>
<Form.Control.Feedback
type="invalid">{errors.sName}</Form.Control.Feedback>
</Form.Group>
<Form.Group className="mb-3">
<Form.Label>Username</Form.Label>
<Form.Control
type="text"
name="sUsername"
value={values.sUsername}
onChange={handleChange}
isInvalid={touched.sUsername && !!errors.sUsername}
/>
<Form.Control.Feedback
type="invalid">{errors.sUsername}</Form.Control.Feedback>
</Form.Group>
<Form.Group className="mb-3">
<Form.Label>Email</Form.Label>
<Form.Control
type="email"
name="sEmail"
value={values.sEmail}
onChange={handleChange}
isInvalid={touched.sEmail && !!errors.sEmail}
/>
<Form.Control.Feedback
type="invalid">{errors.sEmail}</Form.Control.Feedback>
</Form.Group>
<Form.Group className="mb-3">
<Form.Label>Password</Form.Label>
<Form.Control
type="password"
name="sPassword"
value={values.sPassword}
onChange={handleChange}
isInvalid={touched.sPassword && !!errors.sPassword}
/>
<Form.Control.Feedback
type="invalid">{errors.sPassword}</Form.Control.Feedback>
</Form.Group>
<Form.Group className="mb-3">
<Form.Label>Confirm Password</Form.Label>
<Form.Control
type="password"
name="sConfirmPassword"
value={values.sConfirmPassword}
onChange={handleChange}
isInvalid={touched.sConfirmPassword && !!
errors.sConfirmPassword}
/>
<Form.Control.Feedback
type="invalid">{errors.sConfirmPassword}</Form.Control.Feedback>
</Form.Group>
<Form.Group className="mb-3">
<Form.Label>Location</Form.Label>
<Form.Control
type="text"
name="sLocation"
value={values.sLocation}
onChange={handleChange}
isInvalid={touched.sLocation && !!errors.sLocation}
/>
<Form.Control.Feedback
type="invalid">{errors.sLocation}</Form.Control.Feedback>
</Form.Group>
<Form.Group className="mb-3">
<Form.Label>Phone Number</Form.Label>
<Form.Control
type="text"
name="sPhonenumber"
value={values.sPhonenumber}
onChange={handleChange}
isInvalid={touched.sPhonenumber && !!errors.sPhonenumber}
/>
<Form.Control.Feedback
type="invalid">{errors.sPhonenumber}</Form.Control.Feedback>
</Form.Group>
<Form.Group className="mb-3">
<Form.Label>Register as</Form.Label>
<Form.Select
name="sRole"
value={values.sRole}
onChange={handleChange}
isInvalid={touched.sRole && !!errors.sRole}
>
<option value="SELLER">Seller</option>
<option value="BUYER">Buyer</option>
</Form.Select>
<Form.Control.Feedback
type="invalid">{errors.sRole}</Form.Control.Feedback>
</Form.Group>
<Button
variant="primary"
type="submit"
className="w-100"
disabled={isSubmitting}
>
{isSubmitting ? 'Registering...' : 'Register'}
</Button>
</Form>
)}
</Formik>
</Card>
</Container>
);
};
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.message || 'Login failed');
}
return (
<Container className="mt-5">
<Card className="p-4" style={{ maxWidth: '500px', margin: '0 auto' }}>
<h2 className="text-center mb-4">Seller Login</h2>
{apiError && <Alert variant="danger">{apiError}</Alert>}
<Formik
initialValues={{
sUsername: '',
sPassword: '',
}}
validationSchema={validationSchema}
onSubmit={handleSubmit}
>
{({ handleSubmit, handleChange, values, touched, errors,
isSubmitting }) => (
<Form noValidate onSubmit={handleSubmit}>
<Form.Group className="mb-3">
<Form.Label>Username</Form.Label>
<Form.Control
type="text"
name="sUsername"
value={values.sUsername}
onChange={handleChange}
isInvalid={touched.sUsername && !!errors.sUsername}
/>
<Form.Control.Feedback
type="invalid">{errors.sUsername}</Form.Control.Feedback>
</Form.Group>
<Form.Group className="mb-3">
<Form.Label>Password</Form.Label>
<Form.Control
type="password"
name="sPassword"
value={values.sPassword}
onChange={handleChange}
isInvalid={touched.sPassword && !!errors.sPassword}
/>
<Form.Control.Feedback
type="invalid">{errors.sPassword}</Form.Control.Feedback>
</Form.Group>
<Button
variant="primary"
type="submit"
className="w-100"
disabled={isSubmitting}
>
{isSubmitting ? 'Logging in...' : 'Login'}
</Button>
</Form>
)}
</Formik>
</Card>
</Container>
);
};
useEffect(() => {
const fetchSellerInfo = async () => {
const token = localStorage.getItem('authToken');
if (!token) {
navigate('/login');
return;
}
try {
const response = await fetch('/api/seller-info', {
headers: {
'Authorization': `Bearer ${token}`,
},
});
fetchSellerInfo();
}, [navigate]);
try {
const token = localStorage.getItem('authToken');
const response = await fetch('/api/products', {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
},
body: formDataToSend,
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.message || 'Product creation failed');
}
navigate('/');
} catch (error) {
setApiError(error.message);
} finally {
setSubmitting(false);
}
};
if (!sellerInfo) {
return (
<Container className="d-flex justify-content-center align-items-center"
style={{ height: '100vh' }}>
<Spinner animation="border" role="status">
<span className="visually-hidden">Loading...</span>
</Spinner>
</Container>
);
}
return (
<Container className="mt-5">
<Card className="p-4" style={{ maxWidth: '800px', margin: '0 auto' }}>
<h2 className="text-center mb-4">Create New Product</h2>
{apiError && <Alert variant="danger">{apiError}</Alert>}
<Formik
initialValues={{
pName: '',
pDescription: '',
pPrice: '',
pImage: null,
pImagePreview: '',
}}
validationSchema={validationSchema}
onSubmit={handleSubmit}
>
{({
handleSubmit,
setFieldValue,
values,
touched,
errors,
isSubmitting
}) => (
<Form noValidate onSubmit={handleSubmit}>
<Form.Group className="mb-3">
<Form.Label>Product Name</Form.Label>
<Form.Control
type="text"
name="pName"
value={values.pName}
onChange={(e) => setFieldValue('pName', e.target.value)}
isInvalid={touched.pName && !!errors.pName}
/>
<Form.Control.Feedback
type="invalid">{errors.pName}</Form.Control.Feedback>
</Form.Group>
<Form.Group className="mb-3">
<Form.Label>Description</Form.Label>
<Form.Control
as="textarea"
rows={3}
name="pDescription"
value={values.pDescription}
onChange={(e) => setFieldValue('pDescription',
e.target.value)}
isInvalid={touched.pDescription && !!errors.pDescription}
/>
<Form.Control.Feedback
type="invalid">{errors.pDescription}</Form.Control.Feedback>
</Form.Group>
<Form.Group className="mb-3">
<Form.Label>Price ($)</Form.Label>
<Form.Control
type="number"
step="0.01"
name="pPrice"
value={values.pPrice}
onChange={(e) => setFieldValue('pPrice', e.target.value)}
isInvalid={touched.pPrice && !!errors.pPrice}
/>
<Form.Control.Feedback
type="invalid">{errors.pPrice}</Form.Control.Feedback>
</Form.Group>
<Form.Group className="mb-3">
<Form.Label>Product Image</Form.Label>
<Form.Control
type="file"
accept="image/*"
onChange={(e) => {
const file = e.target.files[0];
if (file) {
setFieldValue('pImage', file);
const reader = new FileReader();
reader.onloadend = () => {
setFieldValue('pImagePreview', reader.result);
};
reader.readAsDataURL(file);
}
}}
isInvalid={touched.pImage && !!errors.pImage}
/>
<Form.Control.Feedback
type="invalid">{errors.pImage}</Form.Control.Feedback>
{values.pImagePreview && (
<div className="mt-2">
<img
src={values.pImagePreview}
alt="Preview"
style={{ maxWidth: '200px', maxHeight: '200px' }}
/>
</div>
)}
</Form.Group>
<Form.Group className="mb-3">
<Form.Label>Seller Information</Form.Label>
<div className="p-3 bg-light rounded">
<p><strong>Name:</strong> {sellerInfo.sName}</p>
<p><strong>Location:</strong> {sellerInfo.sLocation}</p>
<p><strong>Phone:</strong> {sellerInfo.sPhonenumber}</p>
</div>
</Form.Group>
<Button
variant="primary"
type="submit"
className="w-100"
disabled={isSubmitting}
>
{isSubmitting ? 'Creating Product...' : 'Create Product'}
</Button>
</Form>
)}
</Formik>
</Card>
</Container>
);
};