-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
36 changed files
with
1,849 additions
and
31 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
# Airtable: https://support.airtable.com/docs/creating-and-using-api-keys-and-access-tokens | ||
AIRTABLE_PERSONAL_ACCESS_TOKEN= | ||
|
||
# BlueDot Impact Slack, #tech-dev-alerts | ||
ALERTS_SLACK_CHANNEL_ID=C04SFUECECU | ||
# For (local) BlueBot see https://airtable.com/appnNmNoNMB6crg6I/tbllthJ2YSPDsKWCt/viwfjWLvplq6Xw93D/recqurdJTPWzm5y4W | ||
# For (prod) BlueBot see https://api.slack.com/apps/A04PV2GAQRY/install-on-team | ||
# Starts 'xoxb-' | ||
ALERTS_SLACK_BOT_TOKEN= |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
# Airtable | ||
AIRTABLE_PERSONAL_ACCESS_TOKEN=FAKE_TOKEN | ||
|
||
# Slack | ||
ALERTS_SLACK_CHANNEL_ID=C04SFUECECU | ||
ALERTS_SLACK_BOT_TOKEN=FAKE_TOKEN |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
FROM node:lts-alpine@sha256:7e227295e96f5b00aa79555ae166f50610940d888fc2e321cf36304cbd17d7d6 AS base | ||
|
||
RUN apk update && apk add --no-cache dumb-init | ||
|
||
WORKDIR /app | ||
|
||
ENV NODE_ENV production | ||
ENV NEXT_TELEMETRY_DISABLED 1 | ||
|
||
# Set the correct permission for prerender cache | ||
RUN mkdir .next | ||
RUN chown node:node .next | ||
|
||
ARG APP_NAME | ||
ENV APP_NAME=${APP_NAME} | ||
|
||
COPY --chown=node:node .next/standalone ./ | ||
COPY --chown=node:node public ./apps/${APP_NAME}/public | ||
COPY --chown=node:node .next/static ./apps/${APP_NAME}/.next/static | ||
|
||
USER node | ||
|
||
EXPOSE 8080 | ||
|
||
CMD HOSTNAME="0.0.0.0" PORT="8080" dumb-init node ./apps/${APP_NAME}/server.js |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
/// <reference types="next" /> | ||
/// <reference types="next/image-types/global" /> | ||
|
||
// NOTE: This file should not be edited | ||
// see https://nextjs.org/docs/basic-features/typescript for more information. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
/** @type {import('next').NextConfig} */ | ||
module.exports = { | ||
transpilePackages: ['@bluedot/ui'], | ||
reactStrictMode: true, | ||
output: 'standalone', | ||
|
||
// We already run eslint as a separate step | ||
eslint: { | ||
ignoreDuringBuilds: true, | ||
}, | ||
|
||
poweredByHeader: false, | ||
headers: async () => { | ||
return [ | ||
// See https://developers.zoom.us/docs/meeting-sdk/web/sharedarraybuffer/ | ||
{ | ||
source: '/', | ||
headers: [ | ||
{ | ||
key: 'Cross-Origin-Embedder-Policy', | ||
value: 'require-corp', | ||
}, | ||
{ | ||
key: 'Cross-Origin-Opener-Policy', | ||
value: 'same-origin', | ||
}, | ||
], | ||
}, | ||
{ | ||
source: '/:path*', | ||
headers: [ | ||
{ | ||
key: 'X-Bluedot-Version', | ||
// eslint-disable-next-line turbo/no-undeclared-env-vars | ||
value: process.env.VERSION_TAG || 'unknown', | ||
}, | ||
], | ||
}, | ||
]; | ||
}, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
{ | ||
"name": "@bluedot/availability", | ||
"version": "1.0.0", | ||
"private": true, | ||
"scripts": { | ||
"postinstall": "shx cp -n .env.local.template .env.local", | ||
"start": "next dev -p 8000", | ||
"build": "next build", | ||
"lint": "eslint .", | ||
"test": "vitest --run", | ||
"test:watch": "vitest", | ||
"deploy:prod": "tools/deployDocker.sh" | ||
}, | ||
"dependencies": { | ||
"@bluedot/ui": "*", | ||
"airtable-ts": "^1.0.0", | ||
"axios": "^1.6.8", | ||
"clsx": "^2.1.0", | ||
"http-errors": "^2.0.0", | ||
"next": "^14.0.3", | ||
"react": "^18.2.0", | ||
"react-dom": "^18.2.0", | ||
"react-hook-form": "^7.51.3", | ||
"react-select": "^5.8.0", | ||
"weekly-availabilities": "^1.0.0" | ||
}, | ||
"devDependencies": { | ||
"@bluedot/eslint-config": "*", | ||
"@bluedot/typescript-config": "*", | ||
"@testing-library/react": "^15.0.2", | ||
"@types/node": "^20.10.3", | ||
"@types/react": "^18.0.22", | ||
"@types/react-dom": "^18.0.7", | ||
"@vitejs/plugin-react": "^4.2.1", | ||
"autoprefixer": "^10.4.19", | ||
"dotenv": "^16.4.5", | ||
"eslint": "^8.53.0", | ||
"happy-dom": "^14.7.1", | ||
"node-mocks-http": "^1.14.1", | ||
"postcss": "^8.4.38", | ||
"shx": "^0.3.4", | ||
"tailwindcss": "^3.4.3", | ||
"typescript": "^5.3.2", | ||
"vitest": "^1.5.0" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
module.exports = { | ||
plugins: { | ||
tailwindcss: {}, | ||
autoprefixer: {}, | ||
}, | ||
}; |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
export const SpinnerIcon: React.FC = () => { | ||
return ( | ||
<svg | ||
className="animate-spin h-5 w-5 text-gray-700" | ||
xmlns="http://www.w3.org/2000/svg" | ||
fill="none" | ||
viewBox="0 0 24 24" | ||
> | ||
<circle | ||
className="opacity-25" | ||
cx="12" | ||
cy="12" | ||
r="10" | ||
stroke="currentColor" | ||
strokeWidth="4" | ||
/> | ||
<path | ||
className="opacity-75" | ||
fill="currentColor" | ||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" | ||
/> | ||
</svg> | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
@tailwind base; | ||
@tailwind components; | ||
@tailwind utilities; | ||
|
||
@font-face { | ||
font-family: "Reckless Neue"; | ||
src: url("/fonts/RecklessNeue-Light.woff2") format("woff2"); | ||
font-weight: 300; | ||
font-style: normal; | ||
-webkit-font-smoothing: antialiased; | ||
-moz-osx-font-smoothing: grayscale; | ||
font-style: normal | ||
} | ||
|
||
@font-face { | ||
font-family: "Reckless Neue"; | ||
src: url("/fonts/RecklessNeue-LightItalic.woff2") format("woff2"); | ||
font-weight: 300; | ||
font-style: normal; | ||
-webkit-font-smoothing: antialiased; | ||
-moz-osx-font-smoothing: grayscale; | ||
font-style: italic | ||
} | ||
|
||
@font-face { | ||
font-family: "Roobert"; | ||
src: url("/fonts/Roobert-Light.woff2") format("woff2"); | ||
font-weight: 300; | ||
font-style: normal; | ||
-webkit-font-smoothing: antialiased; | ||
-moz-osx-font-smoothing: grayscale; | ||
font-style: normal | ||
} | ||
|
||
@font-face { | ||
font-family: "Roobert"; | ||
src: url("/fonts/Roobert-Regular.woff2") format("woff2"); | ||
font-weight: 400; | ||
font-style: normal; | ||
-webkit-font-smoothing: antialiased; | ||
-moz-osx-font-smoothing: grayscale; | ||
font-style: normal | ||
} | ||
|
||
@font-face { | ||
font-family: "Roobert"; | ||
src: url("/fonts/Roobert-Bold.woff2") format("woff2"); | ||
font-weight: 700; | ||
font-style: normal; | ||
-webkit-font-smoothing: antialiased; | ||
-moz-osx-font-smoothing: grayscale; | ||
font-style: normal | ||
} | ||
|
||
/* TODO: move these styles out of here */ | ||
|
||
.setup input { | ||
@apply px-3 py-2 text-gray-700 rounded border focus:outline focus:outline-gray-400; | ||
} | ||
|
||
.setup button { | ||
@apply px-3 py-1 rounded text-white bg-blue-500 hover:bg-blue-600 disabled:bg-blue-300; | ||
} | ||
|
||
.timezone-select.solid-select-container { | ||
@apply w-32; | ||
} | ||
|
||
.timezone-select .solid-select-list { | ||
@apply bg-white border-gray-300 border-2 w-32; | ||
} | ||
|
||
.timezone-select .solid-select-option { | ||
@apply flex justify-center cursor-pointer; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
import createHttpError from 'http-errors'; | ||
import { NextApiHandler } from 'next'; | ||
import { slackAlert } from './slackAlert'; | ||
|
||
export const apiRoute = (handler: NextApiHandler, useAuth: true | 'insecure_no_auth' = true): NextApiHandler => async (req, res) => { | ||
try { | ||
if (useAuth !== 'insecure_no_auth') { | ||
const token = req.headers.authorization?.slice('Bearer '.length).trim(); | ||
if (!token) { | ||
throw new createHttpError.Unauthorized('Missing token'); | ||
} | ||
throw new Error('Authenticated endpoints not supported'); | ||
} | ||
await handler(req, res); | ||
} catch (err: unknown) { | ||
if (createHttpError.isHttpError(err) && err.expose) { | ||
console.warn(`Error handling request on route ${req.method} ${req.url}:`); | ||
console.warn(err); | ||
res.status(err.statusCode).json({ error: err.message }); | ||
return; | ||
} | ||
|
||
console.error(`Internal error handling request on route ${req.method} ${req.url}:`); | ||
console.error(err); | ||
try { | ||
await slackAlert([ | ||
`Error: Failed request on route ${req.method} ${req.url}: ${err instanceof Error ? err.message : String(err)}`, | ||
...(err instanceof Error ? [`Stack:\n\`\`\`${err.stack}\`\`\``] : []), | ||
]); | ||
} catch (slackError) { | ||
console.error('Failed to send Slack', slackError); | ||
} | ||
res.status(createHttpError.isHttpError(err) ? err.statusCode : 500).json({ | ||
error: 'Internal Server Error', | ||
}); | ||
} | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
import { AirtableTs, Table, Item } from 'airtable-ts'; | ||
import env from './env'; | ||
|
||
export default new AirtableTs({ | ||
apiKey: env.AIRTABLE_PERSONAL_ACCESS_TOKEN, | ||
}); | ||
|
||
export interface FormConfiguration extends Item { | ||
'Slug': string, | ||
'Title': string, | ||
'Webhook': string, | ||
'Minimum length': number, | ||
} | ||
|
||
export const formConfigurationTable: Table<FormConfiguration> = { | ||
name: 'form configuration', | ||
baseId: 'app6dkBHka8c4WaEj', | ||
tableId: 'tblvsaRl69XV8azGZ', | ||
schema: { | ||
Slug: 'string', | ||
Title: 'string', | ||
Webhook: 'string', | ||
'Minimum length': 'number', | ||
}, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
import { validateEnv } from '../validateEnv'; | ||
|
||
const envVars = [ | ||
'AIRTABLE_PERSONAL_ACCESS_TOKEN', | ||
|
||
'ALERTS_SLACK_CHANNEL_ID', | ||
'ALERTS_SLACK_BOT_TOKEN', | ||
] as const; | ||
|
||
export type Env = Record<(typeof envVars)[number], string>; | ||
|
||
const env: Env = validateEnv(process.env, envVars); | ||
|
||
export default env; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
import axios from 'axios'; | ||
import env from './env'; | ||
|
||
export const slackAlert = async (messages: string[]): Promise<void> => { | ||
if (messages.length === 0) return; | ||
const res = await sendSingleSlackMessage(messages[0]!); | ||
for (let i = 1; i < messages.length; i++) { | ||
// eslint-disable-next-line no-await-in-loop | ||
await sendSingleSlackMessage(messages[i]!, res.ts); | ||
} | ||
}; | ||
|
||
const sendSingleSlackMessage = async (message: string, threadTs?: string): Promise<{ ts: string }> => { | ||
console.log(`Sending Slack (thread: ${threadTs ?? 'none'}): ${message}`); | ||
return axios({ | ||
method: 'post', | ||
baseURL: 'https://slack.com/api/', | ||
url: 'chat.postMessage', | ||
headers: { | ||
'Content-Type': 'application/json', | ||
Authorization: `Bearer ${env.ALERTS_SLACK_BOT_TOKEN}`, | ||
}, | ||
data: { | ||
channel: env.ALERTS_SLACK_CHANNEL_ID, | ||
text: `time-availability-form: ${message}`, | ||
thread_ts: threadTs, | ||
}, | ||
}).then((res) => { | ||
if (!res.data.ok) { | ||
throw new Error(`Error from Slack API: ${res.data.error}`); | ||
} | ||
return { ts: res.data.ts }; | ||
}); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
import { describe, expect, test } from 'vitest'; | ||
import { offsets, parseOffsetFromStringToMinutes } from './date'; | ||
|
||
describe('parseOffsetFromStringToMinutes', () => { | ||
test.each([ | ||
['UTC00:00', 0], | ||
['UTC-01:00', 60], | ||
['UTC-01:30', 90], | ||
['UTC+01:00', -60], | ||
['UTC+02:00', -120], | ||
['UTC+03:45', -225], | ||
])('%s', (timezone, offset) => { | ||
expect(parseOffsetFromStringToMinutes(timezone)).toBe(offset); | ||
}); | ||
|
||
test('all offsets can be parsed', () => { | ||
offsets.forEach((offset) => expect(() => parseOffsetFromStringToMinutes(offset)).not.toThrow()); | ||
}); | ||
}); |
Oops, something went wrong.