mirror of
https://github.com/BillyOutlast/posthog.com.git
synced 2026-02-04 03:11:21 +01:00
Remove antd (#3754)
* remove antd as a dependency! * update CTAs in blog posts * create tab component to replace antd version * remove references to antd (wip) * rebuild compensation calculator * rebuild contributors page * replace spinner on slack * remove antd from APIEndpoint * redo yc-onboarding * add dark mode styles * fix missing angle bracket * Reset Fonts.scss * update yarn.lock * redirect /startups to /pricing * use pointer on dropdowns * overflow tabs instead of wrap Co-authored-by: Eli Kinsey <eli@ekinsey.dev>
This commit is contained in:
@@ -3,3 +3,5 @@
|
||||
*.lock
|
||||
.cache/
|
||||
public/
|
||||
node_modules/
|
||||
static/
|
||||
|
||||
2
.yarnrc
2
.yarnrc
@@ -1 +1 @@
|
||||
--add.ignore-workspace-root-check=true
|
||||
--add.ignore-workspace-root-check true
|
||||
|
||||
@@ -5,13 +5,12 @@ rootPage: /blog
|
||||
sidebar: Blog
|
||||
showTitle: true
|
||||
hideAnchor: true
|
||||
categories: ["Release notes", "Product updates"]
|
||||
categories: ['Release notes', 'Product updates']
|
||||
featuredImage: ../images/blog/array/default.png
|
||||
featuredImageType: standard
|
||||
---
|
||||
|
||||
import { Link } from 'gatsby'
|
||||
import { Button } from 'antd'
|
||||
|
||||
Release 1.21 is a big one, on top of exciting new features and improvements, we put extra time into the overall stability of PostHog squashing dozens of issues. Some highlights of this release:
|
||||
|
||||
@@ -25,11 +24,13 @@ We received a lot of great feedback and issue reports and over this release cycl
|
||||
|
||||
Make sure to upgrade to get the new features, improvements and fixes.
|
||||
|
||||
<Link to="/docs/deployment">
|
||||
<Button className="center" type="primary" size="large">
|
||||
Install PostHog
|
||||
</Button>
|
||||
</Link>
|
||||
<br />
|
||||
|
||||
<div class="flex items-center justify-center">
|
||||
<CallToAction to="/docs/deployment">
|
||||
Install PostHog
|
||||
</CallToAction>
|
||||
</div>
|
||||
|
||||
<br />
|
||||
|
||||
@@ -383,4 +384,3 @@ In addition to the highlights listed above, we also merged a bunch of PRs improv
|
||||
- Enable SSL PostgreSQL configuration through env variables [\#2967](https://github.com/PostHog/posthog/pull/2967) ([tmilicic](https://github.com/tmilicic))
|
||||
|
||||
<ArrayCTA />
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@ featuredImageType: standard
|
||||
---
|
||||
|
||||
import { Link } from 'gatsby'
|
||||
import { Button } from 'antd'
|
||||
|
||||
PostHog 1.22 is out with awesome new features, usability and performance improvements, and the usual bug squashing.
|
||||
|
||||
@@ -34,11 +33,13 @@ Thank you oshura3, we look forward to collaborating with you more!
|
||||
- **Improvement:** Better tooling for updating data in dashboards
|
||||
- **Improvement:** A whole new UX for individual person pages
|
||||
|
||||
<Link to="https://app.posthog.com/signup">
|
||||
<Button className="center" type="primary" size="large">
|
||||
Try PostHog Cloud Now
|
||||
</Button>
|
||||
</Link>
|
||||
<br />
|
||||
|
||||
<div class="flex items-center justify-center">
|
||||
<CallToAction to="https://app.posthog.com/signup">
|
||||
Try PostHog Cloud Now
|
||||
</CallToAction>
|
||||
</div>
|
||||
|
||||
<br />
|
||||
|
||||
|
||||
@@ -12,8 +12,6 @@ This page refers to our public endpoints, which use the same API key as the [Pos
|
||||
|
||||
> **Note:** For this API, you should use your 'Project API Key' from the 'Project' page in PostHog. This is the same key used in your frontend snippet.
|
||||
|
||||
|
||||
|
||||
# Sending events
|
||||
|
||||
To send events to PostHog, you can use any of [our libraries](/docs/integrate/overview) **or** any Mixpanel library by changing the `api_host` setting to the address of your instance.
|
||||
@@ -162,74 +160,62 @@ curl -v -L --header "Content-Type: application/json" -d '{
|
||||
##### Responses
|
||||
|
||||
```js
|
||||
{
|
||||
status: 1
|
||||
{
|
||||
status: 1
|
||||
}
|
||||
```
|
||||
|
||||
**Meaning:** A `200: OK` response means we have successfully received the payload, it is in the correct format, and the project API key (token) is valid. It **does not** imply that events are valid and will be ingested. As mentioned under [Invalid events](#invalid-events), certain event validation errors may cause an event not to be ingested.
|
||||
|
||||
**Meaning:** A `200: OK` response means we have successfully received the payload, it is in the correct format, and the project API key (token) is valid. It **does not** imply that events are valid and will be ingested. As mentioned under [Invalid events](#invalid-events), certain event validation errors may cause an event not to be ingested.
|
||||
|
||||
#### Status code: 400
|
||||
|
||||
##### Responses
|
||||
|
||||
|
||||
```js
|
||||
{
|
||||
type: 'validation_error',
|
||||
code: 'invalid_project',
|
||||
detail: 'Invalid Project ID.',
|
||||
attr: 'project_id'
|
||||
{
|
||||
type: 'validation_error',
|
||||
code: 'invalid_project',
|
||||
detail: 'Invalid Project ID.',
|
||||
attr: 'project_id'
|
||||
}
|
||||
```
|
||||
|
||||
**Meaning:** We were unable to determine the project to associate the events with.
|
||||
|
||||
|
||||
#### Status code: 401
|
||||
|
||||
##### Responses
|
||||
|
||||
|
||||
```js
|
||||
{
|
||||
type: 'authentication_error',
|
||||
code: 'invalid_api_key',
|
||||
detail: 'Project API key invalid. You can find your project API key in PostHog project settings.',
|
||||
{
|
||||
type: 'authentication_error',
|
||||
code: 'invalid_api_key',
|
||||
detail: 'Project API key invalid. You can find your project API key in PostHog project settings.',
|
||||
}
|
||||
```
|
||||
|
||||
**Meaning:** The token/API key you provided is invalid.
|
||||
|
||||
import { Divider } from 'antd'
|
||||
|
||||
<br />
|
||||
|
||||
<Divider dashed style={{ background: '#eeeeee', height: 1 }} />
|
||||
|
||||
<br />
|
||||
|
||||
|
||||
```js
|
||||
{
|
||||
type: 'authentication_error',
|
||||
code: 'invalid_personal_api_key',
|
||||
detail: 'Invalid Personal API key.',
|
||||
{
|
||||
type: 'authentication_error',
|
||||
code: 'invalid_personal_api_key',
|
||||
detail: 'Invalid Personal API key.',
|
||||
}
|
||||
```
|
||||
|
||||
**Meaning:** The personal API key you used for authentication is invalid.
|
||||
**Meaning:** The personal API key you used for authentication is invalid.
|
||||
|
||||
#### Status code: 503 (Deprecated)
|
||||
|
||||
##### Responses
|
||||
|
||||
|
||||
```js
|
||||
{
|
||||
type: 'server_error',
|
||||
code: 'fetch_team_fail',
|
||||
{
|
||||
type: 'server_error',
|
||||
code: 'fetch_team_fail',
|
||||
detail: 'Unable to fetch team from database.'
|
||||
}
|
||||
```
|
||||
@@ -242,14 +228,13 @@ We perform basic validation on the payload and project API key (token), returnin
|
||||
|
||||
However, we **will not return an error** to the client when the following happens:
|
||||
|
||||
- An event does not have a name
|
||||
- An event does not have the `distinct_id` field set
|
||||
- The `distinct_id` field of an event has an empty value
|
||||
- An event does not have a name
|
||||
- An event does not have the `distinct_id` field set
|
||||
- The `distinct_id` field of an event has an empty value
|
||||
|
||||
The three cases above will cause the event to not be ingested, but you will still receive a `200: OK` response from us.
|
||||
|
||||
This approach allows us to process events asynchronously if necessary, ensuring reliability and low latency for our event ingestion endpoints.
|
||||
The three cases above will cause the event to not be ingested, but you will still receive a `200: OK` response from us.
|
||||
|
||||
This approach allows us to process events asynchronously if necessary, ensuring reliability and low latency for our event ingestion endpoints.
|
||||
|
||||
# Feature flags
|
||||
|
||||
|
||||
@@ -4,9 +4,6 @@ sidebar: Docs
|
||||
showTitle: true
|
||||
---
|
||||
|
||||
import { Tabs } from 'antd'
|
||||
export const TabPane = Tabs.TabPane
|
||||
|
||||
Historical data ingestion (or importing data), opposed to [live data ingestion](/docs/integrate/ingest-live-data), is the process of transporting data from external sources into PostHog so you can benefit from PostHog product analytics on historical data. It may be that you have historical data that you want to analyze along with new live data or that you have a requirement to periodically import data from third-party sources to augment your live data.
|
||||
|
||||
Whatever the reason for the historical data ingestion, this guide covers what to consider during that process.
|
||||
@@ -87,34 +84,46 @@ import GoCapture from './server/go/snippets/capture.mdx'
|
||||
import RubyCapture from './server/ruby/snippets/capture.mdx'
|
||||
import CURLCapture from './server/curl/snippets/capture.mdx'
|
||||
|
||||
import Tab from "components/Tab"
|
||||
|
||||
> The server libraries handle batching capture requests. If you decide to use the API directly you will need to manage this yourself.
|
||||
|
||||
<Tabs defaultActiveKey="1" type="card" size="large" style={{marginBottom: "20px"}}>
|
||||
<TabPane tab="Node.js" key="1">
|
||||
<NodeCapture />
|
||||
<p>For more information see the <a href="/docs/integrate/server/node">Node.js docs</a>.</p>
|
||||
</TabPane>
|
||||
<TabPane tab="Python" key="2">
|
||||
<PythonCapture />
|
||||
<p>For more information see the <a href="/docs/integrate/server/python">python docs</a>.</p>
|
||||
</TabPane>
|
||||
<TabPane tab="PHP" key="3">
|
||||
<PHPCapture />
|
||||
<p>For more information see the <a href="/docs/integrate/server/php">PHP docs</a>.</p>
|
||||
</TabPane>
|
||||
<TabPane tab="Go" key="4">
|
||||
<GoCapture />
|
||||
<p>For more information see the <a href="/docs/integrate/server/go">Go docs</a>.</p>
|
||||
</TabPane>
|
||||
<TabPane tab="Ruby" key="5">
|
||||
<RubyCapture />
|
||||
<p>For more information see the <a href="/docs/integrate/server/ruby">Ruby docs</a>.</p>
|
||||
</TabPane>
|
||||
<TabPane tab="cURL" key="6">
|
||||
<CURLCapture />
|
||||
<p>For more information see the <a href="/docs/api">API docs</a>.</p>
|
||||
</TabPane>
|
||||
</Tabs>
|
||||
<Tab.Group>
|
||||
<Tab.List>
|
||||
<Tab>Node.js</Tab>
|
||||
<Tab>Python</Tab>
|
||||
<Tab>PHP</Tab>
|
||||
<Tab>Go</Tab>
|
||||
<Tab>Ruby</Tab>
|
||||
<Tab>cURL</Tab>
|
||||
</Tab.List>
|
||||
<Tab.Panels>
|
||||
<Tab.Panel>
|
||||
<NodeCapture />
|
||||
<p>For more information see the <a href="/docs/integrate/server/node">Node.js docs</a>.</p>
|
||||
</Tab.Panel>
|
||||
<Tab.Panel>
|
||||
<PythonCapture />
|
||||
<p>For more information see the <a href="/docs/integrate/server/python">python docs</a>.</p>
|
||||
</Tab.Panel>
|
||||
<Tab.Panel>
|
||||
<PHPCapture />
|
||||
<p>For more information see the <a href="/docs/integrate/server/php">PHP docs</a>.</p>
|
||||
</Tab.Panel>
|
||||
<Tab.Panel>
|
||||
<GoCapture />
|
||||
<p>For more information see the <a href="/docs/integrate/server/go">Go docs</a>.</p>
|
||||
</Tab.Panel>
|
||||
<Tab.Panel>
|
||||
<RubyCapture />
|
||||
<p>For more information see the <a href="/docs/integrate/server/ruby">Ruby docs</a>.</p>
|
||||
</Tab.Panel>
|
||||
<Tab.Panel>
|
||||
<CURLCapture />
|
||||
<p>For more information see the <a href="/docs/api">API docs</a>.</p>
|
||||
</Tab.Panel>
|
||||
</Tab.Panels>
|
||||
</Tab.Group>
|
||||
|
||||
## User identification
|
||||
|
||||
@@ -127,29 +136,39 @@ import GoIdentify from './server/go/snippets/identify.mdx'
|
||||
import RubyIdentify from './server/ruby/snippets/identify.mdx'
|
||||
import CURLIdentify from './server/curl/snippets/identify.mdx'
|
||||
|
||||
<Tabs defaultActiveKey="1" type="card" size="large" style={{marginBottom: "20px"}}>
|
||||
<TabPane tab="Node.js" key="1">
|
||||
<NodeIdentify />
|
||||
<p>For more information see the <a href="/docs/integrate/server/node">Node.js docs</a>.</p>
|
||||
</TabPane>
|
||||
<TabPane tab="Python" key="2">
|
||||
<PythonIdentify />
|
||||
<p>For more information see the <a href="/docs/integrate/server/python">Python docs</a>.</p>
|
||||
</TabPane>
|
||||
<TabPane tab="PHP" key="3">
|
||||
<PHPIdentify />
|
||||
<p>For more information see the <a href="/docs/integrate/server/php">PHP docs</a>.</p>
|
||||
</TabPane>
|
||||
<TabPane tab="Go" key="4">
|
||||
<GoIdentify />
|
||||
<p>For more information see the <a href="/docs/integrate/server/go">Go docs</a>.</p>
|
||||
</TabPane>
|
||||
<TabPane tab="Ruby" key="5">
|
||||
<RubyIdentify />
|
||||
<p>For more information see the <a href="/docs/integrate/server/ruby">Ruby docs</a>.</p>
|
||||
</TabPane>
|
||||
<TabPane tab="cURL" key="6">
|
||||
<CURLIdentify />
|
||||
<p>For more information see the <a href="/docs/api">API docs</a>.</p>
|
||||
</TabPane>
|
||||
</Tabs>
|
||||
<Tab.Group>
|
||||
<Tab.List>
|
||||
<Tab>Node.js</Tab>
|
||||
<Tab>Python</Tab>
|
||||
<Tab>PHP</Tab>
|
||||
<Tab>Go</Tab>
|
||||
<Tab>Ruby</Tab>
|
||||
<Tab>cURL</Tab>
|
||||
</Tab.List>
|
||||
<Tab.Panels>
|
||||
<Tab.Panel>
|
||||
<NodeIdentify />
|
||||
<p>For more information see the <a href="/docs/integrate/server/node">Node.js docs</a>.</p>
|
||||
</Tab.Panel>
|
||||
<Tab.Panel>
|
||||
<PythonIdentify />
|
||||
<p>For more information see the <a href="/docs/integrate/server/python">Python docs</a>.</p>
|
||||
</Tab.Panel>
|
||||
<Tab.Panel>
|
||||
<PHPIdentify />
|
||||
<p>For more information see the <a href="/docs/integrate/server/php">PHP docs</a>.</p>
|
||||
</Tab.Panel>
|
||||
<Tab.Panel>
|
||||
<GoIdentify />
|
||||
<p>For more information see the <a href="/docs/integrate/server/go">Go docs</a>.</p>
|
||||
</Tab.Panel>
|
||||
<Tab.Panel>
|
||||
<RubyIdentify />
|
||||
<p>For more information see the <a href="/docs/integrate/server/ruby">Ruby docs</a>.</p>
|
||||
</Tab.Panel>
|
||||
<Tab.Panel>
|
||||
<CURLIdentify />
|
||||
<p>For more information see the <a href="/docs/api">API docs</a>.</p>
|
||||
</Tab.Panel>
|
||||
</Tab.Panels>
|
||||
</Tab.Group>
|
||||
|
||||
@@ -4,9 +4,6 @@ sidebar: Docs
|
||||
showTitle: true
|
||||
---
|
||||
|
||||
import { Tabs } from 'antd'
|
||||
export const TabPane = Tabs.TabPane
|
||||
|
||||
PostHog enables you to analyze data in real-time, as events come in. Make full use of this power by ingesting live data with our analytics integrations: [client libraries](/docs/integrate/overview#client-libraries), [server libraries](/docs/integrate/overview#server-libraries), as well as [third-party platforms](/docs/integrate/overview#integrations).
|
||||
|
||||
The purpose of this guide is to help you understand some key concepts with a goal of ingesting live data into PostHog. For simplicity, we'll focus on _client_ libraries as a means of data ingestion.
|
||||
@@ -37,27 +34,39 @@ import FlutterInstall from './client/flutter/snippets/install.mdx'
|
||||
import ReactNativeInstall from './client/react-native/snippets/install.mdx'
|
||||
import ReactNativeConfigure from './client/react-native/snippets/configure.mdx'
|
||||
|
||||
<Tabs defaultActiveKey="1" type="card" size="large" style={{ marginBottom: '20px' }}>
|
||||
<TabPane tab="JavaScript" key="1">
|
||||
<JSInstall />
|
||||
</TabPane>
|
||||
<TabPane tab="Android" key="2">
|
||||
<AndroidInstall />
|
||||
<AndroidConfigure />
|
||||
</TabPane>
|
||||
<TabPane tab="iOS" key="3">
|
||||
<IOSInstall />
|
||||
<IOSConfigure />
|
||||
</TabPane>
|
||||
<TabPane tab="Flutter" key="4">
|
||||
<FlutterPackage />
|
||||
<FlutterInstall />
|
||||
</TabPane>
|
||||
<TabPane tab="React Native" key="5">
|
||||
<ReactNativeInstall />
|
||||
<ReactNativeConfigure />
|
||||
</TabPane>
|
||||
</Tabs>
|
||||
import Tab from "components/Tab"
|
||||
|
||||
<Tab.Group>
|
||||
<Tab.List>
|
||||
<Tab>JavaScript</Tab>
|
||||
<Tab>Android</Tab>
|
||||
<Tab>iOS</Tab>
|
||||
<Tab>Flutter</Tab>
|
||||
<Tab>React Native</Tab>
|
||||
</Tab.List>
|
||||
|
||||
<Tab.Panels>
|
||||
<Tab.Panel>
|
||||
<JSInstall />
|
||||
</Tab.Panel>
|
||||
<Tab.Panel>
|
||||
<AndroidInstall />
|
||||
<AndroidConfigure />
|
||||
</Tab.Panel>
|
||||
<Tab.Panel>
|
||||
<IOSInstall />
|
||||
<IOSConfigure />
|
||||
</Tab.Panel>
|
||||
<Tab.Panel>
|
||||
<FlutterPackage />
|
||||
<FlutterInstall />
|
||||
</Tab.Panel>
|
||||
<Tab.Panel>
|
||||
<ReactNativeInstall />
|
||||
<ReactNativeConfigure />
|
||||
</Tab.Panel>
|
||||
</Tab.Panels>
|
||||
</Tab.Group>
|
||||
|
||||
# Use autocapture
|
||||
|
||||
@@ -76,26 +85,36 @@ import AndroidCapture from './client/android/snippets/capture.mdx'
|
||||
import IOSCapture from './client/ios/snippets/capture.mdx'
|
||||
import ReactNativeCapture from './client/react-native/snippets/capture.mdx'
|
||||
|
||||
<Tabs defaultActiveKey="1" type="card" size="large" style={{marginBottom: "20px"}}>
|
||||
<TabPane tab="JavaScript" key="1">
|
||||
<JSCapture />
|
||||
</TabPane>
|
||||
<TabPane tab="Android" key="2">
|
||||
<AndroidCapture />
|
||||
<p>For more information see the <a href="/docs/integrate/client/android#capture">Android capture docs</a>.</p>
|
||||
</TabPane>
|
||||
<TabPane tab="iOS" key="3">
|
||||
<IOSCapture />
|
||||
<p>For more information see the <a href="/docs/integrate/client/ios#capture">iOS capture docs</a>.</p>
|
||||
</TabPane>
|
||||
<TabPane tab="Flutter" key="4">
|
||||
<p>See the <a href="/docs/integrate/client/flutter">Flutter library docs</a> for more information.</p>
|
||||
</TabPane>
|
||||
<TabPane tab="React Native" key="5">
|
||||
<ReactNativeCapture />
|
||||
<p>For more information see the <a href="/docs/integrate/client/react-native#capture">React Native capture docs</a>.</p>
|
||||
</TabPane>
|
||||
</Tabs>
|
||||
<Tab.Group>
|
||||
<Tab.List>
|
||||
<Tab>JavaScript</Tab>
|
||||
<Tab>Android</Tab>
|
||||
<Tab>iOS</Tab>
|
||||
<Tab>Flutter</Tab>
|
||||
<Tab>React Native</Tab>
|
||||
</Tab.List>
|
||||
|
||||
<Tab.Panels>
|
||||
<Tab.Panel>
|
||||
<JSCapture />
|
||||
</Tab.Panel>
|
||||
<Tab.Panel>
|
||||
<AndroidCapture />
|
||||
<p>For more information see the <a href="/docs/integrate/client/android#capture">Android capture docs</a>.</p>
|
||||
</Tab.Panel>
|
||||
<Tab.Panel>
|
||||
<IOSCapture />
|
||||
<p>For more information see the <a href="/docs/integrate/client/ios#capture">iOS capture docs</a>.</p>
|
||||
</Tab.Panel>
|
||||
<Tab.Panel>
|
||||
<p>See the <a href="/docs/integrate/client/flutter">Flutter library docs</a> for more information.</p>
|
||||
</Tab.Panel>
|
||||
<Tab.Panel>
|
||||
<ReactNativeCapture />
|
||||
<p>For more information see the <a href="/docs/integrate/client/react-native#capture">React Native capture docs</a>.</p>
|
||||
</Tab.Panel>
|
||||
</Tab.Panels>
|
||||
</Tab.Group>
|
||||
|
||||
# Identify users
|
||||
|
||||
@@ -104,33 +123,41 @@ import AndroidIdentify from './client/android/snippets/identify.mdx'
|
||||
import IOSIdentify from './client/ios/snippets/identify.mdx'
|
||||
import ReactNativeIdentify from './client/react-native/snippets/identify.mdx'
|
||||
|
||||
<Tabs defaultActiveKey="1" type="card" size="large" style={{ marginBottom: '20px' }}>
|
||||
<TabPane tab="JavaScript" key="1">
|
||||
<JSIdentify />
|
||||
</TabPane>
|
||||
<TabPane tab="Android" key="2">
|
||||
<AndroidIdentify />
|
||||
</TabPane>
|
||||
<TabPane tab="iOS" key="3">
|
||||
<IOSIdentify />
|
||||
</TabPane>
|
||||
<TabPane tab="Flutter" key="4">
|
||||
<p>
|
||||
See the <a href="/docs/integrate/client/flutter">Flutter library docs</a> for more information.
|
||||
</p>
|
||||
</TabPane>
|
||||
<TabPane tab="React Native" key="5">
|
||||
<ReactNativeIdentify />
|
||||
</TabPane>
|
||||
</Tabs>
|
||||
<Tab.Group>
|
||||
<Tab.List>
|
||||
<Tab>JavaScript</Tab>
|
||||
<Tab>Android</Tab>
|
||||
<Tab>iOS</Tab>
|
||||
<Tab>Flutter</Tab>
|
||||
<Tab>React Native</Tab>
|
||||
</Tab.List>
|
||||
|
||||
<Tab.Panels>
|
||||
<Tab.Panel>
|
||||
<JSIdentify />
|
||||
</Tab.Panel>
|
||||
<Tab.Panel>
|
||||
<AndroidIdentify />
|
||||
</Tab.Panel>
|
||||
<Tab.Panel>
|
||||
<IOSIdentify />
|
||||
</Tab.Panel>
|
||||
<Tab.Panel>
|
||||
<p> See the <a href="/docs/integrate/client/flutter">Flutter library docs</a> for more information.</p>
|
||||
</Tab.Panel>
|
||||
<Tab.Panel>
|
||||
<ReactNativeIdentify />
|
||||
</Tab.Panel>
|
||||
</Tab.Panels>
|
||||
</Tab.Group>
|
||||
|
||||
# Event ingestion nuances
|
||||
|
||||
It's a priority for us that events are fully processed and saved as soon as possible. However, there is a class of events which we _deliberately_ process with a slight delay. Specifically, an event is delayed by around a minute if it fits **all** of the following three conditions:
|
||||
|
||||
- isn't from an anonymous user (anonymous users are recognized by having the `distinct_id` the same as the `$device_id` property)
|
||||
- isn't an `$identify` event (e.g. from `posthog.identify()`)
|
||||
- its `distinct_id` cannot be matched to an existing person
|
||||
- isn't from an anonymous user (anonymous users are recognized by having the `distinct_id` the same as the `$device_id` property)
|
||||
- isn't an `$identify` event (e.g. from `posthog.identify()`)
|
||||
- its `distinct_id` cannot be matched to an existing person
|
||||
|
||||
This delay mechanism is called the **event buffer**, and it materially improves handling of an edge case which could otherwise inflate unique user counts.
|
||||
|
||||
@@ -138,17 +165,17 @@ This delay mechanism is called the **event buffer**, and it materially improves
|
||||
|
||||
<summary>How does the event buffer help?</summary>
|
||||
|
||||
Starting with version 1.38.0, PostHog stores the person associated with an event inline with the event record. This greatly improves query performance, but because events are immutable, it also means that persons can't be merged retroactively. See this scenario where that's problematic:
|
||||
Starting with version 1.38.0, PostHog stores the person associated with an event inline with the event record. This greatly improves query performance, but because events are immutable, it also means that persons can't be merged retroactively. See this scenario where that's problematic:
|
||||
|
||||
1. User visits signup page, in turn frontend captures anonymous `$pageview` for distinct ID `XYZ` (anonymous distinct ID = device ID).
|
||||
This event gets person ID `A`.
|
||||
2. User click signup button, initiating in a backend request, in turn frontend captures anonymous `$autocapture` (click) for distinct ID `XYZ`.
|
||||
This event gets person ID `A`.
|
||||
3. Signup request is processed in the backend, in turn backend captures identified signup for distinct ID `alice@example`.com.
|
||||
OOPS! We haven't seen `alice@example.com` before, so this event gets person ID `B`.
|
||||
4. Signup request finishes successfully, in turn frontend captures identified $identify aliasing distinct ID `XYZ` to `alice@example.com`.
|
||||
This event gets person ID `A`.
|
||||
1. User visits signup page, in turn frontend captures anonymous `$pageview` for distinct ID `XYZ` (anonymous distinct ID = device ID).
|
||||
This event gets person ID `A`.
|
||||
2. User click signup button, initiating in a backend request, in turn frontend captures anonymous `$autocapture` (click) for distinct ID `XYZ`.
|
||||
This event gets person ID `A`.
|
||||
3. Signup request is processed in the backend, in turn backend captures identified signup for distinct ID `alice@example`.com.
|
||||
OOPS! We haven't seen `alice@example.com` before, so this event gets person ID `B`.
|
||||
4. Signup request finishes successfully, in turn frontend captures identified $identify aliasing distinct ID `XYZ` to `alice@example.com`.
|
||||
This event gets person ID `A`.
|
||||
|
||||
Here, the event from step 3 got a new person ID `B`, impacting unique users counts. If it were delayed just a bit and processed after the event from step 4, all events would get the expected person ID `A`. This is exactly what the event buffer achieves.
|
||||
Here, the event from step 3 got a new person ID `B`, impacting unique users counts. If it were delayed just a bit and processed after the event from step 4, all events would get the expected person ID `A`. This is exactly what the event buffer achieves.
|
||||
|
||||
</details>
|
||||
|
||||
@@ -85,9 +85,6 @@ The following examples use `company` as a group type and `id:5` as the group key
|
||||
|
||||
> **Tip:** Use a singular form of the group type throughout ingestion
|
||||
|
||||
import { Tabs } from 'antd'
|
||||
export const TabPane = Tabs.TabPane
|
||||
|
||||
import GroupsIngestionPosthogJs from './snippets/groups-ingestion-posthog-js.mdx'
|
||||
import GroupsIngestionPython from './snippets/groups-ingestion-python.mdx'
|
||||
import GroupsIngestionPHP from './snippets/groups-ingestion-php.mdx'
|
||||
@@ -96,29 +93,43 @@ import GroupsIngestionNode from './snippets/groups-ingestion-node.mdx'
|
||||
import GroupsIngestionSegment from './snippets/groups-ingestion-segment.mdx'
|
||||
import GroupsIngestionOther from './snippets/groups-ingestion-other.mdx'
|
||||
|
||||
<Tabs defaultActiveKey="1" type="card" size="large" style={{ marginBottom: '20px' }}>
|
||||
<TabPane tab="JavaScript" key="1">
|
||||
import Tab from "components/Tab"
|
||||
|
||||
<Tab.Group>
|
||||
<Tab.List>
|
||||
<Tab>JavaScript</Tab>
|
||||
<Tab>Python</Tab>
|
||||
<Tab>PHP</Tab>
|
||||
<Tab>Go</Tab>
|
||||
<Tab>Node.js</Tab>
|
||||
<Tab>Segment</Tab>
|
||||
<Tab>Other libraries</Tab>
|
||||
</Tab.List>
|
||||
|
||||
<Tab.Panels>
|
||||
<Tab.Panel>
|
||||
<GroupsIngestionPosthogJs />
|
||||
</TabPane>
|
||||
<TabPane tab="Python" key="2">
|
||||
</Tab.Panel>
|
||||
<Tab.Panel>
|
||||
<GroupsIngestionPython />
|
||||
</TabPane>
|
||||
<TabPane tab="PHP" key="3">
|
||||
</Tab.Panel>
|
||||
<Tab.Panel>
|
||||
<GroupsIngestionPHP />
|
||||
</TabPane>
|
||||
<TabPane tab="Golang" key="4">
|
||||
</Tab.Panel>
|
||||
<Tab.Panel>
|
||||
<GroupsIngestionGo />
|
||||
</TabPane>
|
||||
<TabPane tab="NodeJS" key="5">
|
||||
</Tab.Panel>
|
||||
<Tab.Panel>
|
||||
<GroupsIngestionNode />
|
||||
</TabPane>
|
||||
<TabPane tab="Segment" key="6">
|
||||
</Tab.Panel>
|
||||
<Tab.Panel>
|
||||
<GroupsIngestionSegment />
|
||||
</TabPane>
|
||||
<TabPane tab="Other libraries" key="7">
|
||||
</Tab.Panel>
|
||||
<Tab.Panel>
|
||||
<GroupsIngestionOther />
|
||||
</TabPane>
|
||||
</Tabs>
|
||||
</Tab.Panel>
|
||||
</Tab.Panels>
|
||||
</Tab.Group>
|
||||
|
||||
## Analysing group insights
|
||||
|
||||
@@ -174,23 +185,22 @@ import GroupsFeatureFlagsPHP from './snippets/groups-flags-php.mdx'
|
||||
import GroupsFeatureFlagsGo from './snippets/groups-flags-go.mdx'
|
||||
import GroupsFeatureFlagsNode from './snippets/groups-flags-node.mdx'
|
||||
|
||||
<Tabs defaultActiveKey="1" type="card" size="large" style={{ marginBottom: '20px' }}>
|
||||
<TabPane tab="JavaScript" key="1">
|
||||
<GroupsFeatureFlagsPosthogJs />
|
||||
</TabPane>
|
||||
<TabPane tab="Python" key="2">
|
||||
<GroupsFeatureFlagsPython />
|
||||
</TabPane>
|
||||
<TabPane tab="PHP" key="3">
|
||||
<GroupsFeatureFlagsPHP />
|
||||
</TabPane>
|
||||
<TabPane tab="Golang" key="4">
|
||||
<GroupsFeatureFlagsGo />
|
||||
</TabPane>
|
||||
<TabPane tab="NodeJS" key="5">
|
||||
<GroupsFeatureFlagsNode />
|
||||
</TabPane>
|
||||
</Tabs>
|
||||
<Tab.Group>
|
||||
<Tab.List>
|
||||
<Tab>JavaScript</Tab>
|
||||
<Tab>Python</Tab>
|
||||
<Tab>PHP</Tab>
|
||||
<Tab>Go</Tab>
|
||||
<Tab>Node.js</Tab>
|
||||
</Tab.List>
|
||||
<Tab.Panels>
|
||||
<Tab.Panel><GroupsFeatureFlagsPosthogJs /></Tab.Panel>
|
||||
<Tab.Panel><GroupsFeatureFlagsPython /></Tab.Panel>
|
||||
<Tab.Panel><GroupsFeatureFlagsPHP /></Tab.Panel>
|
||||
<Tab.Panel><GroupsFeatureFlagsGo /></Tab.Panel>
|
||||
<Tab.Panel><GroupsFeatureFlagsNode /></Tab.Panel>
|
||||
</Tab.Panels>
|
||||
</Tab.Group>
|
||||
|
||||
## Renaming group types
|
||||
|
||||
@@ -213,6 +223,6 @@ Please [reach out](/slack) if you have feedback!
|
||||
|
||||
Want to know more about what's possible with Groups in PostHog? Try these tutorials:
|
||||
|
||||
- [How to track how teams use your product](/tutorials/tracking-teams)
|
||||
- [How to track how teams use your product](/tutorials/tracking-teams)
|
||||
|
||||
Want more? Check our [full list of PostHog tutorials](https://posthog.com/tutorials).
|
||||
Want more? Check our [full list of PostHog tutorials](https://posthog.com/tutorials).
|
||||
|
||||
@@ -43,15 +43,15 @@ In this case, we pass references to components that can then be used without imp
|
||||
Because of the components passed to `MDXProvider`, I can include this hedgehog by just adding `<BasicHedgehogImage />` in my
|
||||
MDX file - no import needed.
|
||||
|
||||
However, if I want to include something from a module, I can also do so. Here's how one would insert a spinner component from AntD:
|
||||
However, if I want to include something from a module, I can also do so. Here's how one would insert a Transition component from Headless UI:
|
||||
|
||||
```js
|
||||
|
||||
import { Spin } from 'antd'
|
||||
import { Transition } from '@headlessui/react'
|
||||
|
||||
## Some Markdown
|
||||
|
||||
<Spin />
|
||||
<Transition>{/* ... */}</Transition>
|
||||
|
||||
```
|
||||
|
||||
|
||||
13
netlify.toml
13
netlify.toml
@@ -1481,14 +1481,23 @@
|
||||
# Added: 2022-07-08
|
||||
[[redirects]]
|
||||
from = "/startups"
|
||||
to = "/pricing#startup-plan"
|
||||
to = "/pricing"
|
||||
|
||||
# Added: 2022-07-11
|
||||
[[redirects]]
|
||||
from = "/trial"
|
||||
to = "/pricing"
|
||||
|
||||
# Added: 2022-07-12
|
||||
[[redirects]]
|
||||
from = "/schedule-demo"
|
||||
to = "/book-a-demo"
|
||||
|
||||
# Added: 2022-07-14
|
||||
[[redirects]]
|
||||
from = "/apps/airbyte-export/docs"
|
||||
to = "/docs/apps/airbyte-export"
|
||||
|
||||
|
||||
# Added: 2022-07-14
|
||||
[[redirects]]
|
||||
from = "/apps/amazon-kinesis/docs"
|
||||
|
||||
@@ -28,7 +28,8 @@
|
||||
"@ant-design/icons": "^4.1.0",
|
||||
"@docsearch/react": "^3.0.0-alpha.42",
|
||||
"@fontsource/source-code-pro": "^4.5.4",
|
||||
"@headlessui/react": "^1.4.0",
|
||||
"@headlessui/react": "^1.6.6",
|
||||
"@heroicons/react": "^1.0.6",
|
||||
"@mdx-js/mdx": "^1.6.19",
|
||||
"@mdx-js/react": "^1.6.22",
|
||||
"@popperjs/core": "^2.11.2",
|
||||
@@ -36,7 +37,6 @@
|
||||
"@supabase/supabase-js": "^1.29.4",
|
||||
"@typescript-eslint/eslint-plugin": "^4.20.0",
|
||||
"@uiw/react-md-editor": "^3.9.5",
|
||||
"antd": "^3.23.2",
|
||||
"chart.js": "^2.9.4",
|
||||
"chrome-aws-lambda": "^10.1.0",
|
||||
"cntl": "^1.0.0",
|
||||
@@ -149,6 +149,7 @@
|
||||
"@types/chart.js": "^2.9.31",
|
||||
"@types/gatsby-plugin-breakpoints": "^1.3.2",
|
||||
"@types/mdx-js__react": "^1.5.3",
|
||||
"@types/react": "^16.0.0",
|
||||
"@types/react-burger-menu": "^2.8.3",
|
||||
"@types/react-dom": "^18.0.6",
|
||||
"@types/react-helmet": "^6.1.0",
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
import React from 'react'
|
||||
import { MethodTags, method } from '../MethodTags'
|
||||
|
||||
interface EndpointProps {
|
||||
endpoint: string
|
||||
allowedMethods: method[]
|
||||
}
|
||||
|
||||
export const Endpoint = ({ endpoint, allowedMethods }: EndpointProps) => {
|
||||
return (
|
||||
<div className="docs-endpoint-wrapper">
|
||||
<MethodTags allowedMethods={allowedMethods} />
|
||||
<h6 className="endpoint-text">{endpoint}</h6>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
import React from 'react'
|
||||
import { Tag } from 'antd'
|
||||
|
||||
export type method = 'post' | 'put' | 'patch' | 'get' | 'delete'
|
||||
|
||||
interface MethodTagsProps {
|
||||
allowedMethods: method[]
|
||||
}
|
||||
|
||||
const methodToColor: Record<method, string> = {
|
||||
post: 'green',
|
||||
patch: 'purple',
|
||||
get: 'blue',
|
||||
delete: 'red',
|
||||
put: 'gold',
|
||||
}
|
||||
|
||||
const MethodTag = ({ method }: { method: method }) => {
|
||||
return <Tag color={methodToColor[method]}>{method.toUpperCase()}</Tag>
|
||||
}
|
||||
|
||||
export const MethodTags = ({ allowedMethods }: MethodTagsProps) => {
|
||||
return (
|
||||
<>
|
||||
{allowedMethods.map((method) => (
|
||||
<MethodTag method={method.toLowerCase() as method} key={method} />
|
||||
))}
|
||||
</>
|
||||
)
|
||||
}
|
||||
125
src/components/CompensationCalculator/Combobox.tsx
Normal file
125
src/components/CompensationCalculator/Combobox.tsx
Normal file
@@ -0,0 +1,125 @@
|
||||
import React from 'react'
|
||||
import { Combobox as HeadlessCombobox, Transition } from '@headlessui/react'
|
||||
import { SelectorIcon, CheckIcon } from '@heroicons/react/outline'
|
||||
import { classNames } from 'lib/utils'
|
||||
|
||||
type ComboboxProps = {
|
||||
label: string
|
||||
placeholder?: string
|
||||
options: any[]
|
||||
value: any | undefined
|
||||
onChange: (option: any | undefined) => void
|
||||
display?: (option: any) => string
|
||||
}
|
||||
|
||||
export const Combobox = (props: ComboboxProps) => {
|
||||
const [query, setQuery] = React.useState<string>('')
|
||||
const [focused, setFocused] = React.useState<boolean>(false)
|
||||
|
||||
const filteredOptions =
|
||||
query === ''
|
||||
? props.options
|
||||
: props.options.filter((option) =>
|
||||
option.toLowerCase().replace(/\s+/g, '').includes(query.replace(/\s+/g, '').toLowerCase())
|
||||
)
|
||||
|
||||
const currentValue = props.display ? props.display(props.value) : props.value
|
||||
|
||||
return (
|
||||
<HeadlessCombobox
|
||||
as="div"
|
||||
className="relative focus:outline-none"
|
||||
value={props.value}
|
||||
onChange={(value) => {
|
||||
props.onChange(value)
|
||||
setQuery('')
|
||||
}}
|
||||
nullable
|
||||
>
|
||||
{({ open }) => (
|
||||
<>
|
||||
<HeadlessCombobox.Label className="text-sm">{props.label}</HeadlessCombobox.Label>
|
||||
<HeadlessCombobox.Button
|
||||
as="div"
|
||||
className="flex items-center relative w-full max-w-md focus:outline-none shadow-sm mt-1.5"
|
||||
>
|
||||
<HeadlessCombobox.Input
|
||||
onBlur={() => setFocused(false)}
|
||||
onFocus={(event: React.FocusEvent<HTMLInputElement>) => {
|
||||
event.target.value = ''
|
||||
setFocused(true)
|
||||
}}
|
||||
onClick={(event: React.MouseEvent<HTMLInputElement>) =>
|
||||
((event.target as HTMLInputElement).value = '')
|
||||
}
|
||||
onChange={(event) => setQuery(event.target.value)}
|
||||
displayValue={props.display}
|
||||
placeholder={currentValue || props.placeholder || 'Select a value'}
|
||||
className={`relative block w-full text-left bg-white dark:bg-gray-accent-dark px-2.5 py-1.5 rounded border border-black/10 text-xs select-none focus-visible:outline-none focus:ring-1 focus:ring-orange focus:border-orange placeholder:text-gray-600 ${
|
||||
focused ? '' : 'cursor-pointer'
|
||||
}`}
|
||||
/>
|
||||
|
||||
<span className="ml-3 absolute right-0 pr-2 pointer-events-none">
|
||||
<SelectorIcon className="h-4 w-4 text-gray-accent-light" aria-hidden="true" />
|
||||
</span>
|
||||
</HeadlessCombobox.Button>
|
||||
|
||||
<Transition
|
||||
show={open}
|
||||
as={React.Fragment}
|
||||
enter="transition duration-100 ease-out"
|
||||
enterFrom="transform scale-95 opacity-0"
|
||||
enterTo="transform scale-100 opacity-100"
|
||||
leave="transition ease-in duration-200"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<HeadlessCombobox.Options className="absolute top-full mt-1 w-full max-w-lg bg-white dark:bg-gray-accent-dark rounded p-0 z-50 text-xs max-h-[12rem] overflow-y-scroll py-1 focus:outline-none space-y-1 shadow-xl border border-black/10">
|
||||
{filteredOptions.length === 0 && query !== '' ? (
|
||||
<div className="px-2.5 py-1 text-xs text-gray">No results</div>
|
||||
) : (
|
||||
filteredOptions.map((option) => (
|
||||
<HeadlessCombobox.Option
|
||||
value={option}
|
||||
key={option}
|
||||
className={({ active }) => `
|
||||
list-none px-2.5 cursor-pointer focus:outline-none text-xs py-1
|
||||
${active ? 'bg-orange text-white' : ''}
|
||||
`}
|
||||
>
|
||||
{({ selected, active }) => (
|
||||
<div className="flex justify-between items-center">
|
||||
<span
|
||||
className={classNames(
|
||||
selected ? 'font-semibold' : 'font-normal',
|
||||
'ml-1 block truncate text-xs'
|
||||
)}
|
||||
>
|
||||
{props.display ? props.display(option) : option}
|
||||
</span>
|
||||
|
||||
{selected && (
|
||||
<span
|
||||
className={classNames(
|
||||
'flex items-center',
|
||||
active ? 'text-white' : 'text-orange'
|
||||
)}
|
||||
>
|
||||
<CheckIcon className="h-5 w-5" aria-hidden="true" />
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</HeadlessCombobox.Option>
|
||||
))
|
||||
)}
|
||||
</HeadlessCombobox.Options>
|
||||
</Transition>
|
||||
</>
|
||||
)}
|
||||
</HeadlessCombobox>
|
||||
)
|
||||
}
|
||||
|
||||
export default Combobox
|
||||
@@ -1,166 +1,187 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import React from 'react'
|
||||
import { RadioGroup } from '@headlessui/react'
|
||||
import Combobox from './Combobox'
|
||||
|
||||
import { locationFactor } from './compensation_data/location_factor'
|
||||
import { sfBenchmark } from './compensation_data/sf_benchmark'
|
||||
import { Select, Statistic, Tag, Radio } from 'antd'
|
||||
import { levelModifier } from './compensation_data/level_modifier'
|
||||
import { stepModifier } from './compensation_data/step_modifier'
|
||||
import { currencyData } from './compensation_data/currency'
|
||||
|
||||
import 'antd/lib/select/style/css'
|
||||
import 'antd/lib/statistic/style/css'
|
||||
import 'antd/lib/tag/style/css'
|
||||
import 'antd/lib/radio/style/css'
|
||||
import './style.scss'
|
||||
|
||||
const formatCur = (val: number, currency: string) => {
|
||||
const formatCur = (val: number, currency = 'USD') => {
|
||||
currency = currencyData[currency] ? currency : 'USD'
|
||||
const formatter = new Intl.NumberFormat('en-US', {
|
||||
style: 'currency',
|
||||
currency,
|
||||
})
|
||||
|
||||
return formatter.format(Math.round(val * (currencyData[currency] || 1))).replace('.00', '')
|
||||
}
|
||||
|
||||
export const CompensationCalculator = () => {
|
||||
const [job, setJob] = useState('Full Stack Engineer')
|
||||
const [country, setCountry] = useState('United States')
|
||||
const [region, setRegion] = useState('San Francisco, California')
|
||||
const [level, setLevel] = useState('Senior')
|
||||
const [step, setStep] = useState('Thriving')
|
||||
const Factor: React.FC = (props) => {
|
||||
return (
|
||||
<div className="px-1.5 bg-white dark:bg-gray-accent-dark rounded border border-black/10 text-gray-accent-dark dark:text-gray whitespace-nowrap text-2xs my-1">
|
||||
{props.children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (window) {
|
||||
if (localStorage.getItem('job') && sfBenchmark[localStorage.getItem('job')])
|
||||
setJob(localStorage.getItem('job'))
|
||||
if (localStorage.getItem('country')) setCountry(localStorage.getItem('country') || 'United States')
|
||||
if (localStorage.getItem('region')) setRegion(localStorage.getItem('region') || 'San Francisco, California')
|
||||
if (localStorage.getItem('level') && levelModifier[localStorage.getItem('level')])
|
||||
setLevel(localStorage.getItem('level') || 'Senior')
|
||||
if (localStorage.getItem('step') && stepModifier[localStorage.getItem('step')])
|
||||
setStep(localStorage.getItem('step') || 'Thriving')
|
||||
export const CompensationCalculator = () => {
|
||||
const [job, setJob] = React.useState<string | null>('Full Stack Engineer')
|
||||
const [country, setCountry] = React.useState<string | null>('United States')
|
||||
const [region, setRegion] = React.useState<string | null>('San Francisco, California')
|
||||
const [level, setLevel] = React.useState<string | null>('Senior')
|
||||
const [step, setStep] = React.useState<string | null>('Thriving')
|
||||
|
||||
React.useEffect(() => {
|
||||
if (typeof window !== undefined) {
|
||||
const savedState: {
|
||||
job?: string | null
|
||||
country?: string | null
|
||||
region?: string | null
|
||||
level?: string | null
|
||||
step?: string | null
|
||||
} = JSON.parse(localStorage.getItem('posthog-saved-compensation') || '{}')
|
||||
|
||||
if (savedState?.job && sfBenchmark[savedState.job]) {
|
||||
setJob(savedState?.job || null)
|
||||
}
|
||||
|
||||
if (savedState?.country) {
|
||||
setCountry(savedState?.country || null)
|
||||
|
||||
if (savedState?.region) {
|
||||
setRegion(savedState?.region || null)
|
||||
}
|
||||
}
|
||||
|
||||
if (savedState?.level && levelModifier[savedState.level]) {
|
||||
setLevel(savedState?.level || null)
|
||||
}
|
||||
|
||||
if (savedState?.step && stepModifier[savedState.step]) {
|
||||
setRegion(savedState?.region || null)
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
const unique = (arr: string[]) => Array.from(new Set(arr))
|
||||
|
||||
const setItem = (type: string) => {
|
||||
return (value: string | boolean) => {
|
||||
if (type === 'job' && typeof value === 'string') setJob(value)
|
||||
if (type === 'country' && typeof value === 'string') {
|
||||
return (value: any) => {
|
||||
if (type === 'job') setJob(value)
|
||||
if (type === 'country') {
|
||||
setCountry(value)
|
||||
setItem('region')(false)
|
||||
setRegion(null)
|
||||
}
|
||||
if (type === 'region') setRegion(String(value))
|
||||
if (type === 'level' && typeof value === 'string') {
|
||||
setLevel(value)
|
||||
if (type === 'region') setRegion(value)
|
||||
if (type === 'level') setLevel(value)
|
||||
if (type === 'step') setStep(value)
|
||||
|
||||
const state = {
|
||||
job,
|
||||
country,
|
||||
region,
|
||||
level,
|
||||
step,
|
||||
[type]: value,
|
||||
}
|
||||
if (type === 'step' && typeof value === 'string') setStep(value)
|
||||
localStorage.setItem(type, String(value))
|
||||
|
||||
if (type === 'country') {
|
||||
state.region = null
|
||||
}
|
||||
|
||||
localStorage.setItem('posthog-saved-compensation', JSON.stringify(state))
|
||||
}
|
||||
}
|
||||
|
||||
const location =
|
||||
country &&
|
||||
region &&
|
||||
region !== 'false' &&
|
||||
locationFactor.filter((location) => location.country === country && location.area === region)[0]
|
||||
const calculatedLocationFactor = location.locationFactor
|
||||
const findLocation = (country: string | null, region: string | null) => {
|
||||
return region && country
|
||||
? locationFactor.find((location) => location.country === country && location.area === region)
|
||||
: null
|
||||
}
|
||||
|
||||
const countries = unique(locationFactor.map((l) => l.country))
|
||||
const currentLocation = findLocation(country, region)
|
||||
|
||||
const countries = Array.from(new Set(locationFactor.map((l) => l.country)))
|
||||
|
||||
return (
|
||||
<div style={{ fontSize: '0.85rem' }} className="compensation-calculator ph-no-capture">
|
||||
<p>Select a role</p>
|
||||
<Select style={{ width: '100%', marginBottom: '0.75rem' }} value={job} onChange={setItem('job')}>
|
||||
{Object.keys(sfBenchmark).map((job) => (
|
||||
<Select.Option value={job} key={job}>
|
||||
{job}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
<p>Country</p>
|
||||
<Select
|
||||
showSearch
|
||||
style={{ width: '100%', marginBottom: '0.75rem' }}
|
||||
value={country}
|
||||
onChange={setItem('country')}
|
||||
>
|
||||
{countries.map((country) => (
|
||||
<Select.Option value={country} key={country}>
|
||||
{country}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
<p>Region</p>
|
||||
<Select
|
||||
showSearch
|
||||
style={{ width: '100%', marginBottom: '0.75rem' }}
|
||||
value={region === 'false' ? '' : region}
|
||||
<div className="ph-no-capture space-y-4 py-4">
|
||||
<Combobox label="Select a role" value={job} onChange={setItem('job')} options={Object.keys(sfBenchmark)} />
|
||||
|
||||
<Combobox label="Country" value={country} onChange={setItem('country')} options={countries} />
|
||||
|
||||
<Combobox
|
||||
label="Region"
|
||||
value={region}
|
||||
onChange={setItem('region')}
|
||||
>
|
||||
{locationFactor
|
||||
options={locationFactor
|
||||
.filter((location) => location.country === country)
|
||||
.map((countryRegion) => (
|
||||
<Select.Option value={countryRegion.area} key={countryRegion.area}>
|
||||
{countryRegion.area} <span>{countryRegion.locationFactor}</span>
|
||||
</Select.Option>
|
||||
.map((location) => location.area)}
|
||||
display={(area) => (area ? `${area} ${findLocation(country, area)?.locationFactor}` : '')}
|
||||
/>
|
||||
|
||||
<RadioGroup as="div" className="block" value={level} onChange={setItem('level')}>
|
||||
<RadioGroup.Label className="block text-sm">Level</RadioGroup.Label>
|
||||
<div className="w-full max-w-xs md:max-w-full md:w-auto inline-flex flex-col items-stretch md:flex-row md:items-center bg-white dark:bg-gray-accent-dark rounded divide-y md:divide-y-0 md:divide-x divide-black/10 overflow-hidden shadow-sm border border-black/10 text-xs mt-1.5">
|
||||
{Object.entries(levelModifier).map(([level, modifier]) => (
|
||||
<RadioGroup.Option
|
||||
as="button"
|
||||
key={level}
|
||||
value={level}
|
||||
className={({ checked }) => `
|
||||
px-4 py-1.5 whitespace-nowrap text-left md:text-center
|
||||
${checked ? 'bg-orange text-white' : 'hover:bg-black/10'}
|
||||
`}
|
||||
>
|
||||
{level} <span>{modifier}</span>
|
||||
</RadioGroup.Option>
|
||||
))}
|
||||
</Select>
|
||||
<p>Level</p>
|
||||
<Radio.Group
|
||||
style={{ width: '100%', marginBottom: '0.75rem' }}
|
||||
value={level}
|
||||
buttonStyle="solid"
|
||||
onChange={(e) => setItem('level')(e.target.value)}
|
||||
>
|
||||
{Object.entries(levelModifier).map(([level, modifier]) => (
|
||||
<Radio.Button value={level} key={level}>
|
||||
{level} <span>{modifier}</span>
|
||||
</Radio.Button>
|
||||
))}
|
||||
</Radio.Group>
|
||||
<p>Step</p>
|
||||
<Select style={{ width: '100%', marginBottom: '0.75rem' }} value={step} onChange={setItem('step')}>
|
||||
{Object.entries(stepModifier).map(([step, modifier]) => (
|
||||
<Select.Option value={step} key={step}>
|
||||
{step} {modifier[0]} - {modifier[1]}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
<Statistic
|
||||
title={<p>Base salary</p>}
|
||||
value={
|
||||
job && country && region && location && typeof calculatedLocationFactor === 'number'
|
||||
</div>
|
||||
</RadioGroup>
|
||||
|
||||
<Combobox
|
||||
label="Step"
|
||||
value={step}
|
||||
onChange={setItem('step')}
|
||||
options={Object.keys(stepModifier)}
|
||||
display={(step: string) => `${step} ${stepModifier[step]?.[0]} - ${stepModifier[step]?.[1]}`}
|
||||
/>
|
||||
|
||||
<div>
|
||||
<label className="text-sm" htmlFor="compensation">
|
||||
Base salary
|
||||
</label>
|
||||
<div className="text-2xl mt-1" id="compensation">
|
||||
{job && country && region && currentLocation && level && step
|
||||
? formatCur(
|
||||
sfBenchmark[job] *
|
||||
calculatedLocationFactor *
|
||||
currentLocation.locationFactor *
|
||||
levelModifier[level] *
|
||||
stepModifier[step][0],
|
||||
location?.currency
|
||||
currentLocation.currency
|
||||
) +
|
||||
' - ' +
|
||||
formatCur(
|
||||
sfBenchmark[job] *
|
||||
calculatedLocationFactor *
|
||||
currentLocation.locationFactor *
|
||||
levelModifier[level] *
|
||||
stepModifier[step][1],
|
||||
location?.currency
|
||||
currentLocation.currency
|
||||
) +
|
||||
' + equity'
|
||||
: '--'
|
||||
}
|
||||
/>
|
||||
{job && country && region && (
|
||||
<div>
|
||||
<Tag>SF Benchmark: {formatCur(sfBenchmark[job])}</Tag> x{' '}
|
||||
<Tag>Location factor: {calculatedLocationFactor}</Tag> x{' '}
|
||||
<Tag>Level modifier: {levelModifier[level]}</Tag>x{' '}
|
||||
<Tag>
|
||||
Step modifier: {stepModifier[step][0]} - {stepModifier[step][1]}
|
||||
</Tag>
|
||||
: '--'}
|
||||
</div>
|
||||
)}
|
||||
<br />
|
||||
{job && country && currentLocation && level && step && (
|
||||
<div className="flex items-center flex-wrap space-x-2 text-gray">
|
||||
<Factor>SF Benchmark: {formatCur(sfBenchmark[job], currentLocation?.currency)}</Factor>
|
||||
<span>×</span>
|
||||
<Factor>Location factor: {currentLocation?.locationFactor}</Factor> <span>×</span>
|
||||
<Factor>Level modifier: {levelModifier[level]}</Factor> <span>×</span>
|
||||
<Factor>
|
||||
Step modifier: {stepModifier[step][0]} - {stepModifier[step][1]}
|
||||
</Factor>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
.compensation-calculator {
|
||||
p {
|
||||
padding: 0 !important;
|
||||
margin-bottom: 0.25rem !important;
|
||||
font-weight: 500 !important;
|
||||
color: #000;
|
||||
}
|
||||
.ant-tag {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
body.dark {
|
||||
.compensation-calculator {
|
||||
span {
|
||||
color: rgb(58, 58, 58);
|
||||
}
|
||||
span.ant-statistic-content-value {
|
||||
color: #d4d4d4;
|
||||
}
|
||||
.ant-radio-button-wrapper-checked {
|
||||
background: #d7beff !important;
|
||||
border-color: #d7beff !important;
|
||||
box-shadow: none;
|
||||
color: white;
|
||||
}
|
||||
p {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,31 +1,26 @@
|
||||
import { Card, Col, Progress, Tag } from 'antd'
|
||||
import { Spacer } from 'components/Spacer'
|
||||
import { Link } from 'gatsby'
|
||||
import React from 'react'
|
||||
import Tooltip from '../Tooltip'
|
||||
import { emojiKey } from './emojiKey'
|
||||
import Tooltip from 'components/Tooltip'
|
||||
import Link from 'components/Link'
|
||||
import tooltipIcon from './images/tooltip.svg'
|
||||
|
||||
interface ContributorCardStructureMeta {
|
||||
type ContributorCardProps = {
|
||||
name: string
|
||||
link: string
|
||||
imageSrc: string
|
||||
contributions: string[]
|
||||
mvpWins: number
|
||||
contributorLevel: number
|
||||
}
|
||||
|
||||
interface ContributorCardMeta extends ContributorCardStructureMeta {
|
||||
link: string
|
||||
onClick?: () => void | undefined
|
||||
}
|
||||
|
||||
const ContributorCardStructure = ({
|
||||
export const ContributorCard = ({
|
||||
name,
|
||||
link,
|
||||
imageSrc,
|
||||
contributions,
|
||||
mvpWins,
|
||||
contributorLevel,
|
||||
}: ContributorCardStructureMeta) => {
|
||||
}: ContributorCardProps) => {
|
||||
const handleTooltipContentClick = (e: React.MouseEvent, pageKey = '') => {
|
||||
if (window) {
|
||||
e.preventDefault()
|
||||
@@ -47,125 +42,77 @@ const ContributorCardStructure = ({
|
||||
pageKey: string
|
||||
}) => (
|
||||
<Tooltip title={title}>
|
||||
<span onClick={(e) => handleTooltipContentClick(e, pageKey)} className="tooltip-content">
|
||||
{children}
|
||||
</span>
|
||||
<span onClick={(e) => handleTooltipContentClick(e, pageKey)}>{children}</span>
|
||||
</Tooltip>
|
||||
)
|
||||
|
||||
return (
|
||||
<Col sm={12} md={12} lg={8} xl={6} style={{ marginBottom: 20 }}>
|
||||
<Card
|
||||
style={{ height: 450, display: 'flex', marginBottom: 20 }}
|
||||
bodyStyle={{ display: 'flex', flexDirection: 'column', flexGrow: 1 }}
|
||||
className="card-elevated hover:shadow-none"
|
||||
>
|
||||
{mvpWins > 0 ? (
|
||||
<Tag color="transparent" style={{ maxWidth: '30%', position: 'absolute', right: 15, top: 15 }}>
|
||||
<h4>
|
||||
<ContributorCardTooltip title={`Community MVP ${mvpWins}x`} pageKey="community-mvps">
|
||||
{Array.from({ length: mvpWins }).map((_: any, i: number) => (
|
||||
<span key={`trophy_${i}`}>🏆</span>
|
||||
))}
|
||||
</ContributorCardTooltip>
|
||||
</h4>
|
||||
</Tag>
|
||||
) : null}
|
||||
|
||||
<img
|
||||
src={imageSrc}
|
||||
style={{ maxWidth: 60, maxHeight: 60, marginBottom: 0 }}
|
||||
className="center rounded-full overflow-hidden"
|
||||
alt="contributor image"
|
||||
/>
|
||||
|
||||
<h4 className="centered">{name}</h4>
|
||||
|
||||
<h6>
|
||||
Level {contributorLevel}
|
||||
<ContributorCardTooltip title="Number of PRs merged" pageKey="level">
|
||||
<img
|
||||
src={tooltipIcon}
|
||||
width="18"
|
||||
height="18"
|
||||
alt="More info"
|
||||
className="inline-block opacity-25 hover:opacity-50 ml-1"
|
||||
/>
|
||||
<Link to={link} className="relative">
|
||||
{mvpWins > 0 ? (
|
||||
<div className="absolute top-4 right-4 text-xl">
|
||||
<ContributorCardTooltip title={`Community MVP ${mvpWins}x`} pageKey="community-mvps">
|
||||
{Array.from({ length: mvpWins }).map((_: any, i: number) => (
|
||||
<span key={`trophy_${i}`}>🏆</span>
|
||||
))}
|
||||
</ContributorCardTooltip>
|
||||
</h6>
|
||||
<Progress
|
||||
strokeColor={{
|
||||
'0%': '#F1A82C',
|
||||
'100%': '#F54E00',
|
||||
}}
|
||||
percent={contributorLevel >= 50 ? 50 : (100 * contributorLevel) / 50}
|
||||
className="progress-bar rounded-full overflow-hidden bg-gray-accent-light"
|
||||
showInfo={false}
|
||||
/>
|
||||
<Spacer height={40} />
|
||||
</div>
|
||||
) : null}
|
||||
<div className="bg-white rounded shadow-lg flex flex-col items-center pt-6 pb-12 px-6 space-y-6">
|
||||
<div className="space-y-1">
|
||||
<img
|
||||
src={imageSrc}
|
||||
className="mx-auto w-12 h-12 rounded-full overflow-hidden"
|
||||
alt="contributor image"
|
||||
/>
|
||||
|
||||
<h6>
|
||||
Powers
|
||||
<ContributorCardTooltip title="Types of contributions made" pageKey="powers">
|
||||
<img
|
||||
src={tooltipIcon}
|
||||
width="18"
|
||||
height="18"
|
||||
alt="More info"
|
||||
className="inline-block opacity-25 hover:opacity-50 ml-1"
|
||||
<h4 className="text-black/80">{name}</h4>
|
||||
</div>
|
||||
|
||||
<div className="w-full space-y-1.5">
|
||||
<div className="text-black/80">
|
||||
Level {contributorLevel}
|
||||
<ContributorCardTooltip title="Number of PRs merged" pageKey="level">
|
||||
<img
|
||||
src={tooltipIcon}
|
||||
alt="More info"
|
||||
className="w-3.5 h-3.5 inline-block opacity-25 hover:opacity-50 ml-1"
|
||||
/>
|
||||
</ContributorCardTooltip>
|
||||
</div>
|
||||
|
||||
<div className="relative w-full rounded-full overflow-hidden bg-gray-accent-light h-1.5">
|
||||
<div
|
||||
style={{ width: `${contributorLevel >= 50 ? 50 : (100 * contributorLevel) / 50}%` }}
|
||||
className="absolute left-0 inset-y-0 h-full bg-gradient-to-r from-[#F1A82C] to-orange"
|
||||
/>
|
||||
</ContributorCardTooltip>
|
||||
</h6>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Spacer height={20} />
|
||||
<h2>
|
||||
{contributions.map((key) => (
|
||||
<span key={key}>
|
||||
<ContributorCardTooltip title={emojiKey[key].description} pageKey="powers">
|
||||
{emojiKey[key].symbol}
|
||||
</ContributorCardTooltip>{' '}
|
||||
</span>
|
||||
))}
|
||||
</h2>
|
||||
</Card>
|
||||
</Col>
|
||||
<div className="space-y-1.5">
|
||||
<div className="text-black/80">
|
||||
Powers
|
||||
<ContributorCardTooltip title="Types of contributions made" pageKey="powers">
|
||||
<img
|
||||
src={tooltipIcon}
|
||||
alt="More info"
|
||||
className="w-3.5 h-3.5 inline-block opacity-25 hover:opacity-50 ml-1"
|
||||
/>
|
||||
</ContributorCardTooltip>
|
||||
</div>
|
||||
|
||||
<div className="text-xl">
|
||||
{contributions.map((key) => (
|
||||
<span key={key}>
|
||||
<ContributorCardTooltip title={emojiKey[key].description} pageKey="powers">
|
||||
{emojiKey[key].symbol}
|
||||
</ContributorCardTooltip>{' '}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
export const ContributorCard = ({
|
||||
name,
|
||||
link,
|
||||
imageSrc,
|
||||
onClick,
|
||||
contributions,
|
||||
mvpWins,
|
||||
contributorLevel,
|
||||
}: ContributorCardMeta) => {
|
||||
const ContributorDetails = () => (
|
||||
<ContributorCardStructure
|
||||
name={name}
|
||||
imageSrc={imageSrc}
|
||||
contributions={contributions}
|
||||
mvpWins={mvpWins}
|
||||
contributorLevel={contributorLevel}
|
||||
/>
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="contributor-card-wrapper">
|
||||
{onClick ? (
|
||||
<span onClick={onClick}>
|
||||
<ContributorDetails />
|
||||
</span>
|
||||
) : link.includes('.') ? (
|
||||
<a href={link}>
|
||||
<ContributorDetails />
|
||||
</a>
|
||||
) : (
|
||||
<Link to={link}>
|
||||
<ContributorDetails />
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default ContributorCard
|
||||
|
||||
@@ -1,17 +1,38 @@
|
||||
import React from 'react'
|
||||
import { Input } from 'antd'
|
||||
import { useActions } from 'kea'
|
||||
import { contributorsLogic } from 'logic/contributorsLogic'
|
||||
|
||||
export const ContributorSearch = () => {
|
||||
const { processSearchInput } = useActions(contributorsLogic)
|
||||
|
||||
return (
|
||||
<div className="max-w-xs mx-auto">
|
||||
<Input.Search
|
||||
className="contributor-search"
|
||||
size="large"
|
||||
<div className="flex flex-col justify-center relative mx-auto mb-0 w-full max-w-lg">
|
||||
<div className="absolute left-4 w-4 h-4">
|
||||
<svg className="opacity-50" fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 18 18">
|
||||
<g opacity="1" clipPath="url(#a)">
|
||||
<path
|
||||
d="m18 15.964-4.794-4.793A7.2 7.2 0 1 0 .001 7.2a7.2 7.2 0 0 0 11.17 6.006L15.963 18 18 15.964ZM2.04 7.2A5.16 5.16 0 0 1 7.2 2.043 5.16 5.16 0 1 1 2.04 7.2Z"
|
||||
fill="#90794B"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="a">
|
||||
<path fill="#fff" d="M0 0h18v18H0z" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
</div>
|
||||
<input
|
||||
onChange={(e) => processSearchInput(e.target.value)}
|
||||
name="contributor-search"
|
||||
placeholder="Search contributors..."
|
||||
autoFocus={true}
|
||||
className="pl-10 py-3 text-base bg-white dark:bg-gray-accent-dark rounded-full w-full ring-red shadow-lg shadow-[0_100px_80px_0_rgba(0,0,0,0.07),0px_14.5036px_24.1177px_rgba(0,0,0,0.0395839),0_6.68266px_10.0172px_rgba(0,0,0,0.0291065),0_4.88627px_3.62304px_rgba(0,0,0,0.0214061)]"
|
||||
/>
|
||||
|
||||
<button className="hidden px-6 py-3 bg-red text-base shadow-md rounded-sm text-white font-bold">
|
||||
Search
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
font-family: 'MatterVF';
|
||||
/* LOCAL DEVELOPMENT */
|
||||
// src: url('/fonts/MatterSQVF.woff') format('woff supports variations'),
|
||||
// url('/fonts/MatterSQVF.woff') format('woff-variations'),
|
||||
// url('/fonts/MatterSQVF.woff2') format('woff2 supports variations'),
|
||||
// url('/fonts/MatterSQVF.woff2') format('woff2-variations');
|
||||
// url('/fonts/MatterSQVF.woff') format('woff-variations'),
|
||||
// url('/fonts/MatterSQVF.woff2') format('woff2 supports variations'),
|
||||
// url('/fonts/MatterSQVF.woff2') format('woff2-variations');
|
||||
|
||||
/* PRODUCTION */
|
||||
|
||||
@@ -24,9 +24,9 @@
|
||||
font-family: 'MatterVF';
|
||||
/* LOCAL DEVELOPMENT */
|
||||
// src: url('/fonts/MatterSQItalicVF.woff') format('woff supports variations'),
|
||||
// url('/fonts/MatterSQItalicVF.woff') format('woff-variations'),
|
||||
// url('/fonts/MatterSQItalicVF.woff2') format('woff2 supports variations'),
|
||||
// url('/fonts/MatterSQItalicVF.woff2') format('woff2-variations');
|
||||
// url('/fonts/MatterSQItalicVF.woff') format('woff-variations'),
|
||||
// url('/fonts/MatterSQItalicVF.woff2') format('woff2 supports variations'),
|
||||
// url('/fonts/MatterSQItalicVF.woff2') format('woff2-variations');
|
||||
|
||||
/* PRODUCTION */
|
||||
src: url('//d27nj4tzr3d5tm.cloudfront.net/Website-Assets/Fonts/Matter/MatterSQItalicVF.woff2')
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import 'antd/lib/card/style/css'
|
||||
import Byline from 'components/Blog/BlogAuthor/Byline'
|
||||
import { graphql, Link, useStaticQuery } from 'gatsby'
|
||||
import { GatsbyImage, IGatsbyImageData } from 'gatsby-plugin-image'
|
||||
|
||||
23
src/components/Spinner/index.tsx
Normal file
23
src/components/Spinner/index.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import React from 'react'
|
||||
type SpinnerProps = {
|
||||
className?: string
|
||||
}
|
||||
|
||||
export const Spinner = ({ className }: SpinnerProps) => {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
version="1.1"
|
||||
className={`w-8 h-8 animate-spin text-gray-accent-light dark:text-gray-accent-dark ${className}`}
|
||||
>
|
||||
<g stroke="none" strokeWidth="1" fill="none" fillRule="evenodd">
|
||||
<g fill="currentColor" fillRule="evenodd">
|
||||
<path d="M10,3.5 C6.41015,3.5 3.5,6.41015 3.5,10 C3.5,10.4142 3.16421,10.75 2.75,10.75 C2.33579,10.75 2,10.4142 2,10 C2,5.58172 5.58172,2 10,2 C14.4183,2 18,5.58172 18,10 C18,14.4183 14.4183,18 10,18 C9.58579,18 9.25,17.6642 9.25,17.25 C9.25,16.8358 9.58579,16.5 10,16.5 C13.5899,16.5 16.5,13.5899 16.5,10 C16.5,6.41015 13.5899,3.5 10,3.5 Z" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export default Spinner
|
||||
56
src/components/Tab/index.tsx
Normal file
56
src/components/Tab/index.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
import React from 'react'
|
||||
import { Tab as HeadlessTab } from '@headlessui/react'
|
||||
import { classNames } from 'lib/utils'
|
||||
|
||||
export const Tab: React.FC & {
|
||||
Group: typeof HeadlessTab.Group
|
||||
List: typeof HeadlessTab.List
|
||||
Panels: typeof HeadlessTab.Panels
|
||||
Panel: typeof HeadlessTab.Panel
|
||||
} = ({ children }) => {
|
||||
return (
|
||||
<HeadlessTab
|
||||
className={({ selected }) =>
|
||||
classNames(
|
||||
selected ? 'bg-red text-white' : 'bg-white dark:bg-gray-accent-dark',
|
||||
'px-4 py-1.5 rounded shadow-sm text-sm font-medium whitespace-nowrap'
|
||||
)
|
||||
}
|
||||
>
|
||||
{children}
|
||||
</HeadlessTab>
|
||||
)
|
||||
}
|
||||
|
||||
const TabGroup: typeof HeadlessTab.Group = ({ children }) => {
|
||||
return (
|
||||
<HeadlessTab.Group as="div" className="my-6">
|
||||
{children}
|
||||
</HeadlessTab.Group>
|
||||
)
|
||||
}
|
||||
|
||||
TabGroup.displayName = 'TabGroup'
|
||||
|
||||
const TabList: typeof HeadlessTab.List = ({ children, className, ...props }) => {
|
||||
return (
|
||||
<HeadlessTab.List {...props} className={`space-x-3 flex items-center overflow-x-auto ${className}`}>
|
||||
{children}
|
||||
</HeadlessTab.List>
|
||||
)
|
||||
}
|
||||
|
||||
TabList.displayName = 'TabList'
|
||||
|
||||
const TabPanels: typeof HeadlessTab.Panels = ({ children }) => {
|
||||
return <HeadlessTab.Panels className="mt-4">{children}</HeadlessTab.Panels>
|
||||
}
|
||||
|
||||
TabPanels.displayName = 'TabPanels'
|
||||
|
||||
Tab.Group = TabGroup
|
||||
Tab.List = TabList
|
||||
Tab.Panel = HeadlessTab.Panel
|
||||
Tab.Panels = TabPanels
|
||||
|
||||
export default Tab
|
||||
@@ -13,7 +13,7 @@ export default function Tooltip({ children, title }: { children: JSX.Element; ti
|
||||
})}
|
||||
<span
|
||||
role="tooltip"
|
||||
className="bg-primary text-white rounded-md px-2 py-1 group-hover:visible invisible text-xs"
|
||||
className="bg-primary text-white rounded-md px-2 py-1 group-hover:visible invisible text-xs z-50"
|
||||
ref={setPopperElement}
|
||||
style={styles.popper}
|
||||
{...attributes.popper}
|
||||
|
||||
@@ -15,6 +15,10 @@ export const unsafeHash = (str: string) => {
|
||||
return String(a)
|
||||
}
|
||||
|
||||
export const classNames = (...classes: (string | null | undefined)[]) => {
|
||||
return classes.filter(Boolean).join(' ')
|
||||
}
|
||||
|
||||
export const getPluginImageSrc = (plugin: LibraryPluginType) =>
|
||||
plugin.imageLink
|
||||
? plugin.imageLink
|
||||
@@ -72,6 +76,7 @@ export const scrollWithOffset = (id: string, offset: number) => {
|
||||
|
||||
// tests email address for RFC 5322 compliance
|
||||
export function isValidEmailAddress(email: string): boolean {
|
||||
const re = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
|
||||
const re =
|
||||
/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
|
||||
return re.test(String(email).toLowerCase())
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
// AUTO GENERATED FILE
|
||||
|
||||
import { Endpoint } from './components/APIDocs/Endpoint'
|
||||
import { MethodTags } from './components/APIDocs/MethodTags'
|
||||
import { Accordion } from './components/Accordion'
|
||||
import { AnchorScrollNavbar } from './components/AnchorScrollNavbar'
|
||||
import { AnimateIntoView } from './components/AnimateIntoView'
|
||||
@@ -96,10 +94,12 @@ import { FeaturedSectionTextRight } from './components/Sections/FeaturedSectionT
|
||||
import { FeaturedSectionTripleImage } from './components/Sections/FeaturedSectionTripleImage'
|
||||
import { SliderNav } from './components/SliderNav'
|
||||
import { Spacer } from './components/Spacer'
|
||||
import { Spinner } from './components/Spinner'
|
||||
import { StarRepoButton } from './components/StarRepoButton'
|
||||
import { StarUsBanner } from './components/StarUsBanner'
|
||||
import { Structure } from './components/Structure'
|
||||
import { Subscribe } from './components/Subscribe'
|
||||
import { Tab } from './components/Tab'
|
||||
import { TeamQuote } from './components/TeamQuote'
|
||||
import { Tooltip } from './components/Tooltip'
|
||||
import { TotalCountries } from './components/TotalCountries'
|
||||
@@ -112,8 +112,6 @@ import { WorkableSnippet } from './components/WorkableSnippet'
|
||||
import { ZoomImage } from './components/ZoomImage'
|
||||
|
||||
export const shortcodes = {
|
||||
Endpoint,
|
||||
MethodTags,
|
||||
Accordion,
|
||||
AnchorScrollNavbar,
|
||||
AnimateIntoView,
|
||||
@@ -208,10 +206,12 @@ export const shortcodes = {
|
||||
FeaturedSectionTripleImage,
|
||||
SliderNav,
|
||||
Spacer,
|
||||
Spinner,
|
||||
StarRepoButton,
|
||||
StarUsBanner,
|
||||
Structure,
|
||||
Subscribe,
|
||||
Tab,
|
||||
TeamQuote,
|
||||
Tooltip,
|
||||
TotalCountries,
|
||||
|
||||
@@ -1,31 +1,19 @@
|
||||
import React, { useState } from 'react'
|
||||
import Layout from '../components/Layout'
|
||||
import { Spacer } from '../components/Spacer'
|
||||
import { Row, Tabs, Spin } from 'antd'
|
||||
import { useActions, useValues } from 'kea'
|
||||
import React from 'react'
|
||||
import Layout from 'components/Layout'
|
||||
import { Spacer } from 'components/Spacer'
|
||||
import { useValues } from 'kea'
|
||||
import { contributorsLogic } from '../logic/contributorsLogic'
|
||||
import { SEO } from '../components/seo'
|
||||
import { SEO } from 'components/seo'
|
||||
import pluginLibraryOgImage from '../images/posthog-plugins.png'
|
||||
import { ContributorCard } from 'components/ContributorCard'
|
||||
import { Contributor } from 'types'
|
||||
import { ContributorSearch } from 'components/ContributorSearch'
|
||||
import { ContributorsChart } from 'components/ContributorsChart'
|
||||
|
||||
import 'antd/lib/input/style/css'
|
||||
|
||||
const { TabPane } = Tabs
|
||||
import Tab from 'components/Tab'
|
||||
import Spinner from 'components/Spinner'
|
||||
|
||||
export const ContributorsPage = () => {
|
||||
const { setSearchQuery } = useActions(contributorsLogic)
|
||||
const { filteredContributors, contributorsLoading } = useValues(contributorsLogic)
|
||||
const [activeTab, setActiveTab] = useState('list')
|
||||
|
||||
const handleTabClick = (newTab: string) => {
|
||||
setActiveTab(newTab)
|
||||
if (newTab === 'list') {
|
||||
setSearchQuery('')
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="contributors-page-wrapper">
|
||||
@@ -38,39 +26,44 @@ export const ContributorsPage = () => {
|
||||
<div className="centered" style={{ margin: 'auto' }}>
|
||||
<Spacer />
|
||||
<h1 className="center">Contributors</h1>
|
||||
<Tabs activeKey={activeTab} onChange={(key) => handleTabClick(key)}>
|
||||
<TabPane tab="List" key="list" />
|
||||
<TabPane tab="Stats" key="stats" />
|
||||
</Tabs>
|
||||
<Spacer height={20} />
|
||||
<Tab.Group>
|
||||
<Tab.List className="justify-center my-8">
|
||||
<Tab>List</Tab>
|
||||
<Tab>Stats</Tab>
|
||||
</Tab.List>
|
||||
|
||||
{activeTab === 'list' ? (
|
||||
<>
|
||||
<ContributorSearch />
|
||||
<Spacer height={20} />
|
||||
<Row gutter={16} style={{ marginTop: 16, marginRight: 10, marginLeft: 10, minHeight: 600 }}>
|
||||
{contributorsLoading ? (
|
||||
<Spin size="large" style={{ position: 'fixed', top: '50%', left: '50%' }} />
|
||||
) : (
|
||||
<>
|
||||
{filteredContributors.map((contributor: Contributor) => (
|
||||
<ContributorCard
|
||||
key={contributor.login}
|
||||
name={contributor.login}
|
||||
link={contributor.profile}
|
||||
imageSrc={contributor.avatar_url}
|
||||
contributions={contributor.contributions}
|
||||
mvpWins={contributor.mvpWins}
|
||||
contributorLevel={contributor.level}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</Row>
|
||||
</>
|
||||
) : (
|
||||
<ContributorsChart />
|
||||
)}
|
||||
<Tab.Panels>
|
||||
<Tab.Panel>
|
||||
<div className="flex flex-col items-center space-y-8 px-6">
|
||||
<ContributorSearch />
|
||||
|
||||
{contributorsLoading ? (
|
||||
<Spinner />
|
||||
) : (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6 w-full max-w-6xl mx-auto">
|
||||
{filteredContributors.map((contributor: Contributor) => (
|
||||
<ContributorCard
|
||||
key={contributor.login}
|
||||
name={contributor.login}
|
||||
link={contributor.profile}
|
||||
imageSrc={contributor.avatar_url}
|
||||
contributions={contributor.contributions}
|
||||
mvpWins={contributor.mvpWins}
|
||||
contributorLevel={contributor.level}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Tab.Panel>
|
||||
|
||||
<Tab.Panel>
|
||||
<ContributorsChart />
|
||||
</Tab.Panel>
|
||||
</Tab.Panels>
|
||||
</Tab.Group>
|
||||
|
||||
<Spacer height={20} />
|
||||
</div>
|
||||
<Spacer />
|
||||
</Layout>
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
import React from 'react'
|
||||
import Layout from '../components/Layout'
|
||||
import { Spacer } from '../components/Spacer'
|
||||
import './styles/yc-onboarding.scss'
|
||||
import { SEO } from '../components/seo'
|
||||
import Contact from 'components/Contact'
|
||||
|
||||
export const ScheduleDemo = () => (
|
||||
<Layout>
|
||||
<SEO title="Schedule Demo • PostHog" />
|
||||
<div className="get-in-touch-wrapper">
|
||||
<Spacer />
|
||||
<h1 className="centered">Get in touch</h1>
|
||||
<Contact />
|
||||
</div>
|
||||
</Layout>
|
||||
)
|
||||
|
||||
export default ScheduleDemo
|
||||
@@ -1,18 +1,18 @@
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { Spin } from 'antd'
|
||||
import queryString from 'query-string'
|
||||
import { SEO } from '../components/seo'
|
||||
import Spinner from 'components/Spinner'
|
||||
import { SEO } from 'components/seo'
|
||||
|
||||
function Slack() {
|
||||
/* This component will redirect the user to the Slack users group. */
|
||||
const [source, setSource] = useState(null)
|
||||
const [source, setSource] = useState<string | null>(null)
|
||||
const slackUrl = 'https://join.slack.com/t/posthogusers/shared_invite/zt-1chf8vjjr-uyu88Xvsu1cSEi3ILFqSqw'
|
||||
|
||||
useEffect(() => {
|
||||
const { s } = queryString.parse(location.search)
|
||||
const s = new URLSearchParams(window.location.search).get('s')
|
||||
setSource(s)
|
||||
|
||||
/* Wait for any UTM tags to be registered in a $pageview,
|
||||
we wait a few more seconds if the user is coming from app so
|
||||
we wait a few more seconds if the user is coming from app so
|
||||
they can read the additional instructions.
|
||||
*/
|
||||
const waitTime = s === 'app' ? 5000 : 1000
|
||||
@@ -22,22 +22,17 @@ function Slack() {
|
||||
return (
|
||||
<>
|
||||
<SEO title="PostHog Community Slack" />
|
||||
<div
|
||||
style={{ display: 'flex', alignItems: 'center', marginTop: 48, fontSize: 16, flexDirection: 'column' }}
|
||||
>
|
||||
<h1 style={{ marginBottom: '1rem' }}>We're redirecting you to Slack.</h1>
|
||||
<div className="flex flex-col items-center mt-6 space-y-6">
|
||||
<h1 className="mb-0">We're redirecting you to Slack.</h1>
|
||||
|
||||
{source === 'app' && (
|
||||
<div style={{ fontSize: '1.1rem', color: 'var(--muted)' }}>
|
||||
Remember to use the{' '}
|
||||
<b>
|
||||
<span style={{ color: 'var(--danger)' }}>same email</span> you used to sign up
|
||||
</b>{' '}
|
||||
<div className="text-gray">
|
||||
Remember to use the <span className="text-red font-bold">same email</span> you used to sign up
|
||||
in the PostHog app.
|
||||
</div>
|
||||
)}
|
||||
<div style={{ marginTop: '2rem' }}>
|
||||
<Spin size="large" />
|
||||
</div>
|
||||
|
||||
<Spinner />
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
@@ -1,15 +0,0 @@
|
||||
.get-in-touch-wrapper {
|
||||
p {
|
||||
margin: 0 auto 1em auto;
|
||||
font-size: 15px !important;
|
||||
max-width: 36rem;
|
||||
display: block;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 767px) {
|
||||
p {
|
||||
max-width: 90%;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
import React from 'react'
|
||||
import { Link } from 'gatsby'
|
||||
import Layout from '../components/Layout'
|
||||
import { Row, Col, Button, Icon } from 'antd'
|
||||
import { SEO } from '../components/seo'
|
||||
import './styles/trial.scss'
|
||||
|
||||
const TrialPage = () => (
|
||||
<Layout>
|
||||
<div className="trial-page-wrapper">
|
||||
<div className="trial-page-container">
|
||||
<SEO title="PostHog Trial" description="Get started, for free." />
|
||||
<Row gutter={[24, 24]}>
|
||||
<Col span={24} align="middle">
|
||||
<h1>Try PostHog - free for 30 days</h1>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row gutter={[16, 96]} className="card-row">
|
||||
<Col xs={24} sm={24} md={12} lg={12} xl={12} className="card-col">
|
||||
<h2>
|
||||
<Icon type="cloud" theme="filled" /> Cloud
|
||||
</h2>
|
||||
<h3>Just create an account.</h3>
|
||||
<p>
|
||||
Select this option if you want to quickly try the PostHog features and don't want to worry
|
||||
about installing it yourself.
|
||||
</p>
|
||||
<p>
|
||||
<a href="https://app.posthog.com/signup">
|
||||
<Button type="primary" size="large">
|
||||
Sign Up
|
||||
</Button>
|
||||
</a>
|
||||
</p>
|
||||
</Col>
|
||||
<Col xs={24} sm={24} md={12} lg={12} xl={12} className="card-col">
|
||||
<h2>
|
||||
<Icon type="hdd" theme="filled" /> Open Source
|
||||
</h2>
|
||||
<h3>Host your own instance.</h3>
|
||||
<p>
|
||||
Select this option if you want to install our open source platform on your own
|
||||
infrastructure.
|
||||
</p>
|
||||
<p>
|
||||
<Link to="/docs/self-host">
|
||||
<Button type="primary" size="large">
|
||||
Self Deploy
|
||||
</Button>
|
||||
</Link>
|
||||
</p>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row className="spacer-row"></Row>
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
)
|
||||
|
||||
export default TrialPage
|
||||
@@ -3,8 +3,6 @@ import Layout from '../components/Layout'
|
||||
import { DemoScheduler } from '../components/DemoScheduler'
|
||||
import { Spacer } from '../components/Spacer'
|
||||
import { Link } from 'gatsby'
|
||||
import { Button } from 'antd'
|
||||
import './styles/yc-onboarding.scss'
|
||||
|
||||
const DemoCallInfo = () => (
|
||||
<>
|
||||
@@ -38,17 +36,12 @@ export const YCOnboarding = () => {
|
||||
const [showInfo, setShowInfo] = useState(false)
|
||||
return (
|
||||
<Layout>
|
||||
<div className="get-in-touch-wrapper">
|
||||
<div className="flex flex-col items-stretch w-full max-w-4xl mx-auto">
|
||||
<Spacer />
|
||||
<h1 className="centered">PostHog YC Onboarding</h1>
|
||||
<Button
|
||||
className="centered"
|
||||
style={{ margin: 'auto' }}
|
||||
type="primary"
|
||||
onClick={() => setShowInfo(!showInfo)}
|
||||
>
|
||||
<button onClick={() => setShowInfo(!showInfo)} className="text-orange font-semibold w-32 mx-auto">
|
||||
{showInfo ? 'Hide Info' : 'Show Info'}
|
||||
</Button>
|
||||
</button>
|
||||
<Spacer height={25} />
|
||||
{showInfo ? <DemoCallInfo /> : null}
|
||||
<DemoScheduler iframeSrc="https://calendly.com/d/dsb-3y3-9v9" />
|
||||
@@ -1,4 +1,3 @@
|
||||
import { Button, Select } from 'antd'
|
||||
import { Link } from 'react-scroll'
|
||||
import Scrollspy from 'react-scrollspy'
|
||||
import '@fontsource/source-code-pro'
|
||||
@@ -6,7 +5,6 @@ import CodeBlock from 'components/Home/CodeBlock'
|
||||
import Layout from 'components/Layout'
|
||||
import { SEO } from 'components/seo'
|
||||
import 'core-js/features/array/at'
|
||||
import 'core-js/features/string/replace-all'
|
||||
import { graphql } from 'gatsby'
|
||||
import { getCookie, setCookie } from 'lib/utils'
|
||||
import * as OpenAPISampler from 'openapi-sampler'
|
||||
@@ -15,6 +13,8 @@ import React, { useEffect, useRef, useState } from 'react'
|
||||
import { push as Menu } from 'react-burger-menu'
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
import '../styles/api-docs.scss'
|
||||
import { Listbox } from '@headlessui/react'
|
||||
import { SelectorIcon } from '@heroicons/react/outline'
|
||||
import MainSidebar from 'components/Docs/MainSidebar'
|
||||
import Navigation from 'components/Docs/Navigation'
|
||||
import SectionLinks from 'components/SectionLinks'
|
||||
@@ -285,9 +285,9 @@ function ResponseBody({ item, objects }) {
|
||||
<>
|
||||
<h4>Response</h4>
|
||||
<div className="response-wrapper">
|
||||
<Button type="link" style={{ padding: 0 }} onClick={() => setShowResponse(!showResponse)}>
|
||||
<button className="mt-2 text-sm" onClick={() => setShowResponse(!showResponse)}>
|
||||
{showResponse ? 'Hide' : 'Show'} response
|
||||
</Button>
|
||||
</button>
|
||||
<br />
|
||||
{showResponse && (
|
||||
<Params
|
||||
@@ -338,24 +338,36 @@ function RequestExample({ item, objects, exampleLanguage, setExampleLanguage })
|
||||
let queryParams = item.parameters?.filter((param) => param.in === 'query')
|
||||
return (
|
||||
<>
|
||||
<div className="code-example justify-between flex">
|
||||
<div className="code-example justify-between flex my-1.5">
|
||||
<div className="text-gray">
|
||||
<code className={`text-${mapVerbsColor[item.httpVerb]}`}>{item.httpVerb.toUpperCase()} </code>
|
||||
<code>{path}</code>
|
||||
</div>
|
||||
<Select
|
||||
value={exampleLanguage}
|
||||
onChange={(key) => setExampleLanguage(key)}
|
||||
bordered={false}
|
||||
style={{ border: 0, background: 'transparent', width: 90 }}
|
||||
>
|
||||
<Select.Option key="curl" value="curl">
|
||||
curl
|
||||
</Select.Option>
|
||||
<Select.Option key="python" value="python">
|
||||
python
|
||||
</Select.Option>
|
||||
</Select>
|
||||
|
||||
<Listbox as="div" className="relative" value={exampleLanguage} onChange={setExampleLanguage}>
|
||||
<Listbox.Button className="bg-white pl-2 pr-10 py-1 rounded-sm text-xs flex items-center ">
|
||||
<span className="text-gray-accent-dark font-normal">{exampleLanguage}</span>
|
||||
<SelectorIcon className="w-3 h-3 text-gray absolute right-1.5" />
|
||||
</Listbox.Button>
|
||||
<Listbox.Options
|
||||
as="ul"
|
||||
className="absolute right-0 top-full mt-1 bg-white list-none px-0 py-1 rounded-sm shadow focus:outline-none z-50"
|
||||
>
|
||||
{['curl', 'python'].map((option) => (
|
||||
<Listbox.Option
|
||||
key={option}
|
||||
value={option}
|
||||
className={({ active, selected }) =>
|
||||
`${selected ? 'font-semibold' : ''} ${
|
||||
active ? 'bg-orange text-white' : 'text-gray-accent-dark'
|
||||
} w-full pl-3 pr-6 cursor-pointer`
|
||||
}
|
||||
>
|
||||
<span className="text-xs">{option}</span>
|
||||
</Listbox.Option>
|
||||
))}
|
||||
</Listbox.Options>
|
||||
</Listbox>
|
||||
</div>
|
||||
|
||||
{exampleLanguage === 'curl' && (
|
||||
|
||||
Reference in New Issue
Block a user