Quality.
Code.
Design.
I'm Will Fisher — a multi-disciplined technologist delivering sharp QA, clean code, and thoughtful design for products that need to work and look great.
// about me
Built to ship great products
From test suites to pixel-perfect UIs — I cover the full stack of delivery.
I'm a versatile professional with deep roots in Quality Assurance, a passion for clean, purposeful code, and an eye for design that actually converts. I work across the entire product lifecycle.
Whether you need bulletproof test coverage, a feature built from scratch, or a UI redesign that users will love — I bring precision and intent to every stage of the process.
I believe great products are built at the intersection of reliability, functionality, and beauty. That's what I deliver.
// services
What I bring to your project
Three focused disciplines. End-to-end capability.
Quality Assurance
Systematic test strategies that catch bugs before users do. From manual exploratory testing to fully automated regression suites, I ensure your software ships with confidence.
Development
Clean, maintainable code that solves real problems. I build features, integrations, and full applications with a focus on scalability, readability, and performance.
Design
Interfaces that feel as good as they look. I design user experiences grounded in clarity and purpose — wireframes, prototypes, and polished visual systems ready for handoff.
Technical Consulting
Strategic guidance on tooling, process, and architecture. I help teams build the right systems and workflows to move faster without sacrificing quality.
Integrations & Automation
Connect your tools and automate repetitive workflows. I build reliable integrations between platforms, reducing manual effort and keeping your data in sync.
Project Rescue
Struggling with a stalled build, a fragile codebase, or a product falling apart under load? I step in, diagnose the issues, and get things back on track.
// how it works
A process built for clarity
No guesswork. No surprises. Just steady progress toward your goal.
Discovery Call
We talk through your goals, constraints, and what success looks like for your project.
Proposal & Scope
I put together a clear plan with deliverables, timeline, and pricing. No ambiguity.
Execution
Work begins with regular updates through whatever channel works best for you — Slack, email, phone calls. You're always in the loop without being in the weeds.
Review & Deliver
We review together, refine as needed, and I hand over everything clean and documented.
// projects
Things I've built
Personal and independent projects shipped outside of client work.
Longbox
Your comic collection, organised and always with you.
Longbox is an Android app built for comic book collectors. Track your entire physical collection, manage a wishlist, and browse metadata from the Grand Comics Database — all without needing an account or an internet connection. Your library lives on your device, private and offline-first by design.
🧪 Want to help test Longbox before its full release?
Sign up here →
Game Wall
Your game library, organized and always on hand.
Game Wall is a video game collection tracker for Android. Build a personal library and wishlist with cover art and platform details, search IGDB to add titles fast, and browse collection stats at a glance — all stored on your device, no account required.
🧪 Want to help test Game Wall before its full release?
Sign up here →
// under the hood
A look at the code
Real snippets pulled straight from projects I've built — not cherry-picked boilerplate.
The backend handler behind this site's contact form — sanitizes input, verifies reCAPTCHA server-side with Google before touching anything else, then relays to Formspree with a graceful fallback message if delivery fails.
<?php
require_once 'includes/config.php';
header('Content-Type: application/json');
// ── Only accept POST ───────────────────────────────────────────────
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['success' => false, 'message' => 'Method not allowed.']);
exit;
}
// ── Collect & sanitize inputs ──────────────────────────────────────
$name = trim(strip_tags($_POST['name'] ?? ''));
$email = trim(strip_tags($_POST['email'] ?? ''));
$service = trim(strip_tags($_POST['service'] ?? ''));
$message = trim(strip_tags($_POST['message'] ?? ''));
$captcha = $_POST['g-recaptcha-response'] ?? '';
// ── Basic validation ───────────────────────────────────────────────
$errors = [];
if (empty($name)) $errors[] = 'Name is required.';
if (empty($email)) $errors[] = 'Email is required.';
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) $errors[] = 'Please enter a valid email address.';
if (empty($message)) $errors[] = 'Message is required.';
if (empty($captcha)) $errors[] = 'Please complete the CAPTCHA.';
if (!empty($errors)) {
http_response_code(422);
echo json_encode(['success' => false, 'message' => implode(' ', $errors)]);
exit;
}
// ── Verify reCAPTCHA with Google ───────────────────────────────────
$recaptcha_url = 'https://www.google.com/recaptcha/api/siteverify';
$recaptcha_data = [
'secret' => RECAPTCHA_SECRET_KEY,
'response' => $captcha,
'remoteip' => $_SERVER['REMOTE_ADDR'] ?? ''
];
$recaptcha_options = [
'http' => [
'method' => 'POST',
'header' => 'Content-Type: application/x-www-form-urlencoded',
'content' => http_build_query($recaptcha_data)
]
];
$recaptcha_context = stream_context_create($recaptcha_options);
$recaptcha_response = file_get_contents($recaptcha_url, false, $recaptcha_context);
$recaptcha_result = json_decode($recaptcha_response, true);
if (empty($recaptcha_result['success'])) {
http_response_code(422);
echo json_encode(['success' => false, 'message' => 'CAPTCHA verification failed. Please try again.']);
exit;
}
// ── Forward to Formspree ───────────────────────────────────────────
$form_data = http_build_query([
'name' => $name,
'email' => $email,
'service' => $service ?: 'Not specified',
'message' => $message,
'_replyto' => $email,
'_subject' => 'New enquiry via WFCodes.com — ' . $name,
]);
$formspree_options = [
'http' => [
'method' => 'POST',
'header' => implode("\r\n", [
'Content-Type: application/x-www-form-urlencoded',
'Accept: application/json',
'Referer: ' . SITE_URL
]),
'content' => $form_data
]
];
$formspree_context = stream_context_create($formspree_options);
$formspree_response = @file_get_contents(FORMSPREE_ENDPOINT, false, $formspree_context);
$formspree_result = json_decode($formspree_response, true);
// ── Respond to client ──────────────────────────────────────────────
if ($formspree_response !== false && isset($formspree_result['ok'])) {
http_response_code(200);
echo json_encode(['success' => true, 'message' => 'Message sent! I\'ll be in touch within 24 hours.']);
} else {
// Formspree failed — log it server-side for visibility
error_log('[WFCodes contact] Formspree submission failed for ' . $email . ' at ' . date('Y-m-d H:i:s'));
http_response_code(500);
echo json_encode([
'success' => false,
'message' => 'Something went wrong on our end. Please email ' . CONTACT_EMAIL . ' directly.'
]);
}
exit;
Two reusable Jetpack Compose components from Game Wall — a library card that adapts to missing cover art with a letter-tile fallback, and a shared screen header used across Library, Wishlist, and Stats.
@Composable
fun GameCard(
game: Game,
modifier: Modifier = Modifier,
showWishlistPriority: Boolean = false
) {
Card(
modifier = modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(containerColor = Surface2Dark),
shape = RoundedCornerShape(12.dp)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(12.dp),
horizontalArrangement = Arrangement.spacedBy(12.dp),
verticalAlignment = Alignment.CenterVertically
) {
if (game.coverImageUrl.isNotBlank()) {
AsyncImage(
model = IgdbMapper.upgradeCoverUrl(game.coverImageUrl),
contentDescription = game.title,
modifier = Modifier
.size(56.dp, 72.dp)
.clip(RoundedCornerShape(8.dp))
.background(MaterialTheme.colorScheme.surface),
contentScale = ContentScale.Crop
)
} else {
androidx.compose.foundation.layout.Box(
modifier = Modifier
.size(56.dp, 72.dp)
.clip(RoundedCornerShape(8.dp))
.background(MaterialTheme.colorScheme.primary.copy(alpha = 0.2f)),
contentAlignment = Alignment.Center
) {
Text(
text = game.title.take(1).uppercase(),
style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.primary
)
}
}
Column(modifier = Modifier.weight(1f)) {
Row(verticalAlignment = Alignment.CenterVertically) {
Text(
text = game.title,
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.SemiBold,
color = TextPrimary,
maxLines = 2,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.weight(1f, fill = false)
)
if (showWishlistPriority && game.wishlistPriority) {
Icon(
imageVector = Icons.Filled.Star,
contentDescription = "Priority",
tint = AccentGold,
modifier = Modifier.padding(start = 4.dp)
)
}
if (game.isHighlight) {
Icon(
imageVector = Icons.Filled.Star,
contentDescription = "Highlight",
tint = AccentGold,
modifier = Modifier.padding(start = 4.dp)
)
}
}
if (game.subtitleLabel().isNotBlank()) {
Text(
text = game.subtitleLabel(),
style = MaterialTheme.typography.bodySmall,
color = TextMuted,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
if (game.playStatus != com.gamewall.app.domain.model.PlayStatus.UNPLAYED) {
Text(
text = game.playStatus.label,
style = MaterialTheme.typography.labelSmall,
color = TextMuted
)
}
}
}
}
}
@Composable
fun MainScreenHeader(
title: String,
subtitle: String,
onSettingsClick: () -> Unit,
modifier: Modifier = Modifier,
showSettings: Boolean = true
) {
Row(
modifier = modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.Top
) {
Column(modifier = Modifier.weight(1f)) {
Text(
text = title,
style = MaterialTheme.typography.headlineMedium,
fontWeight = FontWeight.Bold,
color = TextPrimary
)
Text(
text = subtitle,
style = MaterialTheme.typography.bodyMedium,
color = TextMuted
)
}
if (showSettings) {
IconButton(onClick = onSettingsClick) {
Icon(
imageVector = Icons.Filled.Settings,
contentDescription = "Settings",
tint = TextPrimary
)
}
}
}
}
Core rendering setup from Fimbulwinter, a custom 2D game engine built on LWJGL/OpenGL — hand-written GLSL shaders, and raw VAO/VBO/EBO buffer management to get a colored quad on screen without a higher-level framework in the way.
public class LevelEditorScene extends Scene {
private String vertexShaderSrc = "#version 330 core\n" +
"layout (location=0) in vec3 aPos;\n" +
"layout (location=1) in vec4 aColor;\n" +
"\n" +
"out vec4 fColor;\n" +
"\n" +
"void main(){\n" +
" fColor = aColor;\n" +
" gl_Position = vec4(aPos, 1.0);\n" +
"}";
private String fragmentShaderSrc = "#version 330 core\n" +
"\n" +
"in vec4 fColor;\n" +
"\n" +
"out vec4 color;\n" +
"\n" +
"void main (){\n" +
" color = fColor;\n" +
"}";
//private int vertexID, fragmentID, shaderProgram;
//sets the color for each vertex
private float[] vertexArray = {
//position //color
0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, //bottom right 0
-0.5f, 0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, //top left 1
0.5f, 0.5f, 0.0f, 0.0f,0.0f, 1.0f, 1.0f, //top right 2
-0.5f, -0.5f, 0.0f, 1.0f, 1.0f, 0.0f, 1.0f, //bottom left 3
};
//IMPORTANT: Must be in counter-clockwise order
//Sets the location for each triangle
private int[] elementArray = {
2,1,0, //top right triangle
0,1,3, //bottom left triangle
};
private int vaoID, vboID, eboID;
private Shader defaultShader;
public LevelEditorScene(){
}
@Override
public void init() {
defaultShader = new Shader("assets/shaders/default.glsl");
defaultShader.compile();
//Generating the VAO, VBO, and EBO buffer objects, and send them to the GPU
vaoID = glGenVertexArrays();
glBindVertexArray(vaoID);
//create a float buffer of vertices
FloatBuffer vertexBuffer = BufferUtils.createFloatBuffer(vertexArray.length);
vertexBuffer.put(vertexArray).flip();
//create VBO upload the vertex buffer
vboID = glGenBuffers();
glBindBuffer(GL_ARRAY_BUFFER, vboID);
glBufferData(GL_ARRAY_BUFFER, vertexBuffer, GL_STATIC_DRAW);
//create the indices and upload
IntBuffer elementBuffer = BufferUtils.createIntBuffer(elementArray.length);
elementBuffer.put(elementArray).flip();
eboID = glGenBuffers();
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, eboID);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, elementBuffer, GL_STATIC_DRAW);
//add the vertex attribute pointers
int positionsSize = 3;
int colorSize = 4;
int floatSizeBytes = 4;
int vertexSizeBytes = (positionsSize + colorSize) * floatSizeBytes;
glVertexAttribPointer(0, positionsSize, GL_FLOAT, false, vertexSizeBytes, 0);
glEnableVertexAttribArray(0);
glVertexAttribPointer(1, colorSize, GL_FLOAT, false, vertexSizeBytes, positionsSize * floatSizeBytes);
glEnableVertexAttribArray(1);
}
@Override
public void update(float dt) {
defaultShader.use();
//bind the VAO
glBindVertexArray(vaoID);
//enable the vertex attribute pointers
glEnableVertexAttribArray(0);
glEnableVertexAttribArray(1);
glDrawElements(GL_TRIANGLES, elementArray.length, GL_UNSIGNED_INT, 0);
//unbind everything
glDisableVertexAttribArray(0);
glEnableVertexAttribArray(1);
glBindVertexArray(0);
defaultShader.detach();
}
}
// contact
Ready to build something?
Drop me a message and I'll get back to you within 24 hours.