Skip to content

Commit

Permalink
Merge branch 'develop' into ON-620_allowing_other_languages
Browse files Browse the repository at this point in the history
  • Loading branch information
mfonsecaOEF authored May 23, 2024
2 parents 9f924e2 + c2eeaf8 commit 6c7136f
Show file tree
Hide file tree
Showing 44 changed files with 2,254 additions and 613 deletions.
7 changes: 7 additions & 0 deletions .github/workflows/web-tag.yml
Original file line number Diff line number Diff line change
Expand Up @@ -135,5 +135,12 @@ jobs:
kubectl set env deployment/cc-web-deploy "ADMIN_NAMES=${{secrets.ADMIN_NAMES}}"
kubectl set env deployment/cc-web-deploy "DEFAULT_ADMIN_EMAIL=${{secrets.DEFAULT_ADMIN_EMAIL}}"
kubectl set env deployment/cc-web-deploy "DEFAULT_ADMIN_PASSWORD=${{secrets.DEFAULT_ADMIN_PASSWORD}}"
kubectl set env deployment/cc-web-deploy \
"HOST=https://citycatalyst.io" \
"NEXTAUTH_URL=https://citycatalyst.io" \
"NEXT_PUBLIC_API_URL=https://api.citycatalyst.io" \
"GLOBAL_API_URL=https://api.citycatalyst.io" \
"NEXT_PUBLIC_OPENCLIMATE_API_URL=https://app.openclimate.network" \
"OPENCLIMATE_API_URL=https://app.openclimate.network"
kubectl create -f k8s/cc-create-admin.yml -n default
kubectl rollout restart deployment cc-web-deploy -n default
519 changes: 112 additions & 407 deletions app/package-lock.json

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions app/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "city-catalyst",
"version": "0.9.0-dev.0",
"version": "0.10.0-dev.0",
"private": true,
"type": "module",
"scripts": {
Expand Down Expand Up @@ -33,7 +33,7 @@
"@chakra-ui/next-js": "^2.2.0",
"@chakra-ui/react": "^2.8.2",
"@huggingface/inference": "^2.6.4",
"@react-email/components": "^0.0.11",
"@react-email/components": "^0.0.16",
"@reduxjs/toolkit": "^2.2.3",
"@storybook/cli": "^8.0.8",
"@storybook/react": "^7.4.5",
Expand Down
6 changes: 3 additions & 3 deletions app/scripts/catalogue-sync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ interface Source {
datasource_name: string;
dataset_name: string;
source_type: string;
url: string;
dataset_url: string;
dataset_description: string;
access_type: string;
geographical_location: string;
Expand Down Expand Up @@ -119,7 +119,7 @@ async function syncDataCatalogue() {

if (!source.notes) {
// publisher_id is still a name at this stage
source.notes = `${source.datasource_name} by ${source.publisher_id}. For more details see ${source.url}`;
source.notes = `${source.datasource_name} by ${source.publisher_id}. For more details see ${source.dataset_url}`;
}

if (source.geographical_location === "global") {
Expand Down Expand Up @@ -156,7 +156,7 @@ async function syncDataCatalogue() {
publisher = await db.models.Publisher.create({
publisherId: randomUUID(),
name: source.publisher_id,
url: source.url,
url: source.dataset_url,
});
}
source.publisher_id = publisher.publisherId;
Expand Down
6 changes: 3 additions & 3 deletions app/src/app/[lng]/onboarding/setup/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -741,9 +741,9 @@ export default function OnboardingSetup({
city = await addCity({
name: data.name,
locode: data.locode!,
area,
region: regionName,
country: countryName,
area: (area) ? Math.round(area) : undefined,
region: regionName ?? undefined,
country: countryName ?? undefined,
regionLocode: region?.actor_id ?? undefined,
countryLocode: country?.actor_id ?? undefined,
}).unwrap();
Expand Down
49 changes: 7 additions & 42 deletions app/src/app/api/v0/city/[city]/file/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ export const GET = apiHandler(async (_req: Request, context) => {

export const POST = apiHandler(
async (req: NextRequest, { params, session }) => {
const service = new NotificationService(); // TODO cache this/ make it a singleton
const service = NotificationService.getInstance(); // TODO cache this/ make it a singleton
const user = session?.user;
const cityId = params.city;

Expand Down Expand Up @@ -128,47 +128,12 @@ export const POST = apiHandler(
fileType: userFile.fileType!,
},
};
const host = process.env.HOST ?? "http://localhost:3000";

const emailTemplate = `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Email Notification</title>
</head>
<body style="background-color: #ffffff; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Ubuntu, Cantarell, 'Helvetica Neue', sans-serif; margin: 0; padding: 20px;">
<div style="margin: 0 auto; max-width: 580px; padding: 20px 0 48px;">
<!-- SVG Placeholder for ExcelFileIcon -->
<!-- Make sure to replace with actual SVG or an <img> tag pointing to the icon -->
<svg width="36" height="36" viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg"><!-- SVG content --></svg>
<h1 style="color: #2351DC; font-size: 20px; line-height: 1.5; font-weight: 700;">CityCatalyst</h1>
<h2 style="color: #484848; font-size: 24px; line-height: 1.3; font-weight: 700; margin-top: 50px;">${user?.name} From ${city.name} Uploaded New Files For Review</h2>
<p style="font-size: 14px; line-height: 1.4; color: #484848;">Hi ${process.env.ADMIN_NAMES},</p>
<p style="font-size: 14px; line-height: 1.4; color: #484848;">${user?.name} (${user?.email}) has uploaded files in CityCatalyst for revision and to upload to their inventories.</p>
<!-- Example for file link; adjust href as needed -->
<a href="${host}/api/v0/user/file/${newFileData.id}/download-file" style="text-decoration: none; color: #2351DC;"><div>
<div style="flex-direction: column; padding-left: 16px; align-items: center; gap: 16px; height: 100px; border-radius: 8px; border: 1px solid #E6E7FF; margin-top: 0px"><div>${file.name}</div><br /><div style="color:a6a6a6">${bytesToMB(file.size)}</div><div style="margin-top: 20px">${newFileData.subsectors.map((item: string) => `<span key=${item} style="background-color: #e8eafb; color: #2351dc; padding: 6px 8px; border-radius: 30px; margin-right: 8px; font-size: 14px; margin-top: 20px">${item}</span>`)}</div></div>
</div></a>
<!-- Placeholder for tags; repeat this structure for each tag as needed -->
<a href="${host}"><button style="font-size: 14px; padding: 16px; background-color: #2351DC; border-radius: 100px; line-height:1.5; color: #FFFFFF; margin-top: 20px; border:none">GOTO REVIEW</button></a>
<!-- Footer -->
<hr style="height: 2px; background: #EBEBEC; margin-top: 36px;" />
<p style="font-size: 12px; line-height: 16px; color: #79797A; font-weight: 400;">Open Earth Foundation is a nonprofit public benefit corporation from California, USA. EIN: 85-3261449</p>
</div>
</body>
</html>
`;

if (process.env.NODE_ENV !== "test") {
await service.sendEmail({
to: process.env.ADMIN_EMAILS!,
subject: "CityCatalyst File Upload",
text: "City Catalyst",
html: emailTemplate,
});
}

await NotificationService.sendNotificationEmail({
user: { email: user?.email!, name: user?.name! },
fileData: newFileData,
city,
});

return NextResponse.json({
data: newFileData,
Expand Down
55 changes: 48 additions & 7 deletions app/src/backend/NotificationService.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,39 @@
import { smtpOptions } from "@/lib/email";
import AdminNotificationTemplate from "@/lib/emails/AdminNotificationTemplate";
import { City } from "@/models/City";
import { User } from "@/models/User";
import { UserFileResponse } from "@/util/types";
import { render } from "@react-email/components";
import nodemailer, { Transporter } from "nodemailer";

interface EmailOptions {
export interface EmailOptions {
to: string;
subject: string;
text: string;
html: string;
}

interface SendEmailResponse {
export interface SendEmailResponse {
success: boolean;
messageId?: string;
error?: any;
}

class NotificationService {
private transporter: Transporter;
constructor() {
this.transporter = nodemailer.createTransport({ ...smtpOptions });
private static instance: NotificationService;
private static transporter: Transporter;

private constructor(transporter?: Transporter) {}

static getInstance(transporter?: Transporter): NotificationService {
if (!NotificationService.instance) {
NotificationService.instance = new NotificationService(transporter);
}

return NotificationService.instance;
}

async sendEmail({
static async sendEmail({
to,
subject,
text,
Expand All @@ -35,14 +48,42 @@ class NotificationService {
};

try {
const info = await this.transporter.sendMail(mailOptions);
const transporter = nodemailer.createTransport({ ...smtpOptions });
const info = await transporter.sendMail(mailOptions);
console.log("Message sent: %s", info.messageId);
return { success: true, messageId: info.messageId };
} catch (error) {
console.error("Error sending email:", error);
return { success: false, error };
}
}

static async sendNotificationEmail({
user,
fileData,
city,
}: {
user: { name: string; email: string };
fileData: UserFileResponse;
city: City;
}) {
await NotificationService.sendEmail({
to: process.env.ADMIN_EMAILS!,
subject: "CityCatalyst File Upload",
text: "City Catalyst",
html: render(
AdminNotificationTemplate({
adminNames: process.env.ADMIN_NAMES!,
file: fileData,
user: {
cityName: city.name!,
email: user?.email!,
name: user?.name!,
},
}),
),
});
}
}

export default NotificationService;
9 changes: 4 additions & 5 deletions app/src/lib/emails/AdminNotificationTemplate.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
import { ExcelFileIcon } from "@/components/icons";
import { CityAttributes } from "@/models/City";
import { UserAttributes } from "@/models/User";
import { UserFileAttributes } from "@/models/UserFile";
import { bytesToMB } from "@/util/helpers";
import { UserFileResponse } from "@/util/types";
import { Tag, TagLabel } from "@chakra-ui/react";
import React from "react";
import {
Body,
Expand Down Expand Up @@ -128,11 +124,14 @@ export default function AdminNotificationTemplate({
const tag = {
padding: "6px",
paddingLeft: "8px",
paddingRight: "8px",
display: "flex",
alignItems: "center",
width: "150px",
overflow: "ellipsis",
whiteSpace: "nowrap",
borderRadius: "30px",
overflow: "hidden",
textOverflow: "ellipsis",
background: "#e8eafb",
color: "#2351dc",
marginRight: "8px",
Expand Down
28 changes: 20 additions & 8 deletions app/tests/api/userfile.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,15 @@ import {

import { db } from "@/models";
import assert from "node:assert";
import { after, before, describe, it } from "node:test";
import {
after,
afterEach,
before,
beforeEach,
describe,
it,
mock,
} from "node:test";
import {
testFileFormat,
filePath,
Expand Down Expand Up @@ -74,11 +82,6 @@ describe("UserFile API", () => {
name: "TEST_CITY",
});
await user.addCity(city);

const service = new NotificationService();
service.sendEmail = async () => {
return { success: true, messageId: "send" };
};
});
after(async () => {
if (db.sequelize) await db.sequelize.close();
Expand All @@ -89,6 +92,10 @@ describe("UserFile API", () => {
});
});

beforeEach(() => {
mock.method(NotificationService, "sendNotificationEmail", () => {});
});

it("should create a user file", async () => {
// stream created file from path
const path = await filePath();
Expand All @@ -110,14 +117,19 @@ describe("UserFile API", () => {
const res = await createUserFile(req, {
params: { city: testCityID },
});
assert.equal(res.status, 200);

const { data } = await res.json();

assert.equal(res.status, 200);
assert.equal(data?.sector, fileData?.sector);
assert.equal(data?.url, fileData.url);
assert.equal(data?.status, fileData.status);
assert.equal(data?.gpcRefNo, fileData.gpc_ref_no);
assert.equal(fileData.data.fileName, data?.file.fileName);
assert.equal(fileData.data.size, data?.file.size);
// @ts-ignore
const calls = NotificationService.sendNotificationEmail.mock.calls.length;
assert.equal(calls, 1);
});

it("should not create a file if data is invalid", async () => {
Expand Down Expand Up @@ -185,7 +197,7 @@ describe("UserFile API", () => {
it("should delete user file", async () => {
const fileStream = await getFileDataFromStream(await filePath());
const formData = new FormData();
formData.append("id", randomUUID());
formData.append("id", fileData.id);
formData.append("userId", fileData.userId);
formData.append("sector", fileData.sector);
formData.append("subsectors", fileData.subsectors);
Expand Down
8 changes: 7 additions & 1 deletion app/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,11 @@
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules", "cypress/**/*", "cypress.config.ts"]
"exclude": [
"node_modules",
"cypress/**/*",
"cypress.config.ts",
"e2e/**/*",
"playwright.config.ts"
]
}
Loading

0 comments on commit 6c7136f

Please sign in to comment.