Loading repository data…
Loading repository data…
DanielT-Dev / repository
A full-stack digital paintings gallery web application with a modern React + TypeScript frontend, ChakraUI and Framer Motion responsive design, Node.js + Express backend REST API and MongoDB Atlas storage
This app is a digital gallery for browsing a collection of paintings online. It works on phones, tablets, and computers, with a clean layout that makes viewing artwork simple and pleasant.
The collection is stored securely in the cloud, so you can access it anytime. The app is built with modern web technologies to ensure a fast, reliable, and smooth experience.
| Category | Technologies |
|---|---|
| Languages | |
| Frontend | |
| Styling | |
| Build Tools | |
| Backend | |
| Database | |
| Security | |
| Logging |
We connect to MongoDB using Mongoose and environment variables for configuration:
const mongoose = require("mongoose");
require("dotenv").config();
const connectDB = async () => {
try {
const conn = await mongoose.connect(process.env.MONGO_URI);
console.log(`MongoDB connected: ${conn.connection.host}`);
} catch (error) {
console.error("MongoDB connection error:", error.message);
process.exit(1);
}
};
module.exports = connectDB;
This ensures:
Paintings are stored using a structured schema that includes metadata and relationships to other paintings.
const mongoose = require("mongoose");
const relatedPaintingSchema = new mongoose.Schema(
{
id: {
type: mongoose.Schema.Types.ObjectId,
ref: "Painting",
required: true,
},
score: {
type: Number,
default: 0,
},
},
{ _id: false }
);
const paintingSchema = new mongoose.Schema(
{
title: String,
artist: String,
year: Number,
medium: String,
description: String,
imageUrls: [String],
tags: [String],
relatedPaintings: [relatedPaintingSchema],
},
{ timestamps: true }
);
module.exports = mongoose.model("Painting", paintingSchema);
This structure allows:

The API follows a controller-based architecture to separate logic from routes.
const Painting = require("../models/Painting");
const logger = require("../utils/logger");
const getPaintings = async (req, res) => {
try {
logger.info("Fetching all paintings");
const paintings = await Painting.find();
logger.info("Paintings fetched successfully", {
count: paintings.length,
});
res.json(paintings);
} catch (err) {
logger.error("Failed to fetch paintings", {
error: err,
});
res.status(500).json({
message: "Server error",
});
}
};
This endpoint fetches a painting and expands its related artworks using populate(), then flattens the result for easier frontend usage.
const getPaintingById = async (req, res) => {
try {
logger.info("Fetching painting", {
paintingId: req.params.id,
});
const painting = await Painting.findById(req.params.id)
.populate("relatedPaintings.id");
if (!painting) {
logger.warn("Painting not found", {
paintingId: req.params.id,
});
return res.status(404).json({
message: "Painting not found",
});
}
const formattedPainting = {
...painting.toObject(),
relatedPaintings: painting.relatedPaintings
.map((r) => {
if (!r.id) return null;
return {
score: r.score,
...r.id.toObject(),
};
})
.filter(Boolean),
};
logger.info("Painting fetched successfully", {
paintingId: painting._id,
title: painting.title,
relatedCount: formattedPainting.relatedPaintings.length,
});
res.json(formattedPainting);
} catch (err) {
logger.error("Failed to fetch painting", {
paintingId: req.params.id,
error: err,
});
res.status(500).json({
message: "Server error",
});
}
};
Users can create accounts and securely log in. Passwords are never stored directly in the database. Instead, they are encrypted using bcrypt hashing before being saved.
After a successful login, the backend generates a JWT (JSON Web Token) which is stored on the client and used to maintain the user's authenticated session.
The User model stores account information and user preferences:
const userSchema = new mongoose.Schema({
username: {
type: String,
required: true,
unique: true,
},
email: {
type: String,
required: true,
unique: true,
},
password: {
type: String,
required: true,
},
favoritePaintings: [
{
type: mongoose.Schema.Types.ObjectId,
ref: "Painting",
},
],
});
During registration, passwords are hashed using bcrypt before being stored in MongoDB:
const hashedPassword = await bcrypt.hash(
password,
10
);
const user = await User.create({
username,
email,
password: hashedPassword,
});
During login, bcrypt compares the entered password with the stored encrypted password:
const passwordMatch = await bcrypt.compare(
password,
user.password
);
After successful authentication, the backend generates a JWT token:
const token = jwt.sign(
{
id: user._id,
username: user.username,
email: user.email,
},
process.env.JWT_SECRET,
{
expiresIn: "7d",
}
);
The token is returned to the frontend and stored locally. It is later used to identify the authenticated user.
The authentication pages provide a modern user experience with:
Password validation checks include:
The signup form prevents weak passwords from being submitted and gives immediate feedback while the user is typing.