mirror of
https://github.com/BillyOutlast/posthog.com.git
synced 2026-02-04 03:11:21 +01:00
remove netlify from codebase (#5064)
This commit is contained in:
34
.github/workflows/checks.yml
vendored
34
.github/workflows/checks.yml
vendored
@@ -44,37 +44,3 @@ jobs:
|
||||
- name: Push changes
|
||||
run: |
|
||||
git push
|
||||
|
||||
js_checks:
|
||||
name: Safe Redirects
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.pull_request.head.repo.full_name == github.repository
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
ref: ${{ github.head_ref }}
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '18'
|
||||
- name: Install dependencies
|
||||
run: yarn
|
||||
- name: Run tests
|
||||
run: yarn test-redirects
|
||||
- name: Configure Git
|
||||
run: |
|
||||
git fetch origin master:master
|
||||
git config user.name PostHog Bot
|
||||
git config user.email hey@posthog.com
|
||||
- name: Add safe redirects
|
||||
if: ${{ github.base_ref == 'master' }}
|
||||
run: yarn generate-redirects
|
||||
- name: Cleanup and commit
|
||||
if: ${{ github.base_ref == 'master' }}
|
||||
run: |
|
||||
rm pr_diff
|
||||
git add netlify.toml
|
||||
[ "$(git status -s)" = "" ] && exit 0 || git commit -m "Add safe redirects (JS)"
|
||||
- name: Push changes
|
||||
run: |
|
||||
git push
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -22,8 +22,6 @@ pr_diff
|
||||
|
||||
recipe-server.log
|
||||
|
||||
# Local Netlify folder
|
||||
.netlify
|
||||
static/fonts/MatterSQItalicVF.woff
|
||||
static/fonts/MatterSQItalicVF.woff2
|
||||
static/fonts/MatterSQVF.woff
|
||||
|
||||
@@ -111,7 +111,7 @@ Near the bottom of a pull request page, you'll see a box like this:
|
||||
|
||||
(Note: This box only appears if you're a member of the PostHog GitHub org - it's not available to the public.)
|
||||
|
||||
You can click the _Details_ link on the line that says `netlify/posthog/deploy-preview` to see the preview.
|
||||
You can click the _Visit Preview_ link in the Vercel bot comment to see the preview.
|
||||
|
||||
### Merging changes
|
||||
|
||||
|
||||
@@ -371,27 +371,20 @@ The sidebar is generated from each of the files in `/src/sidebars`.
|
||||
|
||||
#### Redirects
|
||||
|
||||
Redirects are managed in `netlify.toml` which is located in the root folder.
|
||||
Redirects are managed in `vercel.json` which is located in the root folder.
|
||||
|
||||
To declare a new redirect, open `netlify.toml` and add an entry with the `[[redirects]]` heading:
|
||||
To declare a new redirect, open `vercel.json` and add an entry to the `redirects` list:
|
||||
|
||||
```
|
||||
[[redirects]]
|
||||
from = "/docs/integrations/android-integration"
|
||||
to = "/docs/libraries/android"
|
||||
{ "source": "/docs/contributing/stack", "destination": "/docs/contribute/stack" }
|
||||
```
|
||||
|
||||
The default HTTP status code is 301, but if you need to define a different status code, it can be changed like this:
|
||||
The default HTTP status code is 308 (permanent), but if the redirect should be temporary (307), it can be updated like this:
|
||||
|
||||
```
|
||||
[[redirects]]
|
||||
from = "/docs/integrations/android-integration"
|
||||
to = "/docs/libraries/android"
|
||||
status = 302
|
||||
{ "source": "/docs/contributing/stack", "destination": "/docs/contribute/stack", "permanent": false }
|
||||
```
|
||||
|
||||
> If you ever need to rename a file to get a different slug, a redirect is automatically created with the Safe Redirects action
|
||||
|
||||
## Committing changes
|
||||
|
||||
It's best to create commits that are focused on one specific area. For example, create one commit for textual changes and another for functional ones. Another example is creating a commit for changes to a section of the handbook and a different commit for updates to the documentation. This helps the pull request review process and also means specific commits can be [cherry picked](https://git-scm.com/docs/git-cherry-pick).
|
||||
@@ -508,7 +501,7 @@ If you know who you would like to review the pull request, select them in the **
|
||||
|
||||
## Preview branch
|
||||
|
||||
After a series of checks are run (to ensure nothing in your pull request breaks the website), Netlify will generate a preview link available on the `netlify/posthog/deploy-preview` line. This includes all of your changes so you can preview before your pull request is merged.
|
||||
After a series of checks are run (to ensure nothing in your pull request breaks the website), Vercel will generate a preview link available in the Vercel bot comment. This includes all of your changes so you can preview before your pull request is merged.
|
||||
|
||||

|
||||
|
||||
@@ -520,4 +513,4 @@ To get changes into production, the website deploys automatically from `master`.
|
||||
|
||||
#### Acknowledgements
|
||||
|
||||
This website is based on [Gatsby](https://gatsbyjs.org) and is hosted with [Netlify](https://www.netlify.com/).
|
||||
This website is based on [Gatsby](https://gatsbyjs.org) and is hosted with [Vercel](https://vercel.com).
|
||||
|
||||
@@ -102,7 +102,7 @@ Submit a PR to [posthog/posthog.com](https://github.com/posthog/posthog.com) wit
|
||||
|
||||
- Add a meta description using `description` in the frontmatter section (optional)
|
||||
|
||||
- Set the date of the blog post to the intended publishing date in the format `YYYY-MM-DD`. Posts dated [in the future](https://github.com/PostHog/posthog.com/pull/2964) won't display on the site until their specified date, though a build is required day-of in order to publish the post. (The Website & Docs team or other Netlify admins can kick off a manual build.)
|
||||
- Set the date of the blog post to the intended publishing date in the format `YYYY-MM-DD`. Posts dated [in the future](https://github.com/PostHog/posthog.com/pull/2964) won't display on the site until their specified date, though a build is required day-of in order to publish the post. (The Website & Docs team or other Vercel admins can kick off a manual build.)
|
||||
|
||||
- Create an annotation on [app.posthog.com](https://app.posthog.com) for the content to track the effect.
|
||||
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||
const Busboy = require('busboy')
|
||||
const request = require('request')
|
||||
|
||||
function multipartToAshby(event) {
|
||||
return new Promise((resolve) => {
|
||||
const applicationForm = { fieldSubmissions: [] }
|
||||
const formData = {}
|
||||
const busboy = Busboy({
|
||||
headers: event.headers,
|
||||
})
|
||||
|
||||
busboy.on('file', (name, filestream, file) => {
|
||||
filestream.on('data', (data) => {
|
||||
formData[name] = {
|
||||
value: data,
|
||||
options: {
|
||||
filename: file.filename,
|
||||
contentType: null,
|
||||
},
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
busboy.on('field', (name, value) => {
|
||||
if (name === 'jobPostingId') {
|
||||
formData[name] = value
|
||||
} else {
|
||||
applicationForm.fieldSubmissions.push({
|
||||
path: name,
|
||||
value,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
busboy.on('finish', () => {
|
||||
formData.applicationForm = JSON.stringify(applicationForm)
|
||||
resolve(formData)
|
||||
})
|
||||
|
||||
busboy.end(Buffer.from(event.body, 'base64'))
|
||||
})
|
||||
}
|
||||
|
||||
exports.handler = async (e) => {
|
||||
const formData = await multipartToAshby(e)
|
||||
const options = {
|
||||
method: 'POST',
|
||||
url: 'https://api.ashbyhq.com/applicationForm.submit',
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
Authorization: `Basic ${Buffer.from(`${process.env.ASHBY_API_KEY}:`).toString('base64')}`,
|
||||
},
|
||||
formData,
|
||||
}
|
||||
const submission = await new Promise((resolve, reject) => {
|
||||
request(options, function (err, res) {
|
||||
if (err) throw new Error(err)
|
||||
resolve(res.body)
|
||||
})
|
||||
})
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
body: JSON.stringify({ submission }),
|
||||
}
|
||||
}
|
||||
@@ -1,142 +0,0 @@
|
||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||
const fetch = require('node-fetch')
|
||||
const getGravatar = require('gravatar')
|
||||
|
||||
exports.handler = async (e) => {
|
||||
let { body } = e
|
||||
if (!body) return { statusCode: 500, body: 'Missing body' }
|
||||
body = JSON.parse(body)
|
||||
let avatar
|
||||
if (body.email) {
|
||||
const gravatar = getGravatar.url(body.email)
|
||||
avatar = await fetch(`https:${gravatar}?d=404`).then((res) => (res.ok && `https:${gravatar}`) || '')
|
||||
}
|
||||
|
||||
const message = {
|
||||
channel: process.env.SLACK_QUESTION_CHANNEL,
|
||||
ts: body.timestamp,
|
||||
text: body.question,
|
||||
username: body.name,
|
||||
blocks: [
|
||||
{
|
||||
type: 'header',
|
||||
text: {
|
||||
type: 'plain_text',
|
||||
text: `${body.name} on ${body.slug}`,
|
||||
emoji: true,
|
||||
},
|
||||
block_id: 'name_and_slug',
|
||||
},
|
||||
{
|
||||
type: 'header',
|
||||
text: {
|
||||
type: 'plain_text',
|
||||
text: body.subject,
|
||||
emoji: true,
|
||||
},
|
||||
block_id: 'subject',
|
||||
},
|
||||
{
|
||||
type: 'section',
|
||||
block_id: 'question',
|
||||
text: {
|
||||
type: 'mrkdwn',
|
||||
text: body.question,
|
||||
},
|
||||
...(avatar && {
|
||||
accessory: {
|
||||
type: 'image',
|
||||
image_url: avatar,
|
||||
alt_text: body.name,
|
||||
},
|
||||
}),
|
||||
},
|
||||
{
|
||||
type: 'actions',
|
||||
elements: [
|
||||
{
|
||||
type: 'button',
|
||||
style: 'primary',
|
||||
text: {
|
||||
type: 'plain_text',
|
||||
text: `Publish${body.email ? ' & notify' : ''}`,
|
||||
emoji: true,
|
||||
},
|
||||
value: JSON.stringify({
|
||||
question: body.question,
|
||||
name: body.name,
|
||||
slug: body.slug,
|
||||
email: body.email,
|
||||
subject: body.subject,
|
||||
avatar,
|
||||
}),
|
||||
action_id: 'publish-button',
|
||||
confirm: {
|
||||
title: {
|
||||
type: 'plain_text',
|
||||
text: 'Please confirm',
|
||||
},
|
||||
text: {
|
||||
type: 'mrkdwn',
|
||||
text: `Clicking confirm will add this thread to the website${
|
||||
body.email ? ' and send an email to the original poster' : ''
|
||||
}.`,
|
||||
},
|
||||
confirm: {
|
||||
type: 'plain_text',
|
||||
text: 'Confirm',
|
||||
},
|
||||
deny: {
|
||||
type: 'plain_text',
|
||||
text: 'Cancel',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'button',
|
||||
text: {
|
||||
type: 'plain_text',
|
||||
text: 'Edit',
|
||||
emoji: true,
|
||||
},
|
||||
value: JSON.stringify({
|
||||
question: body.question,
|
||||
name: body.name,
|
||||
slug: body.slug,
|
||||
email: body.email,
|
||||
subject: body.subject,
|
||||
}),
|
||||
action_id: 'edit-question-button',
|
||||
},
|
||||
{
|
||||
type: 'button',
|
||||
text: {
|
||||
type: 'plain_text',
|
||||
text: 'Open page',
|
||||
emoji: true,
|
||||
},
|
||||
url: `https://posthog.com${body.slug}`,
|
||||
action_id: 'open-page-button',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const slack = await fetch(
|
||||
body.timestamp ? 'https://slack.com/api/chat.update' : 'https://slack.com/api/chat.postMessage',
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${process.env.SLACK_API_KEY}`,
|
||||
},
|
||||
body: JSON.stringify(message),
|
||||
}
|
||||
).then((res) => res.json())
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
body: JSON.stringify({ timestamp: slack.ts }),
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||
const fetch = require('node-fetch')
|
||||
|
||||
exports.handler = async (e) => {
|
||||
const { queryStringParameters } = e
|
||||
const companyName = queryStringParameters.name
|
||||
let data = {}
|
||||
if (companyName) {
|
||||
const clearbitData = await fetch(`https://company.clearbit.com/v1/domains/find?name=${companyName}`, {
|
||||
headers: { Authorization: `Bearer ${process.env.CLEARBIT_API_KEY}` },
|
||||
}).then((res) => res.json())
|
||||
const { name, logo } = clearbitData
|
||||
data = {
|
||||
name,
|
||||
logo,
|
||||
}
|
||||
}
|
||||
return {
|
||||
statusCode: 200,
|
||||
body: JSON.stringify(data),
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||
const hubspot = require('@hubspot/api-client')
|
||||
|
||||
exports.handler = async (e) => {
|
||||
let { body } = e
|
||||
if (!body) return { statusCode: 500, body: 'Missing body' }
|
||||
body = JSON.parse(body)
|
||||
const { email, firstName, lastName } = body
|
||||
|
||||
if (!email || !firstName || !lastName) {
|
||||
return {
|
||||
statusCode: 500,
|
||||
body: 'Missing required fields',
|
||||
}
|
||||
}
|
||||
|
||||
const hubspotClient = new hubspot.Client({ accessToken: process.env.HUBSPOT_ACCESS_TOKEN })
|
||||
|
||||
const properties = {
|
||||
email,
|
||||
firstname: firstName,
|
||||
lastname: lastName,
|
||||
}
|
||||
const SimplePublicObjectInput = { properties }
|
||||
|
||||
try {
|
||||
const apiResponse = await hubspotClient.crm.contacts.basicApi.create(SimplePublicObjectInput)
|
||||
console.log(JSON.stringify(apiResponse.body, null, 2))
|
||||
} catch (e) {
|
||||
e.message === 'HTTP request failed' ? console.error(JSON.stringify(e.response, null, 2)) : console.error(e)
|
||||
return {
|
||||
statusCode: 500,
|
||||
body: 'HubSpot request failed',
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||
const fetch = require('node-fetch')
|
||||
const md5 = require('md5')
|
||||
|
||||
exports.handler = async (e) => {
|
||||
let { body } = e
|
||||
if (!body) return { statusCode: 500, body: 'Missing body' }
|
||||
body = JSON.parse(body)
|
||||
const { email, tag } = body
|
||||
if (!email || !tag) return { statusCode: 500, body: 'Missing required fields' }
|
||||
const data = await fetch(`https://us19.api.mailchimp.com/3.0/lists/ef3044881e/members/${md5(email)}/tags`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${process.env.MAILCHIMP_API_KEY}`,
|
||||
},
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ tags: [{ name: tag, status: 'active' }], is_syncing: false }),
|
||||
})
|
||||
return {
|
||||
statusCode: 200,
|
||||
body: JSON.stringify(data),
|
||||
}
|
||||
}
|
||||
@@ -1,525 +0,0 @@
|
||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||
const fetch = require('node-fetch')
|
||||
const queryString = require('query-string')
|
||||
const formData = require('form-data')
|
||||
const Mailgun = require('mailgun.js')
|
||||
const getGravatar = require('gravatar')
|
||||
const { App, AwsLambdaReceiver } = require('@slack/bolt')
|
||||
|
||||
const awsLambdaReceiver = new AwsLambdaReceiver({
|
||||
signingSecret: process.env.SLACK_SIGNING_SECRET,
|
||||
})
|
||||
|
||||
const app = new App({
|
||||
signingSecret: process.env.SLACK_SIGNING_SECRET,
|
||||
token: process.env.SLACK_API_KEY,
|
||||
receiver: awsLambdaReceiver,
|
||||
})
|
||||
|
||||
app.view('submit-button', async ({ ack, view, client }) => {
|
||||
await ack()
|
||||
const {
|
||||
private_metadata,
|
||||
state: { values },
|
||||
} = view
|
||||
|
||||
const body = {
|
||||
slug: null,
|
||||
name: null,
|
||||
email: null,
|
||||
question: null,
|
||||
subject: null,
|
||||
}
|
||||
|
||||
Object.keys(values).forEach((valueKey) => {
|
||||
Object.keys(body).forEach((bodyKey) => {
|
||||
if (values[valueKey][bodyKey]) {
|
||||
body[bodyKey] = values[valueKey][bodyKey].value
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
body.timestamp = private_metadata
|
||||
|
||||
let avatar
|
||||
if (body.email) {
|
||||
const gravatar = getGravatar.url(body.email)
|
||||
avatar = await fetch(`https:${gravatar}?d=404`).then((res) => (res.ok && `https:${gravatar}`) || '')
|
||||
}
|
||||
|
||||
const message = {
|
||||
channel: process.env.SLACK_QUESTION_CHANNEL,
|
||||
ts: body.timestamp,
|
||||
text: body.question,
|
||||
username: body.name,
|
||||
blocks: [
|
||||
{
|
||||
type: 'header',
|
||||
text: {
|
||||
type: 'plain_text',
|
||||
text: `${body.name} on ${body.slug}`,
|
||||
emoji: true,
|
||||
},
|
||||
block_id: 'name_and_slug',
|
||||
},
|
||||
{
|
||||
type: 'header',
|
||||
text: {
|
||||
type: 'plain_text',
|
||||
text: body.subject,
|
||||
emoji: true,
|
||||
},
|
||||
block_id: 'subject',
|
||||
},
|
||||
{
|
||||
type: 'section',
|
||||
block_id: 'question',
|
||||
text: {
|
||||
type: 'mrkdwn',
|
||||
text: body.question,
|
||||
},
|
||||
...(avatar && {
|
||||
accessory: {
|
||||
type: 'image',
|
||||
image_url: avatar,
|
||||
alt_text: body.name,
|
||||
},
|
||||
}),
|
||||
},
|
||||
{
|
||||
type: 'actions',
|
||||
elements: [
|
||||
{
|
||||
type: 'button',
|
||||
style: 'primary',
|
||||
text: {
|
||||
type: 'plain_text',
|
||||
text: 'Publish',
|
||||
emoji: true,
|
||||
},
|
||||
value: JSON.stringify({
|
||||
question: body.question,
|
||||
name: body.name,
|
||||
slug: body.slug,
|
||||
email: body.email,
|
||||
subject: body.subject,
|
||||
avatar,
|
||||
}),
|
||||
action_id: 'publish-button',
|
||||
confirm: {
|
||||
title: {
|
||||
type: 'plain_text',
|
||||
text: 'Please confirm',
|
||||
},
|
||||
text: {
|
||||
type: 'mrkdwn',
|
||||
text: 'Clicking confirm will add this thread to the website.',
|
||||
},
|
||||
confirm: {
|
||||
type: 'plain_text',
|
||||
text: 'Confirm',
|
||||
},
|
||||
deny: {
|
||||
type: 'plain_text',
|
||||
text: 'Cancel',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'button',
|
||||
text: {
|
||||
type: 'plain_text',
|
||||
text: 'Edit',
|
||||
emoji: true,
|
||||
},
|
||||
value: JSON.stringify({
|
||||
question: body.question,
|
||||
name: body.name,
|
||||
slug: body.slug,
|
||||
email: body.email,
|
||||
subject: body.subject,
|
||||
timestamp: body.timestamp,
|
||||
}),
|
||||
action_id: 'edit-question-button',
|
||||
},
|
||||
{
|
||||
type: 'button',
|
||||
text: {
|
||||
type: 'plain_text',
|
||||
text: 'Open page',
|
||||
emoji: true,
|
||||
},
|
||||
url: `https://posthog.com${body.slug}`,
|
||||
action_id: 'open-page-button',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
client.chat.update(message)
|
||||
})
|
||||
|
||||
app.action('edit-question-button', async ({ ack, client, body, logger }) => {
|
||||
await ack()
|
||||
try {
|
||||
const { question, name, email, slug, subject } = JSON.parse(body.actions[0].value)
|
||||
const result = await client.views.open({
|
||||
trigger_id: body.trigger_id,
|
||||
view: {
|
||||
callback_id: 'submit-button',
|
||||
private_metadata: body.message.ts,
|
||||
type: 'modal',
|
||||
title: {
|
||||
type: 'plain_text',
|
||||
text: `Question`,
|
||||
emoji: true,
|
||||
},
|
||||
submit: {
|
||||
type: 'plain_text',
|
||||
text: 'Save',
|
||||
emoji: true,
|
||||
},
|
||||
close: {
|
||||
type: 'plain_text',
|
||||
text: 'Cancel',
|
||||
emoji: true,
|
||||
},
|
||||
blocks: [
|
||||
{
|
||||
type: 'input',
|
||||
element: {
|
||||
type: 'plain_text_input',
|
||||
action_id: 'slug',
|
||||
initial_value: slug,
|
||||
},
|
||||
label: {
|
||||
type: 'plain_text',
|
||||
text: 'Slug',
|
||||
emoji: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
element: {
|
||||
type: 'plain_text_input',
|
||||
action_id: 'subject',
|
||||
initial_value: subject,
|
||||
},
|
||||
label: {
|
||||
type: 'plain_text',
|
||||
text: 'Subject',
|
||||
emoji: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
element: {
|
||||
type: 'plain_text_input',
|
||||
action_id: 'name',
|
||||
initial_value: name,
|
||||
},
|
||||
label: {
|
||||
type: 'plain_text',
|
||||
text: 'Name',
|
||||
emoji: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
optional: true,
|
||||
element: {
|
||||
type: 'plain_text_input',
|
||||
action_id: 'email',
|
||||
initial_value: email || '',
|
||||
},
|
||||
label: {
|
||||
type: 'plain_text',
|
||||
text: 'Email',
|
||||
emoji: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
element: {
|
||||
type: 'plain_text_input',
|
||||
multiline: true,
|
||||
action_id: 'question',
|
||||
initial_value: question,
|
||||
},
|
||||
label: {
|
||||
type: 'plain_text',
|
||||
text: 'Question',
|
||||
emoji: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error(error)
|
||||
}
|
||||
})
|
||||
|
||||
app.action('unpublish-button', async ({ ack, client, body }) => {
|
||||
await ack()
|
||||
const { message, actions } = body
|
||||
const { name, slug, question, avatar, email, subject } = JSON.parse(actions[0].value)
|
||||
const messageUpdate = {
|
||||
channel: process.env.SLACK_QUESTION_CHANNEL,
|
||||
ts: message.ts,
|
||||
text: question,
|
||||
username: name,
|
||||
blocks: [
|
||||
{
|
||||
type: 'header',
|
||||
text: {
|
||||
type: 'plain_text',
|
||||
text: `${name} on ${slug}`,
|
||||
emoji: true,
|
||||
},
|
||||
block_id: 'name_and_slug',
|
||||
},
|
||||
{
|
||||
type: 'header',
|
||||
text: {
|
||||
type: 'plain_text',
|
||||
text: subject,
|
||||
emoji: true,
|
||||
},
|
||||
block_id: 'subject',
|
||||
},
|
||||
{
|
||||
type: 'section',
|
||||
block_id: 'question',
|
||||
text: {
|
||||
type: 'mrkdwn',
|
||||
text: question,
|
||||
},
|
||||
...(avatar && {
|
||||
accessory: {
|
||||
type: 'image',
|
||||
image_url: avatar,
|
||||
alt_text: name,
|
||||
},
|
||||
}),
|
||||
},
|
||||
{
|
||||
type: 'actions',
|
||||
elements: [
|
||||
{
|
||||
type: 'button',
|
||||
style: 'primary',
|
||||
text: {
|
||||
type: 'plain_text',
|
||||
text: 'Publish',
|
||||
emoji: true,
|
||||
},
|
||||
value: JSON.stringify({
|
||||
question: question,
|
||||
name: name,
|
||||
slug: slug,
|
||||
email: email,
|
||||
avatar,
|
||||
}),
|
||||
action_id: 'publish-button',
|
||||
confirm: {
|
||||
title: {
|
||||
type: 'plain_text',
|
||||
text: 'Please confirm',
|
||||
},
|
||||
text: {
|
||||
type: 'mrkdwn',
|
||||
text: 'Clicking confirm will add this thread to the website.',
|
||||
},
|
||||
confirm: {
|
||||
type: 'plain_text',
|
||||
text: 'Confirm',
|
||||
},
|
||||
deny: {
|
||||
type: 'plain_text',
|
||||
text: 'Cancel',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'button',
|
||||
text: {
|
||||
type: 'plain_text',
|
||||
text: 'Edit',
|
||||
emoji: true,
|
||||
},
|
||||
value: JSON.stringify({
|
||||
question: question,
|
||||
name: name,
|
||||
slug: slug,
|
||||
email: email,
|
||||
}),
|
||||
action_id: 'edit-question-button',
|
||||
},
|
||||
{
|
||||
type: 'button',
|
||||
text: {
|
||||
type: 'plain_text',
|
||||
text: 'Open page',
|
||||
emoji: true,
|
||||
},
|
||||
url: `https://posthog.com${slug}`,
|
||||
action_id: 'open-page-button',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
client.chat.update(messageUpdate)
|
||||
})
|
||||
|
||||
app.action('publish-button', async ({ ack, client, body }) => {
|
||||
await ack()
|
||||
const { message, actions } = body
|
||||
const { question, name, slug, email, avatar, subject } = JSON.parse(actions[0].value)
|
||||
const replies = await client.conversations.replies({
|
||||
ts: message.ts,
|
||||
channel: process.env.SLACK_QUESTION_CHANNEL,
|
||||
})
|
||||
|
||||
const answer = replies.messages && replies.messages[1] && replies.messages[1].text
|
||||
if (answer) {
|
||||
if (email) {
|
||||
const mailgun = new Mailgun(formData)
|
||||
const mg = mailgun.client({
|
||||
username: 'api',
|
||||
key: process.env.MAILGUN_API_KEY,
|
||||
url: 'https://api.eu.mailgun.net',
|
||||
})
|
||||
const mailgunData = {
|
||||
from: 'hey@posthog.com',
|
||||
to: email,
|
||||
subject: `Someone answered your question on posthog.com!`,
|
||||
template: 'question-answered',
|
||||
'h:X-Mailgun-Variables': JSON.stringify({
|
||||
question,
|
||||
answer,
|
||||
}),
|
||||
'h:Reply-To': 'hey@posthog.com',
|
||||
}
|
||||
await mg.messages.create(process.env.MAILGUN_DOMAIN, mailgunData).catch((err) => console.log(err))
|
||||
}
|
||||
const messageUpdate = {
|
||||
channel: process.env.SLACK_QUESTION_CHANNEL,
|
||||
ts: message.ts,
|
||||
text: question,
|
||||
username: name,
|
||||
blocks: [
|
||||
{
|
||||
type: 'header',
|
||||
text: {
|
||||
type: 'plain_text',
|
||||
text: `${name} on ${slug}`,
|
||||
emoji: true,
|
||||
},
|
||||
block_id: 'name_and_slug',
|
||||
},
|
||||
{
|
||||
type: 'header',
|
||||
text: {
|
||||
type: 'plain_text',
|
||||
text: subject,
|
||||
emoji: true,
|
||||
},
|
||||
block_id: 'subject',
|
||||
},
|
||||
{
|
||||
type: 'section',
|
||||
block_id: 'question',
|
||||
text: {
|
||||
type: 'mrkdwn',
|
||||
text: question,
|
||||
},
|
||||
...(avatar && {
|
||||
accessory: {
|
||||
type: 'image',
|
||||
image_url: avatar,
|
||||
alt_text: name,
|
||||
},
|
||||
}),
|
||||
},
|
||||
{
|
||||
type: 'section',
|
||||
block_id: 'published',
|
||||
text: {
|
||||
type: 'plain_text',
|
||||
text: '✅ Published',
|
||||
emoji: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'actions',
|
||||
elements: [
|
||||
{
|
||||
type: 'button',
|
||||
style: 'danger',
|
||||
text: {
|
||||
type: 'plain_text',
|
||||
text: 'Unpublish',
|
||||
emoji: true,
|
||||
},
|
||||
value: JSON.stringify({
|
||||
question: question,
|
||||
name: name,
|
||||
slug: slug,
|
||||
email: email,
|
||||
subject: subject,
|
||||
avatar,
|
||||
}),
|
||||
action_id: 'unpublish-button',
|
||||
confirm: {
|
||||
title: {
|
||||
type: 'plain_text',
|
||||
text: 'Please confirm',
|
||||
},
|
||||
text: {
|
||||
type: 'mrkdwn',
|
||||
text: 'Clicking confirm will remove this thread from the website',
|
||||
},
|
||||
confirm: {
|
||||
type: 'plain_text',
|
||||
text: 'Confirm',
|
||||
},
|
||||
deny: {
|
||||
type: 'plain_text',
|
||||
text: 'Cancel',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'button',
|
||||
text: {
|
||||
type: 'plain_text',
|
||||
text: 'Open page',
|
||||
emoji: true,
|
||||
},
|
||||
url: `https://posthog.com${slug}`,
|
||||
action_id: 'open-page-button',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
client.chat.update(messageUpdate)
|
||||
}
|
||||
})
|
||||
|
||||
app.action('open-page-button', async ({ ack }) => {
|
||||
await ack()
|
||||
})
|
||||
|
||||
exports.handler = async (event, context, callback) => {
|
||||
const { body } = event
|
||||
const { payload } = queryString.parse(body)
|
||||
const { token } = JSON.parse(payload)
|
||||
if (token !== process.env.SLACK_VERIFICATION_TOKEN) return { statusCode: 500, body: 'Invalid token' }
|
||||
const handler = await awsLambdaReceiver.start()
|
||||
return handler(event, context, callback)
|
||||
}
|
||||
@@ -1,114 +0,0 @@
|
||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||
const queryString = require('query-string')
|
||||
const { App, AwsLambdaReceiver } = require('@slack/bolt')
|
||||
const { createClient } = require('@supabase/supabase-js')
|
||||
|
||||
const supabase = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_API_KEY)
|
||||
|
||||
const awsLambdaReceiver = new AwsLambdaReceiver({
|
||||
signingSecret: process.env.SLACK_USERS_SIGNING_SECRET,
|
||||
})
|
||||
|
||||
const app = new App({
|
||||
signingSecret: process.env.SLACK_USERS_SIGNING_SECRET,
|
||||
token: process.env.SLACK_USERS_API_KEY,
|
||||
receiver: awsLambdaReceiver,
|
||||
})
|
||||
|
||||
app.shortcut('publish_thread', async ({ shortcut, ack, client, logger }) => {
|
||||
try {
|
||||
await ack()
|
||||
const user = await client.users.info({ user: shortcut.user.id })
|
||||
if (!user.user.is_admin) return
|
||||
const { data, error } = await supabase
|
||||
.from('Messages')
|
||||
.select('slack_timestamp, slug')
|
||||
.eq('slack_timestamp', shortcut.message.ts)
|
||||
|
||||
const view = {
|
||||
trigger_id: shortcut.trigger_id,
|
||||
view: {
|
||||
private_metadata: JSON.stringify({
|
||||
ts: shortcut.message.ts,
|
||||
channel: shortcut.channel.id,
|
||||
user: shortcut.user.id,
|
||||
}),
|
||||
callback_id: 'submit-db-button',
|
||||
type: 'modal',
|
||||
title: {
|
||||
type: 'plain_text',
|
||||
text: 'PostHog Q&A',
|
||||
},
|
||||
submit: {
|
||||
type: 'plain_text',
|
||||
text: 'Publish',
|
||||
emoji: true,
|
||||
},
|
||||
close: {
|
||||
type: 'plain_text',
|
||||
text: 'Cancel',
|
||||
emoji: true,
|
||||
},
|
||||
blocks: [
|
||||
{
|
||||
type: 'input',
|
||||
optional: false,
|
||||
element: {
|
||||
type: 'plain_text_input',
|
||||
action_id: 'slug',
|
||||
},
|
||||
label: {
|
||||
type: 'plain_text',
|
||||
text: 'Slug (eg: /docs/api/insights), separated by commas',
|
||||
emoji: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
if (data.length > 0 && data[0].slug) {
|
||||
view.view.blocks[0].element.initial_value = data[0].slug
|
||||
view.view.blocks[0].optional = true
|
||||
view.view.submit.text = 'Update'
|
||||
}
|
||||
await client.views.open(view)
|
||||
} catch (error) {
|
||||
logger.error(error)
|
||||
}
|
||||
})
|
||||
|
||||
app.view('submit-db-button', async ({ ack, view }) => {
|
||||
await ack()
|
||||
const {
|
||||
private_metadata,
|
||||
state: { values },
|
||||
} = view
|
||||
const body = {
|
||||
slug: null,
|
||||
}
|
||||
|
||||
const { ts, channel, user } = JSON.parse(private_metadata)
|
||||
|
||||
Object.keys(values).forEach((valueKey) => {
|
||||
Object.keys(body).forEach((bodyKey) => {
|
||||
if (values[valueKey][bodyKey]) {
|
||||
body[bodyKey] = values[valueKey][bodyKey].value
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
body.slack_timestamp = ts
|
||||
body.slack_channel = channel
|
||||
body.slack_user = user
|
||||
|
||||
await supabase.from('Messages').upsert([body])
|
||||
})
|
||||
|
||||
exports.handler = async (event, context, callback) => {
|
||||
const { body } = event
|
||||
const { payload } = queryString.parse(body)
|
||||
const { token } = JSON.parse(payload)
|
||||
if (token !== process.env.SLACK_USERS_VERIFICATION_TOKEN) return { statusCode: 500, body: 'Invalid token' }
|
||||
const handler = await awsLambdaReceiver.start()
|
||||
return handler(event, context, callback)
|
||||
}
|
||||
2574
netlify.toml
2574
netlify.toml
File diff suppressed because it is too large
Load Diff
@@ -19,7 +19,6 @@
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"typegen": "kea-typegen write .",
|
||||
"update-sprite": "svg-sprite -s --symbol-dest src/components/productFeature/images/icons --symbol-sprite sprited-icons.svg src/components/productFeature/images/icons/*.svg",
|
||||
"generate-redirects": "git diff origin/master $(git branch --show-current) > pr_diff && DEBUG=true RUN_AS_SCRIPT=true node ./scripts/safe_rename.js",
|
||||
"test-redirects": "jest scripts",
|
||||
"storybook": "start-storybook -s ./static -p 6006",
|
||||
"build-storybook": "build-storybook"
|
||||
|
||||
@@ -1,147 +0,0 @@
|
||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||
const fs = require('fs')
|
||||
const fetch = require('node-fetch')
|
||||
|
||||
let DEBUG = false
|
||||
|
||||
const log = (...args) => {
|
||||
if (DEBUG) {
|
||||
console.log.apply(null, args)
|
||||
}
|
||||
}
|
||||
|
||||
/** Return date (without time) in ISO format. */
|
||||
const formatDate = (date) => {
|
||||
return date.toISOString().slice(0, 10)
|
||||
}
|
||||
|
||||
const redirectText = (from, to) => {
|
||||
return `
|
||||
[[redirects]]
|
||||
from = "${from}"
|
||||
to = "${to}"
|
||||
`
|
||||
}
|
||||
|
||||
const redirectWithDateCommentText = (date, redirect) => {
|
||||
return `
|
||||
|
||||
# Added: ${formatDate(date)}${redirect}`
|
||||
}
|
||||
|
||||
// Rules
|
||||
const samePath = ({ fromPath, toPath }) => {
|
||||
return fromPath === toPath
|
||||
}
|
||||
|
||||
const mdToMdx = ({ fromPath, toPath }) => {
|
||||
// # old rule
|
||||
// # return '.mdx' not in from_paths[i] and '.mdx' in to_paths[i]
|
||||
return fromPath.endsWith('.md') && toPath.endsWith('.mdx')
|
||||
}
|
||||
|
||||
const redirectExists = ({ redirect, localConfig, remoteConfig }) => {
|
||||
return localConfig.indexOf(redirect.trim()) !== -1 || remoteConfig.indexOf(redirect.trim()) !== -1
|
||||
}
|
||||
|
||||
const fromDotStar = ({ fromPath }) => {
|
||||
return fromPath == '(.*)'
|
||||
}
|
||||
|
||||
const isSnippetsRename = ({ fromPath, toPath }) => {
|
||||
return fromPath.indexOf('/snippets/') !== -1 || toPath.indexOf('/snippets') !== -1
|
||||
}
|
||||
|
||||
const skipRules = {
|
||||
samePath,
|
||||
mdToMdx,
|
||||
redirectExists,
|
||||
fromDotStar,
|
||||
isSnippetsRename,
|
||||
}
|
||||
|
||||
// # Load existing remote redirect config
|
||||
const getRemoteConfig = async () => {
|
||||
const response = await fetch('https://raw.githubusercontent.com/PostHog/posthog.com/master/netlify.toml')
|
||||
return await response.text()
|
||||
}
|
||||
|
||||
// # Load existing redirect file to be used to avoid duplicates
|
||||
const getLocalConfig = async () => {
|
||||
return await fs.promises.readFile('./netlify.toml', 'utf8')
|
||||
}
|
||||
|
||||
const appendToLocalConfig = async (newRedirects) => {
|
||||
await fs.promises.appendFile('./netlify.toml', newRedirects)
|
||||
}
|
||||
|
||||
const getRedirects = async ({ gitDiff, localConfig, remoteConfig, debug = false }) => {
|
||||
DEBUG = debug
|
||||
log(gitDiff)
|
||||
|
||||
const renameFromRegex = new RegExp('rename from contents(.*).md', 'g')
|
||||
const renameToRegex = new RegExp('rename to contents(.*).md', 'g')
|
||||
|
||||
const fromPaths = Array.from(gitDiff.matchAll(renameFromRegex), (m) => m[1])
|
||||
const toPaths = Array.from(gitDiff.matchAll(renameToRegex), (m) => m[1])
|
||||
|
||||
log(fromPaths, toPaths)
|
||||
|
||||
const newRedirects = []
|
||||
if (fromPaths.length > 0 && fromPaths.length === toPaths.length) {
|
||||
for (let i = 0; i < fromPaths.length; ++i) {
|
||||
// handle index default directory files. /path/index will become /path
|
||||
const fromPath = fromPaths[i].replace(/\/index$/, '', fromPaths[i])
|
||||
const toPath = toPaths[i].replace(/\/index$/, '', toPaths[i])
|
||||
|
||||
const redirect = redirectText(fromPath, toPath)
|
||||
// console.log(redirect)
|
||||
|
||||
log(`Testing if redirects are required for: "${fromPath}" to "${toPath}"`)
|
||||
|
||||
let skipRedirect = false
|
||||
for (const [skipRuleName, skipRuleFunction] of Object.entries(skipRules)) {
|
||||
if (skipRedirect === false) {
|
||||
skipRedirect = skipRuleFunction({ fromPath, toPath, redirect, localConfig, remoteConfig })
|
||||
log(`Rule: "${skipRuleName}"', 'skipRedirect?', ${skipRedirect}`)
|
||||
}
|
||||
}
|
||||
|
||||
if (skipRedirect === false) {
|
||||
newRedirects.push(redirectWithDateCommentText(new Date(), redirect))
|
||||
} else {
|
||||
log('Not including redirect', redirect)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log('No path changes found')
|
||||
}
|
||||
|
||||
return newRedirects
|
||||
}
|
||||
|
||||
const main = async () => {
|
||||
try {
|
||||
const gitDiff = await fs.promises.readFile('./pr_diff', 'utf8')
|
||||
|
||||
const remoteConfig = await getRemoteConfig()
|
||||
const localConfig = await getLocalConfig()
|
||||
const debug = process.env.DEBUG
|
||||
const redirects = await getRedirects({ gitDiff, localConfig, remoteConfig, debug })
|
||||
|
||||
if (redirects.length > 0) {
|
||||
log('Writing', redirects)
|
||||
await appendToLocalConfig(redirects.join(''))
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
if (process.env.RUN_AS_SCRIPT === 'true') {
|
||||
main()
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getRedirects,
|
||||
}
|
||||
@@ -1,151 +0,0 @@
|
||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||
const getRedirects = require('./safe_rename').getRedirects
|
||||
|
||||
jest.useFakeTimers().setSystemTime(new Date('2021-08-09').getTime())
|
||||
|
||||
const debug = false
|
||||
|
||||
const singleRenameGitDiff = `
|
||||
rename from contents/docs/one.md
|
||||
rename to contents/docs/two.md`
|
||||
|
||||
const multiRenameGitDiff = `
|
||||
rename from contents/docs/one.md
|
||||
rename to contents/docs/two.md
|
||||
|
||||
rename from contents/docs/cheese.md
|
||||
rename to contents/docs/mushroom.md
|
||||
|
||||
rename from contents/docs/fish.md
|
||||
rename to contents/docs/monkey.md`
|
||||
|
||||
const emptyConfig = ``
|
||||
const existingRedirectConfig = `
|
||||
# Added: 2021-08-03
|
||||
[[redirects]]
|
||||
from = "/docs/one"
|
||||
to = "/docs/two"`
|
||||
|
||||
test('redirect will be added for single file rename', async () => {
|
||||
const gitDiff = singleRenameGitDiff
|
||||
|
||||
const localConfig = emptyConfig
|
||||
const remoteConfig = emptyConfig
|
||||
|
||||
const redirects = await getRedirects({ gitDiff, localConfig, remoteConfig, debug })
|
||||
|
||||
expect(redirects.length).toBe(1)
|
||||
expect(redirects[0]).toContain('# Added: 2021-08-09')
|
||||
expect(redirects[0]).toContain('from = "/docs/one"')
|
||||
expect(redirects[0]).toContain('to = "/docs/two"')
|
||||
})
|
||||
|
||||
test('redirect will be added for multi file rename', async () => {
|
||||
const gitDiff = multiRenameGitDiff
|
||||
|
||||
const localConfig = emptyConfig
|
||||
const remoteConfig = emptyConfig
|
||||
|
||||
const redirects = await getRedirects({ gitDiff, localConfig, remoteConfig, debug })
|
||||
|
||||
expect(redirects.length).toBe(3)
|
||||
|
||||
expect(redirects[0]).toContain('# Added: 2021-08-09')
|
||||
expect(redirects[0]).toContain('from = "/docs/one"')
|
||||
expect(redirects[0]).toContain('to = "/docs/two"')
|
||||
|
||||
expect(redirects[1]).toContain('# Added: 2021-08-09')
|
||||
expect(redirects[1]).toContain('from = "/docs/cheese"')
|
||||
expect(redirects[1]).toContain('to = "/docs/mushroom"')
|
||||
expect(redirects[2]).toContain('from = "/docs/fish"')
|
||||
expect(redirects[2]).toContain('to = "/docs/monkey"')
|
||||
})
|
||||
|
||||
test('redirect will only be added for non-existing redirects in multi file rename', async () => {
|
||||
const gitDiff = multiRenameGitDiff
|
||||
|
||||
const localConfig = emptyConfig
|
||||
const remoteConfig = existingRedirectConfig
|
||||
|
||||
const redirects = await getRedirects({ gitDiff, localConfig, remoteConfig, debug })
|
||||
|
||||
expect(redirects.length).toBe(2)
|
||||
expect(redirects[0]).toContain('from = "/docs/cheese"')
|
||||
expect(redirects[0]).toContain('to = "/docs/mushroom"')
|
||||
expect(redirects[1]).toContain('from = "/docs/fish"')
|
||||
expect(redirects[1]).toContain('to = "/docs/monkey"')
|
||||
})
|
||||
|
||||
test('redirect will not be added if it already exists in the remote config', async () => {
|
||||
const gitDiff = singleRenameGitDiff
|
||||
|
||||
const localConfig = emptyConfig
|
||||
const remoteConfig = existingRedirectConfig
|
||||
|
||||
const redirects = await getRedirects({ gitDiff, localConfig, remoteConfig, debug })
|
||||
|
||||
expect(redirects.length).toBe(0)
|
||||
})
|
||||
|
||||
test('redirect will not be added if it already exists in the local config', async () => {
|
||||
const gitDiff = singleRenameGitDiff
|
||||
|
||||
const localConfig = existingRedirectConfig
|
||||
const remoteConfig = emptyConfig
|
||||
|
||||
const redirects = await getRedirects({ gitDiff, localConfig, remoteConfig, debug })
|
||||
|
||||
expect(redirects.length).toBe(0)
|
||||
})
|
||||
|
||||
test('redirect will not be added if rename from /name to /name/index.mdx', async () => {
|
||||
const gitDiff = `
|
||||
rename from contents/docs/filename
|
||||
rename to contents/docs/name/index.mdx`
|
||||
|
||||
const localConfig = emptyConfig
|
||||
const remoteConfig = emptyConfig
|
||||
|
||||
const redirects = await getRedirects({ gitDiff, localConfig, remoteConfig, debug })
|
||||
|
||||
expect(redirects.length).toBe(0)
|
||||
})
|
||||
|
||||
test('redirect will not be added if rename from /name to /name/index.md', async () => {
|
||||
const gitDiff = `
|
||||
rename from contents/docs/name
|
||||
rename to contents/docs/name/index.md`
|
||||
|
||||
const localConfig = emptyConfig
|
||||
const remoteConfig = emptyConfig
|
||||
|
||||
const redirects = await getRedirects({ gitDiff, localConfig, remoteConfig, debug })
|
||||
|
||||
expect(redirects.length).toBe(0)
|
||||
})
|
||||
|
||||
test('redirect will not be added if rename from .md to .mdx', async () => {
|
||||
const gitDiff = `
|
||||
rename from contents/docs/filename.md
|
||||
rename to contents/docs/filename.mdx`
|
||||
|
||||
const localConfig = emptyConfig
|
||||
const remoteConfig = emptyConfig
|
||||
|
||||
const redirects = await getRedirects({ gitDiff, localConfig, remoteConfig, debug })
|
||||
|
||||
expect(redirects.length).toBe(0)
|
||||
})
|
||||
|
||||
test('redirect will not be added when renaming snippets', async () => {
|
||||
const gitDiff = `
|
||||
rename from contents/docs/snippets/js/capture.mdx
|
||||
rename to contents/docs/snippets/js/special-capture.mdx`
|
||||
|
||||
const localConfig = emptyConfig
|
||||
const remoteConfig = emptyConfig
|
||||
|
||||
const redirects = await getRedirects({ gitDiff, localConfig, remoteConfig, debug })
|
||||
|
||||
expect(redirects.length).toBe(0)
|
||||
})
|
||||
@@ -1,79 +0,0 @@
|
||||
import { useLocation } from '@reach/router'
|
||||
import { Formik } from 'formik'
|
||||
import React, { useState } from 'react'
|
||||
import usePostHog from '../../hooks/usePostHog'
|
||||
import AskQuestion from './AskQuestion'
|
||||
import Avatar from './Avatar'
|
||||
import QuestionSubmitted from './QuestionSubmitted'
|
||||
|
||||
export default function AskAQuestion() {
|
||||
const location = useLocation()
|
||||
const posthog = usePostHog()
|
||||
const [timestamp, setTimestamp] = useState(null)
|
||||
const [emailSubmitted, setEmailSubmitted] = useState(false)
|
||||
return (
|
||||
<div className="mt-10">
|
||||
<h4>Ask a question</h4>
|
||||
<div className="flex items-start space-x-4">
|
||||
<Avatar />
|
||||
<div className="w-full max-w-[405px]">
|
||||
<Formik
|
||||
isInitialValid={false}
|
||||
initialValues={{ name: '', question: '', email: '', subject: '', 'mary-chain': '' }}
|
||||
validate={(values) => {
|
||||
const errors = {}
|
||||
if (!values.name) {
|
||||
errors.name = 'Required'
|
||||
}
|
||||
if (!values.subject) {
|
||||
errors.subject = 'Required'
|
||||
}
|
||||
if (!values.question) {
|
||||
errors.question = 'Required'
|
||||
}
|
||||
if (!values.email) {
|
||||
errors.email = 'Required'
|
||||
}
|
||||
return errors
|
||||
}}
|
||||
onSubmit={(values, { setSubmitting, resetForm }) => {
|
||||
if (values['mary-chain']) return
|
||||
setSubmitting(true)
|
||||
const body = JSON.stringify({
|
||||
...values,
|
||||
slug: location.pathname,
|
||||
timestamp,
|
||||
})
|
||||
fetch('/.netlify/functions/ask-a-question', { method: 'POST', body })
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
posthog.capture('Question asked')
|
||||
setTimestamp(data.timestamp)
|
||||
setEmailSubmitted(true)
|
||||
setSubmitting(false)
|
||||
})
|
||||
}}
|
||||
>
|
||||
{({ isSubmitting, isValid, values, setFieldValue, submitForm }) => {
|
||||
return !timestamp ? (
|
||||
<AskQuestion
|
||||
submitForm={submitForm}
|
||||
setFieldValue={setFieldValue}
|
||||
loading={isSubmitting}
|
||||
isValid={isValid}
|
||||
/>
|
||||
) : (
|
||||
<QuestionSubmitted
|
||||
loading={isSubmitting}
|
||||
values={values}
|
||||
emailSubmitted={emailSubmitted}
|
||||
isValid={isValid}
|
||||
/>
|
||||
)
|
||||
}}
|
||||
</Formik>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
import { Field } from 'formik'
|
||||
import React from 'react'
|
||||
import Button from './Button'
|
||||
import RichText from './RichText'
|
||||
|
||||
export default function AskQuestion({ isValid, loading, setFieldValue, submitForm }) {
|
||||
return (
|
||||
<>
|
||||
<Field
|
||||
className="bg-white dark:bg-gray-accent-light dark:text-primary py-2 px-4 text-lg rounded-md w-full shadow-md"
|
||||
type="text"
|
||||
name="name"
|
||||
placeholder="Full name"
|
||||
/>
|
||||
<Field
|
||||
className="bg-white dark:bg-gray-accent-light dark:text-primary py-2 px-4 text-lg rounded-md mt-2 w-full shadow-md"
|
||||
type="email"
|
||||
name="email"
|
||||
placeholder="Email"
|
||||
/>
|
||||
<Field
|
||||
className="bg-white dark:bg-gray-accent-light dark:text-primary py-2 px-4 text-lg rounded-md mt-2 w-full shadow-md"
|
||||
type="text"
|
||||
name="subject"
|
||||
placeholder="Subject"
|
||||
/>
|
||||
<Field className="mary-chain" type="text" placeholder="Ignore if human" name="mary-chain" />
|
||||
<RichText setFieldValue={setFieldValue} />
|
||||
<div className="flex items-center justify-between">
|
||||
<Button onClick={() => submitForm()} loading={loading} type="submit" disabled={!isValid}>
|
||||
Submit
|
||||
</Button>
|
||||
<p className="m-0 flex items-center space-x-2">
|
||||
<span className="text-[13px] opacity-30 font-semibold">Supports</span>
|
||||
<svg
|
||||
className="text-black dark:text-gray-accent-light"
|
||||
width="30"
|
||||
height="18"
|
||||
viewBox="0 0 30 18"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g clipPath="url(#clip0_3781_82340)">
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M2.11791 1.40625C1.72802 1.40625 1.41194 1.72105 1.41194 2.10938V15.8906C1.41194 16.2789 1.72802 16.5938 2.11791 16.5938H27.2505C27.6404 16.5938 27.9565 16.2789 27.9565 15.8906V2.10938C27.9565 1.72105 27.6404 1.40625 27.2505 1.40625H2.11791ZM0 2.10938C0 0.944399 0.948223 0 2.11791 0H27.2505C28.4202 0 29.3684 0.944399 29.3684 2.10938V15.8906C29.3684 17.0556 28.4202 18 27.2505 18H2.11791C0.948223 18 0 17.0556 0 15.8906V2.10938Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M4.23584 13.7812V4.21875H7.05973L9.88361 7.73438L12.7075 4.21875H15.5314V13.7812H12.7075V8.29688L9.88361 11.8125L7.05973 8.29688V13.7812H4.23584ZM21.8851 13.7812L17.6493 9.14062H20.4732V4.21875H23.2971V9.14062H26.121L21.8851 13.7812Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_3781_82340">
|
||||
<rect width="29.3684" height="18" fill="white" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
<a
|
||||
className="text-black dark:text-white hover:text-black dark:hover:text-white"
|
||||
href="https://www.markdownguide.org/basic-syntax/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g opacity="0.2">
|
||||
<path
|
||||
d="M5.99988 0.283127C4.48392 0.283127 3.02988 0.885467 1.95732 1.95749C0.885287 3.02999 0.282959 4.48409 0.282959 6.00005C0.282959 7.51601 0.885299 8.97005 1.95732 10.0426C3.02982 11.1146 4.48392 11.717 5.99988 11.717C7.51584 11.717 8.96988 11.1146 10.0424 10.0426C11.1145 8.97011 11.7168 7.51601 11.7168 6.00005C11.7168 4.48409 11.1145 3.03005 10.0424 1.95749C8.96994 0.885455 7.51584 0.283127 5.99988 0.283127ZM5.99988 2.29217C6.19769 2.29217 6.38754 2.37092 6.52769 2.5106C6.66785 2.65076 6.74659 2.8406 6.74612 3.03888C6.74659 3.23669 6.66784 3.42654 6.52769 3.56669C6.38753 3.70638 6.19769 3.78512 5.99988 3.78512C5.80207 3.78512 5.61222 3.70637 5.47207 3.56669C5.33191 3.42653 5.25317 3.23669 5.25363 3.03888C5.25317 2.8406 5.33192 2.65075 5.47207 2.5106C5.61223 2.37091 5.80207 2.29217 5.99988 2.29217ZM5.99988 4.71893C6.39269 4.71893 6.70862 5.03486 6.70862 5.42767V8.99911C6.70862 9.39192 6.39269 9.70786 5.99988 9.70786C5.60707 9.70786 5.29113 9.39192 5.29113 8.99911V5.42767C5.29113 5.03486 5.60707 4.71893 5.99988 4.71893Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
import React, { useState } from 'react'
|
||||
import { Check } from 'components/Icons/Icons'
|
||||
import GitHubButton from 'react-github-btn'
|
||||
import emailSaved from './email-saved.svg'
|
||||
import Button from './Button'
|
||||
import addToMailchimp from 'gatsby-plugin-mailchimp'
|
||||
|
||||
export default function EmailSubmitted({ email }) {
|
||||
const [subscribed, setSubscribed] = useState(false)
|
||||
|
||||
const handleSubscribe = async () => {
|
||||
await addToMailchimp(email)
|
||||
setSubscribed(true)
|
||||
}
|
||||
return (
|
||||
<div className="text-center">
|
||||
{subscribed ? (
|
||||
<>
|
||||
<p className="flex justify-center items-center space-x-1 font-semibold text-[#43AF79] m-0">
|
||||
<span className=" w-[24px] h-[24px] bg-[#43AF79] rounded-full flex justify-center items-center">
|
||||
<Check className="w-[12px] h-[12px] text-white" />
|
||||
</span>
|
||||
<span>You're subscribed!</span>
|
||||
</p>
|
||||
<h6 className="mt-2 mb-3">
|
||||
Be sure to check us out on <a href="https://github.com/PostHog/posthog">GitHub</a>
|
||||
</h6>
|
||||
<p className="m-0 flex justify-center items-centertext-white font-semibold">
|
||||
<GitHubButton
|
||||
className="text-white hover:text-white"
|
||||
href="https://github.com/posthog/posthog"
|
||||
data-size="large"
|
||||
data-show-count="true"
|
||||
aria-label="Star posthog/posthog on GitHub"
|
||||
>
|
||||
Star
|
||||
</GitHubButton>
|
||||
</p>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="bg-tan dark:bg-primary p-4 rounded-sm mb-4">
|
||||
<h6 className="m-0">We’ll email you.</h6>
|
||||
<p className="text-[14px] opacity-50 m-0">We typically answer in 1-2 days.</p>
|
||||
<img className="mx-auto mt-2" src={emailSaved} />
|
||||
</div>
|
||||
<p className="text-[14px] opacity-50 m-0">While we have you...</p>
|
||||
<h6 className="m-0">Care to receive our email updates?</h6>
|
||||
<Button onClick={handleSubscribe}>Sure, subscribe me to the newsletter!</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
import { MDXProvider } from '@mdx-js/react'
|
||||
import { Blockquote } from 'components/BlockQuote'
|
||||
import { MdxCodeBlock } from 'components/CodeBlock'
|
||||
import { InlineCode } from 'components/InlineCode'
|
||||
import Link from 'components/Link'
|
||||
import { ZoomImage } from 'components/ZoomImage'
|
||||
import { MDXRenderer } from 'gatsby-plugin-mdx'
|
||||
import React from 'react'
|
||||
import Avatar from './Avatar'
|
||||
|
||||
export const Days = ({ ts, url }) => {
|
||||
const days = Number(ts)
|
||||
return (
|
||||
<Link
|
||||
to={url}
|
||||
className="font-normal ml-2 text-black hover:text-black dark:text-white dark:hover:text-white opacity-30"
|
||||
>
|
||||
{ts <= 0 ? 'Today' : `${ts} day${days === 1 ? '' : 's'} ago`}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
export const Reply = ({ avatar, name, childMdx, teamMember, ts, parentId, id, className = '' }) => {
|
||||
const components = {
|
||||
inlineCode: InlineCode,
|
||||
blockquote: Blockquote,
|
||||
pre: MdxCodeBlock,
|
||||
img: ZoomImage,
|
||||
}
|
||||
return (
|
||||
<div id={id} className={`py-4 rounded-md w-full mt-3 ${className}`}>
|
||||
<div className="flex space-x-4 items-start">
|
||||
<Avatar image={teamMember?.frontmatter?.headshot || avatar} />
|
||||
<div className="min-w-0">
|
||||
<p className="m-0 text-[14px] font-semibold text-black dark:text-white">
|
||||
<span>{teamMember?.frontmatter?.name || name}</span>
|
||||
<span className="opacity-50">, {teamMember?.frontmatter?.jobTitle || 'Contributor'}</span>
|
||||
<Days ts={ts} url={`/questions/${parentId}#${id}`} />
|
||||
</p>
|
||||
<div className="my-3">
|
||||
<MDXProvider components={components}>
|
||||
<MDXRenderer>{childMdx.body}</MDXRenderer>
|
||||
</MDXProvider>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function Question({ question, id }) {
|
||||
const components = {
|
||||
inlineCode: InlineCode,
|
||||
blockquote: Blockquote,
|
||||
pre: CodeBlock,
|
||||
img: ZoomImage,
|
||||
}
|
||||
const { avatar, childMdx, name, ts, subject } = question[0]
|
||||
return (
|
||||
<div className="flex items-start space-x-4 w-full">
|
||||
<Avatar image={avatar} />
|
||||
<div className="flex-grow max-w-[405px]">
|
||||
{subject && <h3 className="m-0 mb-1 text-[15px]">{subject}</h3>}
|
||||
<div>
|
||||
<MDXProvider components={components}>
|
||||
<MDXRenderer>{childMdx.body}</MDXRenderer>
|
||||
</MDXProvider>
|
||||
</div>
|
||||
<p className="text-[14px] mb-3 text-black dark:text-white">
|
||||
<span className="font-semibold opacity-50">by {name}</span>{' '}
|
||||
<Days ts={ts} url={`/questions/${id}`} />
|
||||
</p>
|
||||
{question.length > 1 &&
|
||||
question.slice(1).map((reply, index) => <Reply key={index} parentId={id} {...reply} />)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
import { Check } from 'components/Icons/Icons'
|
||||
import React from 'react'
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
import EmailSubmitted from './EmailSubmitted'
|
||||
import SubmitEmail from './SubmitEmail'
|
||||
|
||||
export default function QuestionSubmitted({ values, emailSubmitted, isValid, loading }) {
|
||||
return (
|
||||
<div>
|
||||
<ReactMarkdown>{values.question}</ReactMarkdown>
|
||||
<p className="text-[14px] font-semibold opacity-50">by {values.name}</p>
|
||||
<p className="flex items-center space-x-1 font-semibold text-[#43AF79]">
|
||||
<span className=" w-[24px] h-[24px] bg-[#43AF79] rounded-full flex justify-center items-center">
|
||||
<Check className="w-[12px] h-[12px] text-white" />
|
||||
</span>
|
||||
<span>Question sent. Answer will be posted here.</span>
|
||||
</p>
|
||||
<div className="p-6 bg-white dark:bg-gray-accent-dark rounded-[10px]">
|
||||
{emailSubmitted ? (
|
||||
<EmailSubmitted email={values.email} />
|
||||
) : (
|
||||
<SubmitEmail loading={loading} isValid={isValid} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,128 +0,0 @@
|
||||
import MDEditor from '@uiw/react-md-editor'
|
||||
import React, { useEffect } from 'react'
|
||||
import rehypeSanitize from 'rehype-sanitize'
|
||||
|
||||
const bold = {
|
||||
name: 'bold',
|
||||
keyCommand: 'bold',
|
||||
buttonProps: { 'aria-label': 'Bold', tabIndex: '-1' },
|
||||
icon: (
|
||||
<svg width="11" height="13" viewBox="0 0 11 13" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M0.394742 13H5.65074C8.31474 13 10.1867 11.632 10.1867 9.238C10.1867 7.6 9.21474 6.574 8.08074 6.124C8.80074 5.728 9.53874 4.828 9.53874 3.64C9.53874 1.534 7.79274 0.399999 5.52474 0.399999H0.394742V13ZM3.23874 10.552V7.546H5.41674C6.87474 7.546 7.50474 8.086 7.50474 9.076C7.50474 10.048 6.87474 10.552 5.41674 10.552H3.23874ZM3.23874 5.17V2.632H5.23674C6.42474 2.632 6.92874 3.118 6.92874 3.91C6.92874 4.684 6.42474 5.17 5.29074 5.17H3.23874Z"
|
||||
fill="black"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
execute: (state, api) => {
|
||||
let modifyText = `**${state.selectedText}**`
|
||||
if (!state.selectedText) {
|
||||
modifyText = `****`
|
||||
}
|
||||
api.replaceSelection(modifyText)
|
||||
},
|
||||
}
|
||||
|
||||
const italic = {
|
||||
name: 'italic',
|
||||
keyCommand: 'italic',
|
||||
buttonProps: { 'aria-label': 'Italicize', tabIndex: '-1' },
|
||||
icon: (
|
||||
<svg width="4" height="13" viewBox="0 0 4 13" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M0.0360938 13H1.56609L3.00609 3.928H1.47609L0.0360938 13ZM1.63809 2.2H3.40209L3.70809 0.256H1.94409L1.63809 2.2Z"
|
||||
fill="black"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
execute: (state, api) => {
|
||||
let modifyText = `*${state.selectedText}*`
|
||||
if (!state.selectedText) {
|
||||
modifyText = `**`
|
||||
}
|
||||
api.replaceSelection(modifyText)
|
||||
},
|
||||
}
|
||||
|
||||
const code = {
|
||||
name: 'code',
|
||||
keyCommand: 'code',
|
||||
buttonProps: { 'aria-label': 'Code', tabIndex: '-1' },
|
||||
icon: (
|
||||
<svg width="14" height="10" viewBox="0 0 14 10" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M5.80395 1.35631C5.80395 1.06647 5.69075 0.794129 5.48513 0.588497C5.08756 0.192017 4.34818 0.191471 3.95227 0.589044L0.31773 4.22415C0.115934 4.42594 0 4.70485 0 4.99087C0 5.27634 0.115937 5.5558 0.31773 5.75759L3.95339 9.39326C4.15792 9.59723 4.43026 9.70989 4.71957 9.70989C5.00777 9.70989 5.27957 9.59724 5.48575 9.39216C5.69082 9.18764 5.80402 8.91473 5.80402 8.62544C5.80402 8.33614 5.69082 8.06325 5.48575 7.85871L2.61855 4.99095L5.48533 2.12375C5.69096 1.91923 5.80415 1.64688 5.80415 1.35648L5.80395 1.35631Z"
|
||||
fill="black"
|
||||
/>
|
||||
<path
|
||||
d="M13.6821 4.22397L10.047 0.588315C9.64939 0.19129 8.91274 0.190183 8.51298 0.589409C8.30846 0.793935 8.1958 1.06684 8.1958 1.35559C8.1958 1.64597 8.309 1.91832 8.51298 2.12231L11.3813 4.99063L8.51298 7.85895C8.30846 8.06347 8.1958 8.33638 8.1958 8.62513C8.1958 8.91388 8.30846 9.18676 8.51408 9.39294C8.71915 9.59692 8.99205 9.70958 9.28025 9.70958C9.56846 9.70958 9.8408 9.59692 10.0464 9.3924L13.6815 5.7573C13.8833 5.5555 13.9993 5.27605 13.9993 4.99057C13.9993 4.70455 13.8839 4.42565 13.6821 4.22385L13.6821 4.22397Z"
|
||||
fill="black"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
execute: (state, api) => {
|
||||
let modifyText = `\`\`\`\n${state.selectedText}\n\`\`\``
|
||||
if (!state.selectedText) {
|
||||
modifyText = `\`\`\`\`\`\``
|
||||
}
|
||||
api.replaceSelection(modifyText)
|
||||
},
|
||||
}
|
||||
|
||||
const link = {
|
||||
name: 'link',
|
||||
keyCommand: 'link',
|
||||
buttonProps: { 'aria-label': 'Link', tabIndex: '-1' },
|
||||
icon: (
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clipPath="url(#clip0_3781_82365)">
|
||||
<path
|
||||
d="M12.9069 1.09374C11.449 -0.364215 9.08481 -0.364215 7.62741 1.09374L5.54435 3.17624C5.70951 3.1582 5.87576 3.15054 6.0431 3.15054C6.57028 3.15054 7.08271 3.23421 7.5684 3.39554L8.74856 2.21538C9.15379 1.80961 9.69301 1.58648 10.2667 1.58648C10.8398 1.58648 11.3791 1.80961 11.7849 2.21538C12.1901 2.62061 12.4132 3.15875 12.4132 3.73298C12.4132 4.30612 12.1901 4.84533 11.7849 5.25058L9.47544 7.56002C9.06966 7.9658 8.53044 8.18893 7.95728 8.18893C7.38305 8.18893 6.84494 7.9658 6.43912 7.56002C6.24169 7.3637 6.08803 7.13455 5.98193 6.88518C5.71889 6.89995 5.4728 7.00932 5.28466 7.19691L4.66943 7.81268C4.83787 8.12441 5.05389 8.41862 5.31693 8.68275C6.77489 10.1407 9.13907 10.1407 10.597 8.68275L12.907 6.37219C14.3644 4.91479 14.3644 2.55117 12.907 1.09377L12.9069 1.09374Z"
|
||||
fill="black"
|
||||
/>
|
||||
<path
|
||||
d="M7.98178 10.8489C7.4535 10.8489 6.93614 10.7636 6.43954 10.5951L5.25117 11.7835C4.84594 12.1893 4.30727 12.4124 3.73357 12.4124C3.16044 12.4124 2.62178 12.1893 2.21597 11.7835C1.8102 11.3783 1.58707 10.8396 1.58707 10.2659C1.58707 9.69275 1.8102 9.15354 2.21597 8.74772L4.52541 6.43828C4.93119 6.03305 5.46932 5.81047 6.04301 5.81047C6.61724 5.81047 7.15536 6.0336 7.56117 6.43828C7.7586 6.63571 7.91281 6.86485 8.01945 7.11421C8.28359 7.10054 8.53023 6.99007 8.71781 6.80249L9.33195 6.18726C9.16351 5.87445 8.94695 5.58078 8.68336 5.31663C7.2254 3.85867 4.86122 3.85867 3.40382 5.31663L1.09438 7.62719C-0.364142 9.08515 -0.364142 11.4482 1.09438 12.9067C2.55234 14.3647 4.9154 14.3647 6.37336 12.9067L8.45306 10.827C8.29774 10.8412 8.14133 10.8495 7.98329 10.8495L7.98178 10.8489Z"
|
||||
fill="black"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_3781_82365">
|
||||
<rect width="14" height="14" fill="white" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
),
|
||||
execute: (state, api) => {
|
||||
let modifyText = `[${state.selectedText}]()`
|
||||
if (!state.selectedText) {
|
||||
modifyText = `[]()`
|
||||
}
|
||||
api.replaceSelection(modifyText)
|
||||
},
|
||||
}
|
||||
|
||||
export default function RichText({ setFieldValue }) {
|
||||
const [value, setValue] = React.useState('')
|
||||
|
||||
useEffect(() => {
|
||||
setFieldValue('question', value)
|
||||
}, [value])
|
||||
return (
|
||||
<div className="container">
|
||||
<MDEditor
|
||||
placeholder="Type more details..."
|
||||
visiableDragbar={false}
|
||||
commands={[bold, italic, code, link]}
|
||||
commandsFilter={(cmd) =>
|
||||
cmd && /(fullscreen|divider|preview|live|edit)/.test(cmd.name || cmd.keyCommand) ? false : cmd
|
||||
}
|
||||
preview={'edit'}
|
||||
value={value}
|
||||
onChange={setValue}
|
||||
previewOptions={{
|
||||
rehypePlugins: [[rehypeSanitize]],
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
import React from 'react'
|
||||
import { Form, Field } from 'formik'
|
||||
import Button from './Button'
|
||||
|
||||
export default function SubmitEmail({ isValid, loading }) {
|
||||
return (
|
||||
<>
|
||||
<h4 className="text-[20px] m-0">Get your answer by email</h4>
|
||||
<p>No need to constantly refresh this page for updates!</p>
|
||||
<Form className="grid m-0">
|
||||
<Field
|
||||
className="bg-gray-accent-light py-2 px-4 text-lg rounded-md w-full dark:text-primary"
|
||||
type="email"
|
||||
name="email"
|
||||
placeholder="Email"
|
||||
/>
|
||||
<Button loading={loading} disabled={!isValid} type="submit">
|
||||
Send email updates
|
||||
</Button>
|
||||
</Form>
|
||||
<p className="text-[14px] opacity-50 text-center m-0">
|
||||
We’ll only email you when an answer is posted. We don’t share emails, and if you use Gravatar, we’ll
|
||||
include your photo next to your question.
|
||||
</p>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
import React from 'react'
|
||||
import { Check } from 'components/Icons/Icons'
|
||||
import GitHubButton from 'react-github-btn'
|
||||
|
||||
export default function Subscribed() {
|
||||
return (
|
||||
<>
|
||||
<p className="flex justify-center items-center space-x-1 font-semibold text-[#43AF79] m-0">
|
||||
<span className=" w-[24px] h-[24px] bg-[#43AF79] rounded-full flex justify-center items-center">
|
||||
<Check className="w-[12px] h-[12px] text-white" />
|
||||
</span>
|
||||
<span>You're subscribed!</span>
|
||||
</p>
|
||||
<h6 className="mt-2 mb-3">
|
||||
Be sure to check us out on <a href="https://github.com/PostHog/posthog">Github.com</a>
|
||||
</h6>
|
||||
<p className="m-0 flex justify-center items-center text-white font-semibold h-[28px] w-[105px]">
|
||||
<GitHubButton
|
||||
className="text-white hover:text-white"
|
||||
href="https://github.com/posthog/posthog"
|
||||
data-size="large"
|
||||
data-show-count="true"
|
||||
aria-label="Star posthog/posthog on GitHub"
|
||||
>
|
||||
Star
|
||||
</GitHubButton>
|
||||
</p>
|
||||
</>
|
||||
)
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 2.5 KiB |
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 27 KiB |
Reference in New Issue
Block a user