Formative Review — ShadowMap
Hi. This document is a review of your project designed so you learn, not so you get discouraged. There are things that are really well done, things that are one step away from being great, and some that are worth reviewing before showing the project to an employer.
How to read it:
- Start with what's working well — because you've earned it.
- Continue with what has the most impact if you fix it — the highest effort-to-reward points.
- Then the review by dimensions with examples from your own code.
- At the end you have a prioritised action plan and a reference grade.
If something isn't clear or you disagree, perfect — that means it's worth discussing in class.
1. What's working well
Before any suggestions, there are decisions you made correctly and deserve recognition:
- Password hashing with
werkzeug.security(routes_auth.py:28,routes.py:33). You never stored passwords in plain text — non-negotiable, and you did it right from the start. - Using Blueprints in Flask — you separated
auth_api,routes_api,premium_api, etc. This is the structure of a serious Flask project. - Well-designed relational models — the
Route↔RoutePointrelationship withcascade="all, delete-orphan"(models.py:130-135) shows you understand the ORM and lifecycle management. - Sensitive endpoints protected with
@jwt_required()— the decorator appears where it should. ProtectedRoutefor private routes in React (ProtectedRoute.jsx:7) — correct architectural piece.- Visual identity and narrative — the README, theming, names ("Dark Route", "Active Mission"), avatars with lore… this is what sets a memorable project apart from a forgettable one. This is real talent, not taught.
When you finish reviewing the improvements, come back to this section. It will remind you where you're starting from.
2. What has the most impact if you fix it
These observations have the best effort-to-benefit ratio. Most can be resolved in a few hours and significantly raise the perceived quality of your project.
2.1 Two Flask applications are configured but only one is used
In src/api/__init__.py you define a create_app():
# src/api/__init__.py:18
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///shadowmap.db"
...
from src.api.routes import api
app.register_blueprint(api, url_prefix="/api") And in src/api/app.py you define a different create_app():
# src/api/app.py:26
DB_PATH = os.path.join(BASE_DIR, "instance", "database.db")
...
app.register_blueprint(auth_api, url_prefix="/api")
app.register_blueprint(routes_api, url_prefix="/api")
# ...8 more blueprints They point to different databases and register different blueprints. Probably one was your first attempt and the other your final version. I suggest keeping app.py (the more complete one) and removing __init__.py as a factory, leaving only what's needed for src/api/ to remain a package.
2.2 routes.py defines login endpoints with a test token
# src/api/routes.py:58
return jsonify({
"token": "fake-token",
"user": user.serialize()
}), 200 This file is not registered in app.py, so it never executes. But the problem is: if someone (you, in a week) registers it accidentally, real authentication gets lost. You already have the proper implementation in routes_auth.py, so you can safely delete this one.
2.3 Premium activation fails at runtime
# src/api/routes_premium.py:21
user.is_premium = True The is_premium field is not defined in your User model (models.py:11-33). When a user clicks "Activate Premium", the backend will throw AttributeError. The good news is the fix is straightforward:
- Add the field to the model:
is_premium = db.Column(db.Boolean, default=False, nullable=False) - Create the migration with Flask-Migrate.
- Delete
src/api/premium.py(it's a duplicate with a broken import: usesfrom api.modelsinstead offrom src.api.models).
2.4 Some actions the frontend calls don't exist yet
In these views, actions are invoked that aren't defined:
addPlace.jsx:26→actions.createPlace(...)editPlace.jsx:48→actions.updatePlace(...)placeDetails.jsx:30→actions.deletePlace(...)RouteCreator.jsx:16→actions.publishRoute(...)
When they execute, they fail silently because actions.createPlace is undefined. You have two valid paths:
- Path A (finish what you started): implement these actions in
flux.jsfollowing the same pattern asloginorsaveRouteLocal. - Path B (honest scope): if they don't fit your delivery, temporarily remove the views from the router in
layout.jsx. Better to have fewer working features than more broken ones.
Either path is perfectly legitimate. What I don't recommend is leaving it as-is.
2.5 Two authentication systems coexist
src/context/AuthContext.jsx+src/services/authService.js(Context + custom hooks).src/front/js/store/appContext.jsx+src/front/js/store/flux.js(4Geeks Flux pattern).
Both define token, user, login, logout… but the project only uses the second one. The first is orphaned. When a teammate (or you in six months) tries to understand the authority, it's confusing. My recommendation: pick one and delete the other. The Flux one is already integrated throughout the app, so probably quickest to remove AuthContext and authService.
Note: in the future, on professional projects, the Context + hooks pattern (which you had in
AuthContext.jsx) tends to be more modern and clean than Flux. Your instinct was on the right track.
2.6 JWT_SECRET_KEY is hardcoded
# src/api/app.py:30
app.config["JWT_SECRET_KEY"] = "super-secret-key" Anyone with repo access can forge valid tokens for any user. Change this first:
app.config["JWT_SECRET_KEY"] = os.getenv("JWT_SECRET_KEY") And add the variable to .env.example. A 30-second change that moves the security dimension from level 1 to level 2.
2.7 CORS configuration isn't quite correct
# src/api/app.py:35
CORS(app, resources={r"/*": {"origins": "*"}}, supports_credentials=True, ...) The CORS spec forbids the combination origins="*" + supports_credentials=True. Today it "works" because your frontend doesn't send cookies, but the day you add something with credentials it will break mysteriously. Better be explicit:
CORS(
app,
resources={r"/*": {"origins": [
"http://localhost:3000",
os.getenv("FRONTEND_URL"),
]}},
supports_credentials=True,
) 2.8 Missing ownership verification in sensitive operations
Example in POIs:
# src/api/routes_pois.py:65
@pois_api.route("/pois/<int:poi_id>", methods=["PUT"])
@jwt_required()
def update_poi(poi_id):
poi = POI.query.get(poi_id)
if not poi:
return jsonify({"message": "POI not found"}), 404
# ... The @jwt_required() decorator ensures the user is authenticated, but not that they own the resource. Right now any logged-in user can modify or delete any POI in the system. Same issue in routes_premium.py (UPDATE/DELETE of others' premium routes) and routes_routes.py (sharing others' routes).
To fix it you need:
- Add
user_idto the model (POI, Place, etc.) if there will be owners. - In each PUT/DELETE/SHARE, verify ownership and return 403 otherwise.
This is one of the most typical security critiques in junior projects — fixing it in this project will stick with you forever.
2.9 The password recovery token doesn't expire
# src/api/routes_recover.py:20
token = secrets.token_urlsafe(32)
user.recovery_token = token You generated a cryptographically secure token, which is perfect. But it lacks an expiration date. If someone intercepts that email once, they can use the token weeks later. I suggest adding user.recovery_token_expires_at = datetime.utcnow() + timedelta(minutes=15) and validating in reset_password that the date hasn't passed.
2.10 The email link doesn't match the frontend route
# src/api/routes_recover.py:24
reset_link = f"{os.getenv('FRONTEND_URL')}/reset-password/{token}" But in layout.jsx:58 the router only knows /reset-password. The email takes the user to /reset-password/abc123 and they land on "Not found".
- Option A (recommended): change the router to
/reset-password/:token(you're already reading thetokenwithuseParams()inresetPassword.jsx:9, so this works immediately). - Option B: change the email to use
?token=...and read it withuseSearchParams.
3. Review by dimensions
Dimension 1 — Structure · Level 2
What's working: clear separation between src/api and src/front. Folders views/, component/, store/, styles/.
Small improvements:
- Unify the auth systems (see 2.5).
- Decide on a single
create_app()and remove the other (see 2.1). - In
src/api/routes_places.pythe comment says "the Places module is temporarily disabled (missing Place model)" butPlacedoes exist inmodels.py:40. You're probably carrying an old comment — update it or delete the file. src/api/premium.pylooks like an old version ofroutes_premium.pywith a broken import. Can be eliminated.
To reach level 3: single source per responsibility and zero orphaned files.
Dimension 2 — Frontend · Level 2
What's working: correct use of useState/useEffect/useContext. Separated views. Reasonably small components (with one exception).
Areas to polish:
map.jsxhas gotten large (405 lines). Mixes mission logic, route creation, two modals, and saved routes list. ExtractingMissionPanel,RouteCreatorPanel, andSavedRoutesPanelwill make the parent component much more readable.MapView.jsx:18creates the icon on every render. The icon doesn't depend on props or state, so extract it outside the component or wrap it inuseMemo. A detail, but reflects attention to immutability.- Inline styles and CSS classes coexist in the same file. Either option is valid; what matters is consistency. For reusable pieces, CSS classes scale better.
- Events on
<span>without accessibility — not keyboard-navigable and lacks a semantic role. Use<Link>from React Router or a<button>. alert()andconfirm()for feedback — they block the thread and break the cinematic style you cared for elsewhere. A toast or custom modal fits the ShadowMap aesthetic much better.useEffectwith// eslint-disable-next-line react-hooks/exhaustive-deps: silencing the rule is sometimes necessary, but add a comment explaining why. Otherwise, in six months even you won't know if it was intentional.
Dimension 3 — Backend · Level 2
What's working: separated Blueprints, models with consistent serialize(), @jwt_required() decorators where they belong.
Areas to polish:
extensions.pyandutils.pyimplement the same thing twice. Keep one (theMailerclass integrates better with Flask) and delete the other.print()for logging — convenient in development, lost or messy in production. Theloggingmodule with levels is the standard.- HTML embedded in Python code — if you ever want to redesign the email, you edit Python to touch styles. Better move to a template file (
flask.render_template). - Very basic input validation — checks presence but not email format, password minimum length, or shortname rules. Marshmallow or Pydantic solve this with minimal code and raise perceived quality significantly.
Dimension 4 — State and communication · Level 2
Already covered in 2.5 (two auth systems). Additional details:
- Two keys for localStorage "saved routes" —
flux.js:147uses"savedRoutes_local"andRoutesManager.jsx:4uses"shadowmap_saved_routes". If both run in parallel, each sees a different set and data desynchronises. Unify to a single key. API_URLdefined in two places —authService.js:3usesVITE_API_URLandflux.js:4imports from../../api/config.js. Centralise in one.- Frontend reads
user.is_premium(profile.jsx:74) but backend doesn't send the field. The "Premium Account" badge won't render correctly until you fix 2.3.
Dimension 5 — Security · Level 1
See critiques 2.6 (JWT secret), 2.7 (CORS), 2.8 (ownership), and 2.9 (token expiration). Actionable list:
- Move
JWT_SECRET_KEYto.env(and add to.env.example). - Restrict origins in CORS.
- Verify ownership in PUT/DELETE endpoints.
- Add expiration to
recovery_token. - Validate minimum password length (8 characters is fine).
- Consider rate-limiting with Flask-Limiter on login/register/recover.
On JWT in
localStorage: this is what 4Geeks teaches and is acceptable for bootcamp. To explore further,HttpOnlycookies are the professional way, but outside this project's scope.
Dimension 6 — Functionality end-to-end · Level 2
Honest summary of what works and what doesn't:
- Registration: works.
- Login: works.
- Activate Premium: fails in backend (see 2.3).
- Place management (Add/Edit/Delete): actions don't exist (see 2.4).
- Create and publish paranormal route: "Share" button in
map.jsx:177has noonClick;RouteCreator.jsx:16calls non-existent action. - Password recovery via email: flow breaks at the link (see 2.10).
- Missions (complete locally): works, though persists only in
localStorage.
The good: most are small, localised fixes. You're not far from having a fullstack app that demos well in an interview.
Dimension 7 — Git, docs · Level 2
- Some commits have vague messages (
030526B,daa644a 030526B). Conventional Commits is a clean professional convention:feat:,fix:,docs:,refactor:,chore:. .env.exampledoesn't include variables your code uses:JWT_SECRET_KEY,SENDGRID_API_KEY,SENDGRID_FROM_EMAIL,FRONTEND_URL. Someone cloning your repo won't know what to configure.- The README is visually careful and has narrative — major plus.
- README doesn't explain running the backend beyond
pipenv run start. Missing: where.envgoes, how to run migrations, how to create the first user, which Python version.
Dimension 8 — UX, accessibility · Level 2
- Replacing
alert()/confirm()with custom toasts/modals would fit beautifully with your aesthetic. - Generic error messages like
"Error creating place"can be more specific ("Latitude and longitude required"). - Missing loading states in most forms — even a simple spinner improves perception.
- Inputs without
htmlFor/idlinked to labels — affects accessibility. - Click handlers on
<span>for navigation — use<Link>or<button>.
Your palette, typography, and animations are very good. It's a shame user feedback uses standard browser components.
Dimension 9 — Tests · Level 1
No tests yet. ESLint is configured, which is a good start.
What would earn you major points with minimal effort:
- 5–10 tests with
pytestcovering: valid registration, duplicate email, valid login, wrong credentials, accessing a protected endpoint without a token, POI creation. - Remove
// eslint-disable-next-linecomments without justification.
Dimension 10 — DevOps · Level 2
Procfile,render.yaml,Dockerfile.renderpresent.- Scripts
npm run start,npm run buildcorrect. .env.exampleincomplete (see Dimension 7).- Both
requirements.txtandPipfileexist. Pick one —Pipfilesince you're already using it. routes_health.pyexists but doesn't seem connected to the healthcheck inrender.yaml.
Dimension 11 — Originality and design · Level 3
Here is where you shine most, so I save it for last. The concept "ShadowMap = paranormal registry that watches you" has its own identity. The README copy, avatars with lore, names ("Identity Re-Stabilization", "Dark Route"), the final warning in the README… everything is cared for.
This is what a recruiter remembers. When they receive 30 projects called "TaskApp" or "WeatherForecast", yours is the haunted app. Cherish it and don't underestimate it. If the technical side reaches the design's level, this project is genuinely memorable.
4. Prioritised action plan
🔴 Urgent (without these, parts of the project don't work)
- Add
is_premiumtoUsermodel + migration (see 2.3). - Delete
src/api/routes.py(the one with"fake-token") andsrc/api/premium.py(duplicate). - Decide on a single
create_app()and remove the other (see 2.1). - Implement missing actions in
flux.jsor remove views that call them (see 2.4). - Move
JWT_SECRET_KEYto.env(see 2.6). - Fix email link mismatch vs
/reset-passwordroute (see 2.10). - Pick one auth system and delete the other (see 2.5).
🟠 Important (moves you from "passing" to "good")
- Add
user_idtoPOI/Placeand verify ownership in PUT/DELETE. - Centralise
API_URL. - Unify localStorage key for routes.
- Replace
alert()/confirm()with custom toasts or modals. - Restrict origins in CORS.
- Add expiration to
recovery_token. - Complete
.env.examplewith all variables your code uses. - Update README with environment variables section, migrations, troubleshooting.
🟢 Excellence (takes you to honours)
- Split
map.jsxinto smaller components. - Move email HTML to a template.
- Add tests with
pytest(minimum 5–10 on critical flows). - Configure GitHub Actions with
pytest+eslint. - Input validation with Marshmallow or Pydantic.
- Replace
printwithlogging. - Consider React Query / SWR for fetches.
Practical recommendation: open a branch per block (
fix/critical-issues,feat/ownership-and-security, etc.). You'll end up with a Git history that's great to show in interviews.
5. Reference grade
This grade is a proposal based on the current state of the code. If you self-evaluate and arrive at a similar score, that's an excellent sign: your technical judgement is already calibrated.
| # | Dimension | Weight | Level | Points |
|---|---|---|---|---|
| 1 | Structure | 8% | 2 | 4.00 |
| 2 | Frontend | 15% | 2 | 7.50 |
| 3 | Backend | 15% | 2 | 7.50 |
| 4 | State / API | 10% | 2 | 5.00 |
| 5 | Security | 15% | 1 | 3.75 |
| 6 | Functionality | 10% | 2 | 5.00 |
| 7 | Git / Docs | 5% | 2 | 2.50 |
| 8 | UX / Accessibility | 8% | 2 | 4.00 |
| 9 | Tests | 5% | 1 | 1.25 |
| 10 | DevOps | 5% | 2 | 2.50 |
| 11 | Originality | 4% | 3 | 3.00 |
| Total | 100% | ≈ 46 / 100 |
Honest reading: you're not yet at the passing threshold, but the gap is very small. With the 🔴 block (~1 day of focused work) you probably jump to 55–60. With the 🟠 block (3–4 days) to 70–80. The 🟢 block takes you to honours.
The important thing is you don't need to rewrite the project. You need to finish it.
6. Final message
Your project has something many bootcamp projects don't have: soul. The narrative, the details, the theming, the copy… that matters a lot and isn't taught. When a recruiter opens your repo, they'll remember yours.
What this review is asking isn't "do it all over." It's asking you to finish what you started. 80% of real junior dev work is exactly this: finding the missing is_premium fields, the unimplemented createPlace, the hardcoded JWT_SECRET_KEY, and solving them before a user or teammate finds them.
If you focus on the 🔴 block for a day and then review this document again, you'll see progress very clearly. And when you reach the 🟢 level, your portfolio will have a project worth talking about in interviews.
Trust what you already did well. Finish it. You're closer than you think.