Compare commits

...

6 Commits

Author SHA1 Message Date
xfarrow d043869050 add tests 2024-10-31 17:55:51 +01:00
xfarrow 71caaf6886 add company html 2024-10-31 17:22:42 +01:00
xfarrow 1c75874704 update api 2024-10-31 15:54:31 +01:00
xfarrow 338e314613 add delete method in contactinfo 2024-10-31 15:35:41 +01:00
xfarrow 7378c85d50 update 2024-10-31 15:16:06 +01:00
xfarrow a29a8877d5 add personcontactinfo routes 2024-10-31 09:44:25 +01:00
8 changed files with 632 additions and 130 deletions

View File

@ -5,7 +5,7 @@
{
"type": "http",
"name": "ActivatePerson",
"seq": 23,
"seq": 26,
"request": {
"url": "http://localhost:3000/api/persons/me/activation?q=3ac9c204de1676b54163ed8015c7af00",
"method": "GET",
@ -40,7 +40,7 @@
{
"type": "http",
"name": "AddOrganizationAdmin",
"seq": 11,
"seq": 14,
"request": {
"url": "http://localhost:3000/api/organizations/1/admins",
"method": "POST",
@ -75,7 +75,7 @@
{
"type": "http",
"name": "ChangeApplicationStatus",
"seq": 1,
"seq": 4,
"request": {
"url": "http://localhost:3000/api/organizations/joboffers/applications/5",
"method": "PATCH",
@ -99,7 +99,7 @@
{
"type": "http",
"name": "CreateApplication",
"seq": 6,
"seq": 9,
"request": {
"url": "http://localhost:3000/api/organizations/joboffers/1/applications",
"method": "POST",
@ -122,7 +122,7 @@
{
"type": "http",
"name": "CreateJobOffer",
"seq": 9,
"seq": 12,
"request": {
"url": "http://localhost:3000/api/organizations/1/joboffers",
"method": "POST",
@ -157,7 +157,7 @@
{
"type": "http",
"name": "CreateOrganization",
"seq": 16,
"seq": 19,
"request": {
"url": "http://localhost:3000/api/organizations",
"method": "POST",
@ -192,7 +192,7 @@
{
"type": "http",
"name": "DeleteApplication",
"seq": 2,
"seq": 5,
"request": {
"url": "http://localhost:3000/api/organizations/joboffers/applications/6",
"method": "DELETE",
@ -216,7 +216,7 @@
{
"type": "http",
"name": "DeleteJobOffer",
"seq": 7,
"seq": 10,
"request": {
"url": "http://localhost:3000/api/organizations/joboffers/3",
"method": "DELETE",
@ -245,7 +245,7 @@
{
"type": "http",
"name": "DeleteOrganization",
"seq": 13,
"seq": 16,
"request": {
"url": "http://localhost:3000/api/organizations/1",
"method": "DELETE",
@ -274,7 +274,7 @@
{
"type": "http",
"name": "DeletePerson",
"seq": 19,
"seq": 22,
"request": {
"url": "http://localhost:3000/api/persons/me",
"method": "DELETE",
@ -303,7 +303,7 @@
{
"type": "http",
"name": "FilterOrganizationByPrefix",
"seq": 12,
"seq": 15,
"request": {
"url": "http://localhost:3000/api/organizations/filter",
"method": "POST",
@ -335,10 +335,34 @@
}
}
},
{
"type": "http",
"name": "GetApplicantsByJobOffer",
"seq": 6,
"request": {
"url": "http://localhost:3000/api/organizations/joboffers/1/applications",
"method": "GET",
"headers": [],
"params": [],
"body": {
"mode": "json",
"json": "",
"formUrlEncoded": [],
"multipartForm": []
},
"script": {},
"vars": {},
"assertions": [],
"tests": "",
"auth": {
"mode": "inherit"
}
}
},
{
"type": "http",
"name": "GetApplicantsByOrganization",
"seq": 4,
"seq": 7,
"request": {
"url": "http://localhost:3000/api/organizations/1/joboffers/applications",
"method": "GET",
@ -362,7 +386,7 @@
{
"type": "http",
"name": "GetJobOffers",
"seq": 8,
"seq": 11,
"request": {
"url": "http://localhost:3000/api/organizations/1/joboffers/",
"method": "GET",
@ -391,7 +415,7 @@
{
"type": "http",
"name": "GetMyApplications",
"seq": 5,
"seq": 8,
"request": {
"url": "http://localhost:3000/api/organizations/joboffers/applications/mine",
"method": "GET",
@ -414,7 +438,7 @@
{
"type": "http",
"name": "GetMyself",
"seq": 21,
"seq": 24,
"request": {
"url": "http://localhost:3000/api/persons/me",
"method": "GET",
@ -443,7 +467,7 @@
{
"type": "http",
"name": "GetOrganization",
"seq": 15,
"seq": 18,
"request": {
"url": "http://localhost:3000/api/organizations/1",
"method": "GET",
@ -472,7 +496,7 @@
{
"type": "http",
"name": "GetPerson",
"seq": 22,
"seq": 25,
"request": {
"url": "http://localhost:3000/api/persons/1/details",
"method": "GET",
@ -498,10 +522,33 @@
}
}
},
{
"type": "http",
"name": "InsertContactInfo",
"seq": 3,
"request": {
"url": "http://localhost:3000/persons/myself/contactinfos",
"method": "POST",
"headers": [],
"params": [],
"body": {
"mode": "none",
"formUrlEncoded": [],
"multipartForm": []
},
"script": {},
"vars": {},
"assertions": [],
"tests": "",
"auth": {
"mode": "inherit"
}
}
},
{
"type": "http",
"name": "Login",
"seq": 24,
"seq": 27,
"request": {
"url": "http://localhost:3000/api/persons/me/token",
"method": "POST",
@ -536,7 +583,7 @@
{
"type": "http",
"name": "Register",
"seq": 25,
"seq": 28,
"request": {
"url": "http://localhost:3000/api/persons",
"method": "POST",
@ -568,45 +615,10 @@
}
}
},
{
"type": "http",
"name": "RemoveOrganizationAdmin",
"seq": 10,
"request": {
"url": "http://localhost:3000/api/organizations/1/admins/me",
"method": "DELETE",
"headers": [
{
"name": "Content-Type",
"value": "application/json",
"enabled": true
},
{
"name": "User-Agent",
"value": "insomnia/2023.5.8",
"enabled": true
}
],
"params": [],
"body": {
"mode": "json",
"json": "",
"formUrlEncoded": [],
"multipartForm": []
},
"script": {},
"vars": {},
"assertions": [],
"tests": "",
"auth": {
"mode": "inherit"
}
}
},
{
"type": "http",
"name": "RequestNewPassword",
"seq": 18,
"seq": 21,
"request": {
"url": "http://localhost:3000/api/resetpassword/request",
"method": "POST",
@ -638,10 +650,45 @@
}
}
},
{
"type": "http",
"name": "RemoveOrganizationAdmin",
"seq": 13,
"request": {
"url": "http://localhost:3000/api/organizations/1/admins/me",
"method": "DELETE",
"headers": [
{
"name": "Content-Type",
"value": "application/json",
"enabled": true
},
{
"name": "User-Agent",
"value": "insomnia/2023.5.8",
"enabled": true
}
],
"params": [],
"body": {
"mode": "json",
"json": "",
"formUrlEncoded": [],
"multipartForm": []
},
"script": {},
"vars": {},
"assertions": [],
"tests": "",
"auth": {
"mode": "inherit"
}
}
},
{
"type": "http",
"name": "ResetNewPassword",
"seq": 17,
"seq": 20,
"request": {
"url": "http://localhost:3000/api/resetpassword/reset",
"method": "POST",
@ -676,7 +723,7 @@
{
"type": "http",
"name": "UpdateOrganization",
"seq": 14,
"seq": 17,
"request": {
"url": "http://localhost:3000/api/organizations/1",
"method": "PATCH",
@ -711,7 +758,7 @@
{
"type": "http",
"name": "UpdatePerson",
"seq": 20,
"seq": 23,
"request": {
"url": "http://localhost:3000/api/persons/me",
"method": "PATCH",
@ -745,16 +792,15 @@
},
{
"type": "http",
"name": "GetApplicantsByJobOffer",
"seq": 3,
"name": "GetContactInfosByPerson",
"seq": 2,
"request": {
"url": "http://localhost:3000/api/organizations/joboffers/1/applications",
"url": "http://localhost:3000/api/persons/1/contactinfos",
"method": "GET",
"headers": [],
"params": [],
"body": {
"mode": "json",
"json": "",
"mode": "none",
"formUrlEncoded": [],
"multipartForm": []
},
@ -763,7 +809,30 @@
"assertions": [],
"tests": "",
"auth": {
"mode": "inherit"
"mode": "none"
}
}
},
{
"type": "http",
"name": "DeleteContactInfo",
"seq": 1,
"request": {
"url": "http://localhost:3000/api/persons/contactinfos/2",
"method": "DELETE",
"headers": [],
"params": [],
"body": {
"mode": "none",
"formUrlEncoded": [],
"multipartForm": []
},
"script": {},
"vars": {},
"assertions": [],
"tests": "",
"auth": {
"mode": "none"
}
}
}
@ -774,7 +843,7 @@
"auth": {
"mode": "bearer",
"bearer": {
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJwZXJzb25faWQiOjEsImlhdCI6MTczMDI5ODY0NywiZXhwIjoxNzMwMzI3NDQ3fQ.Quqwib7DasUSmWwLPpaCLQscBna_jNs-P0u2QW12lgA"
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJwZXJzb25faWQiOjEsImlhdCI6MTczMDM4MzIzMiwiZXhwIjoxNzMwNDEyMDMyfQ.sAW5wdB_dmjBxEyWI8dty8en9Fhtwyxbb45YV8q6u90"
}
}
}

View File

@ -430,6 +430,40 @@
}
}
},
{
"name": "GetApplicantsByJobOffer",
"event": [],
"request": {
"method": "GET",
"header": [],
"auth": null,
"description": "",
"url": {
"raw": "http://localhost:3000/api/organizations/joboffers/1/applications",
"protocol": "http",
"host": [
"localhost:3000"
],
"path": [
"api",
"organizations",
"joboffers",
"1",
"applications"
],
"query": [],
"variable": []
},
"body": {
"mode": "raw",
"options": {
"raw": {
"language": "json"
}
}
}
}
},
{
"name": "GetApplicantsByOrganization",
"event": [],
@ -617,6 +651,30 @@
}
}
},
{
"name": "InsertContactInfo",
"event": [],
"request": {
"method": "POST",
"header": [],
"auth": null,
"description": "",
"url": {
"raw": "http://localhost:3000/persons/myself/contactinfos",
"protocol": "http",
"host": [
"localhost:3000"
],
"path": [
"persons",
"myself",
"contactinfos"
],
"query": [],
"variable": []
}
}
},
{
"name": "Login",
"event": [],
@ -709,6 +767,52 @@
}
}
},
{
"name": "RequestNewPassword",
"event": [],
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json",
"disabled": false,
"type": "default"
},
{
"key": "User-Agent",
"value": "insomnia/2023.5.8",
"disabled": false,
"type": "default"
}
],
"auth": null,
"description": "",
"url": {
"raw": "http://localhost:3000/api/resetpassword/request",
"protocol": "http",
"host": [
"localhost:3000"
],
"path": [
"api",
"resetpassword",
"request"
],
"query": [],
"variable": []
},
"body": {
"mode": "raw",
"raw": "{\n \"email\": \"john@mail.org\"\n}",
"options": {
"raw": {
"language": "json"
}
}
}
}
},
{
"name": "RemoveOrganizationAdmin",
"event": [],
@ -756,52 +860,6 @@
}
}
},
{
"name": "RequestNewPassword",
"event": [],
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json",
"disabled": false,
"type": "default"
},
{
"key": "User-Agent",
"value": "insomnia/2023.5.8",
"disabled": false,
"type": "default"
}
],
"auth": null,
"description": "",
"url": {
"raw": "http://localhost:3000/api/resetpassword/request",
"protocol": "http",
"host": [
"localhost:3000"
],
"path": [
"api",
"resetpassword",
"request"
],
"query": [],
"variable": []
},
"body": {
"mode": "raw",
"raw": "{\n \"email\": \"john@mail.org\"\n}",
"options": {
"raw": {
"language": "json"
}
}
}
}
},
{
"name": "ResetNewPassword",
"event": [],
@ -941,7 +999,7 @@
}
},
{
"name": "GetApplicantsByJobOffer",
"name": "GetContactInfosByPerson",
"event": [],
"request": {
"method": "GET",
@ -949,28 +1007,44 @@
"auth": null,
"description": "",
"url": {
"raw": "http://localhost:3000/api/organizations/joboffers/1/applications",
"raw": "http://localhost:3000/api/persons/1/contactinfos",
"protocol": "http",
"host": [
"localhost:3000"
],
"path": [
"api",
"organizations",
"joboffers",
"persons",
"1",
"applications"
"contactinfos"
],
"query": [],
"variable": []
}
}
},
{
"name": "DeleteContactInfo",
"event": [],
"request": {
"method": "DELETE",
"header": [],
"auth": null,
"description": "",
"url": {
"raw": "http://localhost:3000/api/persons/contactinfos/2",
"protocol": "http",
"host": [
"localhost:3000"
],
"path": [
"api",
"persons",
"contactinfos",
"2"
],
"query": [],
"variable": []
},
"body": {
"mode": "raw",
"options": {
"raw": {
"language": "json"
}
}
}
}
}

View File

@ -27,6 +27,7 @@ const jobOffersRoutes = require('./routes/job_offer_routes.js');
const serverRoutes = require('./routes/server_routes.js');
const resetPasswordRoutes = require('./routes/reset_password_routes.js');
const applicationRoutes = require('./routes/job_application_routes.js');
const personContactInfosRoutes = require('./routes/person_contact_info_routes.js');
/*
===== END IMPORTING MODULES =====
@ -50,13 +51,14 @@ app.use(rateLimiter); // Apply the rate limiter middleware to all routes
===== BEGIN ROUTE HANDLING =====
*/
app.use('/api/server', serverRoutes.routes);
app.use('/api/persons', personRoutes.publicRoutes);
app.use('/api/persons', personRoutes.protectedRoutes);
app.use('/api/persons', personRoutes.publicRoutes); // TODO: Change in "/people". Idk why I chose "persons"
app.use('/api/persons', personRoutes.protectedRoutes); // TODO: Change in "/people". Idk why I chose "persons"
app.use('/api/organizations', organizationRoutes.routes);
app.use('/api/organizations', jobOffersRoutes.routes);
app.use('/api/organizations', organizationAdminRoutes.routes);
app.use('/api/resetpassword', resetPasswordRoutes.routes);
app.use('/api/organizations', applicationRoutes.routes);
app.use('/api/persons', personContactInfosRoutes.routes); // TODO: Change in "/people". Idk why I chose "persons"
/*
===== END ROUTE HANDLING =====
@ -76,6 +78,3 @@ app.listen(port, () => {
/*
===== END STARTING THE SERVER =====
*/
// Export the app for testing purposes
module.exports = app;

View File

@ -0,0 +1,43 @@
/*
This code is part of Blink
licensed under GPLv3
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
IN THE SOFTWARE.
*/
const knex = require('../utils/knex_config');
async function insert(personId, infoContent, infoType) {
const contactInfo = await knex('PersonContactInfo')
.insert({
person_id: personId,
info: infoContent,
info_type: infoType
})
.returning("*");
return contactInfo[0];
}
async function getInfoByPerson(personId) {
return await knex('PersonContactInfo')
.where('person_id', personId);
}
async function remove(contactInfoId, personId) {
return (await knex('PersonContactInfo')
.where('id', contactInfoId)
.where('person_id', personId)
.del() === 1)
}
module.exports = {
insert,
remove,
getInfoByPerson
}

View File

@ -0,0 +1,65 @@
/*
This code is part of Blink
licensed under GPLv3
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
IN THE SOFTWARE.
*/
const express = require('express');
const jwtUtils = require('../utils/jwt_utils');
const PersonContactInfo = require('../models/person_contact_info_model');
async function insert(req, res) {
try {
const contactInfo = await PersonContactInfo.insert(req.jwt.person_id, req.body.content, req.body.info_type);
res.set('Location', `/api/persons/${req.jwt.person_id}/contactinfos/${contactInfo.id}`);
return res.status(201).json(contactInfo);
} catch (error) {
console.error(`Error in function ${insert.name}: ${error}`);
res.status(500).json({
error: 'Internal server error'
});
}
}
async function findByPerson(req, res) {
try {
const infos = await PersonContactInfo.getInfoByPerson(req.params.personId);
return res.status(200).json(infos);
} catch (error) {
console.error(`Error in function ${insert.name}: ${error}`);
res.status(500).json({
error: 'Internal server error'
});
}
}
async function remove(req, res) {
try {
const success = await PersonContactInfo.remove(req.params.contactInfoId, req.jwt.person_id);
if (success) {
return res.status(200).send();
} else {
return res.status(400).send();
}
} catch (error) {
console.error(`Error in function ${insert.name}: ${error}`);
res.status(500).json({
error: 'Internal server error'
});
}
}
const routes = express.Router();
routes.post('/myself/contactinfos', jwtUtils.extractToken, insert);
routes.get('/:personId/contactinfos', jwtUtils.extractToken, findByPerson)
routes.delete('/contactinfos/:contactInfoId', jwtUtils.extractToken, remove)
module.exports = {
routes
};

View File

@ -0,0 +1,77 @@
// To execute tests, call: npx jest
// Note: The API should already be running
// Note: These tests will pollute the database. Run only in test environments
const request = require('supertest');
const apiEndpoint = 'http://localhost:3000/api';
function randomString() {
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
const length = Math.floor(Math.random() * 10) + 1;
let result = '';
for (let i = 0; i < length; i++) {
result += characters.charAt(Math.floor(Math.random() * characters.length));
}
return result;
};
const userEmail = randomString() + "_test@mail.org";
describe('Person tests', () => {
it('should return a 201 status code for POST /persons', async () => {
const userData = {
"email": userEmail,
"password": "password",
"displayName": "Test1",
};
const response = await request(apiEndpoint)
.post('/persons')
.send(userData);
expect(response.status).toBe(201);
});
it('should return a 400 status code for POST /persons', async () => {
const userData = {
"password": "password",
"displayName": "Test1",
};
const response = await request(apiEndpoint)
.post('/persons')
.send(userData);
expect(response.status).toBe(400);
});
it('should return a 400 status code for POST /persons', async () => {
const userData = {
"email": "test1@mail.org",
"displayName": "Test1",
};
const response = await request(apiEndpoint)
.post('/persons')
.send(userData);
expect(response.status).toBe(400);
});
it('should return a 400 status code for POST /persons', async () => {
const userData = {
"email": "test1@mail.org",
"password": "password"
};
const response = await request(apiEndpoint)
.post('/persons')
.send(userData);
expect(response.status).toBe(400);
});
it('should return a 400 status code for POST /persons', async () => {
const userData = {
"email": randomString() + "_test_not_an_email",
"password": "password",
"displayName": "Test1",
};
const response = await request(apiEndpoint)
.post('/persons')
.send(userData);
expect(response.status).toBe(400);
});
});

View File

@ -0,0 +1,175 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Company Website</title>
<style>
/* Basic Reset */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: Arial, sans-serif;
color: #333;
background-color: #f9f9f9;
}
/* Header Styles */
header {
background-color: #00509e;
color: white;
padding: 20px 10%;
text-align: center;
}
header img {
max-width: 100px;
border-radius: 10px;
}
header h1 {
font-size: 2.5em;
margin: 10px 0;
}
header p {
font-size: 1.2em;
margin-top: 10px;
}
/* Main Content */
.content {
width: 80%;
margin: 20px auto;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
}
/* Section Styles */
section {
background: white;
border-radius: 10px;
padding: 20px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
section h2 {
color: #00509e;
margin-bottom: 15px;
}
/* Contact Info */
.contact-info p {
line-height: 1.8;
font-size: 1em;
}
/* Open Positions */
.position {
margin-top: 10px;
padding: 10px;
border: 1px solid #eee;
border-radius: 8px;
}
.position h3 {
color: #333;
margin-bottom: 5px;
}
.position p {
font-size: 0.9em;
color: #666;
}
.apply-button {
display: inline-block;
margin-top: 10px;
padding: 8px 16px;
background-color: #00509e;
color: white;
border-radius: 5px;
text-decoration: none;
font-size: 0.9em;
}
.apply-button:hover {
background-color: #003f7f;
}
/* Footer */
footer {
text-align: center;
padding: 15px;
background-color: #00509e;
color: white;
margin-top: 20px;
font-size: 0.9em;
}
</style>
</head>
<body>
<!-- Header Section -->
<header>
<img src="./google.png" alt="Company Logo">
<h1>Google</h1>
<p>Don't be evil</p>
</header>
<!-- Main Content -->
<div class="content">
<!-- Basic Info Section -->
<section class="basic-info">
<h2>About Us</h2>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore
et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
</section>
<!-- Contact Info Section -->
<section class="contact-info">
<h2>Contact Information</h2>
<p><strong>Address:</strong> New York</p>
<p><strong>Email:</strong> contacts@google.com</p>
<p><strong>Phone:</strong> +1 (234) 567-890</p>
<p><strong>Website:</strong> <a href="https://google.com">google.com</a></p>
</section>
<!-- Open Positions Section -->
<section class="open-positions">
<h2>Open Positions</h2>
<div class="position">
<h3>Software Engineer</h3>
<p>Join our team to develop cutting-edge software solutions that impact millions. We are looking for individuals with a passion for technology and a dedication to excellence.</p>
<a href="#" class="apply-button">Apply Now</a>
</div>
<div class="position">
<h3>Product Designer</h3>
<p>We're seeking a creative and innovative Product Designer who can help us shape the future of our products. Join us to bring design excellence to every user interaction.</p>
<a href="#" class="apply-button">Apply Now</a>
</div>
<div class="position">
<h3>Marketing Specialist</h3>
<p>Help us tell our story! We need a Marketing Specialist to craft effective campaigns and elevate our brand presence across various platforms.</p>
<a href="#" class="apply-button">Apply Now</a>
</div>
</section>
</div>
<!-- Footer Section -->
<footer>
<p>&copy; 2024 Awesome Company. All Rights Reserved.</p>
</footer>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB