This was my fourth project during the software engineering bootcamp at General Assembly, London. The assignment was to solo build a full-stack application with a Django backend and React frontend over the course of one week. This project also marked my first exploration into Python, adding an exciting layer of learning to the development process.
OpenStudio is an art marketplace built with a focus on eliminating traditional barriers between artists and collectors. The platform bridges the gap between artists and collectors, removing traditional gallery commission structures to create a more equitable art economy. Built using React and TypeScript, this frontend application provides a platform for artists to showcase and sell their work and for collectors to discover and acquire unique pieces directly from creators.
Developed in parallel with its Django backend counterpart, OpenStudio demonstrates the integration of multiple modern technologies to create a secure, user-centric marketplace experience.
The live application can be found at OpenStudio.
-
Artist Portfolio Management
- Complete artwork management system with multiple image upload capability
- Order processing dashboard for tracking sales and managing inventory
- Real-time order status notifications
-
Collector Experience
- Purchase request system replacing traditional cart functionality
- Order tracking and status updates
- Wishlist functionality for saving favourite pieces in a personal gallery
-
UK-Specific Marketplace
- Built-in shipping cost calculator using UK postal zones
- Custom address verification for UK locations
- Real-time pricing updates including shipping costs
- Advanced artwork search and filtering
- Responsive image carousel display
-
Platform Infrastructure
- Role-based authentication system (Artist/Collector)
- Real-time notification system for order updates
- Comprehensive image management with Cloudinary
- Type-safe form validation throughout
- Advanced order processing system
- React 18.3 with TypeScript
- Vite for build tooling
- React Router for navigation
- Axios for API communication
- React Hooks for local state
- Context API for auth state
- JWT for secure authentication
- Custom-built role-based access control
- Bulma CSS Framework
- Custom CSS Modules
- Responsive design patterns
- ESLint for code quality
- TypeScript for type safety
- Git for version control
- Custom UK shipping calculator
- Real-time notification system
- Multi-step purchase workflow
- Dynamic role-based UI components
- Cloudinary integration for image management
src/
├── components/
│ ├── MemberAccess/ # User access management
│ ├── StudioDisplay/ # Artist features
│ ├── GalleryDisplay/ # Collector features
│ └── Marketplace/ # Shared features
│ └── UtilityComps/ # Reusable form components and utilities
├── interfaces/ # TypeScript definitions
// Example of type-safe foundation
export interface IMember {
id: number;
username: string;
first_name: string;
last_name: string;
email: string;
user_type: "artist" | "collector";
bio?: string;
website?: string;
address: string;
postcode: string;
}
- Node.js (v18 or higher)
- NPM or Yarn
- Git
# Clone the repository
git clone https://github.com/yourusername/OpenStudio-FE.git
cd OpenStudio-FE
# Install dependencies
npm install
# Set up environment variables
# Create a .env file in the root directory with:
VITE_APP_API_URL=your_backend_url
VITE_CLOUDINARY_CLOUD_NAME=your_cloudinary_name
VITE_CLOUDINARY_UPLOAD_PRESET=your_upload_preset
# Run the development server
npm run dev
The application requires the following environment variables:
VITE_APP_API_URL= # Your backend API URL
VITE_CLOUDINARY_CLOUD_NAME= # Cloudinary cloud name for image uploads
VITE_CLOUDINARY_UPLOAD_PRESET= # Cloudinary upload preset
npm run dev # Start development server
npm run build # Build for production
npm run preview # Preview production build
npm run lint # Run ESLint
The authentication system implements JWT-based security with role-based access control, managing user sessions and permissions. The system efficiently handles two distinct user types (artists and collectors) with different access levels and capabilities.
function App() {
const [member, setMember] = useState<IMember | null>(null);
const [isArtist, setIsArtist] = useState(false);
const [isCollector, setIsCollector] = useState(false);
async function fetchMember() {
try {
const token = localStorage.getItem("token");
const response = await axios.get(`${baseUrl}/members/user/`, {
headers: { Authorization: `Bearer ${token}` },
});
setMember(response.data);
} catch (error) {
console.error(error);
}
}
useEffect(() => {
if (member?.user_type === "artist") {
setIsArtist(true);
} else {
setIsArtist(false);
}
}, [member]);
}
The notification system utilises an efficient polling mechanism for order updates with optimised re-render prevention. This provides users with immediate feedback about order status changes and new activities.
function NavbarNotification() {
const [hasUnviewedOrders, setHasUnviewedOrders] = useState(false);
const checkUnviewedOrders = useCallback(async () => {
try {
const token = localStorage.getItem("token");
if (!token) return;
const response = await axios.get(`${baseUrl}/orders/unviewed-orders/`);
setHasUnviewedOrders(response.data.has_unviewed_orders);
} catch (error) {
console.error("Error checking unviewed orders:", error);
}
}, []);
useEffect(() => {
checkUnviewedOrders();
const interval = setInterval(checkUnviewedOrders, 30000);
return () => clearInterval(interval);
}, [checkUnviewedOrders]);
}
The artist studio provides a comprehensive workspace for artwork and sales management. Each artist receives a personalised studio space with their name displayed in the possessive form (e.g., "Sarah's Studio").
Key Features:
- Artwork Portfolio Management
- New Artwork Upload Interface
- Order Management System
function StudioProfile({ member }: { member: IMember | null }) {
const [activeTab, setActiveTab] = useState("artworks");
const studioTitle = member
? `${member.first_name} ${getPossessiveForm(member.last_name)} Studio`
: "Studio";
return (
<section className="section mt-5">
<div className="box mt-5">
<div className="tabs is-centered">
<ul>
<li className={activeTab === "artworks" ? "is-active" : ""}>
<a onClick={() => setActiveTab("artworks")}>Studio Artworks</a>
</li>
<li className={activeTab === "upload" ? "is-active" : ""}>
<a onClick={() => setActiveTab("upload")}>Upload Artwork</a>
</li>
<li className={activeTab === "orders" ? "is-active" : ""}>
<a onClick={() => setActiveTab("orders")}>Order Manager</a>
</li>
</ul>
</div>
</div>
</section>
);
}
![]() |
---|
Example of an artist's studio |
The collector's gallery provides a personalised space for art enthusiasts to curate their collection and manage their purchases. The system maintains a clean separation between saved artworks and active orders.
Key Features:
- Curated artwork collection
- Purchase history tracking
- Order status monitoring
function GalleryArtworks() {
const [galleryArtworks, setGalleryArtworks] = useState<Artworks>(null);
async function getArtworks(page: number) {
try {
const response = await axios.get(`${baseUrl}/galleries/my-gallery/`, {
headers: {
Authorization: `Bearer ${localStorage.getItem("token")}`,
},
params: { page, per_page: itemsPerPage },
});
setGalleryArtworks(response.data.artworks);
setTotalPages(Math.ceil(response.data.total / itemsPerPage));
} catch (error) {
console.error("Error fetching gallery artworks:", error);
}
}
}
![]() |
---|
Example of a collector's gallery |
Custom validation hooks handle UK-specific address formats and business logic.
export const useAddressValidation = (
address: string,
postcode: string
): AddressValidationResult => {
const [validationResult, setValidationResult] = useState({
isValid: false,
errors: {
address: [],
postcode: [],
},
});
useEffect(() => {
const validateAddress = () => {
const addressErrors: string[] = [];
const postcodeErrors: string[] = [];
if (!address.trim()) {
addressErrors.push("Address cannot be empty");
}
if (
!/^[A-Z]{1,2}[0-9][A-Z0-9]? [0-9][ABD-HJLNP-UW-Z]{2}$/.test(
postcode.toUpperCase()
)
) {
postcodeErrors.push("Invalid UK postcode format");
}
setValidationResult({
isValid: addressErrors.length === 0 && postcodeErrors.length === 0,
errors: { address: addressErrors, postcode: postcodeErrors },
});
};
validateAddress();
}, [address, postcode]);
return validationResult;
};
The image carousel component handles multiple image uploads with Cloudinary integration and preview functionality.
function ImageCarousel({ images }: ImageCarouselProps) {
return (
<Carousel useKeyboardArrows={true}>
{images.map((image, index) => (
<div key={image.id || index} className="slide">
<img
src={image.image_url}
alt={`An image of ${image.artwork.title}`}
loading="lazy"
onError={(e) => {
const target = e.target as HTMLImageElement;
target.src = "/placeholder.jpg";
}}
/>
</div>
))}
</Carousel>
);
}
![]() ![]() |
---|
Example of the image carousel, allowing artists to display multiple photos of their work |
The order management system adapts its display and functionality based on user type, providing role-specific features whilst maintaining consistent styling.
Key Features:
- Dynamic role-based display
- Status-based action buttons
- Real-time order updates
function OrdersTable({
orders,
userType,
onAccept,
onShip,
onCancel,
onPay,
}: OrdersTableProps) {
return (
<div className="table-container">
<table className="table is-fullwidth is-striped">
<thead>
<tr>
<th>Order ID</th>
<th>Artwork</th>
<th>{userType === "seller" ? "Buyer" : "Seller"}</th>
<th>Price</th>
<th>Status</th>
<th>Date</th>
<th>Actions</th>
</tr>
</thead>
{/* Table body implementation */}
</table>
</div>
);
}
![]() |
---|
Example of an order table from an artist's perspective |
![]() |
---|
Example of an order table from an collector's perspective |
Implements shipping calculations based on UK postal codes and zones, including insurance and handling costs.
export const calculateShippingCost = ({
weight,
width,
depth,
height,
price,
fromPostcode,
toPostcode,
}: ShippingParams): ShippingCosts => {
const baseRate = 5;
const weightFactor = 2;
const volumeFactor = 0.001;
const insuranceRate = 0.01;
const fromZone = getZone(fromPostcode);
const toZone = getZone(toPostcode);
const zoneDifference = Math.abs(fromZone - toZone);
const zoneMultiplier = 1 + zoneDifference * 0.2;
const volume = width * height * depth;
const volumeCost = volume * volumeFactor;
const weightCost = weight * weightFactor;
const baseShippingCost =
(baseRate + weightCost + volumeCost) * zoneMultiplier;
const insuranceCost = price * insuranceRate;
return {
baseShippingCost: Number(baseShippingCost.toFixed(2)),
insuranceCost: Number(insuranceCost.toFixed(2)),
totalShippingCost: Number((baseShippingCost + insuranceCost).toFixed(2)),
totalPrice: Number((price + baseShippingCost + insuranceCost).toFixed(2)),
};
};
Challenge: Managing complex state across multiple components, especially with the order system's multiple states and user types.
Solution:
- Implemented clear state management within components
- Utilised callbacks and props for state sharing
- Created separate states for artists and collectors
- Built real-time notification system using polling
Challenge: Handling multiple image uploads and managing the relationship with Cloudinary.
Solution:
- Implemented structured image upload process
- Created typed interfaces for image handling
- Managed multiple image selections with preview functionality
Challenge: Creating a complex order system with multiple states and user-specific actions.
Solution:
- Implemented clear status transitions
- Created separate views for buyers and sellers
- Built automated status update system
Challenge: Implementing comprehensive form validation, particularly for UK-specific address formats.
Solution:
- Created custom validation hooks
- Implemented real-time validation feedback
- Built specific UK postcode validation
Challenge: Completing a full-featured marketplace in one week whilst learning new technologies.
Solution:
- Focussed on core functionality first
- Implemented MVP features before enhancements
- Utilised efficient component reuse strategies
Challenge: Ensuring application reliability whilst working under time constraints.
Solution:
- Implemented type checking for early error catching
- Utilised TypeScript for compile-time error prevention
- Created simple error handling systems
- Built full-featured React TypeScript marketplace in one week
- Designed and implemented bespoke UK shipping calculator
- Created custom form validation system for UK addresses
- Developed role-based authentication system
- Built real-time notification system without WebSocket support
- Successfully delivered second TypeScript project, continuing to build experience
- Gained practical experience in full-stack development with Django and React
- Developed understanding of UK-specific business logic implementation
- Enhanced problem-solving skills through complex feature development
- Implement global state management
- Add WebSocket integration for real-time updates
- Enhance error boundary system
- Build comprehensive test suite
- Stripe payment integration
- Chat system between artists and collectors
- Artist collaboration tools for virtual exhibitions
- Mobile-optimised interface
- Advanced analytics for artists
- Value of TypeScript in maintaining code quality
- Importance of planning component architecture
- Benefits of custom hooks for reusable logic
- Impact of proper error handling in user experience
- Importance of MVP approach in time-constrained development
- Value of clear feature prioritisation
- Benefits of modular development approach
- Impact of user-focused decision making
After the initial sprint, I took the time to ensure the application is fully responsive and optimised for mobile devices. By leveraging Bulma's utility classes, I was able to create a consistent and adaptive user experience across different screen sizes.
The key changes made include:
- Applying responsive layout classes (e.g.,
is-desktop
,is-touch
) to ensure proper scaling and element positioning on mobile and desktop. - Adjusting font sizes, padding, and margins to maintain readability and visual balance on smaller screens.
- Optimising the image carousel and other visual components to provide an optimal viewing experience on mobile devices.
Inspired by the Tate Modern's Turbine Hall, I implemented a sophisticated SVG-based concrete texture and color scheme that transforms the application into an authentic gallery space. The design combines an industrial concrete aesthetic with careful attention to how artwork is displayed.
The implementation features:
- A dynamic SVG-based concrete texture using:
- Subtle base concrete pattern using fractal noise
- Realistic studio wear marks and paint splatters
- Paint drips and marks suggesting a working studio space
- A refined color palette:
- Neutral #404040 concrete-inspired base
- Carefully balanced contrast for readability
- Minimalist approach that emphasizes artwork
Technical implementation:
// SVG-based concrete texture with studio effects
const ConcreteBackground = ({ children }: ConcreteBackgroundProps) => {
return (
<div style={{ position: "relative", minHeight: "100vh" }}>
{/* SVG texture layer with concrete base and studio marks */}
{/* Content layer with proper z-index positioning */}
</div>
);
};
This design update replaces the previous peach and blue gradient with an environment that better reflects the platform's artistic purpose (i.e. supporting modern and contemporary artwork), creating a more authentic gallery/studio experience while maintaining optimal functionality and user experience.
The live application can be accessed at OpenStudio.