Merge branch 'main' into Included_more_robust_client_side_validation

pull/1513/head
Roshan Patel 2 months ago committed by GitHub
commit 2195fcd07e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -198,6 +198,9 @@ Compare some programming languages. What are some of the unique traits of JavaSc
Study a bit on the different languages available to the programmer. Try to write a line in one language, and then rewrite it in two others. What did you learn?
## Assignment
[Reading the Docs](assignment.md)
> Note: When selecting tools for your assignment, do not choose editors, browsers, or command line tools already listed above.

@ -1,11 +1,17 @@
# Reading the Docs
## Instructions
There are many tools that a web developer may need that are on the [MDN documentation for client-side tooling](https://developer.mozilla.org/docs/Learn/Tools_and_testing/Understanding_client-side_tools/Overview). Select 3 tools not covered in the lesson, explain why a web developer would use it, and search for a tool that falls under this category and share its documentation. Do not use the same tool example on MDN docs.
There are many tools that a web developer may need that are listed on the [MDN documentation for client-side tooling](https://developer.mozilla.org/docs/Learn/Tools_and_testing/Understanding_client-side_tools/Overview). Select **three tools** that are **not covered in this lesson** (excluding [list specific tools or refer to lesson content]), explain **why** a web developer would use each tool, and find a tool that fits each category. For each, share a link to its official documentation (not the example used on MDN).
**Format:**
- Tool name
- Why a web developer would use it (2-3 sentences)
- Link to documentation
**Length:**
- Each explanation should be 2-3 sentences.
## Rubric
Exemplary | Adequate | Needs Improvement
--- | --- | -- |
|Explained why web developer would use tool| Explained how, but not why developer would use tool| Did not mention how or why a developer would use tool |
Explained why web developer would use tool | Explained how, but not why developer would use tool | Did not mention how or why a developer would use tool |

@ -146,7 +146,7 @@ Let's say you have a folder locally with some code project and you want to start
git push -u origin main
```
This sends your commits in your "main" branch to GitHub.
This sends your commits in your "main" branch to GitHub. Setting the `upstream` branch including `-u` in the command establishes a link between your local branch and the remote branch, so you can simply use git push or git pull without specifying the branch name in the future. Git will automatically use the upstream branch and you won't need to specify the branch name explicitly in future commands.
2. **To add more changes**. If you want to continue making changes and pushing them to GitHub youll just need to use the following three commands:
@ -243,7 +243,11 @@ Let's go through a contributor workflow. Assume the contributor has already _for
git merge main
```
This will bring in all changes from `main` into your branch and hopefully you can just continue. If not, VS Code will tell you where Git is _confused_ and you just alter the affected files to say which content is the most accurate.
The `git merge main` command will bring in all changes from `main` into your branch. Hopefully you can just continue. If not, VS Code will tell you where Git is _confused_ and you just alter the affected files to say which content is the most accurate.
To switch to a different branch, use the modern `git switch` command:
```bash
git switch [branch_name]
1. **Send your work to GitHub**. Sending your work to GitHub means two things. Pushing your branch to your repo and then open up a PR, Pull Request.

@ -1,6 +1,6 @@
# Getting Started with Web Development
In this section of the curriculum, you will be introduced to non project-based concepts important to becoming a professional developer.
In this section of the curriculum, you will be introduced to non project-based concepts important to become a professional developer.
### Topics

@ -0,0 +1,374 @@
# AI Framework
There are many AI frameworks out there that when used can severely quicken up the time it takes to build a project. In this project we will focus on understanding what problems these frameworks address and build such a project ourselves.
## Why a framework
When it comes to using AI there are different approaches and different reasons for choosing these approaches, here are some:
- **No SDK**, most AI models allows you to interact directly with the AI model via for example HTTP requests. That approach works and may sometimes be your only option if an SDK option is missing.
- **SDK**. Using an SDK is usually the recommended approach as it allows you to type less code to interact with your model. It usually is limited to a specific model and if using different models, you might need to write new code to support those additional models.
- **A framework**. A framework usually takes things to another level in the sense that if you need to use different models, there's one API for all of them, what differs is usually the initial set up. Additionally frameworks brings in useful abstractions like in the AI space, they can deal with tools, memory, workflows, agents and more while writing less code. Because frameworks are usually opinionated they can really be helpful if you buy into how they do things but may fall short if you try to do something bespoke that the framework isn't made for. Sometimes a framework can also simplify too much and you may therefore not learn an important topic that later may harm performance for example.
Generally, use the right tool for the job.
## Introduction
In this lesson, we'll learn to:
- Use a common AI framework.
- Address common problems like chat conversations, tool usage, memory and context.
- Leverage this to build AI apps.
## First prompt
In our first app example, we'll learn how to connect to an AI model and query it using a prompt.
### Using Python
For this example, we'll use Langchain to connect to GitHub Models. We can use a class called `ChatOpenAI` and give it the fields `api_key`, `base_url` and `model`. The token is something that automatically is populated within GitHub Codespaces and if you're running the app locally, you need to set up a personal access token for this to work.
```python
from langchain_openai import ChatOpenAI
import os
llm = ChatOpenAI(
api_key=os.environ["GITHUB_TOKEN"],
base_url="https://models.github.ai/inference",
model="openai/gpt-4o-mini",
)
# works
response = llm.invoke("What's the capital of France?")
print(response.content)
```
In this code, we:
- Call `ChatOpenAI` to create a client.
- Use `llm.invoke` with a prompt to create a response
- Print the response with `print(response.content)`.
You should see a response similar to:
```text
The capital of France is Paris.
```
## Chat conversation
In the preceding section, you saw how we used what's normally known as zero shot prompting, a single prompt followed by a response.
However, often you find yourself in a situation where you need to maintain a conversation of several messages being exchanged between yourself and the AI assistant.
### Using Python
In langchain, we can store the conversation in a list. The `HumanMessage` represents a message from a user, and `SystemMessage` is a message meant to set the "personality" of the AI. In below example you see how we instruct the AI to assume the personality of Captain Picard and for the human/user to ask "Tell me about you" as the prompt.
```python
messages = [
SystemMessage(content="You are Captain Picard of the Starship Enterprise"),
HumanMessage(content="Tell me about you"),
]
```
The full code for this example looks like so:
```python
from langchain_core.messages import HumanMessage, SystemMessage
from langchain_openai import ChatOpenAI
import os
llm = ChatOpenAI(
api_key=os.environ["GITHUB_TOKEN"],
base_url="https://models.github.ai/inference",
model="openai/gpt-4o-mini",
)
messages = [
SystemMessage(content="You are Captain Picard of the Starship Enterprise"),
HumanMessage(content="Tell me about you"),
]
# works
response = llm.invoke(messages)
print(response.content)
```
You should see an outcome similar to:
```text
I am Captain Jean-Luc Picard, the commanding officer of the USS Enterprise (NCC-1701-D), a starship in the United Federation of Planets. My primary mission is to explore new worlds, seek out new life and new civilizations, and boldly go where no one has gone before.
I believe in the importance of diplomacy, reason, and the pursuit of knowledge. My crew is diverse and skilled, and we often face challenges that test our resolve, ethics, and ingenuity. Throughout my career, I have encountered numerous species, grappled with complex moral dilemmas, and have consistently sought peaceful solutions to conflicts.
I hold the ideals of the Federation close to my heart, believing in the importance of cooperation, understanding, and respect for all sentient beings. My experiences have shaped my leadership style, and I strive to be a thoughtful and just captain. How may I assist you further?
```
To keep the state of the conversation, you can add the response from a chat, so conversation is remembered, here's how to do that:
```python
from langchain_core.messages import HumanMessage, SystemMessage
from langchain_openai import ChatOpenAI
import os
llm = ChatOpenAI(
api_key=os.environ["GITHUB_TOKEN"],
base_url="https://models.github.ai/inference",
model="openai/gpt-4o-mini",
)
messages = [
SystemMessage(content="You are Captain Picard of the Starship Enterprise"),
HumanMessage(content="Tell me about you"),
]
# works
response = llm.invoke(messages)
print(response.content)
print("---- Next ----")
messages.append(response)
messages.append(HumanMessage(content="Now that I know about you, I'm Chris, can I be in your crew?"))
response = llm.invoke(messages)
print(response.content)
```
What we can see from the above conversation is how we invoke the LLM two times, first with the conversation consisting of just two messages but then a second time with more messages added to the conversation.
In fact, if you run this, you will see the second response being something like:
```text
Welcome aboard, Chris! It's always a pleasure to meet those who share a passion for exploration and discovery. While I cannot formally offer you a position on the Enterprise right now, I encourage you to pursue your aspirations. We are always in need of talented individuals with diverse skills and backgrounds.
If you are interested in space exploration, consider education and training in the sciences, engineering, or diplomacy. The values of curiosity, resilience, and teamwork are crucial in Starfleet. Should you ever find yourself on a starship, remember to uphold the principles of the Federation: peace, understanding, and respect for all beings. Your journey can lead you to remarkable adventures, whether in the stars or on the ground. Engage!
```
I'll take that as a maybe ;)
## Streaming responses
TODO
## Prompt templates
TODO
## Structured output
TODO
## Tool calling
Tools are how we give the LLM extra skills. The idea is to tell the LLM about functions it has and if a prompt is made that matches the description of one of these tools then we call them.
### Using Python
Let's add some tools like so:
```python
from typing_extensions import Annotated, TypedDict
class add(TypedDict):
"""Add two integers."""
# Annotations must have the type and can optionally include a default value and description (in that order).
a: Annotated[int, ..., "First integer"]
b: Annotated[int, ..., "Second integer"]
tools = [add]
functions = {
"add": lambda a, b: a + b
}
```
What we're doing here is to create a description of a tool called `add`. By inheriting from `TypedDict` and adding members like `a` and `b` of type `Annotated` this can be converted to a schema that the LLM can understand. The creation of functions is a dictionary that ensures that we know what to do if a specific tool is identified.
Let's see how we call the LLM with this tool next:
```python
llm = ChatOpenAI(
api_key=os.environ["GITHUB_TOKEN"],
base_url="https://models.github.ai/inference",
model="openai/gpt-4o-mini",
)
llm_with_tools = llm.bind_tools(tools)
```
Here we call `bind_tools` with our `tools` array and thereby the LLM `llm_with_tools` now has knowledge of this tool.
To use this new LLM, we can type the following code:
```python
query = "What is 3 + 12?"
res = llm_with_tools.invoke(query)
if(res.tool_calls):
for tool in res.tool_calls:
print("TOOL CALL: ", functions[tool["name"]](**tool["args"]))
print("CONTENT: ",res.content)
```
Now that we call `invoke` on this new llm, that has tools, we maybe the the property `tool_calls` populated. If so, any identified tools has a `name` and `args` property that identifies what tool should be called and with arguments. The full code looks like so:
```python
from langchain_core.messages import HumanMessage, SystemMessage
from langchain_openai import ChatOpenAI
import os
from typing_extensions import Annotated, TypedDict
class add(TypedDict):
"""Add two integers."""
# Annotations must have the type and can optionally include a default value and description (in that order).
a: Annotated[int, ..., "First integer"]
b: Annotated[int, ..., "Second integer"]
tools = [add]
functions = {
"add": lambda a, b: a + b
}
llm = ChatOpenAI(
api_key=os.environ["GITHUB_TOKEN"],
base_url="https://models.github.ai/inference",
model="openai/gpt-4o-mini",
)
llm_with_tools = llm.bind_tools(tools)
query = "What is 3 + 12?"
res = llm_with_tools.invoke(query)
if(res.tool_calls):
for tool in res.tool_calls:
print("TOOL CALL: ", functions[tool["name"]](**tool["args"]))
print("CONTENT: ",res.content)
```
Running this code, you should see output similar to:
```text
TOOL CALL: 15
CONTENT:
```
What this output mean is that the LLM analyzed the prompt "What is 3 + 12" as meaning that the `add` tool should be called and it knew that thanks to its name, description and member field descriptions. That the answer is 15 is because of our code using the dictionary `functions` to invoke it:
```python
print("TOOL CALL: ", functions[tool["name"]](**tool["args"]))
```
### A more interesting tool that calls a web api
Tools that adds two numbers is interesting as it illustrates how tool calling works but usually tools tend to do something more interesting like for example calling a Web API, let's do just that with this code:
```python
class joke(TypedDict):
"""Tell a joke."""
# Annotations must have the type and can optionally include a default value and description (in that order).
category: Annotated[str, ..., "The joke category"]
def get_joke(category: str) -> str:
response = requests.get(f"https://api.chucknorris.io/jokes/random?category={category}", headers={"Accept": "application/json"})
if response.status_code == 200:
return response.json().get("value", f"Here's a {category} joke!")
return f"Here's a {category} joke!"
functions = {
"add": lambda a, b: a + b,
"joke": lambda category: get_joke(category)
}
query = "Tell me a joke about animals"
# the rest of the code is the same
```
Now if you run this code you will get a response saying something like:
```text
TOOL CALL: Chuck Norris once rode a nine foot grizzly bear through an automatic car wash, instead of taking a shower.
CONTENT:
```
Here's the code in its entirety:
```python
from langchain_openai import ChatOpenAI
import requests
import os
from typing_extensions import Annotated, TypedDict
class add(TypedDict):
"""Add two integers."""
# Annotations must have the type and can optionally include a default value and description (in that order).
a: Annotated[int, ..., "First integer"]
b: Annotated[int, ..., "Second integer"]
class joke(TypedDict):
"""Tell a joke."""
# Annotations must have the type and can optionally include a default value and description (in that order).
category: Annotated[str, ..., "The joke category"]
tools = [add, joke]
def get_joke(category: str) -> str:
response = requests.get(f"https://api.chucknorris.io/jokes/random?category={category}", headers={"Accept": "application/json"})
if response.status_code == 200:
return response.json().get("value", f"Here's a {category} joke!")
return f"Here's a {category} joke!"
functions = {
"add": lambda a, b: a + b,
"joke": lambda category: get_joke(category)
}
llm = ChatOpenAI(
api_key=os.environ["GITHUB_TOKEN"],
base_url="https://models.github.ai/inference",
model="openai/gpt-4o-mini",
)
llm_with_tools = llm.bind_tools(tools)
query = "Tell me a joke about animals"
res = llm_with_tools.invoke(query)
if(res.tool_calls):
for tool in res.tool_calls:
# print("TOOL CALL: ", tool)
print("TOOL CALL: ", functions[tool["name"]](**tool["args"]))
print("CONTENT: ",res.content)
```
## Embedding
vectorize content, compare via cosine similarity
https://python.langchain.com/docs/how_to/embed_text/
### document loaders
pdf and csv
## Building an app
TODO
## Assignment
## Summary

@ -0,0 +1,29 @@
from langchain_core.messages import HumanMessage, SystemMessage
from langchain_openai import ChatOpenAI
import os
llm = ChatOpenAI(
api_key=os.environ["GITHUB_TOKEN"],
base_url="https://models.github.ai/inference",
model="openai/gpt-4o-mini",
)
messages = [
SystemMessage(content="You are Captain Picard of the Starship Enterprise"),
HumanMessage(content="Tell me about you"),
]
# works
response = llm.invoke(messages)
print(response.content)
print("---- Next ----")
messages.append(response)
messages.append(HumanMessage(content="Now that I know about you, I'm Chris, can I be in your crew?"))
response = llm.invoke(messages)
print(response.content)

@ -0,0 +1,47 @@
from langchain_openai import ChatOpenAI
import requests
import os
from typing_extensions import Annotated, TypedDict
class add(TypedDict):
"""Add two integers."""
# Annotations must have the type and can optionally include a default value and description (in that order).
a: Annotated[int, ..., "First integer"]
b: Annotated[int, ..., "Second integer"]
class joke(TypedDict):
"""Tell a joke."""
# Annotations must have the type and can optionally include a default value and description (in that order).
category: Annotated[str, ..., "The joke category"]
tools = [add, joke]
def get_joke(category: str) -> str:
response = requests.get(f"https://api.chucknorris.io/jokes/random?category={category}", headers={"Accept": "application/json"})
if response.status_code == 200:
return response.json().get("value", f"Here's a {category} joke!")
return f"Here's a {category} joke!"
functions = {
"add": lambda a, b: a + b,
"joke": lambda category: get_joke(category)
}
llm = ChatOpenAI(
api_key=os.environ["GITHUB_TOKEN"],
base_url="https://models.github.ai/inference",
model="openai/gpt-4o-mini",
)
llm_with_tools = llm.bind_tools(tools)
query = "Tell me a joke about animals"
res = llm_with_tools.invoke(query)
if(res.tool_calls):
for tool in res.tool_calls:
# print("TOOL CALL: ", tool)
print("TOOL CALL: ", functions[tool["name"]](**tool["args"]))
print("CONTENT: ",res.content)

@ -0,0 +1,14 @@
# pip install -qU "langchain[openai]"
from langchain_openai import ChatOpenAI
import os
llm = ChatOpenAI(
api_key=os.environ["GITHUB_TOKEN"],
base_url="https://models.github.ai/inference",
model="openai/gpt-4o-mini",
)
# works
response = llm.invoke("What is the capital of France")
print(response.content)

@ -2,10 +2,10 @@
## Instructions
Imagine you are building a shopping cart. Write some documentation on the data types that you would need to complete your shopping experience. How did you arrive at your choices?
Imagine you are building a shopping cart. Write documentation on the data types you would need to complete your shopping experience. For each data type, explain how and why you would use it, and provide an example. The six JavaScript data types are: String, Number, Boolean, Null, Undefined, and Object.
## Rubric
Criteria | Exemplary | Adequate | Needs Improvement
--- | --- | --- | -- |
||The six data types are listed and explored in detail, documenting their use|Four datatypes are explored|Two data types are explored|
Data Types | All six data types are listed, explored in detail, and documented with examples | Four data types are explored with some explanation | Two data types are explored with minimal explanation |

@ -1,11 +1,39 @@
# Practice your HTML: Build a blog mockup
# HTML Practice Assignment: Build a Blog Mockup
## Objective
Design and hand-code the HTML structure for a personal blog homepage. This exercise will help you practice semantic HTML, layout planning, and code organization.
## Instructions
Imagine you are designing, or redesigning, your personal web site. Create a graphical mockup of your site, and then write down the HTML markup you would use to build out the various elements of the site. You can do this on paper, and scan it, or use software of your choice, just make sure to hand-code the HTML markup.
1. **Design Your Blog Mockup**
- Sketch a visual mockup of your blog homepage. Include key sections such as header, navigation, main content, sidebar, and footer.
- You may use paper and scan your sketch, or use digital tools (e.g., Figma, Adobe XD, Canva, or even PowerPoint).
2. **Identify HTML Elements**
- List the HTML elements you plan to use for each section (e.g., `<header>`, `<nav>`, `<main>`, `<article>`, `<aside>`, `<footer>`, `<section>`, `<h1>``<h6>`, `<p>`, `<img>`, `<ul>`, `<li>`, `<a>`, etc.).
3. **Write the HTML Markup**
- Hand-code the HTML for your mockup. Focus on semantic structure and best practices.
- Include at least 10 distinct HTML elements.
- Add comments to explain your choices and structure.
4. **Submit Your Work**
- Upload your sketch/mockup and your HTML file.
- Optionally, provide a brief reflection (23 sentences) on your design decisions.
## Rubric
| Criteria | Exemplary | Adequate | Needs Improvement |
| -------- | ----------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- |
| | A blog layout is represented visually with at least 10 elements of markup displayed | A blog layout is represented visually with around 5 elements of markup displayed | A blog layout is represented visually with at most 3 elements of markup displayed |
| Criteria | Exemplary | Adequate | Needs Improvement |
|------------------|--------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------|---------------------------------------------------------------------------------|
| Visual Mockup | Clear, detailed mockup with labeled sections and thoughtful layout | Basic mockup with some labeled sections | Minimal or unclear mockup; lacks section labels |
| HTML Elements | Uses 10+ semantic HTML elements; demonstrates understanding of structure and best practices | Uses 59 HTML elements; some semantic structure | Uses fewer than 5 elements; lacks semantic structure |
| Code Quality | Well-organized, readable code with comments; follows HTML standards | Mostly organized code; few comments | Disorganized code; lacks comments |
| Reflection | Insightful reflection on design choices and challenges | Basic reflection | No reflection or lacks relevance |
## Tips
- Use semantic HTML tags for better accessibility and SEO.
- Organize your code with indentation and comments.
- Refer to [MDN HTML Elements Reference](https://developer.mozilla.org/en-US/docs/Web/HTML/Element) for guidance.
- Think about how your layout could be extended or styled in future assignments.

@ -1,11 +1,27 @@
# CSS Refactoring
# CSS Refactoring Assignment
## Objective
Refactor the terrarium project to use **Flexbox** or **CSS Grid** for layout. Update the HTML and CSS as needed to achieve a modern, responsive design. You do not need to implement draggable elements—focus on layout and styling only.
## Instructions
Restyle the terrarium using either Flexbox or CSS Grid, and take screenshots to show that you have tested it on several browsers. You might need to change the markup so create a new version of the app with the art in place for your refactor. Don't worry about making the elements draggable; only refactor the HTML and CSS for now.
1. **Create a new version** of the terrarium app. Update the markup and CSS to use Flexbox or CSS Grid for layout.
2. **Ensure the art and elements are in place** as in the original version.
3. **Test your design** in at least two different browsers (e.g., Chrome, Firefox, Edge).
4. **Take screenshots** of your terrarium in each browser to demonstrate cross-browser compatibility.
5. **Submit** your updated code and screenshots.
## Rubric
| Criteria | Exemplary | Adequate | Needs Improvement |
| -------- | ----------------------------------------------------------------- | ----------------------------- | ------------------------------------ |
| | Present a completely restyled terrarium using Flexbox or CSS Grid | Restyle a few of the elements | Fail to restyle the terrarium at all |
| Criteria | Exemplary | Adequate | Needs Improvement |
|------------|--------------------------------------------------------------------------|---------------------------------------|----------------------------------------|
| Layout | Fully refactored using Flexbox or CSS Grid; visually appealing and responsive | Some elements refactored; partial use of Flexbox or Grid | Little or no use of Flexbox or Grid; layout unchanged |
| Cross-Browser | Screenshots provided for multiple browsers; consistent appearance | Screenshots for one browser; minor inconsistencies | No screenshots or major inconsistencies |
| Code Quality | Clean, well-organized HTML/CSS; clear comments | Some organization; few comments | Disorganized code; lacks comments |
## Tips
- Review [Flexbox](https://css-tricks.com/snippets/css/a-guide-to-flexbox/) and [CSS Grid](https://css-tricks.com/snippets/css/complete-guide-grid/) guides.
- Use browser developer tools to test responsiveness.
- Comment your code for clarity.

@ -1,76 +1,109 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Welcome to my Virtual Terrarium</title>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<!-- import the webpage's stylesheet -->
<link rel="stylesheet" href="./style.css" />
<!-- import the webpage's JavaScript file -->
<script src="./script.js" defer></script>
</head>
<head>
<title>Welcome to my Virtual Terrarium</title>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<!-- Font Awesome CDN for icons -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css" />
<!-- import the webpage's stylesheet -->
<link rel="stylesheet" href="./style.css" />
<!-- import the webpage's JavaScript file -->
<script src="./script.js" defer></script>
</head>
<body>
<div id="page">
<!-- Navigation Bar -->
<nav>
<ul>
<li><a href="#"><i class="fas fa-home"></i> Home</a></li>
<li><a href="#"><i class="fas fa-leaf"></i> Plants</a></li>
<li><a href="#"><i class="fas fa-seedling"></i> Care Tips</a></li>
<li><a href="#"><i class="fas fa-info-circle"></i> About</a></li>
<li><a href="#"><i class="fas fa-envelope"></i> Contact</a></li>
</ul>
</nav>
<body>
<div id="page">
<h1>My Terrarium</h1>
<header>
<h1><i class="fas fa-jar"></i> My Terrarium</h1>
<p>Welcome to your virtual plant paradise! <i class="fas fa-smile-beam"></i></p>
</header>
<div id="left-container" class="container">
<div class="plant-holder">
<img class="plant" alt="plant" id="plant1" src="./images/plant1.png" />
</div>
<div class="plant-holder">
<img class="plant" alt="plant" id="plant2" src="./images/plant2.png" />
</div>
<div class="plant-holder">
<img class="plant" alt="plant" id="plant3" src="./images/plant3.png" />
</div>
<div class="plant-holder">
<img class="plant" alt="plant" id="plant4" src="./images/plant4.png" />
</div>
<div class="plant-holder">
<img class="plant" alt="plant" id="plant5" src="./images/plant5.png" />
</div>
<div class="plant-holder">
<img class="plant" alt="plant" id="plant6" src="./images/plant6.png" />
</div>
<div class="plant-holder">
<img class="plant" alt="plant" id="plant7" src="./images/plant7.png" />
</div>
</div>
<div id="right-container" class="container">
<div class="plant-holder">
<img class="plant" alt="plant" id="plant8" src="./images/plant8.png" />
</div>
<div class="plant-holder">
<img class="plant" alt="plant" id="plant9" src="./images/plant9.png" />
</div>
<div class="plant-holder">
<img class="plant" alt="plant" id="plant10" src="./images/plant10.png" />
</div>
<div class="plant-holder">
<img class="plant" alt="plant" id="plant11" src="./images/plant11.png" />
</div>
<div class="plant-holder">
<img class="plant" alt="plant" id="plant12" src="./images/plant12.png" />
</div>
<div class="plant-holder">
<img class="plant" alt="plant" id="plant13" src="./images/plant13.png" />
</div>
<div class="plant-holder">
<img class="plant" alt="plant" id="plant14" src="./images/plant14.png" />
</div>
</div>
<main>
<section id="plant-selection">
<div id="left-container" class="container">
<!-- Left-side plants -->
<div class="plant-holder">
<img class="plant" alt="plant" id="plant1" src="./images/plant1.png" />
</div>
<div class="plant-holder">
<img class="plant" alt="plant" id="plant2" src="./images/plant2.png" />
</div>
<div class="plant-holder">
<img class="plant" alt="plant" id="plant3" src="./images/plant3.png" />
</div>
<div class="plant-holder">
<img class="plant" alt="plant" id="plant4" src="./images/plant4.png" />
</div>
<div class="plant-holder">
<img class="plant" alt="plant" id="plant5" src="./images/plant5.png" />
</div>
<div class="plant-holder">
<img class="plant" alt="plant" id="plant6" src="./images/plant6.png" />
</div>
<div class="plant-holder">
<img class="plant" alt="plant" id="plant7" src="./images/plant7.png" />
</div>
</div>
<div id="right-container" class="container">
<!-- Right-side plants -->
<div class="plant-holder">
<img class="plant" alt="plant" id="plant8" src="./images/plant8.png" />
</div>
<div class="plant-holder">
<img class="plant" alt="plant" id="plant9" src="./images/plant9.png" />
</div>
<div class="plant-holder">
<img class="plant" alt="plant" id="plant10" src="./images/plant10.png" />
</div>
<div class="plant-holder">
<img class="plant" alt="plant" id="plant11" src="./images/plant11.png" />
</div>
<div class="plant-holder">
<img class="plant" alt="plant" id="plant12" src="./images/plant12.png" />
</div>
<div class="plant-holder">
<img class="plant" alt="plant" id="plant13" src="./images/plant13.png" />
</div>
<div class="plant-holder">
<img class="plant" alt="plant" id="plant14" src="./images/plant14.png" />
</div>
</div>
</section>
<div id="terrarium">
<div class="jar-top"></div>
<div class="jar-walls">
<div class="jar-glossy-long"></div>
<div class="jar-glossy-short"></div>
</div>
<div class="dirt"></div>
<div class="jar-bottom"></div>
</div>
</div>
</body>
</html>
<section id="terrarium-section">
<div id="terrarium">
<div class="jar-top"></div>
<div class="jar-walls">
<div class="jar-glossy-long"></div>
<div class="jar-glossy-short"></div>
</div>
<div class="dirt"></div>
<div class="jar-bottom"></div>
</div>
</section>
</main>
<footer>
<p>
<i class="fas fa-copyright"></i> 2025 My Terrarium. All rights reserved.
&nbsp;|&nbsp;
<a href="https://github.com/" target="_blank"><i class="fab fa-github"></i> GitHub</a>
&nbsp;|&nbsp;
<a href="mailto:info@myterrarium.com"><i class="fas fa-envelope"></i> Email</a>
</p>
</footer>
</div>
</body>
</html>

@ -103,3 +103,108 @@ h1 {
bottom: 45%;
left: 5%;
}
/* ...existing code... */
/* Navigation Bar Styles */
nav {
background: linear-gradient(90deg, #d1e1df 60%, #b2c9c6 100%);
box-shadow: 0 2px 8px rgba(58,36,29,0.08);
padding: 0.5rem 0;
margin-bottom: 1rem;
}
nav ul {
display: flex;
justify-content: center;
align-items: center;
gap: 2rem;
list-style: none;
margin: 0;
padding: 0;
}
nav li a {
color: #3a241d;
text-decoration: none;
font-size: 1.1rem;
font-weight: 600;
transition: color 0.2s;
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
border-radius: 0.5rem;
}
nav li a:hover, nav li a:focus {
background: #e6f2f0;
color: #2d7c6f;
box-shadow: 0 2px 8px rgba(45,124,111,0.08);
}
/* Font Awesome Icon Styles */
.fas, .fab {
color: #2d7c6f;
font-size: 1.2em;
vertical-align: middle;
transition: color 0.2s;
}
nav li a:hover .fas,
nav li a:hover .fab {
color: #3a241d;
}
header h1 .fas {
color: #3a241d;
margin-right: 0.5rem;
font-size: 1.3em;
}
header p .fas {
color: #2d7c6f;
margin-left: 0.3rem;
}
footer {
background: #d1e1df;
text-align: center;
padding: 1rem 0;
margin-top: 2rem;
font-size: 1rem;
color: #3a241d;
border-top: 2px solid #b2c9c6;
}
footer a {
color: #2d7c6f;
text-decoration: none;
margin: 0 0.5rem;
font-weight: 600;
transition: color 0.2s;
}
footer a:hover, footer a:focus {
color: #3a241d;
text-decoration: underline;
}
footer .fab, footer .fas {
margin-right: 0.3rem;
font-size: 1.1em;
vertical-align: middle;
}
/* Responsive adjustments for navigation */
@media (max-width: 700px) {
nav ul {
flex-direction: column;
gap: 1rem;
}
nav li a {
font-size: 1rem;
padding: 0.5rem 0.5rem;
}
}
/* ...existing code... */

@ -6,3 +6,151 @@
background-color: lightcoral;
border-color: red;
}
/* Global Styles */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: "Segoe UI", Roboto, Arial, sans-serif;
}
body {
background: linear-gradient(135deg, #f9fafb, #e3f2ef);
color: #2d3436;
display: flex;
flex-direction: column;
align-items: center;
padding: 2rem;
min-height: 100vh;
}
/* Title */
h1 {
font-size: 2.5rem;
font-weight: 700;
color: #2d7c6f;
display: flex;
align-items: center;
gap: 0.6rem;
margin-bottom: 1rem;
text-shadow: 1px 2px 4px rgba(0, 0, 0, 0.1);
}
/* Info */
div i.fa-info-circle {
color: #00796b;
margin-right: 0.4rem;
}
div {
font-size: 1rem;
margin-bottom: 1rem;
text-align: center;
max-width: 500px;
color: #555;
}
/* Quote Display */
#quote {
font-size: 1.2rem;
font-weight: 500;
color: #333;
margin: 1rem auto;
padding: 1rem;
border-left: 4px solid #2d7c6f;
background: #f1fdfb;
border-radius: 8px;
max-width: 600px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
}
/* Message */
#message {
font-size: 1.1rem;
font-weight: 600;
margin: 0.8rem;
color: #ff7043;
text-align: center;
}
/* Input Box */
input#typed-value {
padding: 0.8rem 1rem;
font-size: 1rem;
width: 280px;
border: 2px solid #2d7c6f;
border-radius: 12px;
outline: none;
transition: 0.3s ease;
margin-right: 0.4rem;
}
input#typed-value:focus {
border-color: #00796b;
box-shadow: 0 0 8px rgba(0, 121, 107, 0.3);
}
/* Pen Icon */
.fa-pen {
color: #2d7c6f;
font-size: 1.2rem;
vertical-align: middle;
}
/* Start Button */
button#start {
background: linear-gradient(135deg, #2d7c6f, #26a69a);
color: white;
font-size: 1rem;
padding: 0.8rem 1.6rem;
border: none;
border-radius: 12px;
margin-top: 1.5rem;
cursor: pointer;
transition: transform 0.2s ease, box-shadow 0.3s ease;
}
button#start:hover {
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.2);
}
button#start i {
margin-right: 0.4rem;
}
/* Date & Time */
#datetime {
margin-top: 1.5rem;
font-size: 1rem;
font-weight: 600;
color: #37474f;
display: flex;
align-items: center;
justify-content: center;
gap: 0.6rem;
}
#datetime .fas {
color: #2d7c6f;
}
/* Responsive */
@media (max-width: 600px) {
h1 {
font-size: 2rem;
}
#quote {
font-size: 1rem;
padding: 0.8rem;
}
input#typed-value {
width: 200px;
}
button#start {
width: 100%;
}
}

@ -1,23 +1,110 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Typing</title>
<link rel="stylesheet" href="./index.css" />
</head>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Typing</title>
<!-- Font Awesome CDN for icons -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css" />
<link rel="stylesheet" href="./index.css" />
<style>
#datetime {
text-align: center;
margin-bottom: 1rem;
font-size: 1.1rem;
color: #2d7c6f;
}
#datetime .fas {
margin-right: 0.3rem;
color: #3a241d;
font-size: 1.1em;
vertical-align: middle;
}
/* Hide label visually but keep for accessibility */
.hidden-label {
position: absolute;
left: -10000px;
top: auto;
width: 1px;
height: 1px;
overflow: hidden;
}
/* ...existing code... */
</style>
</head>
<body>
<h1>Practice your typing</h1>
<div>Click start to have a quote displayed. Type the quote as fast as you can!</div>
<body>
<h1>
<i class="fas fa-keyboard"></i> Practice your typing
</h1>
<div>
<i class="fas fa-info-circle"></i>
Click start to have a quote displayed. Type the quote as fast as you can!
</div>
<p id="quote"></p>
<p id="message"></p>
<div>
<input type="text" aria-label="current word" id="typed-value" />
</div>
<div>
<button id="start" type="button">Start</button>
</div>
<script src="./index.js"></script>
</body>
<p id="quote"></p>
<p id="message"></p>
<div>
<label for="typed-value" class="hidden-label">Current word</label>
<input type="text" aria-label="current word" id="typed-value" placeholder="Type here..." />
<i class="fas fa-pen"></i>
</div>
<div>
<button id="start" type="button">
<i class="fas fa-play"></i> Start
</button>
</div>
<script src="./index.js"></script>
<!-- ...existing code... -->
<body>
<h1>
<i class="fas fa-keyboard"></i> Practice your typing
</h1>
<div id="datetime">
<i class="fas fa-calendar-day"></i>
<span id="current-date"></span>
&nbsp;|&nbsp;
<i class="fas fa-clock"></i>
<span id="current-time"></span>
</div>
<!-- ...existing code... -->
</body>
<!-- ...existing code... -->
<script>
// ...existing code...
function updateDateTime() {
const now = new Date();
// Format date as DD/MM/YYYY
const day = String(now.getDate()).padStart(2, '0');
const month = String(now.getMonth() + 1).padStart(2, '0');
const year = now.getFullYear();
const formattedDate = `${day}/${month}/${year}`;
// Format time as HH:MM AM/PM (12-hour)
let hours = now.getHours();
const minutes = String(now.getMinutes()).padStart(2, '0');
const ampm = hours >= 12 ? 'PM' : 'AM';
hours = hours % 12;
hours = hours ? hours : 12; // 0 should be 12
const formattedTime = `${hours}:${minutes} ${ampm}`;
document.getElementById('current-date').textContent = formattedDate;
document.getElementById('current-time').textContent = formattedTime;
}
// Initial call and update every minute
updateDateTime();
setInterval(updateDateTime, 60000);
// ...existing code...
</script>
</body>
</html>

@ -1,94 +1,94 @@
// Typing Game - Modern ES6+ Version
// Quotes pool
const quotes = [
'When you have eliminated the impossible, whatever remains, however improbable, must be the truth.',
'There is nothing more deceptive than an obvious fact.',
'I ought to know by this time that when a fact appears to be opposed to a long train of deductions it invariably proves to be capable of bearing some other interpretation.',
'I never make exceptions. An exception disproves the rule.',
'What one man can invent another can discover.',
'Nothing clears up a case so much as stating it to another person.',
'Education never ends, Watson. It is a series of lessons, with the greatest for the last.',
"When you have eliminated the impossible, whatever remains, however improbable, must be the truth.",
"There is nothing more deceptive than an obvious fact.",
"I ought to know by this time that when a fact appears to be opposed to a long train of deductions it invariably proves to be capable of bearing some other interpretation.",
"I never make exceptions. An exception disproves the rule.",
"What one man can invent another can discover.",
"Nothing clears up a case so much as stating it to another person.",
"Education never ends, Watson. It is a series of lessons, with the greatest for the last."
];
// array for storing the words of the current challenge
// State
let words = [];
// stores the index of the word the player is currently typing
let wordIndex = 0;
// default value for startTime (will be set on start)
let startTime = Date.now();
let startTime = 0;
// UI Elements
const quoteElement = document.querySelector("#quote");
const messageElement = document.querySelector("#message");
const typedValueElement = document.querySelector("#typed-value");
const startButton = document.querySelector("#start");
// Messages system
const messages = {
success: (seconds) => `🎉 Congratulations! You finished in ${seconds.toFixed(2)} seconds.`,
error: "⚠️ Oops! There's a mistake.",
start: "⌨️ Start typing to begin the game."
};
// grab UI items
const quoteElement = document.getElementById('quote');
const messageElement = document.getElementById('message')
const typedValueElement = document.getElementById('typed-value');
// Utility: pick random quote
const getRandomQuote = () => quotes[Math.floor(Math.random() * quotes.length)];
document.getElementById('start').addEventListener('click', function () {
// get a quote
const quoteIndex = Math.floor(Math.random() * quotes.length);
const quote = quotes[quoteIndex];
// Put the quote into an array of words
words = quote.split(' ');
// reset the word index for tracking
wordIndex = 0;
// Utility: render quote as spans
const renderQuote = (quote) => {
quoteElement.innerHTML = quote
.split(" ")
.map((word, i) => `<span ${i === 0 ? 'class="highlight"' : ""}>${word}</span>`)
.join(" ");
};
// UI updates
// Create an array of span elements so we can set a class
const spanWords = words.map(function (word) { return `<span>${word} </span>` });
// Convert into string and set as innerHTML on quote display
quoteElement.innerHTML = spanWords.join('');
// Highlight the first word
quoteElement.childNodes[0].className = 'highlight';
// Clear any prior messages
messageElement.innerText = '';
// Utility: highlight current word
const highlightWord = (index) => {
[...quoteElement.children].forEach((el, i) => {
el.classList.toggle("highlight", i === index);
});
};
// Setup the textbox
// Clear the textbox
typedValueElement.value = '';
// set focus
typedValueElement.focus();
// set the event handler
// Game start
const startGame = () => {
const quote = getRandomQuote();
words = quote.split(" ");
wordIndex = 0;
renderQuote(quote);
// Start the timer
startTime = new Date().getTime();
});
messageElement.textContent = messages.start;
typedValueElement.value = "";
typedValueElement.focus();
typedValueElement.addEventListener('input', (e) => {
// Get the current word
const currentWord = words[wordIndex];
// get the current value
const typedValue = typedValueElement.value;
startTime = Date.now();
};
if (typedValue === currentWord && wordIndex === words.length - 1) {
// end of quote
// Display success
const elapsedTime = new Date().getTime() - startTime;
const message = `CONGRATULATIONS! You finished in ${elapsedTime / 1000} seconds.`;
messageElement.innerText = message;
} else if (typedValue.endsWith(' ') && typedValue.trim() === currentWord) {
// end of word
// clear the typedValueElement for the new word
typedValueElement.value = '';
// move to the next word
wordIndex++;
// reset the class name for all elements in quote
for (const wordElement of quoteElement.childNodes) {
wordElement.className = '';
}
// highlight the new word
quoteElement.childNodes[wordIndex].className = 'highlight';
} else if (currentWord.startsWith(typedValue)) {
// currently correct
// highlight the next word
typedValueElement.className = '';
} else {
// error state
typedValueElement.className = 'error';
}
});
// Typing logic
const handleTyping = () => {
const currentWord = words[wordIndex];
const typedValue = typedValueElement.value;
// Add this at the end of the file
const messages = {
success: "CONGRATULATIONS! You finished in {seconds} seconds.",
error: "Oops! There's a mistake.",
start: "Start typing to begin the game."
if (typedValue === currentWord && wordIndex === words.length - 1) {
// Game finished
const elapsedTime = (Date.now() - startTime) / 1000;
messageElement.textContent = messages.success(elapsedTime);
typedValueElement.disabled = true;
} else if (typedValue.endsWith(" ") && typedValue.trim() === currentWord) {
// Word completed
typedValueElement.value = "";
wordIndex++;
highlightWord(wordIndex);
} else if (currentWord.startsWith(typedValue)) {
// Correct so far
typedValueElement.classList.remove("error");
} else {
// Error
typedValueElement.classList.add("error");
messageElement.textContent = messages.error;
}
};
export default messages;
// Event Listeners
startButton.addEventListener("click", startGame);
typedValueElement.addEventListener("input", handleTyping);
// Default state
messageElement.textContent = "👉 Click Start to begin!";

@ -115,12 +115,12 @@ And finally, in `/dist/background.js`, add the listener for these background act
```JavaScript
chrome.runtime.onMessage.addListener(function (msg, sender, sendResponse) {
if (msg.action === 'updateIcon') {
chrome.browserAction.setIcon({ imageData: drawIcon(msg.value) });
chrome.action.setIcon({ imageData: drawIcon(msg.value) });
}
});
//borrowed from energy lollipop extension, nice feature!
function drawIcon(value) {
let canvas = document.createElement('canvas');
let canvas = new OffscreenCanvas(200, 200);
let context = canvas.getContext('2d');
context.beginPath();

@ -1,11 +1,11 @@
chrome.runtime.onMessage.addListener(function (msg, sender, sendResponse) {
if (msg.action === 'updateIcon') {
chrome.browserAction.setIcon({ imageData: drawIcon(msg.value) });
chrome.action.setIcon({ imageData: drawIcon(msg.value) });
}
});
//borrowed from energy lollipop extension, nice feature!
function drawIcon(value) {
let canvas = document.createElement('canvas');
let canvas = new OffscreenCanvas(200, 200);
let context = canvas.getContext('2d');
context.beginPath();

@ -9,7 +9,7 @@
"version": "1.0.0",
"license": "MIT",
"dependencies": {
"axios": "^1.8.2"
"axios": "^1.12.0"
},
"devDependencies": {
"webpack": "^5.94.0",
@ -362,13 +362,13 @@
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
},
"node_modules/axios": {
"version": "1.8.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.8.2.tgz",
"integrity": "sha512-ls4GYBm5aig9vWx8AWDSGLpnpDQRtWAfrjU+EuytuODrFBkqesN2RkOQCBzrA1RQNHw1SmRMSDDDSwzNAYQ6Rg==",
"version": "1.12.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.12.0.tgz",
"integrity": "sha512-oXTDccv8PcfjZmPGlWsPSwtOJCZ/b6W5jAMCNcfwJbCzDckwG0jrYJFaWH1yvivfCXjVzV/SPDEhMB3Q+DSurg==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.0",
"form-data": "^4.0.4",
"proxy-from-env": "^1.1.0"
}
},

@ -24,6 +24,6 @@
"webpack-cli": "^5.1.4"
},
"dependencies": {
"axios": "^1.8.2"
"axios": "^1.12.0"
}
}

@ -9,7 +9,7 @@
"version": "1.0.0",
"license": "MIT",
"dependencies": {
"axios": "^1.8.2"
"axios": "^1.12.0"
},
"devDependencies": {
"webpack": "^5.99.9",
@ -425,13 +425,13 @@
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
},
"node_modules/axios": {
"version": "1.8.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.8.2.tgz",
"integrity": "sha512-ls4GYBm5aig9vWx8AWDSGLpnpDQRtWAfrjU+EuytuODrFBkqesN2RkOQCBzrA1RQNHw1SmRMSDDDSwzNAYQ6Rg==",
"version": "1.12.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.12.0.tgz",
"integrity": "sha512-oXTDccv8PcfjZmPGlWsPSwtOJCZ/b6W5jAMCNcfwJbCzDckwG0jrYJFaWH1yvivfCXjVzV/SPDEhMB3Q+DSurg==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.0",
"form-data": "^4.0.4",
"proxy-from-env": "^1.1.0"
}
},

@ -24,6 +24,6 @@
"webpack-cli": "^5.1.4"
},
"dependencies": {
"axios": "^1.8.2"
"axios": "^1.12.0"
}
}

@ -1,34 +1,47 @@
function loadTexture(path) {
return new Promise((resolve) => {
const img = new Image();
img.src = path;
img.onload = () => {
resolve(img);
async function loadTexture(path) {
return new Promise(resolve => {
const img = new Image();
img.src = path;
img.onload = () => resolve(img);
});
}
})
}
function createEnemies(ctx, canvas, enemyImg) {
const MONSTER_TOTAL = 5;
const MONSTER_WIDTH = MONSTER_TOTAL * 98;
const START_X = (canvas.width - MONSTER_WIDTH) / 2;
const STOP_X = START_X + MONSTER_WIDTH;
for (let x = START_X; x < STOP_X; x += 98) {
for (let y = 0; y < 50 * 5; y += 50) {
ctx.drawImage(enemyImg, x, y);
function drawText(ctx, text, x, y, fontSize = 40, color = "#ff007f", align = "center") {
ctx.save();
ctx.font = `${fontSize}px Orbitron, monospace`;
ctx.fillStyle = color;
ctx.textAlign = align;
ctx.shadowColor = "#181733";
ctx.shadowBlur = 12;
ctx.fillText(text, x, y);
ctx.restore();
}
}
}
window.onload = async() => {
canvas = document.getElementById("canvas");
ctx = canvas.getContext("2d");
const heroImg = await loadTexture('assets/player.png')
const enemyImg = await loadTexture('assets/enemyShip.png')
ctx.fillStyle = 'black';
ctx.fillRect(0,0, canvas.width, canvas.height);
ctx.drawImage(heroImg, canvas.width/2 - 45, canvas.height - (canvas.height /4));
createEnemies(ctx, canvas, enemyImg);
};
function createEnemies(ctx, canvas, enemyImg) {
const MONSTER_TOTAL = 7;
const MONSTER_ROWS = 5;
const ENEMY_SIZE = 65;
const START_X = (canvas.width - (MONSTER_TOTAL * ENEMY_SIZE)) / 2;
for (let row = 0; row < MONSTER_ROWS; row++) {
for (let col = 0; col < MONSTER_TOTAL; col++) {
const x = START_X + col * ENEMY_SIZE;
const y = 60 + row * ENEMY_SIZE;
ctx.drawImage(enemyImg, x, y, ENEMY_SIZE, ENEMY_SIZE);
}
}
}
window.onload = async () => {
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
// Use emoji icons as game characters if images aren't available
const heroImg = await loadTexture('assets/player.png');
const enemyImg = await loadTexture('assets/enemyShip.png');
ctx.fillStyle = '#181733';
ctx.fillRect(0, 0, canvas.width, canvas.height);
drawText(ctx, "Prepare for Battle!", canvas.width/2, 70, 48, "#fff");
// Draw hero
ctx.drawImage(heroImg, canvas.width/2 - 45, canvas.height - (canvas.height / 4), 90, 90);
createEnemies(ctx, canvas, enemyImg);
// Example: Draw power up icon beside hero
ctx.font = "38px FontAwesome";
ctx.fillStyle = "#ffd700";
ctx.fillText("\uf059", canvas.width/2 + 60, canvas.height - (canvas.height / 4) + 42); // Star icon
};

@ -1,6 +1,49 @@
<html>
<body>
<canvas id ="canvas" width="1024" height="768"></canvas>
<script src="app.js"></script>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Enhanced Canvas Game</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css">
<style>
body {
background: linear-gradient(135deg, #2d2a4a 0%, #181733 100%);
color: #fff;
font-family: 'Orbitron', 'Arial', sans-serif;
margin: 0;
text-align: center;
}
h1 {
font-family: 'Orbitron', monospace;
margin-top: 32px;
font-size: 2.7rem;
letter-spacing: 2px;
text-shadow: 0 3px 12px #ff007f;
}
.info-bar {
margin: 20px 0;
font-size: 1.3rem;
}
#canvas {
border: 4px solid #ff007f;
box-shadow: 0 0 40px #ff007f22;
display: block;
margin: 20px auto;
background: #12121c;
}
</style>
</head>
<body>
<h1>
<i class="fa-solid fa-rocket"></i>
Arcade Monster Attack
<i class="fa-solid fa-skull-crossbones"></i>
</h1>
<div class="info-bar">
<i class="fa-solid fa-star"></i> Score: <span id="score">0</span>
<i class="fa-solid fa-heart"></i> Lives: <span id="lives">3</span>
</div>
<canvas id="canvas" width="1024" height="768"></canvas>
</html>

File diff suppressed because it is too large Load Diff

@ -1,20 +1,523 @@
<!DOCTYPE html>
<html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Space Game</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="description" content="Canvas-based Space Game with accessible controls, HUD, and settings." />
<meta name="theme-color" content="#0b1023" />
<!-- Google Fonts: performance preconnect + fonts (apply via CSS later) -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<!-- Swap or add families as desired -->
<link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700&family=Poppins:wght@400;600;800&display=swap" rel="stylesheet">
<!-- Font Awesome 6 (Free) via cdnjs -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.6.0/css/all.min.css" integrity="sha512-bWn0gM3Yx6w5OV6v4p+WB6l3Zl9sJ2jQ4kN0N1bK2qgkvI6l3F8Vd6k1oB7f/0g0a9Qyxgkq+K6X1JQmJv9zYg==" crossorigin="anonymous" referrerpolicy="no-referrer">
<!-- Optional PWA hooks (files to be created later) -->
<link rel="manifest" href="manifest.webmanifest">
</head>
<style>
/* Space Game — CSS */
/* Typography + Theme */
:root {
--bg: #070a1a;
--bg-2: #0b1023;
--panel: rgba(12, 17, 38, 0.75);
--panel-solid: #0c1126;
--primary: #5ac8fa;
--accent: #a78bfa;
--danger: #ff6b6b;
--success: #34d399;
--warning: #fbbf24;
--text: #e6eaf2;
--muted: #9aa4bf;
--border: #192243;
--shadow: rgba(0, 0, 0, 0.6);
}
*,
*::before,
*::after {
box-sizing: border-box;
}
html, body {
height: 100%;
}
body {
margin: 0;
color: var(--text);
background:
radial-gradient(circle at 20% 10%, #0f1533 0%, transparent 50%),
radial-gradient(circle at 80% 30%, #0b1b3a 0%, transparent 40%),
radial-gradient(circle at 30% 80%, #111a39 0%, transparent 40%),
var(--bg);
font-family: "Poppins", system-ui, -apple-system, Segoe UI, Roboto, "Helvetica Neue", Arial, "Noto Sans", "Apple Color Emoji", "Segoe UI Emoji";
line-height: 1.5;
}
/* Header + Nav */
header {
position: sticky;
top: 0;
z-index: 20;
background: linear-gradient(180deg, rgba(7,10,26,0.85) 0%, rgba(7,10,26,0.6) 100%);
backdrop-filter: blur(6px);
border-bottom: 1px solid var(--border);
padding: 0.75rem 1rem;
}
header h1 {
margin: 0;
font-family: "Orbitron", "Poppins", sans-serif;
font-weight: 800;
letter-spacing: 0.5px;
display: inline-flex;
align-items: center;
gap: 0.6rem;
}
nav[aria-label="Primary"] {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin-top: 0.5rem;
}
nav[aria-label="Primary"] button {
appearance: none;
border: 1px solid var(--border);
background: linear-gradient(180deg, #121a3a 0%, #0d1430 100%);
color: var(--text);
border-radius: 10px;
padding: 0.55rem 0.85rem;
display: inline-flex;
align-items: center;
gap: 0.5rem;
cursor: pointer;
transition: transform 0.06s ease, box-shadow 0.2s ease, border-color 0.2s ease;
box-shadow: 0 2px 12px var(--shadow);
}
nav[aria-label="Primary"] button:hover {
border-color: var(--primary);
box-shadow: 0 4px 18px rgba(90, 200, 250, 0.25);
}
nav[aria-label="Primary"] button:active {
transform: translateY(1px) scale(0.98);
}
nav [data-action="start"] { background: linear-gradient(180deg, #10314a 0%, #0d2438 100%); }
nav [data-action="pause"] { background: linear-gradient(180deg, #1a1f3f 0%, #131936 100%); }
nav [data-action="reset"] { background: linear-gradient(180deg, #3b0d0d 0%, #2a0a0a 100%); }
nav [data-action="mute"][aria-pressed="true"] { background: linear-gradient(180deg, #3f1a1a 0%, #2a0e0e 100%); }
nav [data-action="fullscreen"] { background: linear-gradient(180deg, #162c3a 0%, #0f2331 100%); }
nav [data-action="open-settings"] { background: linear-gradient(180deg, #1a1440 0%, #13103a 100%); }
/* Main Layout */
main {
max-width: 1200px;
margin: 0 auto;
padding: 1rem;
}
/* HUD overlay */
#hud {
position: fixed;
left: 50%;
top: 64px;
transform: translateX(-50%);
width: min(100% - 2rem, 1180px);
z-index: 15;
}
#hud [role="status"] {
display: flex;
flex-wrap: wrap;
gap: 0.75rem 1rem;
align-items: center;
background: var(--panel);
border: 1px solid var(--border);
border-radius: 12px;
padding: 0.5rem 0.75rem;
backdrop-filter: blur(4px);
}
#hud span {
display: inline-flex;
align-items: center;
gap: 0.4rem;
color: var(--muted);
font-weight: 600;
}
#hud output {
color: var(--primary);
font-family: "Orbitron", "Poppins", sans-serif;
letter-spacing: 0.4px;
}
/* Canvas section */
#play {
position: relative;
margin-top: 1rem;
}
#play figure {
margin: 0;
border: 1px solid var(--border);
border-radius: 14px;
background:
radial-gradient(1200px 600px at 70% 30%, rgba(70, 78, 150, 0.15), transparent 60%),
radial-gradient(800px 400px at 20% 70%, rgba(88, 131, 186, 0.12), transparent 60%),
#081022;
box-shadow: 0 8px 28px rgba(0,0,0,0.45), inset 0 0 0 1px rgba(255,255,255,0.02);
overflow: hidden;
}
#play figcaption {
position: absolute;
z-index: 5;
inset: 0 auto auto 0;
margin: 0.5rem 0 0 0.5rem;
padding: 0.25rem 0.5rem;
background: rgba(8, 16, 34, 0.7);
border: 1px solid var(--border);
border-radius: 8px;
color: var(--muted);
font-size: 0.85rem;
}
#myCanvas {
display: block;
width: 100%;
height: auto;
background: linear-gradient(180deg, #000814 0%, #001d3d 100%);
}
/* Help + Leaderboard */
#help, #leaderboard {
margin-top: 1rem;
background: var(--panel);
border: 1px solid var(--border);
border-radius: 12px;
padding: 1rem;
}
#help h2, #leaderboard h2 {
margin-top: 0;
font-family: "Orbitron", "Poppins", sans-serif;
}
#help ul {
margin: 0.5rem 0 0 1rem;
padding: 0;
}
#scores {
margin: 0.5rem 0 0 1rem;
padding: 0;
}
#scores li {
margin: 0.25rem 0;
color: var(--text);
}
/* Dialogs */
dialog {
border: 1px solid var(--border);
border-radius: 14px;
padding: 1rem;
background: var(--panel-solid);
color: var(--text);
width: min(520px, calc(100% - 2rem));
box-shadow: 0 24px 60px rgba(0,0,0,0.55);
}
dialog header h2 {
margin: 0 0 0.5rem 0;
font-family: "Orbitron", "Poppins", sans-serif;
}
dialog section {
display: grid;
gap: 0.75rem;
margin-bottom: 0.5rem;
}
dialog label {
font-size: 0.95rem;
color: var(--muted);
}
dialog input[type="range"],
dialog select,
dialog input[type="url"],
dialog input[type="file"] {
width: 100%;
color: var(--text);
background: #0f1733;
border: 1px solid var(--border);
border-radius: 10px;
padding: 0.5rem;
}
dialog menu {
display: flex;
justify-content: flex-end;
gap: 0.5rem;
margin: 0;
padding: 0.5rem 0 0 0;
}
dialog button {
appearance: none;
border: 1px solid var(--border);
background: linear-gradient(180deg, #121a3a 0%, #0d1430 100%);
color: var(--text);
border-radius: 10px;
padding: 0.5rem 0.85rem;
cursor: pointer;
}
dialog::backdrop {
background: rgba(3, 6, 20, 0.6);
backdrop-filter: blur(3px);
}
/* Footer */
footer {
margin: 1rem 0;
color: var(--muted);
text-align: center;
}
/* Focus styles */
:focus-visible {
outline: 2px solid var(--primary);
outline-offset: 2px;
border-radius: 6px;
}
/* Fullscreen adjustments */
#play:fullscreen, /* section fullscreen */
#myCanvas:fullscreen {
background: #000;
}
#play:fullscreen #myCanvas,
#myCanvas:fullscreen {
width: 100vw;
height: 100vh;
}
/* Small screens */
@media (max-width: 600px) {
#hud [role="status"] {
justify-content: space-between;
}
nav[aria-label="Primary"] {
gap: 0.4rem;
}
nav[aria-label="Primary"] button {
padding: 0.5rem 0.65rem;
}
}
</style>
<body>
<p>Image to use:</p>
<p>Canvas:</p>
<a href="#play" aria-label="Skip to game">Skip to Game</a>
<header>
<h1 aria-label="Space Game">
<i class="fa-solid fa-rocket" aria-hidden="true"></i>
Space Game
</h1>
<nav aria-label="Primary">
<button type="button" data-action="start" aria-label="Start">
<i class="fa-solid fa-play" aria-hidden="true"></i> Start
</button>
<button type="button" data-action="pause" aria-label="Pause">
<i class="fa-solid fa-circle-pause" aria-hidden="true"></i> Pause
</button>
<button type="button" data-action="reset" aria-label="Reset">
<i class="fa-solid fa-rotate-right" aria-hidden="true"></i> Reset
</button>
<button type="button" data-action="mute" aria-pressed="false" aria-label="Mute">
<i class="fa-solid fa-volume-xmark" aria-hidden="true"></i> Mute
</button>
<button type="button" data-action="fullscreen" aria-label="Toggle fullscreen">
<i class="fa-solid fa-expand" aria-hidden="true"></i> Fullscreen
</button>
<button type="button" data-action="open-settings" aria-controls="settingsDialog" aria-haspopup="dialog">
<i class="fa-solid fa-gear" aria-hidden="true"></i> Settings
</button>
</nav>
</header>
<main id="main" tabindex="-1">
<!-- HUD -->
<section id="hud" aria-label="Game HUD">
<div role="status" aria-live="polite">
<span><i class="fa-solid fa-trophy" aria-hidden="true"></i> Score: <output id="score" name="score">0</output></span>
<span><i class="fa-solid fa-layer-group" aria-hidden="true"></i> Level: <output id="level" name="level">1</output></span>
<span><i class="fa-solid fa-heart" aria-hidden="true"></i> Lives: <output id="lives" name="lives">3</output></span>
<span><i class="fa-solid fa-shield-halved" aria-hidden="true"></i> Shield: <output id="shield" name="shield">100%</output></span>
<span><i class="fa-solid fa-gauge" aria-hidden="true"></i> FPS: <output id="fps" name="fps">0</output></span>
<span><i class="fa-regular fa-clock" aria-hidden="true"></i> Time: <output id="time" name="time">00:00</output></span>
</div>
</section>
<!-- Game Canvas -->
<section id="play" aria-label="Game Canvas">
<figure>
<figcaption>Canvas</figcaption>
<canvas id="myCanvas" width="1024" height="768">
Your browser does not support the HTML5 canvas tag.
</canvas>
</figure>
</section>
<!-- Controls Help -->
<section id="help" aria-label="Controls Guide">
<h2><i class="fa-solid fa-gamepad" aria-hidden="true"></i> Controls</h2>
<ul>
<li>Move: Arrow keys or WASD</li>
<li>Fire: Space</li>
<li>Special: Shift</li>
<li>Pause: P</li>
<li>Toggle Fullscreen: F</li>
</ul>
</section>
<!-- Assets and Audio Hooks (managed by app.js) -->
<section id="assets" hidden aria-hidden="true">
<h2><i class="fa-regular fa-image" aria-hidden="true"></i> Assets</h2>
<!-- Optional: let players provide an image URL for background/sprites (read by app.js) -->
<label for="bgImageUrl">Background image URL</label>
<input id="bgImageUrl" type="url" placeholder="https://example.com/space.jpg" inputmode="url">
<label for="spriteUpload">Upload sprite images</label>
<input id="spriteUpload" type="file" accept="image/*" multiple>
<!-- Preloadable audio (sources to be provided) -->
<audio id="sfxShoot" preload="auto">
<source src="assets/audio/shoot.mp3" type="audio/mpeg">
</audio>
<audio id="sfxExplosion" preload="auto">
<source src="assets/audio/explosion.mp3" type="audio/mpeg">
</audio>
<audio id="bgMusic" preload="auto" loop>
<source src="assets/audio/music.mp3" type="audio/mpeg">
</audio>
</section>
<!-- Leaderboard -->
<section id="leaderboard" aria-label="Leaderboard">
<h2><i class="fa-solid fa-ranking-star" aria-hidden="true"></i> Leaderboard</h2>
<ol id="scores">
<li data-initials="AAA" data-score="10000">AAA — 10,000</li>
<li data-initials="BBB" data-score="8000">BBB — 8,000</li>
<li data-initials="CCC" data-score="5000">CCC — 5,000</li>
</ol>
</section>
<!-- Settings Dialog (open via Settings button; JS can call showModal()) -->
<dialog id="settingsDialog" aria-labelledby="settingsTitle">
<form method="dialog">
<header>
<h2 id="settingsTitle">
<i class="fa-solid fa-sliders" aria-hidden="true"></i>
Settings
</h2>
</header>
<section>
<label for="difficulty">Difficulty</label>
<select id="difficulty" name="difficulty">
<option value="easy">Easy</option>
<option value="normal" selected>Normal</option>
<option value="hard">Hard</option>
<option value="insane">Insane</option>
</select>
<label for="graphics">Graphics</label>
<select id="graphics" name="graphics">
<option value="low">Low</option>
<option value="medium" selected>Medium</option>
<option value="high">High</option>
<option value="ultra">Ultra</option>
</select>
<label for="musicVolume">Music Volume</label>
<input id="musicVolume" name="musicVolume" type="range" min="0" max="100" value="60" />
<label for="sfxVolume">SFX Volume</label>
<input id="sfxVolume" name="sfxVolume" type="range" min="0" max="100" value="80" />
<label for="controlScheme">Control Scheme</label>
<select id="controlScheme" name="controlScheme">
<option value="wasd" selected>WASD + Space</option>
<option value="arrows">Arrows + Space</option>
<option value="custom">Custom</option>
</select>
</section>
<menu>
<button value="cancel" aria-label="Close settings">
<i class="fa-solid fa-xmark" aria-hidden="true"></i>
Close
</button>
<button value="apply" data-action="apply-settings" aria-label="Apply settings">
<i class="fa-solid fa-check" aria-hidden="true"></i>
Apply
</button>
</menu>
</form>
</dialog>
<!-- Pause Overlay (can be shown via JS) -->
<dialog id="pauseDialog" aria-labelledby="pauseTitle">
<form method="dialog">
<h2 id="pauseTitle"><i class="fa-solid fa-circle-pause" aria-hidden="true"></i> Paused</h2>
<p>Game is paused.</p>
<menu>
<button value="resume" data-action="resume">
<i class="fa-solid fa-play" aria-hidden="true"></i>
Resume
</button>
<button value="restart" data-action="reset">
<i class="fa-solid fa-rotate-right" aria-hidden="true"></i>
Restart
</button>
</menu>
</form>
</dialog>
<canvas id="myCanvas" width="1024" height="768" style="border:1px solid #d3d3d3;">
Your browser does not support the HTML5 canvas tag.</canvas>
</main>
<script src="app.js">
<footer>
<small>
<i class="fa-regular fa-circle-question" aria-hidden="true"></i>
Tip: Ensure app.js wires up data-action buttons, dialog show/close, audio, and canvas rendering.
</small>
</footer>
</script>
<noscript>
JavaScript is required to play this game.
</noscript>
<!-- Keep your game logic here -->
<script defer src="app.js"></script>
</body>
</html>
</html>

@ -212,7 +212,7 @@ curl --request POST \
http://localhost:5000/api/accounts/test/transactions
```
Try refreshing your the dashboard page in the browser now. What happens? Do you see the new transaction?
Try refreshing your dashboard page in the browser now. What happens? Do you see the new transaction?
The state is persisted indefinitely thanks to the `localStorage`, but that also means it's never updated until you log out of the app and log in again!

@ -2,191 +2,286 @@
// Constants
// ---------------------------------------------------------------------------
const serverUrl = 'http://localhost:5000/api';
const serverUrl = 'http://localhost:5000/api'; // reserved for future server swap
const storageKey = 'savedAccount';
const accountsKey = 'accounts'; // New key for all accounts
const accountsKey = 'accounts';
const schemaKey = 'schemaVersion';
const schemaVersion = 1;
// ---------------------------------------------------------------------------
// Router
// Intl helpers
// ---------------------------------------------------------------------------
const routes = {
  '/dashboard': { title: 'My Account', templateId: 'dashboard', init: refresh },
  '/login': { title: 'Login', templateId: 'login' }
};
function navigate(path) {
  window.history.pushState({}, path, window.location.origin + path);
  updateRoute();
const userLocale = navigator.language || 'en-IN';
function isIsoCurrency(code) {
if (!code) return false;
const c = String(code).trim().toUpperCase();
if (!/^[A-Z]{3}$/.test(c)) return false;
// Verify against supported values when available
try {
if (typeof Intl.supportedValuesOf === 'function') {
return Intl.supportedValuesOf('currency').includes(c);
}
} catch {}
return true; // fallback accept 3-letter code
}
function updateRoute() {
  const path = window.location.pathname;
  const route = routes[path];
  if (!route) {
    return navigate('/dashboard');
  }
  const template = document.getElementById(route.templateId);
  const view = template.content.cloneNode(true);
  const app = document.getElementById('app');
  app.innerHTML = '';
  app.appendChild(view);
function toCurrency(amount, currency) {
const n = Number(amount);
if (!Number.isFinite(n)) return String(amount);
const c = String(currency || '').trim();
if (isIsoCurrency(c)) {
// Accounting style shows negatives in parentheses if supported
const fmt = new Intl.NumberFormat(userLocale, {
style: 'currency',
currency: c.toUpperCase(),
currencySign: 'accounting',
maximumFractionDigits: 2
});
return fmt.format(n);
}
// Fallback: symbol + localized number
const num = new Intl.NumberFormat(userLocale, {
maximumFractionDigits: 2
}).format(n);
return c ? `${c} ${num}` : num;
}
  if (typeof route.init === 'function') {
    route.init();
  }
function toDate(dateStr) {
// Expect yyyy-mm-dd; fallback to today if invalid
const d = dateStr ? new Date(dateStr) : new Date();
if (Number.isNaN(d.getTime())) return new Date();
return d;
}
  document.title = route.title;
function formatDate(dateStr) {
const d = toDate(dateStr);
const fmt = new Intl.DateTimeFormat(userLocale, { dateStyle: 'medium' });
return fmt.format(d);
}
// ---------------------------------------------------------------------------
// API interactions (replaced with localStorage logic)
// Storage and state
// ---------------------------------------------------------------------------
function safeParse(json, fallback) {
try { return JSON.parse(json); } catch { return fallback; }
}
function getAccounts() {
  return JSON.parse(localStorage.getItem(accountsKey) || '[]');
return safeParse(localStorage.getItem(accountsKey), []);
}
function saveAccounts(accounts) {
  localStorage.setItem(accountsKey, JSON.stringify(accounts));
}
function migrateSchema() {
const v = Number(localStorage.getItem(schemaKey) || 0);
if (v >= schemaVersion) return;
let accounts = getAccounts();
// Example migration scaffolding:
// if (v < 1) { /* future migrations */ }
saveAccounts(accounts);
localStorage.setItem(schemaKey, String(schemaVersion));
}
function findAccount(user) {
  const accounts = getAccounts();
  return accounts.find(acc => acc.user === user) || null;
}
async function getAccount(user) {
  // Simulate async
  return new Promise(resolve => {
    setTimeout(() => {
      const acc = findAccount(user);
      if (!acc) resolve({ error: 'Account not found' });
      else resolve(acc);
    }, 100);
  });
return new Promise(resolve => {
setTimeout(() => {
const acc = findAccount(user);
resolve(acc || { error: 'Account not found' });
}, 60);
});
}
function uuid() {
// Works on HTTPS and localhost; fallback otherwise
if (globalThis.crypto && typeof crypto.randomUUID === 'function') {
return crypto.randomUUID();
}
return 'tx-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 8);
}
async function createAccount(accountJson) {
  return new Promise(resolve => {
    setTimeout(() => {
      let data;
      try {
        data = JSON.parse(accountJson);
      } catch (e) {
        return resolve({ error: 'Malformed account data' });
      }
      if (!data.user) return resolve({ error: 'Username required' });
      if (findAccount(data.user)) return resolve({ error: 'User already exists' });
      // Set up initial account structure
      const newAcc = {
        user: data.user,
        description: data.description || '',
        balance: 0,
        currency: data.currency || 'USD',
        transactions: []
      };
      const accounts = getAccounts();
      accounts.push(newAcc);
      saveAccounts(accounts);
      resolve(newAcc);
    }, 100);
  });
return new Promise(resolve => {
setTimeout(() => {
const data = safeParse(accountJson, null);
if (!data) return resolve({ error: 'Malformed account data' });
const user = String(data.user || '').trim();
if (!user) return resolve({ error: 'Username required' });
if (findAccount(user)) return resolve({ error: 'User already exists' });
const currency = String(data.currency || 'INR').trim();
const newAcc = {
user,
description: String(data.description || ''),
balance: Number(data.balance || 0) || 0,
currency: isIsoCurrency(currency) ? currency.toUpperCase() : currency,
transactions: []
};
const accounts = getAccounts();
accounts.push(newAcc);
saveAccounts(accounts);
resolve(newAcc);
}, 80);
});
}
async function createTransaction(user, transactionJson) {
  return new Promise(resolve => {
    setTimeout(() => {
      const accounts = getAccounts();
      const idx = accounts.findIndex(acc => acc.user === user);
      if (idx === -1) return resolve({ error: 'Account not found' });
      const tx = JSON.parse(transactionJson);
      tx.amount = parseFloat(tx.amount);
      tx.date = tx.date || new Date().toISOString().slice(0, 10);
      accounts[idx].balance += tx.amount;
      accounts[idx].transactions.push(tx);
      saveAccounts(accounts);
      resolve(tx);
    }, 100);
  });
return new Promise(resolve => {
setTimeout(() => {
const tx = safeParse(transactionJson, null);
if (!tx) return resolve({ error: 'Malformed transaction data' });
const amount = Number(tx.amount);
const object = String(tx.object || '').trim();
const dateStr = tx.date || new Date().toISOString().slice(0, 10);
if (!Number.isFinite(amount)) return resolve({ error: 'Amount must be a valid number' });
if (!object) return resolve({ error: 'Object is required' });
const accounts = getAccounts();
const idx = accounts.findIndex(acc => acc.user === user);
if (idx === -1) return resolve({ error: 'Account not found' });
const newTx = {
id: uuid(),
date: dateStr,
object,
amount
};
// Update balance and push transaction
accounts[idx].balance = (Number(accounts[idx].balance) || 0) + amount;
accounts[idx].transactions = (accounts[idx].transactions || []);
accounts[idx].transactions.push(newTx);
saveAccounts(accounts);
resolve(newTx);
}, 80);
});
}
// ---------------------------------------------------------------------------
// Global state
// ---------------------------------------------------------------------------
// Keep a frozen state object to avoid accidental mutations
let state = Object.freeze({
  account: null
});
function updateState(property, newData) {
  state = Object.freeze({
    ...state,
    [property]: newData
  });
  localStorage.setItem(storageKey, JSON.stringify(state.account));
state = Object.freeze({ ...state, [property]: newData });
// Persist active account only
localStorage.setItem(storageKey, JSON.stringify(state.account));
}
// Cross-tab sync: refresh when accounts or active account change in another tab
window.addEventListener('storage', (e) => {
if (e.key === accountsKey || e.key === storageKey) {
if (state.account?.user) {
refresh().catch(() => {});
}
}
});
// ---------------------------------------------------------------------------
// Login/register
// DOM helpers
// ---------------------------------------------------------------------------
// Client-side validation for the login form
function validateLogin(form) {
const user = form.user.value.trim();
if (user === '') {
updateElement('loginError', 'Username cannot be empty.');
return false;
}
updateElement('loginError', ''); // Clear previous error
return true;
function qs(id) {
return document.getElementById(id);
}
// Client-side validation for the register form
function validateRegister(form) {
const formData = new FormData(form);
const data = Object.fromEntries(formData);
updateElement('registerError', ''); // Clear previous error
function updateElement(id, textOrNode) {
const el = qs(id);
if (!el) return;
while (el.firstChild) el.removeChild(el.firstChild);
el.append(textOrNode);
}
if (data.user.trim() === '') {
updateElement('registerError', 'Username is required.');
return false;
}
if (data.currency.trim() === '') {
updateElement('registerError', 'Currency is required.');
return false;
// Resolve multiple possible IDs used by older markup
function setTextByAnyId(ids, text) {
for (const id of ids) {
const el = qs(id);
if (el) { el.textContent = text; return true; }
}
return true;
return false;
}
async function login() {
  const loginForm = document.getElementById('loginForm');
 
// Run client-side validation first
if (!validateLogin(loginForm)) {
return;
// ---------------------------------------------------------------------------
// Router
// ---------------------------------------------------------------------------
const routes = {
'/dashboard': { title: 'My Account', templateId: 'dashboard', init: refresh },
'/login': { title: 'Login', templateId: 'login', init: attachAuthHandlers }
};
function navigate(path) {
// Store path in history state and URL
history.pushState({ path }, '', path);
updateRoute();
}
function updateRoute() {
const path = history.state?.path || window.location.pathname;
const route = routes[path] || routes['/dashboard'];
const template = document.getElementById(route.templateId);
const view = template.content.cloneNode(true);
const app = document.getElementById('app');
app.innerHTML = '';
app.appendChild(view);
// Attach handlers after DOM is rendered
attachGlobalHandlers();
if (typeof route.init === 'function') {
Promise.resolve(route.init()).catch(err => console.error(err));
}
document.title = route.title;
}
  const user = loginForm.user.value;
  const data = await getAccount(user);
// Browser back/forward
window.addEventListener('popstate', () => updateRoute());
  if (data.error) {
    return updateElement('loginError', data.error);
  }
// ---------------------------------------------------------------------------
// Auth
// ---------------------------------------------------------------------------
async function login() {
const form = qs('loginForm');
if (!form) return;
if (!form.checkValidity()) { form.reportValidity(); return; }
const user = String(form.user.value || '').trim();
const data = await getAccount(user);
if (data.error) return updateElement('loginError', data.error);
  updateState('account', data);
  navigate('/dashboard');
}
async function register() {
  const registerForm = document.getElementById('registerForm');
// Run client-side validation first
if (!validateRegister(registerForm)) {
return;
}
const form = qs('registerForm');
if (!form) return;
if (!form.checkValidity()) { form.reportValidity(); return; }
const data = Object.fromEntries(new FormData(form));
data.user = String(data.user || '').trim();
data.currency = String(data.currency || '').trim();
data.description = String(data.description || '').trim();
data.balance = Number(data.balance || 0) || 0;
const jsonData = JSON.stringify(data);
const result = await createAccount(jsonData);
if (result.error) return updateElement('registerError', result.error);
  const formData = new FormData(registerForm);
  const data = Object.fromEntries(formData);
@ -201,20 +296,27 @@ async function register() {
  navigate('/dashboard');
}
function attachAuthHandlers() {
const loginForm = qs('loginForm');
if (loginForm) {
loginForm.addEventListener('submit', (e) => { e.preventDefault(); login(); });
}
const registerForm = qs('registerForm');
if (registerForm) {
registerForm.addEventListener('submit', (e) => { e.preventDefault(); register(); });
}
}
// ---------------------------------------------------------------------------
// Dashboard
// ---------------------------------------------------------------------------
async function updateAccountData() {
  const account = state.account;
  if (!account) {
    return logout();
  }
const account = state.account;
if (!account) return logout();
  const data = await getAccount(account.user);
  if (data.error) {
    return logout();
  }
const data = await getAccount(account.user);
if (data.error) return logout();
  updateState('account', data);
}
@ -225,75 +327,134 @@ async function refresh() {
}
function updateDashboard() {
  const account = state.account;
  if (!account) {
    return logout();
  }
  updateElement('description', account.description);
  updateElement('balance', account.balance.toFixed(2));
  updateElement('currency', account.currency); // Fixed ID for currency display
  // Update transactions
  const transactionsRows = document.createDocumentFragment();
  for (const transaction of account.transactions) {
    const transactionRow = createTransactionRow(transaction);
    transactionsRows.appendChild(transactionRow);
  }
  updateElement('transactions', transactionsRows);
}
const account = state.account;
if (!account) return logout();
// Description (support both #description and #transactions-description subtitles)
setTextByAnyId(['description', 'transactions-description'], account.description || 'Transactions');
// Balance and currency
const balanceText = toCurrency(account.balance, account.currency);
setTextByAnyId(['balance', 'balance-value'], balanceText);
// Some markups use a separate currency span; keep it empty when using formatted output
setTextByAnyId(['balance-currency', 'currency'], '');
// Transactions (sorted by date desc, then by insertion order)
const tbody = qs('transactions');
if (!tbody) return;
const frag = document.createDocumentFragment();
const template = document.getElementById('transaction');
const sorted = [...(account.transactions || [])]
.sort((a, b) => String(b.date).localeCompare(String(a.date)));
for (const tx of sorted) {
const row = template.content.cloneNode(true);
const tr = row.querySelector('tr');
const tds = tr.children;
tds[0].textContent = formatDate(tx.date);
tds[1].textContent = tx.object;
tds[2].textContent = toCurrency(tx.amount, account.currency);
if (tx.amount < 0) tr.classList.add('debit');
if (tx.amount > 0) tr.classList.add('credit');
frag.appendChild(row);
}
function createTransactionRow(transaction) {
  const template = document.getElementById('transaction');
  const transactionRow = template.content.cloneNode(true);
  const tr = transactionRow.querySelector('tr');
  tr.children[0].textContent = transaction.date;
  tr.children[1].textContent = transaction.object;
  tr.children[2].textContent = transaction.amount.toFixed(2);
  return transactionRow;
tbody.innerHTML = '';
tbody.appendChild(frag);
}
function addTransaction() {
  const dialog = document.getElementById('transactionDialog');
  dialog.classList.add('show');
const dialog = qs('transactionDialog');
if (!dialog) return;
dialog.classList.add('show');
// Reset form and set today
const form = qs('transactionForm');
if (form) {
form.reset();
form.date.valueAsDate = new Date();
// Move focus to first field
form.date.focus();
}
// Close handlers
const backdrop = dialog.querySelector('[data-dismiss]') || dialog;
backdrop.addEventListener('click', onDialogDismissClick);
dialog.addEventListener('keydown', onDialogKeydown);
}
  // Reset form
  const transactionForm = document.getElementById('transactionForm');
  transactionForm.reset();
function onDialogDismissClick(e) {
if (e.target?.hasAttribute?.('data-dismiss')) {
cancelTransaction();
}
}
  // Set date to today
  transactionForm.date.valueAsDate = new Date();
function onDialogKeydown(e) {
if (e.key === 'Escape') {
cancelTransaction();
}
}
async function confirmTransaction() {
  const dialog = document.getElementById('transactionDialog');
  dialog.classList.remove('show');
const form = qs('transactionForm');
if (!form) return;
// Inline validation
const amountVal = Number(form.amount.value);
const objectVal = String(form.object.value || '').trim();
if (!Number.isFinite(amountVal)) {
setFormError('transactionError', 'Amount must be a valid number');
return;
}
if (!objectVal) {
setFormError('transactionError', 'Object is required');
return;
}
  const transactionForm = document.getElementById('transactionForm');
clearFormError('transactionError');
  const formData = new FormData(transactionForm);
  const jsonData = JSON.stringify(Object.fromEntries(formData));
  const data = await createTransaction(state.account.user, jsonData);
const jsonData = JSON.stringify(Object.fromEntries(new FormData(form)));
const data = await createTransaction(state.account.user, jsonData);
  if (data.error) {
    return updateElement('transactionError', data.error);
  }
if (data.error) {
setFormError('transactionError', data.error);
return;
}
  // Update local state with new transaction
  const newAccount = {
    ...state.account,
    balance: state.account.balance + data.amount,
    transactions: [...state.account.transactions, data]
  }
  updateState('account', newAccount);
// Update local state
const newAccount = {
...state.account,
balance: (Number(state.account.balance) || 0) + data.amount,
transactions: [...(state.account.transactions || []), data]
};
updateState('account', newAccount);
// Close dialog and update view
cancelTransaction();
updateDashboard();
}
  // Update display
  updateDashboard();
function setFormError(id, message) {
updateElement(id, message);
const el = qs(id);
if (el) el.focus();
}
function clearFormError(id) {
const el = qs(id);
if (el) el.textContent = '';
}
async function cancelTransaction() {
  const dialog = document.getElementById('transactionDialog');
  dialog.classList.remove('show');
function cancelTransaction() {
const dialog = qs('transactionDialog');
if (!dialog) return;
dialog.classList.remove('show');
dialog.removeEventListener('keydown', onDialogKeydown);
const opener = document.querySelector('button[onclick="addTransaction()"]');
if (opener) opener.focus();
}
function logout() {
@ -302,13 +463,31 @@ function logout() {
}
// ---------------------------------------------------------------------------
// Utils
// Global listeners
// ---------------------------------------------------------------------------
function updateElement(id, textOrNode) {
  const element = document.getElementById(id);
  element.textContent = ''; // Removes all children
  element.append(textOrNode);
function attachGlobalHandlers() {
// Intercept form submissions for inline handlers in markup
const loginForm = qs('loginForm');
if (loginForm && !loginForm.__wired) {
loginForm.__wired = true;
loginForm.addEventListener('submit', (e) => { e.preventDefault(); login(); });
}
const registerForm = qs('registerForm');
if (registerForm && !registerForm.__wired) {
registerForm.__wired = true;
registerForm.addEventListener('submit', (e) => { e.preventDefault(); register(); });
}
const txForm = qs('transactionForm');
if (txForm && !txForm.__wired) {
txForm.__wired = true;
txForm.addEventListener('submit', (e) => { e.preventDefault(); confirmTransaction(); });
}
const cancelBtn = document.querySelector('#transactionDialog [data-dismiss]');
if (cancelBtn && !cancelBtn.__wired) {
cancelBtn.__wired = true;
cancelBtn.addEventListener('click', cancelTransaction);
}
}
// ---------------------------------------------------------------------------
@ -316,15 +495,21 @@ function updateElement(id, textOrNode) {
// ---------------------------------------------------------------------------
function init() {
  // Restore state
  const savedState = localStorage.getItem(storageKey);
  if (savedState) {
    updateState('account', JSON.parse(savedState));
  }
// Schema migration
migrateSchema();
// Restore active account
const saved = safeParse(localStorage.getItem(storageKey), null);
if (saved) updateState('account', saved);
// Seed history state if missing
if (!history.state || !history.state.path) {
const initialPath = state.account ? '/dashboard' : '/login';
history.replaceState({ path: initialPath }, '', initialPath);
}
  // Update route for browser back/next buttons
  window.onpopstate = () => updateRoute();
  updateRoute();
// Initial route render
updateRoute();
}
init();

@ -1,47 +1,96 @@
<!DOCTYPE html>
<html lang="en">
<html lang="en" data-theme="auto">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">
<meta name="theme-color" content="#0a7cff">
<title>Squirrel Banking</title>
<!-- Local Font Awesome (ensure this path exists locally) -->
<link rel="stylesheet" href="assets/fontawesome/css/all.min.css">
<!-- App styles -->
<link rel="stylesheet" href="styles.css">
<!-- App logic -->
<script src="app.js" defer></script>
</head>
<body>
<!-- Placeholder where we will insert our app HTML based on route -->
<div id="app">Loading...</div>
<noscript>Please enable JavaScript to use this app.</noscript>
<!-- SPA mount point -->
<div id="app" aria-live="polite">Loading…</div>
<!-- Login page template -->
<template id="login">
<section class="login-page">
<div class="login-container">
<div class="login-title text-center">
<span class="hide-xs">Squirrel</span>
<img class="login-logo" src="logo.svg" alt="Squirrel Banking Logo">
<span class="hide-xs">Banking</span>
<section class="page page-auth" aria-labelledby="loginTitle">
<div class="auth-card">
<div class="brand">
<i class="fa-solid fa-squirrel brand-icon" aria-hidden="true"></i>
<h1 id="loginTitle" class="brand-title">
<span class="hide-xs">Squirrel</span>
<img class="brand-logo" src="logo.svg" alt="Squirrel Banking Logo">
<span class="hide-xs">Banking</span>
</h1>
</div>
<div class="login-content">
<h2 class="text-center">Login</h2>
<form id="loginForm" action="javascript:login()">
<label for="username">Username</label>
<input id="username" name="user" type="text" maxlength="20" required>
<div id="loginError" class="error" role="alert"></div>
<button>Login</button>
</form>
<p class="login-separator text-center"><span>OR</span></p>
<h2 class="text-center">Register</h2>
<form id="registerForm" action="javascript:register()">
<label for="user">Username (required)</label>
<input id="user" name="user" type="text" maxlength="20" required>
<label for="currency">Currency (required)</label>
<input id="currency" name="currency" type="text" maxlength="5" value="$" required>
<label for="description">Description</label>
<input id="description" name="description" type="text" maxlength="100">
<label for="current-balance">Current balance</label>
<input id="current-balance" name="balance" type="number" value="0">
<div id="registerError" class="error" role="alert"></div>
<button>Register</button>
</form>
<div class="auth-panels">
<section class="auth-panel" aria-labelledby="loginHeading">
<h2 id="loginHeading" class="panel-title">
<i class="fa-solid fa-right-to-bracket" aria-hidden="true"></i> Login
</h2>
<form id="loginForm" action="javascript:void(0)" novalidate>
<label for="username" class="field-label">Username</label>
<div class="field">
<i class="fa-solid fa-user" aria-hidden="true"></i>
<input id="username" name="user" type="text" maxlength="20" autocomplete="username" required>
</div>
<div id="loginError" class="error" role="alert" aria-live="polite"></div>
<button class="btn btn-primary" type="submit">
<i class="fa-solid fa-arrow-right-to-bracket" aria-hidden="true"></i>
Login
</button>
</form>
</section>
<div class="auth-sep" role="separator" aria-label="Or">OR</div>
<section class="auth-panel" aria-labelledby="registerHeading">
<h2 id="registerHeading" class="panel-title">
<i class="fa-solid fa-user-plus" aria-hidden="true"></i> Register
</h2>
<form id="registerForm" action="javascript:void(0)" novalidate>
<label for="user" class="field-label">Username (required)</label>
<div class="field">
<i class="fa-solid fa-id-badge" aria-hidden="true"></i>
<input id="user" name="user" type="text" maxlength="20" autocomplete="username" required>
</div>
<label for="currency" class="field-label">Currency (required)</label>
<div class="field">
<i class="fa-solid fa-coins" aria-hidden="true"></i>
<input id="currency" name="currency" type="text" maxlength="5" placeholder="INR or ₹" required>
</div>
<label for="description" class="field-label">Description</label>
<div class="field">
<i class="fa-solid fa-note-sticky" aria-hidden="true"></i>
<input id="description" name="description" type="text" maxlength="100" placeholder="About the account">
</div>
<label for="current-balance" class="field-label">Current balance</label>
<div class="field">
<i class="fa-solid fa-wallet" aria-hidden="true"></i>
<input id="current-balance" name="balance" type="number" value="0" step="any" inputmode="decimal">
</div>
<div id="registerError" class="error" role="alert" aria-live="polite"></div>
<button class="btn btn-ghost" type="submit">
<i class="fa-solid fa-user-check" aria-hidden="true"></i>
Register
</button>
</form>
</section>
</div>
</div>
</section>
@ -49,48 +98,109 @@
<!-- Dashboard page template -->
<template id="dashboard">
<section class="dashboard-page">
<header class="dashboard-header">
<img class="dashboard-logo" src="logo.svg" alt="Squirrel Banking Logo">
<h1 class="dashboard-title hide-xs">Squirrel Banking</h1>
<button onclick="logout()">Logout</button>
<section class="page page-dash">
<header class="dash-header">
<div class="brand compact">
<img class="brand-logo" src="logo.svg" alt="">
<h1 class="brand-title hide-xs">Squirrel Banking</h1>
</div>
<div class="header-actions">
<button class="btn btn-icon" type="button" id="themeToggle" aria-label="Toggle theme">
<i class="fa-solid fa-moon" aria-hidden="true"></i>
</button>
<button class="btn btn-icon" type="button" onclick="logout()" aria-label="Logout">
<i class="fa-solid fa-right-from-bracket" aria-hidden="true"></i>
</button>
</div>
</header>
<div class="balance">
<div>Balance</div>
<span id="balance"></span>
<span id="balance-currency"></span>
</div>
<div class="dashboard-content">
<section class="summary">
<div class="balance" aria-live="polite">
<div class="balance-label">
<i class="fa-solid fa-chart-line" aria-hidden="true"></i>
Balance
</div>
<div class="balance-value">
<span id="balance">0</span>
<span id="balance-currency"></span>
</div>
</div>
<div class="account-meta">
<i class="fa-solid fa-circle-info" aria-hidden="true"></i>
<span id="account-info" class="muted"></span>
</div>
</section>
<section class="dash-content">
<div class="transactions-title">
<h2 id="transactions-description"></h2>
<button onclick="addTransaction()">Add transaction</button>
<h2 id="transactions-description">
<i class="fa-solid fa-receipt" aria-hidden="true"></i>
Transactions
</h2>
<button class="btn btn-primary" type="button" onclick="addTransaction()">
<i class="fa-solid fa-plus" aria-hidden="true"></i>
Add transaction
</button>
</div>
<table class="transactions-table" aria-label="Transactions">
<thead>
<tr>
<th>Date</th>
<th>Object</th>
<th>Amount</th>
<th scope="col"><i class="fa-solid fa-calendar-day" aria-hidden="true"></i> Date</th>
<th scope="col"><i class="fa-solid fa-file-lines" aria-hidden="true"></i> Object</th>
<th scope="col" class="num"><i class="fa-solid fa-indian-rupee-sign" aria-hidden="true"></i> Amount</th>
</tr>
</thead>
<tbody id="transactions"></tbody>
</table>
</div>
</section>
</section>
<section id="transactionDialog" class="dialog">
<div class="dialog-content">
<h2 class="text-center">Add transaction</h2>
<form id="transactionForm" action="javascript:void(0)">
<label for="date">Date</label>
<input id="date" name="date" type="date" required>
<label for="object">Object</label>
<input id="object" name="object" type="text" maxlength="50" required>
<label for="amount">Amount (use negative value for debit)</label>
<input id="amount" name="amount" type="number" value="0" step="any" required>
<div id="transactionError" class="error" role="alert"></div>
<!-- Modal dialog: Add transaction -->
<section id="transactionDialog" class="dialog" hidden>
<div class="dialog-backdrop" data-dismiss></div>
<div class="dialog-content"
role="dialog"
aria-modal="true"
aria-labelledby="txDialogTitle"
aria-describedby="txDialogDesc">
<h2 id="txDialogTitle" class="text-center">
<i class="fa-solid fa-square-plus" aria-hidden="true"></i>
Add transaction
</h2>
<p id="txDialogDesc" class="muted text-center">
Use negative amount for debit and positive for credit; Esc closes [web guideline aligned].
</p>
<form id="transactionForm" action="javascript:void(0)" novalidate>
<label for="date">Date</label>
<div class="field">
<i class="fa-solid fa-calendar-days" aria-hidden="true"></i>
<input id="date" name="date" type="date" required>
</div>
<label for="object">Object</label>
<div class="field">
<i class="fa-solid fa-tag" aria-hidden="true"></i>
<input id="object" name="object" type="text" maxlength="50" required>
</div>
<label for="amount">Amount (negative for debit)</label>
<div class="field">
<i class="fa-solid fa-money-bill-transfer" aria-hidden="true"></i>
<input id="amount" name="amount" type="number" value="0" step="any" inputmode="decimal" required>
</div>
<div id="transactionError" class="error" role="alert" aria-live="polite"></div>
<div class="dialog-buttons">
<button type="button" class="button-alt" formaction="javascript:cancelTransaction()" formnovalidate>Cancel</button>
<button formaction="javascript:confirmTransaction()">OK</button>
<button type="button" class="btn btn-ghost" data-dismiss>
<i class="fa-solid fa-xmark" aria-hidden="true"></i>
Cancel
</button>
<button type="submit" class="btn btn-primary">
<i class="fa-solid fa-check" aria-hidden="true"></i>
OK
</button>
</div>
</form>
</div>
@ -102,8 +212,8 @@
<tr>
<td></td>
<td></td>
<td></td>
<td class="num"></td>
</tr>
</template>
</body>
</html>
</html>

@ -1,125 +1,267 @@
/* ==========================================================================
Design Tokens and Theming
========================================================================== */
:root {
/* Colors */
/* Brand */
--primary: #0091ea;
--primary-light: #7bbde6;
--primary-600: #007ccc;
--primary-700: #0066a8;
/* Neutrals */
--text: #1b1f24;
--muted: #5a6370;
--white: #ffffff;
--surface: #ffffff;
--surface-2: #f5f7fb;
--border: #c7cdd8;
/* Accents */
--accent: #546e7a;
--grey: #445;
--error: #f52;
--error: #ff5522;
/* Backgrounds */
--background: #f5f5f6;
--background-accent: #cfe5f2;
--white: #fff;
--border: #99a;
/* Sizes */
--radius: 10px;
--space-xs: 5px;
--background-accent: #e8f2fb;
/* Effects */
--shadow-sm: 0 1px 2px rgba(16, 24, 40, .06);
--shadow-md: 0 6px 16px rgba(16, 24, 40, .12);
--focus: 3px solid rgba(0, 145, 234, .4);
/* Radius and Spacing */
--radius: 12px;
--radius-sm: 8px;
--space-2xs: 4px;
--space-xs: 6px;
--space-sm: 10px;
--space-md: 20px;
--space-lg: 28px;
--space-xl: 40px;
}
/* ------------------------------------------------------------------------ */
/* Micro reset */
/* ------------------------------------------------------------------------ */
/* Typography */
--font-ui: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", sans-serif;
--font-size: 16px;
/* Opt into user color schemes */
color-scheme: light dark;
}
/* Dark theme via user preference */
@media (prefers-color-scheme: dark) {
:root {
--text: #e9edf4;
--muted: #aab4c3;
--surface: #131720;
--surface-2: #0f131b;
--background: #0b0e14;
--background-accent: #0f1822;
--border: #2a3443;
--shadow-sm: 0 1px 2px rgba(0,0,0,.45);
--shadow-md: 0 8px 24px rgba(0,0,0,.5);
--focus: 3px solid rgba(0, 145, 234, .55);
}
}
* {
box-sizing: border-box;
/* Respect reduced motion */
@media (prefers-reduced-motion: reduce) {
* {
animation-duration: 1ms !important;
animation-iteration-count: 1 !important;
transition-duration: 1ms !important;
scroll-behavior: auto !important;
}
}
/* ==========================================================================
Base and Reset
========================================================================== */
* { box-sizing: border-box; }
html, body, #app {
margin: 0;
padding: 0;
height: 100%;
}
/* ------------------------------------------------------------------------ */
/* General styles */
/* ------------------------------------------------------------------------ */
html {
font-size: 100%;
}
body {
font-family: Arial, Helvetica, sans-serif;
font-size: 16px;
font-family: var(--font-ui);
font-size: var(--font-size);
color: var(--text);
background: var(--background);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
accent-color: var(--primary);
}
/* Utilities */
.text-center { text-align: center; }
.hide-xs { display: none; }
@media (min-width: 480px) { .hide-xs { display: initial; } }
.sr-only {
position: absolute;
width: 1px; height: 1px;
padding: 0; margin: -1px;
overflow: hidden; clip: rect(0, 0, 1px, 1px);
white-space: nowrap; border: 0;
}
/* Focus states (WCAG-friendly) */
:where(a, button, [role="button"], input, select, textarea):focus-visible {
outline: var(--focus);
outline-offset: 2px;
border-radius: 6px;
}
/* ==========================================================================
Headings and Text
========================================================================== */
h2 {
color: var(--primary);
text-transform: uppercase;
font-weight: bold;
font-weight: 700;
font-size: 1.5rem;
margin: var(--space-md) 0;
}
/* Muted text helper */
.muted { color: var(--muted); }
/* ==========================================================================
Forms
========================================================================== */
form {
display: flex;
flex-direction: column;
margin: var(--space-sm) var(--space-md);
gap: var(--space-xs);
}
input {
margin-top: var(--space-xs);
margin-bottom: var(--space-sm);
height: 45px;
padding: var(--space-xs) var(--space-sm);
label {
color: var(--muted);
text-transform: uppercase;
font-size: 80%;
letter-spacing: .02em;
}
input, select, textarea {
height: 44px;
padding: 0 var(--space-sm);
border: 1px solid var(--border);
border-radius: var(--radius);
border-radius: var(--radius-sm);
background: var(--surface);
color: var(--text);
box-shadow: var(--shadow-sm);
transition: border-color .18s ease, box-shadow .18s ease, background-color .18s ease;
}
input::placeholder,
textarea::placeholder { color: color-mix(in oklab, var(--muted) 80%, var(--text)); }
input:focus {
border-color: var(--primary);
border-color: var(--primary-600);
outline: 0;
box-shadow: 0 0 0 4px color-mix(in oklab, var(--primary) 20%, transparent);
}
label {
color: var(--grey);
text-transform: uppercase;
font-size: 80%;
.error {
color: var(--error);
margin: var(--space-2xs) 0;
}
button {
font-weight: bold;
.error:empty { display: none; }
/* Native UI controls pick up brand via accent-color */
input[type="checkbox"],
input[type="radio"],
input[type="range"],
progress { accent-color: var(--primary); }
/* ==========================================================================
Buttons
========================================================================== */
button,
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
font-weight: 700;
background-color: var(--primary);
color: var(--white);
height: 40px;
padding: var(--space-xs);
padding: 0 var(--space-md);
border: 0;
border-radius: var(--radius);
border-radius: var(--radius-sm);
text-transform: uppercase;
min-width: 100px;
min-width: 110px;
margin: var(--space-sm) 0;
box-shadow: var(--shadow-sm);
transition: filter .12s ease, transform .12s ease, background-color .12s ease;
}
.button-alt {
button:hover,
.btn:hover { filter: brightness(110%); cursor: pointer; }
button:active,
.btn:active { transform: translateY(1px); }
button:focus-visible,
.btn:focus-visible { outline: var(--focus); }
.button-alt,
.btn-ghost {
background-color: transparent;
color: var(--primary);
border: 1px solid color-mix(in oklab, var(--primary) 40%, var(--border));
box-shadow: none;
}
button:hover {
filter: brightness(115%);
cursor: pointer;
.btn-icon {
min-width: auto;
width: 40px; height: 40px;
padding: 0;
border-radius: 50%;
}
button:focus {
outline: none;
border: 3px solid var(--grey);
}
/* ==========================================================================
Login Page
========================================================================== */
.error {
color: var(--error);
margin: var(--space-xs) 0;
.login-page {
display: flex;
height: 100%;
align-items: center;
justify-content: center;
background:
radial-gradient(800px 500px at 20% -10%, color-mix(in oklab, var(--primary) 30%, transparent), transparent 60%),
radial-gradient(800px 500px at 120% 10%, color-mix(in oklab, var(--primary-600) 20%, transparent), transparent 55%),
linear-gradient(var(--primary), var(--primary-600));
}
.error:empty {
display: none;
.login-container {
flex: auto;
max-width: 520px;
max-height: 100%;
overflow: auto;
padding: var(--space-sm);
}
/* ------------------------------------------------------------------------ */
/* Login page */
/* ------------------------------------------------------------------------ */
.login-title {
font-size: 2rem;
font-weight: bold;
font-weight: 800;
color: var(--white);
margin: var(--space-md);
text-align: center;
}
.login-logo {
@ -127,24 +269,12 @@ button:focus {
vertical-align: middle;
}
.login-page {
display: flex;
height: 100%;
align-items: center;
justify-content: center;
background: linear-gradient(var(--primary), var(--primary-light));
}
.login-container {
flex: auto;
max-width: 480px;
max-height: 100%;
overflow: auto;
}
.login-content {
background-color: var(--background);
padding: var(--space-sm);
background-color: var(--surface);
padding: var(--space-md);
border-radius: var(--radius);
box-shadow: var(--shadow-md);
border: 1px solid var(--border);
}
.login-separator {
@ -157,14 +287,15 @@ button:focus {
.login-separator > span {
position: relative;
top: -0.5em;
background-color: var(--background);
padding: var(--space-sm);
top: -0.6em;
background-color: var(--surface);
padding: 0 var(--space-sm);
color: var(--muted);
}
/* ------------------------------------------------------------------------ */
/* Dashboard page */
/* ------------------------------------------------------------------------ */
/* ==========================================================================
Dashboard
========================================================================== */
.dashboard-page {
display: flex;
@ -172,23 +303,35 @@ button:focus {
flex-direction: column;
}
/* Make content responsive to its own width (container queries) */
.dashboard-content {
width: 100%;
max-width: 960px;
align-self: center;
padding: var(--space-sm);
container-type: inline-size;
container-name: dash;
}
.dashboard-header {
background-color: var(--grey);
padding: 0 var(--space-sm)
background-color: var(--accent);
padding: var(--space-xs) var(--space-sm);
display: flex;
align-items: center;
justify-content: space-between;
}
.dashboard-header button {
float: right;
border: 1px solid;
border: 1px solid color-mix(in oklab, var(--white) 35%, transparent);
background-color: transparent;
color: var(--white);
}
.dashboard-title {
font-size: 1.5rem;
font-weight: bold;
font-weight: 800;
color: var(--white);
vertical-align: middle;
margin: 0 var(--space-sm)
margin: 0 var(--space-sm);
}
.dashboard-logo {
@ -198,19 +341,22 @@ button:focus {
}
.balance {
background: radial-gradient(circle at center, var(--primary), var(--primary-light));
background: radial-gradient(circle at center, var(--primary), var(--primary-600));
text-align: center;
padding: var(--space-md) var(--space-sm);
}
.balance > div {
color: var(--white);
padding-top: var(--space-xs);
padding-top: var(--space-2xs);
text-transform: uppercase;
letter-spacing: .08em;
}
.balance > span {
color: var(--white);
font-size: 3rem;
font-size: clamp(2rem, 5vw, 3rem);
font-weight: 800;
}
.transactions-title {
@ -219,8 +365,9 @@ button:focus {
align-items: center;
padding: 0 var(--space-sm);
color: var(--accent);
font-weight: bold;
font-size: 1.5rem;
font-weight: 800;
font-size: 1.25rem;
gap: var(--space-sm);
}
.transactions-title > div {
@ -229,21 +376,30 @@ button:focus {
white-space: nowrap;
}
/* ==========================================================================
Transactions Table
========================================================================== */
.transactions-table {
width: 100%;
font-size: 1.2rem;
font-size: 1.05rem;
padding: var(--space-sm);
margin: 0;
border-spacing: 0;
background-color: var(--background);
background-color: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
box-shadow: var(--shadow-sm);
}
.transactions-table thead th {
border-bottom: 1px solid var(--border);
color: var(--muted);
font-weight: 700;
}
.transactions-table tr:nth-child(even) {
background-color: var(--background-accent);
background-color: var(--surface-2);
}
.transactions-table td,
@ -254,7 +410,6 @@ button:focus {
.transactions-table td:first-child,
.transactions-table th:first-child {
/* Make first column use the minimum width */
width: 1%;
white-space: nowrap;
}
@ -264,88 +419,90 @@ button:focus {
text-align: right;
}
/* Condense table in narrow containers */
@container dash (width < 520px) {
.transactions-table {
font-size: 1rem;
border-radius: var(--radius-sm);
padding: var(--space-xs);
}
}
/* ==========================================================================
Dialog (Modal)
========================================================================== */
.dialog {
display: none;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
left: 0;
top: 0;
position: fixed;
inset: 0;
overflow: auto;
background-color: rgba(0,0,0,0.4);
animation: slideFromTop 0.3s ease-in-out;
background-color: rgba(0,0,0,.45);
justify-content: center;
align-items: flex-start;
padding: var(--space-sm);
z-index: 1000;
}
.dialog.show {
display: flex;
}
@keyframes slideFromTop {
from {
top: -300px;
opacity: 0;
}
to {
top: 0;
opacity: 1;
}
}
.dialog.show { display: flex; }
.dialog-content {
flex: auto;
background-color: var(--white);
max-width: 480px;
background-color: var(--surface);
color: var(--text);
max-width: 520px;
max-height: 100%;
padding: var(--space-sm);
}
.dialog-buttons {
text-align: right;
padding: var(--space-md);
border-radius: var(--radius);
border: 1px solid var(--border);
box-shadow: var(--shadow-md);
margin-top: var(--space-xl);
transform: translateY(-8px);
animation: dialogIn .25s ease-out both;
}
/* ------------------------------------------------------------------------ */
/* Utilities */
/* ------------------------------------------------------------------------ */
.text-center {
text-align: center;
@keyframes dialogIn {
from { opacity: 0; transform: translateY(-14px); }
to { opacity: 1; transform: translateY(0); }
}
.hide-xs {
display: none;
.dialog-buttons {
display: flex;
justify-content: flex-end;
gap: var(--space-sm);
}
/* ------------------------------------------------------------------------ */
/* Responsive adaptations */
/* ------------------------------------------------------------------------ */
@media only screen and (min-width: 480px) {
.hide-xs {
display: initial;
}
/* ==========================================================================
Responsive
========================================================================== */
@media (min-width: 480px) {
.login-content,
.dialog-content {
border-radius: var(--radius);
}
}
.dialog-content {
margin-top: var(--space-xl);
@media (min-width: 768px) {
.dashboard-content {
max-width: 900px;
}
}
@media only screen and (min-width: 768px) {
.transactions-table {
border-radius: var(--radius);
}
/* ==========================================================================
Legacy compatibility mappings (existing class names)
========================================================================== */
.dashboard-content {
width: 100%;
max-width: 768px;
align-self: center;
}
.button-alt { /* keep existing alias */
background-color: transparent;
color: var(--primary);
border: 1px solid color-mix(in oklab, var(--primary) 40%, var(--border));
}
/* Keep original variable names for backward compatibility where possible */
:root {
--space-xs: var(--space-xs);
--space-sm: var(--space-sm);
--space-md: var(--space-md);
--space-xl: var(--space-xl);
}

@ -1,161 +1,164 @@
# Using a code editor
This lesson covers the basics of using [VSCode.dev](https://vscode.dev) a web-based code editor so that you can make changes to your code and contribute to a project without installing anything on your computer.
<!----
TODO: add an optional image
![Using a code editor](../../sketchnotes/webdev101-vscode-dev.png)
> Sketchnote by [Author name](https://example.com)
---->
***
<!---
## Pre-Lecture Quiz
[Pre-lecture quiz](https://ff-quizzes.netlify.app/web/quiz/3)
---->
# Using a Code Editor: Mastering [VSCode.dev](https://vscode.dev)
## Learning objectives
**Welcome!**
This lesson takes you from the basics to advanced use of [VSCode.dev](https://vscode.dev)—the powerful, web-based code editor. Youll learn how to confidently edit code, manage projects, track changes, install extensions, and collaborate like a pro—all from your browser, with zero installation required.
In this lesson, you'll learn how to:
***
- Use a code editor in a code project
- Keep track of changes with version control
- Customize the editor for development
## Learning Objectives
### Prerequisites
By the end of this lesson, youll be able to:
Before you begin, you'll need to create an account with [GitHub](https://github.com). Navigate to [GitHub](https://github.com/) and create an account if you haven't already.
- Efficiently use a code editor on any project, anywhere
- Seamlessly track your work with built-in version control
- Personalize and boost your development workflow with editor customizations and extensions
### Introduction
***
A code editor is an essential tool for writing programs and collaborating with existing coding projects. Once you understand the basics of an editor and how to make use of its features, you'll be able to apply them when writing code.
## Prerequisites
## Getting started with VSCode.dev
To get started, **sign up for a free [GitHub](https://github.com) account**, which lets you manage code repositories and collaborate worldwide. If you dont have an account yet, [create one here](https://github.com/).
[VSCode.dev](https://vscode.dev) is a code editor on the web. You don't need to install anything in order to use it, just like opening any other website. To get started with the editor, open the following link: [https://vscode.dev](https://vscode.dev). If you aren't signed in into [GitHub](https://github.com/), follow the prompts to sign in or create a new account and then sign in.
***
Once it loads, it should look similar to this image:
## Why Use a Web-based Code Editor?
![Default VSCode.dev](../images/default-vscode-dev.png)
A **code editor** like VSCode.dev is your command center for writing, editing, and managing code. With an intuitive interface, tons of features, and immediate access via the browser, you can:
There are three main sections, starting from the far left and moving to the right:
- Edit projects on any device
- Avoid the hassle of installations
- Collaborate and contribute instantly
1. The _activity bar_ which includes some icons, like the magnifying glass 🔎, the gear ⚙️, and a few others
1. The expanded activity bar which defaults to the _Explorer_, called the _side bar_.
1. And finally, the code area to the right.
Once youre comfortable with VSCode.dev, youll be prepared to tackle coding tasks from anywhere, anytime.
Click on each of the icons to display a different menu. Once done, click on the _Explorer_ so you are back where you started.
***
When you start creating code or modifying existing code, it will happen in the biggest area to the right. You'll use this area to visualize existing code as well, which you'll do next.
## Getting Started with VSCode.dev
Navigate to **[VSCode.dev](https://vscode.dev)**—no install, no downloads. Signing in with GitHub unlocks full access, including syncing your settings, extensions, and repositories. If prompted, connect your GitHub account.
## Open a GitHub repository
After loading, your workspace will look like this:
The first thing you'll need is to open a GitHub repository. There are multiple ways of opening a repository. In this section you'll see two different ways you can open a repository so that you can start working on changes.
![Default VSCode.dev](../images/default-vscode-dev has three core sections from left to right:
- **Activity bar:** The icons such as 🔎 (Search), ⚙️ (Settings), files, source control, etc.
- **Sidebar:** Changes context based on the activity bar icon selected (defaults to *Explorer* to show files).
- **Editor/code area:** The largest section to the right—where youll actually edit and view code.
### 1. With the editor
Click through the icons to explore features, but return to the _Explorer_ to keep your place.
Use the editor itself to open a remote repository. If you go to [VSCode.dev](https://vscode.dev) you will see an _"Open Remote Repository"_ button:
***
![Open remote repository](../images/open-remote-repository.png)
## Opening a GitHub Repository
You can also use the command palette. The command palette is an input box where you can type any word that is part of a command or an action to find the right command to execute. Use the menu to the top-left, then select _View_, and then pick _Command Palette_, or using the following keyboard shortcut: Ctrl-Shift-P (on MacOS it would be Command-Shift-P).
### Method 1: From the Editor
![Palette Menu](../images/palette-menu.png)
1. Go to [VSCode.dev](https://vscode.dev). Click **"Open Remote Repository."**
Once the menu opens, type _open remote repository_, and then select the first option. Multiple repositories that you are part of or that you've opened recently will show up. You can also use a full GitHub URL to select one. Use the following URL and paste into the box:
![Open remote repository](../images/open-remote-repository use the _Command Palette_ (Ctrl-Shift-P, or Cmd-Shift-P on Mac).
```
https://github.com/microsoft/Web-Dev-For-Beginners
```
![Palette Menu](../images/palette-menu.pngopen remote repository.”
- Select the option.
- Paste your GitHub repo URL (e.g., `https://github.com/microsoft/Web-Dev-For-Beginners`) and hit Enter.
✅ If successful, you'll see all files for this repository loaded in the text editor.
If successful, youll see the entire project loaded and ready to edit!
***
### 2. Using the URL
### Method 2: Instantly via URL
You can also use a url directly to load a repository. For example, the full URL for the current repo is [https://github.com/microsoft/Web-Dev-For-Beginners](https://github.com/microsoft/Web-Dev-For-Beginners), but you can swap the GitHub domain with `VSCode.dev/github` and load the repository directly. The resulting URL would be [https://vscode.dev/github/microsoft/Web-Dev-For-Beginners](https://vscode.dev/github/microsoft/Web-Dev-For-Beginners).
Transform any GitHub repo URL to open directly in VSCode.dev by replacing `github.com` with `vscode.dev/github`.
E.g.:
- GitHub: `https://github.com/microsoft/Web-Dev-For-Beginners`
- VSCode.dev: `https://vscode.dev/github/microsoft/Web-Dev-For-Beginners`
## Edit files
Once you have opened the repository on the browser/ vscode.dev, the next step would be to make updates or changes to the project.
This feature supercharges quick access to ANY project.
### 1. Create a new file
***
You can either create a file inside an existing folder, or create it in the root directory/folder. To create a new file, open a location/directory to which you want the file to be saved and select the _'New file ...'_ icon on the activity bar _(left)_, give it a name and hit enter.
## Editing Files in Your Project
![Create a new file](../images/create-new-file.png)
Once your repo is open, you can:
### 2. Edit and save a file on the repository
### 1. **Create a New File**
- In the *Explorer* sidebar, navigate to your desired folder or use the root.
- Click the _New file ..._ icon.
- Name your file, press **Enter**, and your file appears instantly.
Using vscode.dev is helpful whenever you want to make quick updates to your project without having to load any software locally.
To update your code, click the 'Explorer' icon, also located on the activity bar to view files & folders in the repository.
Select a file to open it on the code area, make your changes and save.
![Create a new file](../images/create-new-file 2. **Edit and Save Files**
![Edit a file](../images/edit-a-file.png)
- Click on a file in the *Explorer* to open it in the code area.
- Make your changes as needed.
- VSCode.dev automatically saves your changes, but you can press Ctrl+S to save manually.
Once you are done updating your project, select the _`source control`_ icon which contains all the new changes you have made to your repository.
![Edit a file](../images/edit-a-file.png. **Track & Commit Changes with Version Control**
To view the changes you made to your project, select the file(s) in the `Changes` folder in the expanded activity bar. This will open a 'Working Tree' for you to visually see the changes you made to the file. Red shows an omission to the project, while green signifies an addition.
VSCode.dev has integrated **Git** version control!
![View changes](../images/working-tree.png)
- Click the _'Source Control'_ icon to view all changes made.
- Files in the `Changes` folder show additions (green) and deletions (red).
![View changes](../images/working-tree.png changes by clicking the `+` next to files to prepare for commit.
- **Discard** unwanted changes by clicking the undo icon.
- Type a clear commit message, then click the checkmark to commit and push.
If you are satisfied with the changes you made, hover on the `Changes` folder and click the `+` button to stage the changes. Staging simply means preparing your changes to commit them to GitHub.
To return to your repository on GitHub, select the hamburger menu at the top left.
If however you are not comfortable with some changes and you want to discard them, hover on the `Changes` folder and select the `undo` icon.
![Stage & commit changes](../images/edit-vscode.dev Up with Extensions
Then, type in a `commit message` _(A description of the change you have made to the project)_, click the `check icon` to commit and push your changes.
Extensions let you add languages, themes, debuggers, and productivity tools to VSCode.dev—making your coding life easier and more fun.
Once done working on your project, select the `hamburger menu icon` at the top left to return to the repository on github.com.
### Browsing and Managing Extensions
![Stage & commit changes](../images/edit-vscode.dev.gif)
- Click the **Extensions icon** on the activity bar.
- Search for an extension in the _'Search Extensions in Marketplace'_ box.
## Using extensions
Installing extensions on VSCode allows you to add new features and customized development environment options on your editor to improve your development workflow. These extensions also help you add support for multiple programming languages and are often either generic extensions or language-based extensions.
![Extension details](../images/extension-details:
- **Installed**: All extensions youve added
- **Popular**: Industry favorites
- **Recommended**: Tailored to your workflow
To browse through the list of all available extensions, click the _`Extensions icon`_ on the activity bar and start typing the name of the extension on the text field labelled _'Search Extensions in Marketplace'_.
You will see a list of extensions, each one containing **the extension name, publisher's name, a 1 sentence description, number of downloads** and **a star rating**.
![View extensions](
![Extension details](../images/extension-details.png)
You can also view all previously installed extensions by expanding the _`Installed folder`_ , popular extensions used by most developers in the _`Popular folder`_ and recommended extensions for you either by users in the same workspace or based on your recently opened files in the _`recommended folder`_ .
***
![View extensions](../images/extensions.png)
### 1. **Install Extensions**
- Enter the extensions name in search, click it, and review details in the editor.
- Hit the **blue Install button** in the sidebar _or_ in the main code area.
### 1. Install Extensions
To install an extension, type the extension's name in the search field and click on it to view additional information about the extension on the code area once it appears on the expanded activity bar.
![Install extensions](../images/install-extension 2. **Customize Extensions**
You can either click the _blue install button_ on the expanded activity bar to install or use the install button that appears on the code area once you select the extension to load additional information.
- Find your installed extension.
- Click the **Gear icon** → select _Extension Settings_ to fine-tune behaviors to your liking.
![Install extensions](../images/install-extension.gif)
![Modify extension settings](../images/extension-settings 3. **Manage Extensions**
You can:
### 2. Customize Extensions
After installing the extension, you may need to modify its behaviour and customize it based on your preferences. To do this, select the Extensions icon, and this time, your extension will appear in the _Installed folder_, click on the _**Gear icon**_ and navigate to _Extensions Setting_
- **Disable:** Temporarily turn off an extension while keeping it installed
- **Uninstall:** Permanently remove it if no longer needed
![Modify extension settings](../images/extension-settings.png)
Find the extension, hit the Gear icon, and select Disable or Uninstall, or use the blue buttons in the code area.
### 3. Manage Extensions
After installing and using your extension, vscode.dev offers options to manage your extension based on different needs. For example, you may choose to:
- **Disable:** _(You temporarily disable an extension when you no longer need it but don't want to uninstall it completely)_
Select the installed extension on the expanded activity bar > click the Gear icon > select 'Disable' or 'Disable (Workspace)' **OR** Open the extension on the code area and click the blue Disable button.
- **Uninstall:** Select installed extension on the expanded activity bar > click the Gear icon > select 'Uninstall' **OR** Open the extension on the code area and click the blue Uninstall button.
***
## Assignment
Test your skills: [Create a resume website using vscode.dev](https://github.com/microsoft/Web-Dev-For-Beginners/blob/main/8-code-editor/1-using-a-code-editor/assignment.md)
---
***
## Assignment
[Create a resume website using vscode.dev](https://github.com/microsoft/Web-Dev-For-Beginners/blob/main/8-code-editor/1-using-a-code-editor/assignment.md)
## Further Exploration and Self-Study
- Dive deeper with [the official VSCode Web Docs](https://code.visualstudio.com/docs/editor/vscode-web?WT.mc_id=academic-0000-alfredodeza).
- Explore advanced workspace features, keyboard shortcuts, and settings.
<!----
## Post-Lecture Quiz
[Post-lecture quiz](https://ff-quizzes.netlify.app/web/quiz/4)
---->
***
## Review & Self Study
**Now youre ready to code, create, and collaborate—from anywhere, on any device, using VSCode.dev!**
Read more about [VSCode.dev](https://code.visualstudio.com/docs/editor/vscode-web?WT.mc_id=academic-0000-alfredodeza) and some of its other features.

@ -1,30 +1,56 @@
# api
from flask import Flask, request, jsonify
from llm import call_llm
from flask_cors import CORS
import logging
app = Flask(__name__)
CORS(app) # * example.com
# Configure CORS (allow all origins for development; restrict in production)
CORS(app, resources={r"/*": {"origins": "*"}})
# Set up logging
logging.basicConfig(level=logging.INFO)
@app.route("/", methods=["GET"])
def index():
return "Welcome to this lesson"
"""Root endpoint for API status."""
return jsonify({
"status": "ok",
"message": "Welcome to the Chat Project API"
})
@app.route("/health", methods=["GET"])
def health():
"""Health check endpoint."""
return jsonify({"status": "healthy"}), 200
@app.route("/test", methods=["GET"])
def test():
return "Test"
"""Simple test endpoint."""
return jsonify({"result": "Test successful"}), 200
@app.route("/hello", methods=["POST"])
def hello():
# get message from request body { "message": "do this taks for me" }
data = request.get_json()
message = data.get("message", "")
"""
Chat endpoint.
Expects JSON: { "message": "your message" }
Returns: { "response": "LLM response" }
"""
try:
data = request.get_json(force=True)
message = data.get("message", "").strip()
if not message:
logging.warning("No message provided in request.")
return jsonify({"error": "No message provided."}), 400
response = call_llm(message, "You are a helpful assistant.")
return jsonify({
"response": response
})
logging.info(f"Received message: {message}")
response = call_llm(message, "You are a helpful assistant.")
return jsonify({"response": response}), 200
except Exception as e:
logging.error(f"Error in /hello endpoint: {e}")
return jsonify({"error": "Internal server error."}), 500
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5000)
# Run the app with debug mode for development
app.run(host="0.0.0.0", port=5000, debug=True)

@ -1,29 +1,53 @@
import os
import logging
from openai import OpenAI
# To authenticate with the model you will need to generate a personal access token (PAT) in your GitHub settings.
# Create your PAT token by following instructions here: https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens
# Configure logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# Environment variable check for GitHub token
GITHUB_TOKEN = os.environ.get("GITHUB_TOKEN")
if not GITHUB_TOKEN:
logger.error("GITHUB_TOKEN environment variable not set.")
raise EnvironmentError("GITHUB_TOKEN environment variable not set.")
# Model and endpoint configuration
MODEL_NAME = os.environ.get("LLM_MODEL", "openai/gpt-4o-mini")
BASE_URL = os.environ.get("LLM_BASE_URL", "https://models.github.ai/inference")
# Initialize OpenAI client
client = OpenAI(
base_url="https://models.github.ai/inference",
api_key=os.environ["GITHUB_TOKEN"],
base_url=BASE_URL,
api_key=GITHUB_TOKEN,
)
def call_llm(prompt: str, system_message: str):
response = client.chat.completions.create(
messages=[
{
"role": "system",
"content": system_message,
},
{
"role": "user",
"content": prompt,
}
],
model="openai/gpt-4o-mini",
temperature=1,
max_tokens=4096,
top_p=1
)
def call_llm(prompt: str, system_message: str, temperature: float = 1.0, max_tokens: int = 4096, top_p: float = 1.0) -> str:
"""
Calls the LLM with the given prompt and system message.
Returns the model's response as a string.
"""
try:
logger.info(f"Calling LLM model '{MODEL_NAME}' with prompt: {prompt}")
response = client.chat.completions.create(
messages=[
{"role": "system", "content": system_message},
{"role": "user", "content": prompt}
],
model=MODEL_NAME,
temperature=temperature,
max_tokens=max_tokens,
top_p=top_p
)
content = response.choices[0].message.content
logger.info("LLM response received successfully.")
return content
except Exception as e:
logger.error(f"Error calling LLM: {e}")
return "Sorry, there was an error processing your request."
return response.choices[0].message.content
# Example usage (for testing)
if __name__ == "__main__":
test_prompt = "Hello, how are you?"
test_system = "You are a friendly assistant."
print(call_llm(test_prompt, test_system))

@ -1,35 +1,179 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Stellar AI Chat</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="styles.css">
</head>
<body>
<div class="app">
<header class="header">
<div style="display:flex;align-items:center;gap:12px;">
<div class="logo">🤖</div>
<div>
<h1>My company</h1>
<p class="subtitle">Dark-mode chat UI — powered by the backend AI</p>
</div>
<head>
<!-- Character encoding and viewport -->
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<!-- Explicit color-scheme support and order of preference -->
<meta name="color-scheme" content="dark light" />
<!-- Theming the browser UI for light/dark modes -->
<meta name="theme-color" media="(prefers-color-scheme: light)" content="#ffffff" />
<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#0b0b0f" />
<!-- Site metadata -->
<title>Stellar AI Chat</title>
<meta name="description" content="Accessible, dark-mode chat UI powered by a backend AI with live-region announcements and robust keyboard navigation." />
<meta name="referrer" content="no-referrer" />
<!-- PWA hooks (optional; provide files if used) -->
<link rel="manifest" href="manifest.webmanifest" />
<!-- Performance: DNS + connection warming -->
<link rel="dns-prefetch" href="https://fonts.googleapis.com" />
<link rel="dns-prefetch" href="https://fonts.gstatic.com" />
<link rel="dns-prefetch" href="https://cdnjs.cloudflare.com" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link rel="preconnect" href="https://cdnjs.cloudflare.com" />
<!-- Fonts: Inter variable -->
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@300..900&display=swap"
rel="stylesheet"
/>
<!-- Icons: Font Awesome 6 -->
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"
referrerpolicy="no-referrer"
/>
<!-- App styles -->
<link rel="stylesheet" href="styles.css" />
<!-- Respect reduced motion; define in CSS with @media (prefers-reduced-motion: reduce) -->
</head>
<body>
<noscript>
<p role="status" aria-live="polite">
JavaScript is required for live chat updates and sending messages.
</p>
</noscript>
<div class="app" id="app" data-theme="dark">
<header class="header">
<div class="brand">
<div class="logo" aria-hidden="true">
<i class="fa-solid fa-robot" aria-hidden="true"></i>
</div>
</header>
<div>
<h1 class="title">My company</h1>
<p class="subtitle">Darkmode chat UI — powered by the backend AI</p>
</div>
</div>
<nav class="actions" aria-label="Header actions">
<button
class="icon-btn"
id="themeToggle"
type="button"
aria-label="Toggle theme"
aria-pressed="false"
>
<i class="fa-solid fa-moon" aria-hidden="true"></i>
</button>
<button
class="icon-btn"
id="clearChat"
type="button"
aria-label="Clear chat"
>
<i class="fa-solid fa-trash" aria-hidden="true"></i>
</button>
<button
class="icon-btn"
id="settings"
type="button"
aria-label="Settings"
aria-haspopup="dialog"
aria-expanded="false"
>
<i class="fa-solid fa-gear" aria-hidden="true"></i>
</button>
</nav>
</header>
<main class="chat" id="chat">
<!-- Live message stream for assistive tech:
role="log" conveys an updating feed; aria-live polite avoids interruption; aria-relevant focuses announcements -->
<div
class="messages"
id="messages"
role="log"
aria-live="polite"
aria-relevant="additions text"
aria-atomic="false"
aria-label="Chat messages"
></div>
<main class="chat" id="chat">
<div class="messages" id="messages" aria-live="polite"></div>
<form id="composer" class="composer" action="#" novalidate>
<button
class="icon-left"
type="button"
id="attachBtn"
aria-label="Attach"
>
<i class="fa-solid fa-paperclip" aria-hidden="true"></i>
</button>
<form id="composer" class="composer" action="#">
<textarea id="input" placeholder="Say hello — press Enter to send (Shift+Enter for newline)" rows="2"></textarea>
<button id="send" type="submit">Send</button>
</form>
</main>
<!-- Associate a real label for better accessibility -->
<label for="input" class="visually-hidden">Message</label>
<textarea
id="input"
class="input"
placeholder="Say hello — Enter to send, Shift+Enter for newline"
rows="1"
spellcheck="true"
autocomplete="on"
autocapitalize="sentences"
enterkeyhint="send"
aria-label="Message input"
aria-describedby="composerHelp"
></textarea>
<div id="composerHelp" class="visually-hidden">
Press Enter to send, Shift+Enter for newline.
</div>
<div class="composer-actions">
<button
class="icon-btn"
type="button"
id="micBtn"
aria-label="Voice"
aria-pressed="false"
>
<i class="fa-solid fa-microphone" aria-hidden="true"></i>
</button>
<button
id="send"
class="send-btn"
type="submit"
aria-label="Send"
>
<span>Send</span>
<i class="fa-solid fa-paper-plane" aria-hidden="true"></i>
</button>
</div>
</form>
</main>
<footer class="footer">Running with backend at <code>http://127.0.0.1:5000/hello</code></footer>
</div>
<footer class="footer">
<!-- role="status" is appropriate for non-interruptive live updates -->
<div class="status" role="status" aria-live="polite">
<span class="dot" id="statusDot" aria-hidden="true"></span>
<span>Backend:</span>
<code>http://127.0.0.1:5000/hello</code>
</div>
</footer>
</div>
<script src="app.js"></script>
</body>
</html>
<!-- App script; consider type=module and defer for performance -->
<script src="app.js" defer></script>
</body>
</html>

@ -0,0 +1,401 @@
# AGENTS.md
## Project Overview
This is an educational curriculum repository for teaching web development fundamentals to beginners. The curriculum is a comprehensive 12-week course developed by Microsoft Cloud Advocates, featuring 24 hands-on lessons covering JavaScript, CSS, and HTML.
### Key Components
- **Educational Content**: 24 structured lessons organized into project-based modules
- **Practical Projects**: Terrarium, Typing Game, Browser Extension, Space Game, Banking App, Code Editor, and AI Chat Assistant
- **Interactive Quizzes**: 48 quizzes with 3 questions each (pre/post-lesson assessments)
- **Multi-language Support**: Automated translations for 50+ languages via GitHub Actions
- **Technologies**: HTML, CSS, JavaScript, Vue.js 3, Vite, Node.js, Express, Python (for AI projects)
### Architecture
- Educational repository with lesson-based structure
- Each lesson folder contains README, code examples, and solutions
- Standalone projects in separate directories (quiz-app, various lesson projects)
- Translation system using GitHub Actions (co-op-translator)
- Documentation served via Docsify and available as PDF
## Setup Commands
This repository is primarily for educational content consumption. For working with specific projects:
### Main Repository Setup
```bash
git clone https://github.com/microsoft/Web-Dev-For-Beginners.git
cd Web-Dev-For-Beginners
```
### Quiz App Setup (Vue 3 + Vite)
```bash
cd quiz-app
npm install
npm run dev # Start development server
npm run build # Build for production
npm run lint # Run ESLint
```
### Bank Project API (Node.js + Express)
```bash
cd 7-bank-project/api
npm install
npm start # Start API server
npm run lint # Run ESLint
npm run format # Format with Prettier
```
### Browser Extension Projects
```bash
cd 5-browser-extension/solution
npm install
# Follow browser-specific extension loading instructions
```
### Space Game Projects
```bash
cd 6-space-game/solution
npm install
# Open index.html in browser or use Live Server
```
### Chat Project (Python Backend)
```bash
cd 9-chat-project/solution/backend/python
pip install openai
# Set GITHUB_TOKEN environment variable
python api.py
```
## Development Workflow
### For Content Contributors
1. **Fork the repository** to your GitHub account
2. **Clone your fork** locally
3. **Create a new branch** for your changes
4. Make changes to lesson content or code examples
5. Test any code changes in relevant project directories
6. Submit pull requests following contribution guidelines
### For Learners
1. Fork or clone the repository
2. Navigate to lesson directories sequentially
3. Read README files for each lesson
4. Complete pre-lesson quizzes at https://ff-quizzes.netlify.app/web/
5. Work through code examples in lesson folders
6. Complete assignments and challenges
7. Take post-lesson quizzes
### Live Development
- **Documentation**: Run `docsify serve` in root (port 3000)
- **Quiz App**: Run `npm run dev` in quiz-app directory
- **Projects**: Use VS Code Live Server extension for HTML projects
- **API Projects**: Run `npm start` in respective API directories
## Testing Instructions
### Quiz App Testing
```bash
cd quiz-app
npm run lint # Check for code style issues
npm run build # Verify build succeeds
```
### Bank API Testing
```bash
cd 7-bank-project/api
npm run lint # Check for code style issues
node server.js # Verify server starts without errors
```
### General Testing Approach
- This is an educational repository without comprehensive automated tests
- Manual testing focuses on:
- Code examples run without errors
- Links in documentation work correctly
- Project builds complete successfully
- Examples follow best practices
### Pre-submission Checks
- Run `npm run lint` in directories with package.json
- Verify markdown links are valid
- Test code examples in browser or Node.js
- Check that translations maintain proper structure
## Code Style Guidelines
### JavaScript
- Use modern ES6+ syntax
- Follow standard ESLint configurations provided in projects
- Use meaningful variable and function names for educational clarity
- Add comments explaining concepts for learners
- Format using Prettier where configured
### HTML/CSS
- Semantic HTML5 elements
- Responsive design principles
- Clear class naming conventions
- Comments explaining CSS techniques for learners
### Python
- PEP 8 style guidelines
- Clear, educational code examples
- Type hints where helpful for learning
### Markdown Documentation
- Clear heading hierarchy
- Code blocks with language specification
- Links to additional resources
- Screenshots and images in `images/` directories
- Alt text for images for accessibility
### File Organization
- Lessons numbered sequentially (1-getting-started-lessons, 2-js-basics, etc.)
- Each project has `solution/` and often `start/` or `your-work/` directories
- Images stored in lesson-specific `images/` folders
- Translations in `translations/{language-code}/` structure
## Build and Deployment
### Quiz App Deployment (Azure Static Web Apps)
The quiz-app is configured for Azure Static Web Apps deployment:
```bash
cd quiz-app
npm run build # Creates dist/ folder
# Deploys via GitHub Actions workflow on push to main
```
Azure Static Web Apps configuration:
- **App location**: `/quiz-app`
- **Output location**: `dist`
- **Workflow**: `.github/workflows/azure-static-web-apps-ashy-river-0debb7803.yml`
### Documentation PDF Generation
```bash
npm install # Install docsify-to-pdf
npm run convert # Generate PDF from docs
```
### Docsify Documentation
```bash
npm install -g docsify-cli # Install Docsify globally
docsify serve # Serve on localhost:3000
```
### Project-specific Builds
Each project directory may have its own build process:
- Vue projects: `npm run build` creates production bundles
- Static projects: No build step, serve files directly
## Pull Request Guidelines
### Title Format
Use clear, descriptive titles indicating the area of change:
- `[Quiz-app] Add new quiz for lesson X`
- `[Lesson-3] Fix typo in terrarium project`
- `[Translation] Add Spanish translation for lesson 5`
- `[Docs] Update setup instructions`
### Required Checks
Before submitting a PR:
1. **Code Quality**:
- Run `npm run lint` in affected project directories
- Fix all linting errors and warnings
2. **Build Verification**:
- Run `npm run build` if applicable
- Ensure no build errors
3. **Link Validation**:
- Test all markdown links
- Verify image references work
4. **Content Review**:
- Proofread for spelling and grammar
- Ensure code examples are correct and educational
- Verify translations maintain original meaning
### Contribution Requirements
- Agree to Microsoft CLA (automated check on first PR)
- Follow the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/)
- See [CONTRIBUTING.md](./CONTRIBUTING.md) for detailed guidelines
- Reference issue numbers in PR description if applicable
### Review Process
- PRs reviewed by maintainers and community
- Educational clarity is prioritized
- Code examples should follow current best practices
- Translations reviewed for accuracy and cultural appropriateness
## Translation System
### Automated Translation
- Uses GitHub Actions with co-op-translator workflow
- Translates to 50+ languages automatically
- Source files in main directories
- Translated files in `translations/{language-code}/` directories
### Adding Manual Translation Improvements
1. Locate file in `translations/{language-code}/`
2. Make improvements while preserving structure
3. Ensure code examples remain functional
4. Test any localized quiz content
### Translation Metadata
Translated files include metadata header:
```markdown
<!--
CO_OP_TRANSLATOR_METADATA:
{
"original_hash": "...",
"translation_date": "...",
"source_file": "...",
"language_code": "..."
}
-->
```
## Debugging and Troubleshooting
### Common Issues
**Quiz app fails to start**:
- Check Node.js version (v14+ recommended)
- Delete `node_modules` and `package-lock.json`, run `npm install` again
- Check for port conflicts (default: Vite uses port 5173)
**API server won't start**:
- Verify Node.js version meets minimum (node >=10)
- Check if port is already in use
- Ensure all dependencies installed with `npm install`
**Browser extension won't load**:
- Verify manifest.json is properly formatted
- Check browser console for errors
- Follow browser-specific extension installation instructions
**Python chat project issues**:
- Ensure OpenAI package installed: `pip install openai`
- Verify GITHUB_TOKEN environment variable is set
- Check GitHub Models access permissions
**Docsify not serving docs**:
- Install docsify-cli globally: `npm install -g docsify-cli`
- Run from repository root directory
- Check that `docs/_sidebar.md` exists
### Development Environment Tips
- Use VS Code with Live Server extension for HTML projects
- Install ESLint and Prettier extensions for consistent formatting
- Use browser DevTools for debugging JavaScript
- For Vue projects, install Vue DevTools browser extension
### Performance Considerations
- Large number of translated files (50+ languages) means full clones are large
- Use shallow clone if only working on content: `git clone --depth 1`
- Exclude translations from searches when working on English content
- Build processes may be slow on first run (npm install, Vite build)
## Security Considerations
### Environment Variables
- API keys should never be committed to repository
- Use `.env` files (already in `.gitignore`)
- Document required environment variables in project READMEs
### Python Projects
- Use virtual environments: `python -m venv venv`
- Keep dependencies updated
- GitHub tokens should have minimal required permissions
### GitHub Models Access
- Personal Access Tokens (PAT) required for GitHub Models
- Tokens should be stored as environment variables
- Never commit tokens or credentials
## Additional Notes
### Target Audience
- Complete beginners to web development
- Students and self-learners
- Teachers using the curriculum in classrooms
- Content is designed for accessibility and gradual skill building
### Educational Philosophy
- Project-based learning approach
- Frequent knowledge checks (quizzes)
- Hands-on coding exercises
- Real-world application examples
- Focus on fundamentals before frameworks
### Repository Maintenance
- Active community of learners and contributors
- Regular updates to dependencies and content
- Issues and discussions monitored by maintainers
- Translation updates automated via GitHub Actions
### Related Resources
- [Microsoft Learn modules](https://docs.microsoft.com/learn/)
- [Student Hub resources](https://docs.microsoft.com/learn/student-hub/)
- [GitHub Copilot](https://marketplace.visualstudio.com/items?itemName=GitHub.copilot) recommended for learners
- Additional courses: Generative AI, Data Science, ML, IoT curricula available
### Working with Specific Projects
For detailed instructions on individual projects, refer to the README files in:
- `quiz-app/README.md` - Vue 3 quiz application
- `7-bank-project/README.md` - Banking application with authentication
- `5-browser-extension/README.md` - Browser extension development
- `6-space-game/README.md` - Canvas-based game development
- `9-chat-project/README.md` - AI chat assistant project
### Monorepo Structure
While not a traditional monorepo, this repository contains multiple independent projects:
- Each lesson is self-contained
- Projects don't share dependencies
- Work on individual projects without affecting others
- Clone entire repo for the full curriculum experience

@ -0,0 +1,129 @@
## Basics of GIT for web-dev beginners👶
## What is `Git`?
1. Git is a distributed version control system.
2. The entire codebase and history is available on every developers computer,
which allows for easy branching and merging.
3. It is used as Version Control System (VCS) for tracking changes in computer files.
* Distributed version control
* Coordinates work between multiple developers
* Who made what changes and when
* Revert back at any time
* Local & remote repos
## CONCEPTS OF GIT
* Keeps track of code history
* Takes "snapshots" of your files
* You decide when to take a snapshot by making a "commit"
* You can visit any snapshot at any time
* You can stage files before committing
### Difference Between Git & GitHub
| Git | GitHub |
| ------- | ----------- |
| Git is a software | GitHub is a cloud service |
| Git is installed locally on the system | GitHub is hosted on the web |
| It is command-line tool | It is a graphical user interface |
| Git is maintained by Linux | GitHub is maintained by Microsoft |
| It is focused on version control and code sharing | It is focused on centralized source code hosting |
| Git is open-source licensed | GitHub includes a free-tier and pay-for-use tier |
| Git was released in 2005 | GitHub was released in 2008 |
## GIT Installation
* Linux(Debian)
`$sudo apt-get install git`
* Linux(Fedora)
`$sudo yum install git`
* [Download](http://git-scm.com/download/mac) for Mac
* [Download](http://git-scm.com/download/win) for Windows
### Installation Process Steps:
1. <img width="500" height="400" src="https://user-images.githubusercontent.com/61585443/190359823-e421b976-515a-4565-990d-2f2e4e62977a.png"/>
2. <img width="500" height="400" src="https://user-images.githubusercontent.com/61585443/190360530-a7bfa681-47f4-4859-9b8a-4120e0cad348.png"/>
3. <img width="500" height="400" src="https://user-images.githubusercontent.com/61585443/190360760-30db7768-19e0-4848-a99d-a6c955e041e2.png"/>
4. <img width="500" height="400" src="https://user-images.githubusercontent.com/61585443/190360896-473e1e54-f083-4b5c-a5f0-539f70469142.png"/>
5. <img width="500" height="400" src="https://user-images.githubusercontent.com/61585443/190361144-bc670a2b-b776-4867-9785-7b509d416fbb.png"/>
6. And then Continue Next > Next > Next > <b>Install</b>
7. <img width="500" height="400" src="https://user-images.githubusercontent.com/61585443/190361548-4b700d85-c7d5-4d26-90e7-e5cc3ce24311.png"/>
### After Installation We need To configure git using git bash
1. `git config --global user.name 'YourName'`
2. `git config --global user.email 'YourEmail'`
___
## Git Commands
___
### Getting & Creating Projects
| Command | Description |
| ------- | ----------- |
| `git init` | Initialize a local Git repository |
| `git clone ssh://git@github.com/[username]/[repository-name].git` | Create a local copy of a remote repository |
### Basic Snapshotting
| Command | Description |
| ------- | ----------- |
| `git status` | Check status |
| `git add [file-name.txt]` | Add a file to the staging area |
| `git add -A` | Add all new and changed files to the staging area |
| `git commit -m "[commit message]"` | Commit changes |
| `git rm -r [file-name.txt]` | Remove a file (or folder) |
| `git push` | Push To Remote Repository |
| `git pull` | Pull Latest Changes From Remote Repository |
### Branching & Merging
| Command | Description |
| ------- | ----------- |
| `git branch` | List branches (the asterisk denotes the current branch) |
| `git branch -a` | List all branches (local and remote) |
| `git branch [branch name]` | Create a new branch |
| `git branch -D [branch name]` | Delete a branch |
| `git push origin --delete [branch name]` | Delete a remote branch |
| `git checkout -b [branch name]` | Create a new branch and switch to it |
| `git checkout -b [branch name] origin/[branch name]` | Clone a remote branch and switch to it |
| `git branch -m [old branch name] [new branch name]` | Rename a local branch |
| `git checkout [branch name]` | Switch to a branch |
| `git checkout -` | Switch to the branch last checked out |
| `git checkout -- [file-name.txt]` | Discard changes to a file |
| `git merge [branch name]` | Merge a branch into the active branch |
| `git merge [source branch] [target branch]` | Merge a branch into a target branch |
| `git stash` | Stash changes in a dirty working directory |
| `git stash clear` | Remove all stashed entries |
### Sharing & Updating Projects
| Command | Description |
| ------- | ----------- |
| `git push origin [branch name]` | Push a branch to your remote repository |
| `git push -u origin [branch name]` | Push changes to remote repository (and remember the branch) |
| `git push` | Push changes to remote repository (remembered branch) |
| `git push origin --delete [branch name]` | Delete a remote branch |
| `git pull` | Update local repository to the newest commit |
| `git pull origin [branch name]` | Pull changes from remote repository |
| `git remote add origin ssh://git@github.com/[username]/[repository-name].git` | Add a remote repository |
| `git remote set-url origin ssh://git@github.com/[username]/[repository-name].git` | Set a repository's origin branch to SSH |
### Inspection & Comparison
| Command | Description |
| ------- | ----------- |
| `git log` | View changes |
| `git log --summary` | View changes (detailed) |
| `git log --oneline` | View changes (briefly) |
| `git diff [source branch] [target branch]` | Preview changes before merging |

@ -10,7 +10,11 @@
[![](https://dcbadge.vercel.app/api/server/ByRwuEEgH4)](https://discord.gg/zxKYvhSnVp?WT.mc_id=academic-000002-leestott)
[![Open in Visual Studio Code](https://img.shields.io/static/v1?logo=visualstudiocode&label=&message=Open%20in%20Visual%20Studio%20Code&labelColor=2c2c32&color=007acc&logoColor=007acc)](https://open.vscode.dev/microsoft/Web-Dev-For-Beginners)
# Web Development for Beginners - A Curriculum
Learn the fundamentals of web development with our 12-week comprehensive course by Microsoft Cloud Advocates. Each of the 24 lessons dives into JavaScript, CSS, and HTML through hands-on projects like terrariums, browser extensions, and space games. Engage with quizzes, discussions, and practical assignments. Enhance your skills and optimize your knowledge retention with our effective project-based pedagogy. Start your coding journey today!
Join the Azure AI Foundry Discord Community
[![Microsoft Azure AI Foundry Discord](https://dcbadge.limes.pink/api/server/ByRwuEEgH4)](https://discord.com/invite/ByRwuEEgH4)
@ -19,18 +23,18 @@ Follow these steps to get started using these resources:
2. **Clone the Repository**: `git clone https://github.com/microsoft/Web-Dev-For-Beginners.git`
3. [**Join The Azure AI Foundry Discord and meet experts and fellow developers**](https://discord.com/invite/ByRwuEEgH4)
# Web Development for Beginners - A Curriculum
Learn the fundamentals of web development with our 12-week comprehensive course by Microsoft Cloud Advocates. Each of the 24 lessons dives into JavaScript, CSS, and HTML through hands-on projects like terrariums, browser extensions, and space games. Engage with quizzes, discussions, and practical assignments. Enhance your skills and optimize your knowledge retention with our effective project-based pedagogy. Start your coding journey today!
### 🌐 Multi-Language Support
#### Supported via GitHub Action (Automated & Always Up-to-Date)
[French](./translations/fr/README.md) | [Spanish](./translations/es/README.md) | [German](./translations/de/README.md) | [Russian](./translations/ru/README.md) | [Arabic](./translations/ar/README.md) | [Persian (Farsi)](./translations/fa/README.md) | [Urdu](./translations/ur/README.md) | [Chinese (Simplified)](./translations/zh/README.md) | [Chinese (Traditional, Macau)](./translations/mo/README.md) | [Chinese (Traditional, Hong Kong)](./translations/hk/README.md) | [Chinese (Traditional, Taiwan)](./translations/tw/README.md) | [Japanese](./translations/ja/README.md) | [Korean](./translations/ko/README.md) | [Hindi](./translations/hi/README.md) | [Bengali](./translations/bn/README.md) | [Marathi](./translations/mr/README.md) | [Nepali](./translations/ne/README.md) | [Punjabi (Gurmukhi)](./translations/pa/README.md) | [Portuguese (Portugal)](./translations/pt/README.md) | [Portuguese (Brazil)](./translations/br/README.md) | [Italian](./translations/it/README.md) | [Polish](./translations/pl/README.md) | [Turkish](./translations/tr/README.md) | [Greek](./translations/el/README.md) | [Thai](./translations/th/README.md) | [Swedish](./translations/sv/README.md) | [Danish](./translations/da/README.md) | [Norwegian](./translations/no/README.md) | [Finnish](./translations/fi/README.md) | [Dutch](./translations/nl/README.md) | [Hebrew](./translations/he/README.md) | [Vietnamese](./translations/vi/README.md) | [Indonesian](./translations/id/README.md) | [Malay](./translations/ms/README.md) | [Tagalog (Filipino)](./translations/tl/README.md) | [Swahili](./translations/sw/README.md) | [Hungarian](./translations/hu/README.md) | [Czech](./translations/cs/README.md) | [Slovak](./translations/sk/README.md) | [Romanian](./translations/ro/README.md) | [Bulgarian](./translations/bg/README.md) | [Serbian (Cyrillic)](./translations/sr/README.md) | [Croatian](./translations/hr/README.md) | [Slovenian](./translations/sl/README.md) | [Ukrainian](./translations/uk/README.md) | [Burmese (Myanmar)](./translations/my/README.md)
<!-- CO-OP TRANSLATOR LANGUAGES TABLE START -->
[Arabic](./translations/ar/README.md) | [Bengali](./translations/bn/README.md) | [Bulgarian](./translations/bg/README.md) | [Burmese (Myanmar)](./translations/my/README.md) | [Chinese (Simplified)](./translations/zh/README.md) | [Chinese (Traditional, Hong Kong)](./translations/hk/README.md) | [Chinese (Traditional, Macau)](./translations/mo/README.md) | [Chinese (Traditional, Taiwan)](./translations/tw/README.md) | [Croatian](./translations/hr/README.md) | [Czech](./translations/cs/README.md) | [Danish](./translations/da/README.md) | [Dutch](./translations/nl/README.md) | [Estonian](./translations/et/README.md) | [Finnish](./translations/fi/README.md) | [French](./translations/fr/README.md) | [German](./translations/de/README.md) | [Greek](./translations/el/README.md) | [Hebrew](./translations/he/README.md) | [Hindi](./translations/hi/README.md) | [Hungarian](./translations/hu/README.md) | [Indonesian](./translations/id/README.md) | [Italian](./translations/it/README.md) | [Japanese](./translations/ja/README.md) | [Korean](./translations/ko/README.md) | [Lithuanian](./translations/lt/README.md) | [Malay](./translations/ms/README.md) | [Marathi](./translations/mr/README.md) | [Nepali](./translations/ne/README.md) | [Norwegian](./translations/no/README.md) | [Persian (Farsi)](./translations/fa/README.md) | [Polish](./translations/pl/README.md) | [Portuguese (Brazil)](./translations/br/README.md) | [Portuguese (Portugal)](./translations/pt/README.md) | [Punjabi (Gurmukhi)](./translations/pa/README.md) | [Romanian](./translations/ro/README.md) | [Russian](./translations/ru/README.md) | [Serbian (Cyrillic)](./translations/sr/README.md) | [Slovak](./translations/sk/README.md) | [Slovenian](./translations/sl/README.md) | [Spanish](./translations/es/README.md) | [Swahili](./translations/sw/README.md) | [Swedish](./translations/sv/README.md) | [Tagalog (Filipino)](./translations/tl/README.md) | [Tamil](./translations/ta/README.md) | [Thai](./translations/th/README.md) | [Turkish](./translations/tr/README.md) | [Ukrainian](./translations/uk/README.md) | [Urdu](./translations/ur/README.md) | [Vietnamese](./translations/vi/README.md)
<!-- CO-OP TRANSLATOR LANGUAGES TABLE END -->
**If you wish to have additional translations languages supported are listed [here](https://github.com/Azure/co-op-translator/blob/main/getting_started/supported-languages.md)**
[![Open in Visual Studio Code](https://img.shields.io/static/v1?logo=visualstudiocode&label=&message=Open%20in%20Visual%20Studio%20Code&labelColor=2c2c32&color=007acc&logoColor=007acc)](https://open.vscode.dev/microsoft/Web-Dev-For-Beginners)
#### 🧑‍🎓 _Are you a student?_
Visit [**Student Hub page**](https://docs.microsoft.com/learn/student-hub/?WT.mc_id=academic-77807-sagibbon) where you will find beginner resources, Student packs and even ways to get a free certificate voucher. This is the page you want to bookmark and check from time to time as we switch out content monthly.
@ -88,7 +92,7 @@ Follow these steps:
In your copy of this repository that you created, click the **Code** button and select **Open with Codespaces**. This will create a new Codespace for you to work in.
[!Codespace]()./images/createcodespace.png)
![Codespace](./images/createcodespace.png)
#### Running the curriculum locally on your computer
@ -99,7 +103,7 @@ Our recommendation is to use [Visual Studio Code](https://code.visualstudio.com/
1. Clone your repository to your computer. You can do this by clicking the **Code** button and copying the URL:
[!CodeSpace](./images/createcodespace.png)
[CodeSpace](./images/createcodespace.png)
Then, open [Terminal](https://code.visualstudio.com/docs/terminal/basics/?WT.mc_id=academic-77807-sagibbon) within [Visual Studio Code](https://code.visualstudio.com/?WT.mc_id=academic-77807-sagibbon) and run the following command, replacing `<your-repository-url>` with the URL you just copied:
@ -193,7 +197,9 @@ A PDF of all of the lessons can be found [here](https://microsoft.github.io/Web-
Our team produces other courses! Check out:
- [Generative AI for Beginners](https://aka.ms/genai-beginners)
- [MCP for Beginners](https://aka.ms/mcp-for-beginners)
- [Edge AI for Beginners](https://aka.ms/edgeai-for-beginners)
- [AI Agents for Beginners](https://aka.ms/ai-agents-beginners)
- [Generative AI for Beginners .NET](https://github.com/microsoft/Generative-AI-for-beginners-dotnet)
- [Generative AI with JavaScript](https://github.com/microsoft/generative-ai-with-javascript)
- [Generative AI with Java](https://github.com/microsoft/Generative-AI-for-beginners-java)
@ -208,6 +214,16 @@ Our team produces other courses! Check out:
- [Mastering GitHub Copilot for C#/.NET Developers](https://github.com/microsoft/mastering-github-copilot-for-dotnet-csharp-developers)
- [Choose Your Own Copilot Adventure](https://github.com/microsoft/CopilotAdventures)
## Getting Help
If you get stuck or have any questions about building AI apps, join:
[![Azure AI Foundry Discord](https://img.shields.io/badge/Discord-Azure_AI_Foundry_Community_Discord-blue?style=for-the-badge&logo=discord&color=5865f2&logoColor=fff)](https://aka.ms/foundry/discord)
If you have product feedback or errors while building visit:
[![Azure AI Foundry Developer Forum](https://img.shields.io/badge/GitHub-Azure_AI_Foundry_Developer_Forum-blue?style=for-the-badge&logo=github&color=000000&logoColor=fff)](https://aka.ms/foundry/forum)
## License
This repository is licensed under the MIT license. See the [LICENSE](LICENSE) file for more information.

@ -1,16 +1,37 @@
## For Educators
### For Educators
Would you like to use this curriculum in your classroom? Please feel free to!
You are welcome to use this curriculum in your classroom. It works seamlessly with GitHub Classroom and leading LMS platforms, and it can also be used as a standalone repo with your students.
In fact, you can use it within GitHub itself by using GitHub Classroom.
### Use with GitHub Classroom
To do that, fork this repo. You are going to need to create a repo for each lesson, so you're going to need to extract each folder into a separate repo. That way, [GitHub Classroom](https://classroom.github.com/classrooms) can pick up each lesson separately.
To manage lessons and assignments per cohort, create one repository per lesson so GitHub Classroom can attach each assignment independently.
These [full instructions](https://github.blog/2020-03-18-set-up-your-digital-classroom-with-github-classroom/) will give you an idea of how to set up your classroom.
- Fork this repo to your organization.
- Create a separate repo for each lesson by extracting each lesson folder into its own repository.
- Option A: Create empty repos (one per lesson) and copy the lesson folder contents into each.
- Option B: Use a Git historypreserving approach (e.g., split a folder into a new repo) if you need provenance.
- In GitHub Classroom, create an assignment per lesson and point it to the corresponding lesson repo.
- Recommended settings:
- Repository visibility: private for student work.
- Use starter code from the lesson repos default branch.
- Add issue and pull request templates for quizzes and submissions.
- Optionally configure autograding and tests if your lessons include them.
- Conventions that help:
- Repository names like lesson-01-intro, lesson-02-html, etc.
- Labels: quiz, assignment, needs-review, late, resubmission.
- Tags/releases per cohort (e.g., v2025-term1).
## Using it in Moodle, Canvas or Blackboard
Tip: Avoid storing repositories inside synced folders (e.g., OneDrive/Google Drive) to prevent Git conflicts on Windows.
This curriculum works well in these Learning Management Systems! Use the [Moodle upload file](/teaching-files/webdev-moodle.mbz) for the full content, or try the [Common Cartridge file](/teaching-files/webdev-common-cartridge.imscc) which contains some of the content. Moodle Cloud does not support full Common Cartridge exports, so it is preferable to use the Moodle download file which can be uploaded into Canvas. Please let us know how we can improve this experience.
### Use with Moodle, Canvas, or Blackboard
This curriculum includes importable packages for common LMS workflows.
- Moodle: Use the Moodle upload file [Moodle upload file](/teaching-files/webdev-moodle.mbz) to load the full course.
- Common Cartridge: Use the Common Cartridge file [Common Cartridge file](/teaching-files/webdev-common-cartridge.imscc) for broader LMS compatibility.
- Notes:
- Moodle Cloud has limited Common Cartridge support. Prefer the Moodle file above, which can also be uploaded into Canvas.
- After import, review modules, due dates, and quiz settings to match your term schedule.
![Moodle](/teaching-files/moodle.png)
> The curriculum in a Moodle classroom
@ -18,16 +39,26 @@ This curriculum works well in these Learning Management Systems! Use the [Moodle
![Canvas](/teaching-files/canvas.png)
> The curriculum in Canvas
## Using the repo as is
### Use the repo directly (no Classroom)
If you would like to use this repo as it currently stands, without using GitHub Classroom, that can be done as well. You would need to communicate with your students which lesson to work through together.
If you prefer not to use GitHub Classroom, you can run the course directly from this repo.
In an online format (Zoom, Teams, or other) you might form breakout rooms for the quizzes, and mentor students to help them get ready to learn. Then invite students to the quizzes and submit their answers as 'issues' at a certain time. You might do the same with assignments if you want students to work collaboratively out in the open.
- Synchronous/online formats (Zoom/Teams):
- Run short mentorled warmups; use breakout rooms for quizzes.
- Announce a time window for quizzes; students submit answers as GitHub Issues.
- For collaborative assignments, students work in public lesson repos and open pull requests.
- Private/asynchronous formats:
- Students fork each lesson to their own **private** repos and add you as a collaborator.
- They submit via Issues (quizzes) and Pull Requests (assignments) on your classroom repo or their private forks.
If you prefer a more private format, ask your students to fork the curriculum, lesson by lesson, to their own GitHub repos as private repos, and give you access. Then they can complete quizzes and assignments privately and submit them to you via issues on your classroom repo.
### Best practices
There are many ways to make this work in an online classroom format. Please let us know what works best for you!
- Provide an orientation lesson on Git/GitHub basics, Issues, and PRs.
- Use checklists in Issues for multistep quizzes/assignments.
- Add CONTRIBUTING.md and CODE_OF_CONDUCT.md to set classroom norms.
- Add accessibility notes (alt text, captions) and offer printable PDFs.
- Version your content per term and freeze lesson repos after publishing.
## Please give us your thoughts!
### Feedback and support
We want to make this curriculum work for you and your students. Connect with us at the [Teacher corner](https://github.com/microsoft/Web-Dev-For-Beginners/discussions/categories/teacher-corner) and open a [**new issue**](https://github.com/microsoft/Web-Dev-For-Beginners/issues/new/choose) for any requests, bugs and feedback.
We want this curriculum to work for you and your students. Please open a new Issue in this repository for bugs, requests, or improvements, or start a discussion in the Teacher Corner.

@ -1,3 +1,108 @@
.app-nav ul li {
width: max-content;
}
body {
font-family: 'Inter', Arial, sans-serif;
}
/* Body and Typography */
body {
font-family: 'Inter', Arial, sans-serif;
background: #f7f8fa;
color: #222;
margin: 0;
padding: 0;
}
/* Header Styling */
h1, h2, h3 {
font-weight: 700;
margin-top: 1.5em;
margin-bottom: 0.5em;
letter-spacing: 0.02em;
}
h1 i, h2 i, h3 i {
color: #0078d4;
margin-right: 0.5em;
}
/* Navigation Bar */
.app-nav {
background: #fff;
box-shadow: 0 2px 8px rgba(0,0,0,0.04);
padding: 1em 2em;
}
.app-nav ul {
list-style: none;
display: flex;
gap: 2em;
margin: 0;
padding: 0;
}
.app-nav ul li {
width: max-content;
}
.app-nav a {
text-decoration: none;
color: #0078d4;
font-weight: 500;
transition: color 0.2s;
}
.app-nav a:hover {
color: #005fa3;
}
/* Card Component */
.card {
background: #fff;
border-radius: 12px;
box-shadow: 0 4px 16px rgba(0,0,0,0.07);
padding: 2em;
margin: 2em 0;
transition: box-shadow 0.2s;
}
.card:hover {
box-shadow: 0 8px 32px rgba(0,0,0,0.12);
}
/* Button Styling */
.button {
background: #0078d4;
color: #fff;
border: none;
border-radius: 6px;
padding: 0.75em 1.5em;
font-size: 1em;
font-weight: 600;
cursor: pointer;
transition: background 0.2s;
display: inline-flex;
align-items: center;
gap: 0.5em;
}
.button:hover {
background: #005fa3;
}
/* Font Awesome Icon Styling */
.fa {
margin-right: 0.5em;
vertical-align: middle;
}
/* Responsive Layout */
@media (max-width: 600px) {
.app-nav ul {
flex-direction: column;
gap: 1em;
}
.card {
padding: 1em;
}
}

@ -1,4 +1,5 @@
<!DOCTYPE html>
<!-- Add Google Fonts -->
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" rel="stylesheet"><!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
@ -13,7 +14,8 @@
<body>
<div id="app"></div>
<script type="text/javascript">
(function(c,l,a,r,i,t,y){
(function(c,l,a,r,i,t,y){ <!-- Add Font Awesome -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css">
c[a]=c[a]||function(){(c[a].q=c[a].q||[]).push(arguments)};
t=l.createElement(r);t.async=1;t.src="https://www.clarity.ms/tag/"+i;
y=l.getElementsByTagName(r)[0];y.parentNode.insertBefore(t,y);

@ -0,0 +1,41 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# env files (can opt-in for committing if needed)
.env*
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts

@ -0,0 +1,36 @@
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
## Getting Started
First, run the development server:
```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
You can start editing the page by modifying `app/page.js`. The page auto-updates as you edit the file.
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
## Learn More
To learn more about Next.js, take a look at the following resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
## Deploy on Vercel
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

@ -0,0 +1,26 @@
@import "tailwindcss";
:root {
--background: #ffffff;
--foreground: #171717;
}
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono);
}
@media (prefers-color-scheme: dark) {
:root {
--background: #0a0a0a;
--foreground: #ededed;
}
}
body {
background: var(--background);
color: var(--foreground);
font-family: Arial, Helvetica, sans-serif;
}

@ -0,0 +1,29 @@
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});
export const metadata = {
title: "Memory Game",
description: "A fun and challenging memory game built with Next.js.",
};
export default function RootLayout({ children }) {
return (
<html lang="en">
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
{children}
</body>
</html>
);
}

@ -0,0 +1,12 @@
import MemoryGame from '@/components/MemoryGame'
import React from 'react'
const page = () => {
return (
<div>
<MemoryGame />
</div>
)
}
export default page

@ -0,0 +1,162 @@
"use client";
import React, { useEffect, useState, useCallback } from "react";
// Fisher-Yates shuffle for unbiased randomization
function fisherYatesShuffle(array) {
const arr = array.slice();
for (let i = arr.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[arr[i], arr[j]] = [arr[j], arr[i]];
}
return arr;
}
const MemoryGame = () => {
const [gridSize, setGridSize] = useState(2);
const [array, setArray] = useState([]);
const [flipped, setFlipped] = useState([]);
const [selectedPairs, setSelectedPairs] = useState([]);
const [disabled, setDisabled] = useState(false);
const [error, setError] = useState("");
const [won, setWon] = useState(false);
const handleGridSize = (e) => {
const size = parseInt(e.target.value);
if (2 <= size && size <= 10 && (size % 2 === 0)) {
setGridSize(size);
setError("");
} else {
setError("Please enter a grid size where size is even (e.g., 2, 4, 6, 8, 10)");
}
};
const initializeGame = useCallback(() => {
const totalCards = gridSize * gridSize;
const pairCount = totalCards / 2;
const numbers = [...Array(pairCount).keys()].map((n) => n + 1);
const cardNumbers = [...numbers, ...numbers];
const shuffledCardNumbers = fisherYatesShuffle(cardNumbers);
const shuffledCards = shuffledCardNumbers.map((number, index) => ({
id: index,
number,
}));
setArray(shuffledCards);
setFlipped([]);
setSelectedPairs([]);
setDisabled(false);
setWon(false);
}, [gridSize]);
useEffect(() => {
initializeGame();
}, [initializeGame]);
const handleMatch = (secondId) => {
const [firstId] = flipped;
if (array[firstId].number === array[secondId].number) {
setSelectedPairs([...selectedPairs, firstId, secondId]);
setFlipped([]);
setDisabled(false);
} else {
setTimeout(() => {
setDisabled(false);
setFlipped([]);
}, 1000);
}
};
const handleClick = (id) => {
if (disabled || won) return;
if (flipped.length === 0) {
setFlipped([id]);
return;
}
if (flipped.length === 1) {
setDisabled(true);
if (id !== flipped[0]) {
setFlipped([...flipped, id]);
handleMatch(id);
} else {
setFlipped([]);
setDisabled(false);
}
}
};
const isFlipped = (id) => flipped.includes(id) || selectedPairs.includes(id);
const isSelectedPairs = (id) => selectedPairs.includes(id);
useEffect(() => {
if (selectedPairs.length === array.length && array.length > 0) {
setWon(true);
}
}, [selectedPairs, array]);
return (
<div className="h-screen flex flex-col justify-center items-center p-4 bg-gray-100 ">
{/* Heading */}
<h1 className="text-3xl font-bold mb-6">Memory Game</h1>
{/* Grid Size */}
<div className="mb-4">
<label htmlFor="gridSize">Grid Size: (max 10)</label>
<input
type="number"
className="w-[50px] ml-3 rounded border-2 px-1.5 py-1"
min="2"
max="10"
value={gridSize}
onChange={handleGridSize}
/>
{error && (
<div className="text-sm text-red-500 mt-2">{error}</div>
)}
</div>
{/* Cards */}
<div
className="grid gap-2 mb-4"
style={{
gridTemplateColumns: `repeat(${gridSize}, minmax(0,1fr))`,
width: `min(100%,${gridSize * 5.5}rem)`,
}}
>
{array.map((card) => (
<div
key={card.id}
onClick={() => handleClick(card.id)}
className={`aspect-square flex items-center justify-center text-xl transition-all duration-300 font-bold rounded-lg cursor-pointer ${
isFlipped(card.id)
? isSelectedPairs(card.id)
? "bg-green-500 text-white"
: "bg-blue-500 text-white"
: "bg-gray-300 text-gray-400"
}`}
>
{isFlipped(card.id) ? card.number : "?"}
</div>
))}
</div>
{/* Result */}
<div className="text-2xl text-green-500 font-bold">
{won ? "You Won!" : ""}
</div>
{/* Reset Button */}
<button
className="px-5 py-2 bg-green-500 rounded text-white mt-5"
onClick={initializeGame}
>
Reset
</button>
</div>
);
};
export default MemoryGame;

@ -0,0 +1,25 @@
import { dirname } from "path";
import { fileURLToPath } from "url";
import { FlatCompat } from "@eslint/eslintrc";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const compat = new FlatCompat({
baseDirectory: __dirname,
});
const eslintConfig = [
...compat.extends("next/core-web-vitals"),
{
ignores: [
"node_modules/**",
".next/**",
"out/**",
"build/**",
"next-env.d.ts",
],
},
];
export default eslintConfig;

@ -0,0 +1,7 @@
{
"compilerOptions": {
"paths": {
"@/*": ["./*"]
}
}
}

@ -0,0 +1,4 @@
/** @type {import('next').NextConfig} */
const nextConfig = {};
export default nextConfig;

File diff suppressed because it is too large Load Diff

@ -0,0 +1,23 @@
{
"name": "memory-game",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev --turbopack",
"build": "next build --turbopack",
"start": "next start",
"lint": "eslint"
},
"dependencies": {
"react": "19.1.0",
"react-dom": "19.1.0",
"next": "15.5.2"
},
"devDependencies": {
"@tailwindcss/postcss": "^4",
"tailwindcss": "^4",
"eslint": "^9",
"eslint-config-next": "15.5.2",
"@eslint/eslintrc": "^3"
}
}

@ -0,0 +1,5 @@
const config = {
plugins: ["@tailwindcss/postcss"],
};
export default config;

@ -0,0 +1 @@
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 391 B

@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>

After

Width:  |  Height:  |  Size: 128 B

@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>

After

Width:  |  Height:  |  Size: 385 B

@ -15,7 +15,7 @@
"@vitejs/plugin-vue": "^5.2.4",
"eslint": "^8.57.0",
"eslint-plugin-vue": "^9.23.0",
"vite": "^6.3.5"
"vite": "^6.3.6"
}
},
"node_modules/@babel/helper-string-parser": {
@ -2299,9 +2299,9 @@
"dev": true
},
"node_modules/vite": {
"version": "6.3.5",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz",
"integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==",
"version": "6.3.6",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.6.tgz",
"integrity": "sha512-0msEVHJEScQbhkbVTb/4iHZdJ6SXp/AvxL2sjwYQFfBqleHtnCqv1J3sa9zbWz/6kW1m9Tfzn92vW+kZ1WV6QA==",
"dev": true,
"license": "MIT",
"dependencies": {

@ -17,6 +17,6 @@
"@vitejs/plugin-vue": "^5.2.4",
"eslint": "^8.57.0",
"eslint-plugin-vue": "^9.23.0",
"vite": "^6.3.5"
"vite": "^6.3.6"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 281 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 279 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 295 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 291 KiB

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save