Forum

Code Exchange is a full-stack Q&A forum web application built entirely in TypeScript, inspired by platforms like Stack Overflow. Developed collaboratively with a classmate as part of our HBO-ICT Software Engineering program, the app lets users register accounts, post coding questions with embedded code snippets, answer each other's questions, and rate both questions and answers with an upvote/downvote system. Questions can be tagged with a programming language and filtered by date, expertise level, or whether they have answers. The frontend uses Vite as a build tool with libraries like Marked and EasyMDE for markdown support and Highlight.js for syntax highlighting. All data is persisted through a relational SQL database accessed via the HBO-ICT Cloud API. Building this project gave us hands-on experience with the MVC pattern, session-based authentication, collaborative Git workflows, and iterating on our UI through guerrilla user testing.

technologies

TypeScript
HTML
CSS
SQL
codeSnippet

Creating a question — input validation and database insertion This first snippet shows the createQuestion method, which handles the full flow of submitting a new question. It validates that the user is logged in and that both the title and description fields are filled in, asks for confirmation, then posts the question along with any optional code snippet. After the question is stored, it retrieves the new question's ID and assigns the selected programming language tag to it.

1/**
2     * Checks if question input is valid and creates a question
3     * @author Milan
4     */
5    private async createQuestion(): Promise<void> {
6        try {
7            let idQuestion: number = 0;
8            const loggedIn: LoggedIn = session.get("LoggedIn") as LoggedIn;
9            if (loggedIn.isLoggedIn) {
10                if (!this.inputElement.value) {
11                    alert("Uw antwoord mag niet leeg zijn!");
12                    return; // Exit the function if the description is empty
13                }
14                if (!this.titleElement.value) {
15                    alert("Uw titel mag niet leeg zijn!");
16                    return; // Exit the function if the description is empty
17                }
18
19                const result: boolean = confirm("Weet je zeker of je deze bericht wilt sturen?");
20                if (result) {
21                    if (this.snippetElement.value)
22                        await this.postQuestion(loggedIn, this.titleElement.value,
23                            this.inputElement.value, this.snippetElement.value);
24                    else
25                        await this.postQuestion(loggedIn, this.titleElement.value,
26                            this.inputElement.value, "");
27
28                    // gets id from the answer so it can be used to assign the code tag to the answer
29
30                    const _codeTagTypes: NodeListOf<HTMLInputElement> =
31                    document.querySelectorAll(".tag-container input[type='radio']");
32                    idQuestion = await this.getQuestionId();
33
34                    for (const radio of Array.from(_codeTagTypes)) {
35                        const input: HTMLInputElement = radio;
36                        if (input.checked) {
37                            console.log(input.value);
38                            console.log(idQuestion);
39                            await CodeTag.setCodeTagQuestion(input.value as CODELANGUAGE, idQuestion);
40                        }
41                    }
42
43                    console.log("ANSWER POSTED!");
44                }
45                else
46                    console.log("Post stopped!");
47            }
48            else {
49                alert("Alleen ingelogde gebruikers mogen reageren!");
50                return; // Exit the function for non-logged-in users
51            }
52        }
53        catch (error) {
54            console.error("Error posting answer:", error);
55            alert("Er is een fout opgetreden. Probeer het opnieuw!");
56            return; // Prevent reloading if an error occurred
57        }
58
59        // Reloads the page after login is succesfull
60        location.reload();
61    }

The setQuestion model method executes the actual SQL insert, storing the title, description, user ID, and optional code snippet in the database:

1public static async setQuestion(title: string, description: string, idUser: number, code: string): Promise<void> {
2    try {
3        await api.queryDatabase(`INSERT INTO question (title, description, idUser, code) 
4            VALUES ('${title}', '${description}', '${idUser}', '${code}')`);
5    }
6    catch (reason) {
7        console.error(reason);
8    }
9}

Reusable confirmation dialog for account deletion This snippet demonstrates a reusable createForm method I wrote for the account deletion flow. The same function renders both a confirmation prompt ("Are you sure?") and a success message ("Account deleted!") by toggling its parameters. Depending on the hasOptions and canReturnToHomePage flags, it dynamically builds the appropriate buttons and attaches the right event handlers — keeping the UI logic clean and avoiding duplication.

1private createForm(hasOptions: boolean, canReturnToHomePage: boolean, message: string): void {
2    const formContainer: HTMLDivElement = this.view.parentElement as HTMLDivElement;
3
4    this._formElement = document.createElement("form");
5    this._formElement.innerHTML = message;
6    this._formElement.classList.add("delete-account-container");
7
8    const buttonContainer: HTMLDivElement = document.createElement("div");
9    buttonContainer.className = "delete-options-container";
10    this._formElement.appendChild(buttonContainer);
11
12    if (hasOptions) {
13        const yesButton: HTMLButtonElement = document.createElement("button");
14        if (canReturnToHomePage)
15            yesButton.addEventListener("click", this.onReturnToHome.bind(this));
16        yesButton.addEventListener("click", this.onDeleteUser.bind(this));
17        yesButton.setAttribute("type", "button");
18        yesButton.classList.add("button-create-question");
19        yesButton.innerText = "Ja verwijder!";
20
21        const noButton: HTMLButtonElement = document.createElement("button");
22        noButton.addEventListener("click", this.onCloseForm.bind(this));
23        noButton.setAttribute("type", "button");
24        noButton.classList.add("button-create-question");
25        noButton.innerText = "Nee ga terug!";
26        buttonContainer.appendChild(yesButton);
27        buttonContainer.appendChild(noButton);
28    }
29    else {
30        const yesButton: HTMLButtonElement = document.createElement("button");
31        if (canReturnToHomePage)
32            yesButton.addEventListener("click", this.onReturnToHome.bind(this));
33        else
34            yesButton.addEventListener("click", this.onCloseForm.bind(this));
35        yesButton.setAttribute("type", "button");
36        yesButton.classList.add("button-create-question");
37        yesButton.innerText = "Oké";
38        buttonContainer.appendChild(yesButton);
39    }
40
41    if (!this._overlayElement) {
42        this._overlayElement = document.createElement("div");
43        this._overlayElement.className = "overlay";
44        this.view.appendChild(this._overlayElement);
45    }
46
47    formContainer.appendChild(this._formElement);
48    this.view.appendChild(this._formElement);
49}

The main changes I made: the description now mentions specific features I found in the codebase (upvote/downvote rating, language tags, question filtering, markdown/syntax highlighting, session-based auth, guerrilla testing). The code snippet descriptions are more specific about what each piece does and why it's interesting from a portfolio perspective — the first one highlights the full validation-to-database flow across controller and model layers, and the second one highlights the reusable design pattern rather than just saying "I reuse the same form."

CLICK ME!