Merge branch 'main' into issue_1981
4
.babelrc
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"presets": ["next/babel"],
|
||||
"plugins": [["styled-components", { "ssr": true }]]
|
||||
}
|
||||
33
.github/ISSUE_COMMENT_TEMPLATE/batch-status.yml
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
---
|
||||
name: Batch Status Update
|
||||
description: Batch Status Update
|
||||
body:
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: Status
|
||||
options:
|
||||
- label: "GREEN \U0001F7E2 (All good, smooth sailing)"
|
||||
value: 'Status: GREEN'
|
||||
- label:
|
||||
"YELLOW \U0001F7E1 (We've identified areas of concern, but have them under
|
||||
control at this time)"
|
||||
value: 'Status: YELLOW'
|
||||
- label: "RED \U0001F534 (We need help, there are blockers beyond our control)"
|
||||
value: 'Status: RED'
|
||||
- label: GREY ⚪️ (Not started, paused, not currently being worked on)
|
||||
value: 'Status: GREY'
|
||||
- label: "BLACK ⚫️ (We shipped it \U0001F389)"
|
||||
value: 'Status: BLACK'
|
||||
- type: input
|
||||
attributes:
|
||||
label: Target date
|
||||
format: date
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Update
|
||||
placeholder: A few words on how it's going
|
||||
- type: input
|
||||
attributes:
|
||||
label: 'Attribution'
|
||||
value: '_created with :heart: by typing_ `/status`'
|
||||
format: text
|
||||
29
.github/ISSUE_COMMENT_TEMPLATE/quick-status.yml
vendored
@@ -1,29 +0,0 @@
|
||||
---
|
||||
name: A brief status update
|
||||
description: A brief status update.
|
||||
body:
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: Status
|
||||
options:
|
||||
- label: 'GREY ⚪️ (Not started, paused, not currently being worked on)'
|
||||
value: 'Status: GREY'
|
||||
- label: 'GREEN 🟢 (All good, smooth sailing)'
|
||||
value: 'Status: GREEN'
|
||||
- label: "YELLOW \U0001F7E1 (On track, with hurdles to work through)"
|
||||
value: 'Status: YELLOW'
|
||||
- label: "RED \U0001F534 (BLOCKED)"
|
||||
value: 'Status: RED'
|
||||
- label: "BLACK ⚫️ (We shipped it \U0001F389)"
|
||||
value: 'Status: BLACK'
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Update Summary
|
||||
placeholder:
|
||||
Brief summary of the status and next steps. Any blockers should be
|
||||
called out specifically.
|
||||
- type: input
|
||||
attributes:
|
||||
label: 'Attribution'
|
||||
value: '_created with :heart: by typing_ `/status`'
|
||||
format: text
|
||||
9
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -12,19 +12,20 @@ Thanks again!
|
||||
|
||||
### Why:
|
||||
|
||||
Closes [issue link]
|
||||
|
||||
<!--
|
||||
- If there's an existing issue for your change, please link to it.
|
||||
- If there's _not_ an existing issue, please open one first to make it more likely that this update will be accepted: https://github.com/github/docs/issues/new/choose. -->
|
||||
**Closes [issue link]**
|
||||
|
||||
### What's being changed:
|
||||
|
||||
<!-- Share artifacts of the changes, be they code snippets, GIFs or screenshots; whatever shares the most context. -->
|
||||
|
||||
### Check off the following:
|
||||
- [ ] I have reviewed my changes in staging. (look for the **deploy-to-heroku** link in your pull request, then click **View deployment**)
|
||||
- [ ] For content changes, I have reviewed the [localization checklist](https://github.com/github/docs/blob/main/contributing/localization-checklist.md)
|
||||
- [ ] For content changes, I have reviewed the [Content style guide for GitHub Docs](https://github.com/github/docs/blob/main/contributing/content-style-guide.md).
|
||||
|
||||
- [ ] I have reviewed my changes in staging (look for the **deploy-to-heroku** link in your pull request, then click **View deployment**).
|
||||
- [ ] For content changes, I have completed the [self-review checklist](https://github.com/github/docs/blob/main/CONTRIBUTING.md#self-review).
|
||||
|
||||
### Writer impact (This section is for GitHub staff members only):
|
||||
|
||||
|
||||
232
.github/actions-scripts/fr-add-docs-reviewers-requests.py
vendored
Normal file
@@ -0,0 +1,232 @@
|
||||
# TODO: Convert to JavaScript for language consistency
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import requests
|
||||
|
||||
# Constants
|
||||
endpoint = 'https://api.github.com/graphql'
|
||||
|
||||
# ID of the github/github repo
|
||||
github_repo_id = "MDEwOlJlcG9zaXRvcnkz"
|
||||
|
||||
# ID of the docs-reviewers team
|
||||
docs_reviewers_id = "MDQ6VGVhbTQzMDMxMzk="
|
||||
|
||||
# ID of the "Docs content first responder" board
|
||||
docs_project_id = "MDc6UHJvamVjdDQ1NzI0ODI="
|
||||
|
||||
# ID of the "OpenAPI review requests" column on the "Docs content first responder" board
|
||||
docs_column_id = "PC_lAPNJr_OAEXFQs4A2OFq"
|
||||
|
||||
# 100 is an educated guess of how many PRs are opened in a day on the github/github repo
|
||||
# If we are missing PRs, either increase this number or increase the frequency at which this script is run
|
||||
num_prs_to_search = 100
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def find_open_prs_for_repo(repo_id: str, num_prs: int):
|
||||
"""Return data about a specified number of open PRs for a specified repo
|
||||
|
||||
Arguments:
|
||||
repo_id: The node ID of the repo to search
|
||||
num_prs: The max number of PRs to return
|
||||
|
||||
Returns:
|
||||
Returns a JSON object of this structure:
|
||||
{
|
||||
"data": {
|
||||
"node": {
|
||||
"pullRequests": {
|
||||
"nodes": [
|
||||
{
|
||||
"id": str,
|
||||
"isDraft": bool,
|
||||
"reviewRequests": {
|
||||
"nodes": [
|
||||
{
|
||||
"requestedReviewer": {
|
||||
"id": str
|
||||
}
|
||||
}...
|
||||
]
|
||||
},
|
||||
"projectCards": {
|
||||
"nodes": [
|
||||
{
|
||||
"project": {
|
||||
"id": str
|
||||
}
|
||||
}...
|
||||
]
|
||||
}
|
||||
}...
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
query = """query ($repo_id: ID!, $num_prs: Int!) {
|
||||
node(id: $repo_id) {
|
||||
... on Repository {
|
||||
pullRequests(last: $num_prs, states: OPEN) {
|
||||
nodes {
|
||||
id
|
||||
isDraft
|
||||
reviewRequests(first: 10) {
|
||||
nodes {
|
||||
requestedReviewer {
|
||||
... on Team {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
projectCards(first: 10) {
|
||||
nodes {
|
||||
project {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
variables = {
|
||||
"repo_id": github_repo_id,
|
||||
"num_prs": num_prs
|
||||
}
|
||||
|
||||
response = requests.post(
|
||||
endpoint,
|
||||
json={'query': query, 'variables': variables},
|
||||
headers = {'Authorization': f"bearer {os.environ['TOKEN']}"}
|
||||
)
|
||||
|
||||
response.raise_for_status()
|
||||
|
||||
json_response = json.loads(response.text)
|
||||
|
||||
if 'errors' in json_response:
|
||||
raise RuntimeError(f'Error in GraphQL response: {json_response}')
|
||||
|
||||
return json_response
|
||||
|
||||
def add_prs_to_board(prs_to_add: list, column_id: str):
|
||||
"""Adds PRs to a column of a project board
|
||||
|
||||
Arguments:
|
||||
prs_to_add: A list of PR node IDs
|
||||
column_id: The node ID of the column to add the PRs to
|
||||
|
||||
Returns:
|
||||
Nothing
|
||||
"""
|
||||
|
||||
logger.info(f"adding: {prs_to_add}")
|
||||
|
||||
mutation = """mutation($pr_id: ID!, $column_id: ID!) {
|
||||
addProjectCard(input:{contentId: $pr_id, projectColumnId: $column_id}) {
|
||||
projectColumn {
|
||||
name
|
||||
}
|
||||
}
|
||||
}"""
|
||||
|
||||
for pr_id in prs_to_add:
|
||||
logger.info(f"Attempting to add {pr_id} to board")
|
||||
|
||||
variables = {
|
||||
"pr_id": pr_id,
|
||||
"column_id": column_id
|
||||
}
|
||||
|
||||
response = requests.post(
|
||||
endpoint,
|
||||
json={'query': mutation, 'variables': variables},
|
||||
headers = {'Authorization': f"bearer {os.environ['TOKEN']}"}
|
||||
)
|
||||
|
||||
json_response = json.loads(response.text)
|
||||
|
||||
if 'errors' in json_response:
|
||||
logger.info(f"GraphQL error when adding {pr_id}: {json_response}")
|
||||
|
||||
def filter_prs(data, reviewer_id: str, project_id):
|
||||
"""Given data about the draft state, reviewers, and project boards for PRs,
|
||||
return just the PRs that are:
|
||||
- not draft
|
||||
- are requesting a review for the specified team
|
||||
- are not already on the specified project board
|
||||
|
||||
Arguments:
|
||||
data: A JSON object of this structure:
|
||||
{
|
||||
"data": {
|
||||
"node": {
|
||||
"pullRequests": {
|
||||
"nodes": [
|
||||
{
|
||||
"id": str,
|
||||
"isDraft": bool,
|
||||
"reviewRequests": {
|
||||
"nodes": [
|
||||
{
|
||||
"requestedReviewer": {
|
||||
"id": str
|
||||
}
|
||||
}...
|
||||
]
|
||||
},
|
||||
"projectCards": {
|
||||
"nodes": [
|
||||
{
|
||||
"project": {
|
||||
"id": str
|
||||
}
|
||||
}...
|
||||
]
|
||||
}
|
||||
}...
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
reviewer_id: The node ID of the reviewer to filter for
|
||||
project_id: The project ID of the project to filter against
|
||||
|
||||
Returns:
|
||||
A list of node IDs of the PRs that met the requirements
|
||||
"""
|
||||
|
||||
pr_data = data['data']['node']['pullRequests']['nodes']
|
||||
|
||||
prs_to_add = []
|
||||
|
||||
for pr in pr_data:
|
||||
if (
|
||||
not pr['isDraft'] and
|
||||
reviewer_id in [req_rev['requestedReviewer']['id'] for req_rev in pr['reviewRequests']['nodes'] if req_rev['requestedReviewer']] and
|
||||
project_id not in [proj_card['project']['id'] for proj_card in pr['projectCards']['nodes']]
|
||||
):
|
||||
prs_to_add.append(pr['id'])
|
||||
|
||||
return prs_to_add
|
||||
|
||||
def main():
|
||||
query_data = find_open_prs_for_repo(github_repo_id, num_prs_to_search)
|
||||
prs_to_add = filter_prs(query_data, docs_reviewers_id, docs_project_id)
|
||||
add_prs_to_board(prs_to_add, docs_column_id)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
1
.github/allowed-actions.js
vendored
@@ -9,6 +9,7 @@ module.exports = [
|
||||
"actions/github-script@626af12fe9a53dc2972b48385e7fe7dec79145c9", // v3.0.0
|
||||
"actions/labeler@5f867a63be70efff62b767459b009290364495eb", // v2.2.0
|
||||
"actions/setup-node@c46424eee26de4078d34105d3de3cc4992202b1e", // v2.1.4
|
||||
"actions/setup-python@dc73133d4da04e56a135ae2246682783cc7c7cb6", // v2.2.2
|
||||
"ruby/setup-ruby@fdcfbcf14ec9672f6f615cb9589a1bc5dd69d262", // v1.64.1
|
||||
"actions/stale@9d6f46564a515a9ea11e7762ab3957ee58ca50da", // v3.0.16
|
||||
"alex-page/github-project-automation-plus@fdb7991b72040d611e1123d2b75ff10eda9372c9",
|
||||
|
||||
34
.github/review-template.md
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
## Author self-review
|
||||
|
||||
- [ ] The changes in this PR meet the user experience and goals outlined in the content design plan.
|
||||
- [ ] I've compared my PR's source changes to staging and reviewed for versioning issues, redirects, the [style guide](https://github.com/github/docs/blob/main/contributing/content-style-guide.md), [content model](https://github.com/github/docs-content-strategy/blob/main/content-design/models.md), or [localization checklist](https://github.com/github/docs/blob/main/contributing/localization-checklist.md) rendering problems, typos, and wonky screenshots.
|
||||
- [ ] I've worked through build failures and tests are passing.
|
||||
- [ ] For REST API content, I've verified that endpoints, parameters, and responses are correct and work as expected and provided curl samples below.
|
||||
|
||||
For more information, check out our [full review guidelines and checklist](https://github.com/github/docs-content/blob/main/docs-content-docs/docs-content-workflows/reviews-and-feedback/review-process.md).
|
||||
|
||||
## Review request
|
||||
|
||||
### Summary
|
||||
|
||||
_Help reviewers understand this project and its context by writing a paragraph summarizing its goals and intended user experience and explaining how the PR meets those goals._
|
||||
[Content design plan](LINK HERE)
|
||||
|
||||
### Docs Content review
|
||||
|
||||
_Give Docs Content any extra context, highlight areas for them to consider in their review, and ask them questions you need answered to ship the PR._
|
||||
|
||||
### Technical review
|
||||
|
||||
_Ping in technical reviewers, asking them to review whether content is technically accurate and right for the audience._
|
||||
_Highlight areas for them to consider in their review and ask them questions you need answered to ship the PR._
|
||||
|
||||
### Content changes
|
||||
|
||||
[PR on staging](LINK HERE)
|
||||
|
||||
_Give a high-level overview of the changes in your PR and how they support the overall goals of the PR. Share links to important articles or changes in source and on staging. If your PR is large or complex, use a table to highlight changes with high user impact._
|
||||
|
||||
### Notes
|
||||
|
||||
_Discuss test failures, versioning issues, or anything else reviewers should know to consider the overall user experience of the PR._
|
||||
36
.github/workflows/add-review-template.yml
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
name: Add review template
|
||||
|
||||
# **What it does**: When a specific label is added to a PR, adds the contents of .github/review-template.md as a comment in the PR
|
||||
# **Why we have it**: To help Docs Content team members ensure that their PR is ready for review
|
||||
# **Who does it impact**: docs-internal maintainers and contributors
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types:
|
||||
- labeled
|
||||
|
||||
jobs:
|
||||
comment-that-approved:
|
||||
name: Add review template
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.label.name == 'docs-content-ready-for-review' && github.repository == 'github/docs-internal'
|
||||
|
||||
steps:
|
||||
- name: check out repo content
|
||||
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f
|
||||
|
||||
# Jump through some hoops to work with a multi-line file
|
||||
- name: Store review template in variable
|
||||
run: |
|
||||
TEMPLATE=$(cat .github/review-template.md)
|
||||
echo "TEMPLATE<<EOF" >> $GITHUB_ENV
|
||||
echo "$TEMPLATE" >> $GITHUB_ENV
|
||||
echo "EOF" >> $GITHUB_ENV
|
||||
|
||||
- name: Comment on the PR
|
||||
run: |
|
||||
gh pr comment $PR --body "$TEMPLATE"
|
||||
env:
|
||||
GITHUB_TOKEN: ${{secrets.DOCUBOT_FR_PROJECT_BOARD_WORKFLOWS_REPO_ORG_READ_SCOPES}}
|
||||
PR: ${{ github.event.pull_request.html_url }}
|
||||
TEMPLATE: ${{ env.TEMPLATE }}
|
||||
35
.github/workflows/docs-review-collect.yml
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
name: Add docs-reviewers request to FR board
|
||||
|
||||
# **What it does**: Adds PRs in github/github that requested a review from docs-reviewers to the FR board
|
||||
# **Why we have it**: To catch docs-reviewers requests in github/github
|
||||
# **Who does it impact**: docs-content maintainers
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: '25 4 * * *'
|
||||
|
||||
jobs:
|
||||
add-requests-to-board:
|
||||
name: Add requests to board
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Check out repo content
|
||||
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f
|
||||
|
||||
- name: Set up Python 3.9
|
||||
uses: actions/setup-python@dc73133d4da04e56a135ae2246682783cc7c7cb6
|
||||
with:
|
||||
python-version: '3.9'
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install requests
|
||||
|
||||
- name: Run script
|
||||
run: |
|
||||
python .github/actions-scripts/fr-add-docs-reviewers-requests.py
|
||||
env:
|
||||
TOKEN: ${{ secrets.DOCS_BOT }}
|
||||
@@ -51,7 +51,7 @@ jobs:
|
||||
with:
|
||||
github-token: ${{ secrets.DOCUBOT_READORG_REPO_WORKFLOW_SCOPES }}
|
||||
script: |
|
||||
var column_id = 13445932;
|
||||
var column_id = 12860544;
|
||||
try {
|
||||
github.projects.createCard({
|
||||
column_id: column_id,
|
||||
|
||||
@@ -158,12 +158,12 @@ We (usually the docs team, but sometimes GitHub product managers, engineers, or
|
||||
You should always review your own PR first.
|
||||
|
||||
For content changes, make sure that you:
|
||||
- [ ] Confirm that the changes address every part of the content design plan from your issue (if there are differences, explain them).
|
||||
- [ ] Confirm that the changes meet the user experience and goals outlined in the content design plan (if there is one).
|
||||
- [ ] Compare your pull request's source changes to staging to confirm that the output matches the source and that everything is rendering as expected. This helps spot issues like typos, content that doesn't follow the style guide, or content that isn't rendering due to versioning problems. Remember that lists and tables can be tricky.
|
||||
- [ ] Review the content for technical accuracy.
|
||||
- [ ] Review the entire pull request using the [localization checklist](contributing/localization-checklist.md).
|
||||
- [ ] Copy-edit the changes for grammar, spelling, and adherence to the style guide.
|
||||
- [ ] Copy-edit the changes for grammar, spelling, and adherence to the [style guide](https://github.com/github/docs/blob/main/contributing/content-style-guide.md).
|
||||
- [ ] Check new or updated Liquid statements to confirm that versioning is correct.
|
||||
- [ ] Check that all of your changes render correctly in staging. Remember, that lists and tables can be tricky.
|
||||
- [ ] If there are any failing checks in your PR, troubleshoot them until they're all passing.
|
||||
|
||||
### Pull request template
|
||||
|
||||
BIN
assets/images/help/codespaces/add-dotnet-prebuilt-container.png
Normal file
|
After Width: | Height: | Size: 40 KiB |
BIN
assets/images/help/codespaces/add-dotnet-version.png
Normal file
|
After Width: | Height: | Size: 5.8 KiB |
BIN
assets/images/help/codespaces/add-extension.png
Normal file
|
After Width: | Height: | Size: 8.9 KiB |
BIN
assets/images/help/codespaces/add-java-prebuilt-container.png
Normal file
|
After Width: | Height: | Size: 67 KiB |
BIN
assets/images/help/codespaces/add-java-version.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
assets/images/help/codespaces/autofetch-all.png
Normal file
|
After Width: | Height: | Size: 102 KiB |
BIN
assets/images/help/codespaces/autofetch-search.png
Normal file
|
After Width: | Height: | Size: 316 KiB |
BIN
assets/images/help/codespaces/branch-in-status-bar.png
Normal file
|
After Width: | Height: | Size: 119 KiB |
|
Before Width: | Height: | Size: 196 KiB After Width: | Height: | Size: 140 KiB |
|
Before Width: | Height: | Size: 9.7 KiB After Width: | Height: | Size: 190 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
BIN
assets/images/help/codespaces/codespace-overview-annotated.png
Normal file
|
After Width: | Height: | Size: 338 KiB |
|
Before Width: | Height: | Size: 95 KiB After Width: | Height: | Size: 316 KiB |
|
After Width: | Height: | Size: 96 KiB |
|
Before Width: | Height: | Size: 8.0 KiB After Width: | Height: | Size: 156 KiB |
|
Before Width: | Height: | Size: 6.4 KiB After Width: | Height: | Size: 75 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 92 KiB |
|
Before Width: | Height: | Size: 8.4 KiB After Width: | Height: | Size: 126 KiB |
|
After Width: | Height: | Size: 22 KiB |
BIN
assets/images/help/codespaces/codespaces-manage.png
Normal file
|
After Width: | Height: | Size: 103 KiB |
BIN
assets/images/help/codespaces/codespaces-npm-run-dev.png
Normal file
|
After Width: | Height: | Size: 36 KiB |
BIN
assets/images/help/codespaces/codespaces-option-secrets-org.png
Normal file
|
After Width: | Height: | Size: 48 KiB |
BIN
assets/images/help/codespaces/codespaces-option-secrets.png
Normal file
|
After Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 117 KiB After Width: | Height: | Size: 73 KiB |
BIN
assets/images/help/codespaces/create-new-branch.png
Normal file
|
After Width: | Height: | Size: 46 KiB |
BIN
assets/images/help/codespaces/custom-prompt.png
Normal file
|
After Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 354 KiB After Width: | Height: | Size: 407 KiB |
|
Before Width: | Height: | Size: 8.9 KiB After Width: | Height: | Size: 46 KiB |
BIN
assets/images/help/codespaces/dotnet-extensions.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
assets/images/help/codespaces/dotnet-options.png
Normal file
|
After Width: | Height: | Size: 8.5 KiB |
BIN
assets/images/help/codespaces/fairyfloss.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 6.6 KiB |
BIN
assets/images/help/codespaces/manage-button.png
Normal file
|
After Width: | Height: | Size: 85 KiB |
|
After Width: | Height: | Size: 109 KiB |
BIN
assets/images/help/codespaces/quickstart-port-toast.png
Normal file
|
After Width: | Height: | Size: 7.5 KiB |
BIN
assets/images/help/codespaces/secret-repository-access.png
Normal file
|
After Width: | Height: | Size: 94 KiB |
|
After Width: | Height: | Size: 62 KiB |
|
After Width: | Height: | Size: 87 KiB |
|
After Width: | Height: | Size: 68 KiB |
BIN
assets/images/help/codespaces/source-control-ellipsis-button.png
Normal file
|
After Width: | Height: | Size: 114 KiB |
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 88 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 87 KiB |
|
Before Width: | Height: | Size: 8.8 KiB After Width: | Height: | Size: 45 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 202 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 132 KiB |
BIN
assets/images/help/organizations/domains-approve-domain.png
Normal file
|
After Width: | Height: | Size: 88 KiB |
BIN
assets/images/help/organizations/domains-approve-it-instead.png
Normal file
|
After Width: | Height: | Size: 57 KiB |
BIN
assets/images/help/organizations/domains-delete.png
Normal file
|
After Width: | Height: | Size: 93 KiB |
|
Before Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 67 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 92 KiB |
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 53 KiB |
|
Before Width: | Height: | Size: 119 KiB After Width: | Height: | Size: 786 KiB |
BIN
assets/images/help/pull_requests/pull-request-body.png
Normal file
|
After Width: | Height: | Size: 279 KiB |
BIN
assets/images/help/pull_requests/pull-request-comment.png
Normal file
|
After Width: | Height: | Size: 79 KiB |
BIN
assets/images/help/repository/environments-top.png
Normal file
|
After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 32 KiB |
BIN
assets/images/help/settings/codespaces-audit-log-org.png
Normal file
|
After Width: | Height: | Size: 337 KiB |
BIN
assets/images/help/settings/codespaces-audit-log.png
Normal file
|
After Width: | Height: | Size: 244 KiB |
|
After Width: | Height: | Size: 129 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 55 KiB |
|
After Width: | Height: | Size: 67 KiB |
BIN
assets/images/help/sponsors/disable-your-account-button.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
assets/images/help/sponsors/unpublish-profile-button.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
assets/images/help/sponsors/unpublish-profile-dialog.png
Normal file
|
After Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 39 KiB |
|
Before Width: | Height: | Size: 36 KiB |
42
components/Breadcrumbs.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import cx from 'classnames'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useMainContext } from './context/MainContext'
|
||||
|
||||
export type BreadcrumbT = {
|
||||
title: string
|
||||
documentType?: string
|
||||
href?: string
|
||||
}
|
||||
|
||||
type Props = {}
|
||||
export const Breadcrumbs = (props: Props) => {
|
||||
const router = useRouter()
|
||||
const pathWithLocale = `/${router.locale}${router.asPath}`
|
||||
const { breadcrumbs } = useMainContext()
|
||||
|
||||
return (
|
||||
<nav className="breadcrumbs f5" aria-label="Breadcrumb">
|
||||
{Object.values(breadcrumbs).map((breadcrumb) => {
|
||||
const title = `${breadcrumb.documentType}: ${breadcrumb.title}`
|
||||
return !breadcrumb.href ? (
|
||||
<span key={title} title={title}>
|
||||
{breadcrumb.title}
|
||||
</span>
|
||||
) : (
|
||||
<Link key={title} href={breadcrumb.href}>
|
||||
<a
|
||||
title={title}
|
||||
className={cx(
|
||||
'd-inline-block',
|
||||
pathWithLocale === breadcrumb.href && 'color-text-tertiary'
|
||||
)}
|
||||
>
|
||||
{breadcrumb.title}
|
||||
</a>
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
29
components/Contribution.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import { GitPullRequestIcon } from '@primer/octicons-react'
|
||||
import { useMainContext } from 'components/context/MainContext'
|
||||
import { useTranslation } from 'components/hooks/useTranslation'
|
||||
|
||||
export const Contribution = () => {
|
||||
const { relativePath } = useMainContext()
|
||||
const { t } = useTranslation('contribution_cta')
|
||||
|
||||
const contribution_href = relativePath
|
||||
? `https://github.com/github/docs/edit/main/content/${relativePath}`
|
||||
: 'https://github.com/github/docs'
|
||||
|
||||
return (
|
||||
<div className="f5 contribution">
|
||||
<h2 className="f4">{t`title`}</h2>
|
||||
<p className="color-text-secondary f6">{t`body`}</p>
|
||||
<a className="btn btn-outline" href={contribution_href}>
|
||||
<GitPullRequestIcon size="small" className="octicon mr-1" />
|
||||
{t`button`}
|
||||
</a>
|
||||
<p className="color-text-secondary f6 mt-2">
|
||||
{t`or`}{' '}
|
||||
<a href="https://github.com/github/docs/blob/main/CONTRIBUTING.md" target="_blank">
|
||||
{t`to_guidelines`}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
35
components/DefaultLayout.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import Head from 'next/head'
|
||||
|
||||
// import { Sidebar } from 'components/Sidebar'
|
||||
import { Header } from 'components/Header'
|
||||
import { SmallFooter } from 'components/SmallFooter'
|
||||
import { ScrollButton } from 'components/ScrollButton'
|
||||
import { SupportSection } from 'components/SupportSection'
|
||||
import { DeprecationBanner } from 'components/DeprecationBanner'
|
||||
import { useMainContext } from 'components/context/MainContext'
|
||||
|
||||
type Props = { children?: React.ReactNode }
|
||||
export const DefaultLayout = (props: Props) => {
|
||||
const { builtAssets, expose } = useMainContext()
|
||||
return (
|
||||
<div className="d-lg-flex">
|
||||
<Head>
|
||||
<link rel="stylesheet" href={builtAssets.main.css} />
|
||||
<script id="expose" type="application/json" dangerouslySetInnerHTML={{ __html: expose }} />
|
||||
<script src={builtAssets.main.js} />
|
||||
</Head>
|
||||
{/* <Sidebar /> */}
|
||||
|
||||
<main className="width-full">
|
||||
<Header />
|
||||
<DeprecationBanner />
|
||||
|
||||
{props.children}
|
||||
|
||||
<SupportSection />
|
||||
<SmallFooter />
|
||||
<ScrollButton />
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
38
components/DeprecationBanner.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import { useMainContext } from 'components/context/MainContext'
|
||||
import { useVersion } from 'components/hooks/useVersion'
|
||||
|
||||
export const DeprecationBanner = () => {
|
||||
const { data, enterpriseServerReleases } = useMainContext()
|
||||
const { currentVersion } = useVersion()
|
||||
|
||||
if (!currentVersion.includes(enterpriseServerReleases.oldestSupported)) {
|
||||
return null
|
||||
}
|
||||
|
||||
const message = enterpriseServerReleases.isOldestReleaseDeprecated
|
||||
? data.reusables.enterprise_deprecation.version_was_deprecated
|
||||
: data.reusables.enterprise_deprecation.version_will_be_deprecated
|
||||
|
||||
return (
|
||||
<div className="deprecation-banner border rounded-1 mb-2 color-bg-warning p-3 color-border-warning f5">
|
||||
<p>
|
||||
<b>
|
||||
<span dangerouslySetInnerHTML={{ __html: message }} />{' '}
|
||||
<span
|
||||
data-date={enterpriseServerReleases.nextDeprecationDate}
|
||||
data-format="%B %d, %Y"
|
||||
title={enterpriseServerReleases.nextDeprecationDate}
|
||||
>
|
||||
{enterpriseServerReleases.nextDeprecationDate}
|
||||
</span>
|
||||
.
|
||||
</b>{' '}
|
||||
<span
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: data.reusables.enterprise_deprecation.deprecation_details,
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
113
components/Header.tsx
Normal file
@@ -0,0 +1,113 @@
|
||||
import { useState } from 'react'
|
||||
import Link from 'next/link'
|
||||
import cx from 'classnames'
|
||||
import { css } from 'styled-components'
|
||||
import { useRouter } from 'next/router'
|
||||
import { ChevronDownIcon, MarkGithubIcon, ThreeBarsIcon, XIcon } from '@primer/octicons-react'
|
||||
import { ButtonOutline } from '@primer/components'
|
||||
|
||||
import { useMainContext } from './context/MainContext'
|
||||
import { LanguagePicker } from './LanguagePicker'
|
||||
import { HeaderNotifications } from 'components/HeaderNotifications'
|
||||
import { MobileProductDropdown } from 'components/MobileProductDropdown'
|
||||
import { useTranslation } from 'components/hooks/useTranslation'
|
||||
|
||||
export const Header = () => {
|
||||
const router = useRouter()
|
||||
const { currentProduct, relativePath, error } = useMainContext()
|
||||
const { t } = useTranslation(['header', 'homepage'])
|
||||
|
||||
const [isMenuOpen, setIsMenuOpen] = useState(false)
|
||||
|
||||
return (
|
||||
<div className="border-bottom color-border-secondary no-print">
|
||||
{error !== '404' && <HeaderNotifications />}
|
||||
|
||||
<header className="container-xl px-3 px-md-6 pt-3 pb-2 position-relative d-flex flex-justify-between width-full">
|
||||
<div
|
||||
className="d-flex flex-items-center d-lg-none"
|
||||
style={{ zIndex: 3 }}
|
||||
id="github-logo-mobile"
|
||||
role="banner"
|
||||
>
|
||||
<Link href={`/${router.locale}`}>
|
||||
<a aria-hidden="true" tabIndex={-1}>
|
||||
<MarkGithubIcon size={32} className="color-icon-primary" />
|
||||
</a>
|
||||
</Link>
|
||||
|
||||
<Link href={`/${router.locale}`}>
|
||||
<a className="h4-mktg color-text-primary no-underline no-wrap pl-2">
|
||||
{t('github_docs')}
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="width-full">
|
||||
<div className="d-inline-block width-full d-md-flex" style={{ zIndex: 1 }}>
|
||||
<div className="float-right d-md-none position-relative" style={{ zIndex: 3 }}>
|
||||
<ButtonOutline css onClick={() => setIsMenuOpen(!isMenuOpen)}>
|
||||
{isMenuOpen ? <XIcon size="small" /> : <ThreeBarsIcon size="small" />}
|
||||
</ButtonOutline>
|
||||
</div>
|
||||
<div
|
||||
style={{ zIndex: 2 }}
|
||||
className={cx('nav-mobile-dropdown width-full', isMenuOpen && 'js-open')}
|
||||
>
|
||||
<div className="d-md-flex flex-justify-between flex-items-center">
|
||||
<div className="py-2 py-md-0 d-md-inline-block">
|
||||
<h4 className="text-mono f5 text-normal color-text-secondary d-md-none">
|
||||
{t('explore_by_product')}
|
||||
</h4>
|
||||
<details className="dropdown-withArrow position-relative details details-reset d-md-none close-when-clicked-outside">
|
||||
<summary
|
||||
className="nav-desktop-productDropdownButton color-text-link py-2"
|
||||
role="button"
|
||||
aria-label="Toggle products list"
|
||||
>
|
||||
<div
|
||||
id="current-product"
|
||||
className="d-flex flex-items-center flex-justify-between"
|
||||
style={{ paddingTop: 2 }}
|
||||
>
|
||||
{/* <!-- Product switcher - GitHub.com, Enterprise Server, etc -->
|
||||
<!-- 404 and 500 error layouts are not real pages so we need to hardcode the name for those --> */}
|
||||
{currentProduct.name}
|
||||
<ChevronDownIcon size={24} className="arrow ml-md-1" />
|
||||
</div>
|
||||
</summary>
|
||||
|
||||
<MobileProductDropdown />
|
||||
</details>
|
||||
</div>
|
||||
|
||||
{/* <!-- Versions picker that only appears in the header on landing pages --> */}
|
||||
{/* {% include header-version-switcher %} */}
|
||||
|
||||
<div className="d-md-inline-block">
|
||||
{/* <!-- Language picker - 'English', 'Japanese', etc --> */}
|
||||
<div className="border-top border-md-top-0 py-2 py-md-0 d-md-inline-block">
|
||||
<LanguagePicker />
|
||||
</div>
|
||||
|
||||
{/* <!-- GitHub.com homepage and 404 page has a stylized search; Enterprise homepages do not --> */}
|
||||
{relativePath !== 'index.md' && error !== '404'}
|
||||
<div
|
||||
className="pt-3 pt-md-0 d-md-inline-block ml-md-3 border-top border-md-top-0"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
<div id="search-input-container" aria-hidden="true"></div>
|
||||
<div id="search-results-container"></div>
|
||||
<div class="search-overlay-desktop"></div>
|
||||
`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
89
components/HeaderNotifications.tsx
Normal file
@@ -0,0 +1,89 @@
|
||||
import { useRouter } from 'next/router'
|
||||
import cx from 'classnames'
|
||||
|
||||
import { useMainContext } from 'components/context/MainContext'
|
||||
import { useTranslation } from 'components/hooks/useTranslation'
|
||||
import { ExcludesNull } from 'components/lib/ExcludesNull'
|
||||
import { useVersion } from './hooks/useVersion'
|
||||
|
||||
enum NotificationType {
|
||||
RELEASE = 'RELEASE',
|
||||
TRANSLATION = 'TRANSLATION',
|
||||
EARLY_ACCESS = 'EARLY_ACCESS',
|
||||
}
|
||||
|
||||
type Notif = {
|
||||
content: string
|
||||
type: NotificationType
|
||||
}
|
||||
export const HeaderNotifications = () => {
|
||||
const router = useRouter()
|
||||
const { currentVersion } = useVersion()
|
||||
const { relativePath, allVersions, data, languages, currentLanguage } = useMainContext()
|
||||
const { t } = useTranslation('header')
|
||||
|
||||
const translationNotices: Array<Notif> = []
|
||||
if (router.locale !== 'en') {
|
||||
if (relativePath?.includes('/site-policy')) {
|
||||
translationNotices.push({
|
||||
type: NotificationType.TRANSLATION,
|
||||
content: data.reusables.policies.translation,
|
||||
})
|
||||
} else if (languages[currentLanguage].wip !== true) {
|
||||
translationNotices.push({
|
||||
type: NotificationType.TRANSLATION,
|
||||
content: t('notices.localization_complete'),
|
||||
})
|
||||
} else if (languages[currentLanguage].wip) {
|
||||
translationNotices.push({
|
||||
type: NotificationType.TRANSLATION,
|
||||
content: t('notices.localization_in_progress'),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const releaseNotices: Array<Notif> = []
|
||||
if (currentVersion === 'github-ae@latest') {
|
||||
releaseNotices.push({
|
||||
type: NotificationType.RELEASE,
|
||||
content: t('notices.ghae_silent_launch'),
|
||||
})
|
||||
} else if (currentVersion === data.variables.release_candidate.version) {
|
||||
releaseNotices.push({
|
||||
type: NotificationType.RELEASE,
|
||||
content: `${allVersions[currentVersion].versionTitle}${t('notices.release_candidate')}`,
|
||||
})
|
||||
}
|
||||
|
||||
const allNotifications: Array<Notif> = [
|
||||
...translationNotices,
|
||||
...releaseNotices,
|
||||
// ONEOFF EARLY ACCESS NOTICE
|
||||
(relativePath || '').includes('early-access/')
|
||||
? {
|
||||
type: NotificationType.EARLY_ACCESS,
|
||||
content: t('notices.early_access'),
|
||||
}
|
||||
: null,
|
||||
].filter(ExcludesNull)
|
||||
|
||||
return (
|
||||
<>
|
||||
{allNotifications.map(({ type, content }, i) => {
|
||||
const isLast = i !== allNotifications.length - 1
|
||||
return (
|
||||
<div
|
||||
className={cx(
|
||||
'header-notifications text-center f5 color-text-primary py-4 px-6',
|
||||
type === NotificationType.TRANSLATION && 'translation_notice color-bg-info',
|
||||
type === NotificationType.RELEASE && 'release_notice color-bg-info',
|
||||
type === NotificationType.EARLY_ACCESS && 'early_access color-bg-danger',
|
||||
!isLast && 'border-bottom color-border-tertiary'
|
||||
)}
|
||||
dangerouslySetInnerHTML={{ __html: content }}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</>
|
||||
)
|
||||
}
|
||||
83
components/Helpfulness.tsx
Normal file
@@ -0,0 +1,83 @@
|
||||
import { ThumbsdownIcon, ThumbsupIcon } from '@primer/octicons-react'
|
||||
import { useTranslation } from 'components/hooks/useTranslation'
|
||||
|
||||
export const Helpfulness = () => {
|
||||
const { t } = useTranslation('helpfulness')
|
||||
|
||||
return (
|
||||
<form className="js-helpfulness f5">
|
||||
<h2 data-help-start data-help-yes data-help-no className="mb-1 f4">
|
||||
{t`able_to_find`}
|
||||
</h2>
|
||||
<p className="f6">
|
||||
<a href="/github/site-policy/github-privacy-statement">Privacy policy</a>
|
||||
</p>
|
||||
<p className="radio-group" data-help-start data-help-yes data-help-no>
|
||||
<input
|
||||
hidden
|
||||
id="helpfulness-yes"
|
||||
type="radio"
|
||||
name="helpfulness-vote"
|
||||
value="Yes"
|
||||
aria-label={t('yes')}
|
||||
/>
|
||||
<label className="btn x-radio-label mr-1" htmlFor="helpfulness-yes">
|
||||
<ThumbsupIcon size={24} className="color-text-tertiary" />
|
||||
</label>
|
||||
<input
|
||||
hidden
|
||||
id="helpfulness-no"
|
||||
type="radio"
|
||||
name="helpfulness-vote"
|
||||
value="No"
|
||||
aria-label={t`no`}
|
||||
/>
|
||||
<label className="btn x-radio-label" htmlFor="helpfulness-no">
|
||||
<ThumbsdownIcon size={24} className="color-text-tertiary" />
|
||||
</label>
|
||||
</p>
|
||||
<p className="color-text-secondary f6" hidden data-help-yes>
|
||||
{t('yes_feedback')}
|
||||
</p>
|
||||
<p className="color-text-secondary f6" hidden data-help-no>
|
||||
{t('no_feedback')}
|
||||
</p>
|
||||
<input type="text" className="d-none" name="helpfulness-token" aria-hidden="true" />
|
||||
<p hidden data-help-no>
|
||||
<label className="d-block mb-1 f6" htmlFor="helpfulness-comment">
|
||||
<span>{t('comment_label')}</span>
|
||||
<span className="text-normal color-text-tertiary float-right ml-1">{t('optional')}</span>
|
||||
</label>
|
||||
<textarea
|
||||
className="form-control input-sm width-full"
|
||||
name="helpfulness-comment"
|
||||
id="helpfulness-comment"
|
||||
></textarea>
|
||||
</p>
|
||||
<p>
|
||||
<label className="d-block mb-1 f6" htmlFor="helpfulness-email" hidden data-help-no>
|
||||
{t('email_label')}
|
||||
<span className="text-normal color-text-tertiary float-right ml-1">{t('optional')}</span>
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
className="form-control input-sm width-full"
|
||||
name="helpfulness-email"
|
||||
id="helpfulness-email"
|
||||
placeholder={t('email_placeholder')}
|
||||
hidden
|
||||
data-help-yes
|
||||
data-help-no
|
||||
/>
|
||||
</p>
|
||||
<p className="text-right" hidden data-help-yes data-help-no>
|
||||
<button type="submit" className="btn btn-sm">
|
||||
{t('send')}
|
||||
</button>
|
||||
</p>
|
||||
<p className="color-text-secondary f6" hidden data-help-end>
|
||||
{t('feedback')}
|
||||
</p>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
42
components/LanguagePicker.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
import { Dropdown } from '@primer/components'
|
||||
import { useMainContext } from './context/MainContext'
|
||||
|
||||
export const LanguagePicker = () => {
|
||||
const router = useRouter()
|
||||
const { languages } = useMainContext()
|
||||
const locale = router.locale || 'en'
|
||||
const langs = Object.values(languages)
|
||||
const selectedLang = languages[locale]
|
||||
|
||||
return (
|
||||
<div className="ml-4 d-flex flex-justify-center flex-items-center">
|
||||
<Dropdown css>
|
||||
<summary>
|
||||
{selectedLang.nativeName || selectedLang.name}
|
||||
<Dropdown.Caret />
|
||||
</summary>
|
||||
<Dropdown.Menu direction="sw">
|
||||
{langs.map((lang) => {
|
||||
return (
|
||||
<Dropdown.Item key={lang.code}>
|
||||
<Link href={router.asPath} locale={lang.hreflang}>
|
||||
<a>
|
||||
{lang.nativeName ? (
|
||||
<>
|
||||
{lang.nativeName} ({lang.name})
|
||||
</>
|
||||
) : (
|
||||
lang.name
|
||||
)}
|
||||
</a>
|
||||
</Link>
|
||||
</Dropdown.Item>
|
||||
)
|
||||
})}
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
44
components/MobileProductDropdown.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
import { LinkExternalIcon } from '@primer/octicons-react'
|
||||
import cx from 'classnames'
|
||||
|
||||
import { useMainContext } from 'components/context/MainContext'
|
||||
|
||||
export const MobileProductDropdown = () => {
|
||||
const router = useRouter()
|
||||
const { activeProducts, currentProduct } = useMainContext()
|
||||
|
||||
return (
|
||||
<div
|
||||
id="homepages"
|
||||
className="position-md-absolute nav-desktop-productDropdown p-md-4 left-md-n4 top-md-6"
|
||||
style={{ zIndex: 6 }}
|
||||
>
|
||||
{activeProducts.map((product) => {
|
||||
return (
|
||||
<Link
|
||||
key={product.id}
|
||||
href={`${product.external ? '' : `/${router.locale}`}${product.href}`}
|
||||
>
|
||||
<a
|
||||
className={cx(
|
||||
'd-block py-2',
|
||||
product.id === currentProduct.id
|
||||
? 'color-text-link text-underline active'
|
||||
: 'Link--primary no-underline'
|
||||
)}
|
||||
>
|
||||
{product.name}
|
||||
{product.external && (
|
||||
<span className="ml-1">
|
||||
<LinkExternalIcon size="small" />
|
||||
</span>
|
||||
)}
|
||||
</a>
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
9
components/ScrollButton.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import { ChevronUpIcon } from '@primer/octicons-react'
|
||||
|
||||
export const ScrollButton = () => {
|
||||
return (
|
||||
<button className="arrow-for-scrolling-top" id="js-scroll-top">
|
||||
<ChevronUpIcon />
|
||||
</button>
|
||||
)
|
||||
}
|
||||
48
components/SmallFooter.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import { MarkGithubIcon } from '@primer/octicons-react'
|
||||
import { useTranslation } from 'components/hooks/useTranslation'
|
||||
|
||||
export const SmallFooter = () => {
|
||||
const { t } = useTranslation('footer')
|
||||
return (
|
||||
<footer className="py-6 text-small">
|
||||
<div className="container-xl d-flex px-3 px-md-6">
|
||||
<ul className="d-flex list-style-none flex-wrap flex-justify-center flex-xl-justify-start">
|
||||
<li className="d-flex mr-xl-3 color-text-secondary">
|
||||
<MarkGithubIcon className="mr-2 mr-xl-3" size={20} />
|
||||
<span>© {new Date().getFullYear()} GitHub, Inc.</span>
|
||||
</li>
|
||||
<li className="ml-3">
|
||||
<a href="/github/site-policy/github-terms-of-service">{t`terms`}</a>
|
||||
</li>
|
||||
<li className="ml-3">
|
||||
<a href="/github/site-policy/github-privacy-statement">{t`privacy`} </a>
|
||||
</li>
|
||||
<li className="ml-3">
|
||||
<a href="https://github.com/security">{t`product.links.security`}</a>
|
||||
</li>
|
||||
<li className="ml-3">
|
||||
<a href="https://www.githubstatus.com/">{t`support.links.status`}</a>
|
||||
</li>
|
||||
<li className="ml-3">
|
||||
<a href="/">{t`support.links.help`}</a>
|
||||
</li>
|
||||
<li className="ml-3">
|
||||
<a href="https://support.github.com">{t`support.links.contact_github`}</a>
|
||||
</li>
|
||||
<li className="ml-3">
|
||||
<a href="https://github.com/pricing">{t`product.links.pricing`}</a>
|
||||
</li>
|
||||
<li className="ml-3">
|
||||
<a href="/developers">{t`platform.links.developer_api`}</a>
|
||||
</li>
|
||||
<li className="ml-3">
|
||||
<a href="https://services.github.com/">{t`support.links.training`}</a>
|
||||
</li>
|
||||
<li className="ml-3">
|
||||
<a href="https://github.com/about">{t`company.links.about`}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</footer>
|
||||
)
|
||||
}
|
||||
31
components/Support.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import { PeopleIcon, CommentDiscussionIcon } from '@primer/octicons-react'
|
||||
|
||||
import { useTranslation } from 'components/hooks/useTranslation'
|
||||
import { useVersion } from 'components/hooks/useVersion'
|
||||
|
||||
export const Support = () => {
|
||||
const { isEnterprise } = useVersion()
|
||||
const { t } = useTranslation('support')
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h3 className="mb-2 f4">{t`still_need_help`}</h3>
|
||||
<a id="ask-community" href="https://github.community" className="btn btn-outline mr-4 mt-2">
|
||||
<PeopleIcon size="small" className="octicon mr-1" />
|
||||
{t`ask_community`}
|
||||
</a>
|
||||
<a
|
||||
id="contact-us"
|
||||
href={
|
||||
isEnterprise
|
||||
? 'https://enterprise.github.com/support'
|
||||
: 'https://support.github.com/contact'
|
||||
}
|
||||
className="btn btn-outline mt-2"
|
||||
>
|
||||
<CommentDiscussionIcon size="small" className="octicon mr-1" />
|
||||
{t`contact_support`}
|
||||
</a>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
30
components/SupportSection.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import { Helpfulness } from 'components/Helpfulness'
|
||||
import { Contribution } from 'components/Contribution'
|
||||
import { Support } from 'components/Support'
|
||||
import { useMainContext } from './context/MainContext'
|
||||
import { useVersion } from 'components/hooks/useVersion'
|
||||
|
||||
export const SupportSection = () => {
|
||||
const { currentVersion } = useVersion()
|
||||
const { enterpriseServerReleases } = useMainContext()
|
||||
|
||||
const isDeprecated =
|
||||
enterpriseServerReleases.isOldestReleaseDeprecated &&
|
||||
currentVersion.includes(enterpriseServerReleases.oldestSupported)
|
||||
|
||||
return (
|
||||
<section className="mt-lg-9 py-7 px-3 px-md-6 no-print color-bg-tertiary">
|
||||
<div className="container-xl gutter-lg-spacious clearfix">
|
||||
<div className="col-12 col-lg-6 col-xl-4 mb-6 mb-xl-0 float-left">
|
||||
{!isDeprecated && <Helpfulness />}
|
||||
</div>
|
||||
<div className="col-12 col-lg-6 col-xl-4 mb-6 mb-xl-0 float-left">
|
||||
{!isDeprecated && <Contribution />}
|
||||
</div>
|
||||
<div className="col-12 col-lg-12 col-xl-4 float-left">
|
||||
<Support />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
25
components/TruncateLines.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import React, { ReactNode, ReactHTML } from 'react'
|
||||
import cx from 'classnames'
|
||||
|
||||
type Props = {
|
||||
as?: keyof ReactHTML
|
||||
maxLines: number
|
||||
children: ReactNode
|
||||
className?: string
|
||||
}
|
||||
export const TruncateLines = (props: Props) => {
|
||||
const Component = props.as || 'div'
|
||||
return (
|
||||
<Component className={cx('root', props.className)}>
|
||||
{props.children}
|
||||
<style jsx>{`
|
||||
.root {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: ${props.maxLines};
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
`}</style>
|
||||
</Component>
|
||||
)
|
||||
}
|
||||
44
components/VersionPicker.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
import { Dropdown } from '@primer/components'
|
||||
import { useMainContext } from './context/MainContext'
|
||||
import { useVersion } from './hooks/useVersion'
|
||||
|
||||
export const VersionPicker = () => {
|
||||
const router = useRouter()
|
||||
const { currentVersion } = useVersion()
|
||||
const { allVersions } = useMainContext()
|
||||
|
||||
const versions = Object.values(allVersions)
|
||||
const activeVersion = allVersions[currentVersion]
|
||||
|
||||
return (
|
||||
<div className="ml-4 d-flex flex-justify-center flex-items-center">
|
||||
<Dropdown css>
|
||||
<summary>
|
||||
{activeVersion.versionTitle}
|
||||
<Dropdown.Caret />
|
||||
</summary>
|
||||
<Dropdown.Menu direction="sw">
|
||||
{versions.map((version) => {
|
||||
return (
|
||||
<Dropdown.Item key={version.version}>
|
||||
<Link
|
||||
href={{
|
||||
pathname: router.pathname,
|
||||
query: {
|
||||
...router.query,
|
||||
versionId: version.version,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<a>{version.versionTitle}</a>
|
||||
</Link>
|
||||
</Dropdown.Item>
|
||||
)
|
||||
})}
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
116
components/context/MainContext.tsx
Normal file
@@ -0,0 +1,116 @@
|
||||
import { createContext, useContext } from 'react'
|
||||
|
||||
import type { BreadcrumbT } from 'components/Breadcrumbs'
|
||||
|
||||
type ProductT = {
|
||||
external: boolean
|
||||
href: string
|
||||
id: string
|
||||
name: string
|
||||
}
|
||||
|
||||
type LanguageItem = {
|
||||
name: string
|
||||
nativeName: string
|
||||
code: string
|
||||
hreflang: string
|
||||
wip?: boolean
|
||||
}
|
||||
|
||||
type VersionItem = {
|
||||
version: string
|
||||
versionTitle: string
|
||||
}
|
||||
|
||||
type DataT = {
|
||||
ui: Record<string, any>
|
||||
reusables: {
|
||||
enterprise_deprecation: {
|
||||
version_was_deprecated: string
|
||||
version_will_be_deprecated: string
|
||||
deprecation_details: string
|
||||
isOldestReleaseDeprecated: boolean
|
||||
}
|
||||
policies: {
|
||||
translation: string
|
||||
}
|
||||
}
|
||||
variables: {
|
||||
release_candidate: { version: string }
|
||||
}
|
||||
}
|
||||
type EnterpriseServerReleases = {
|
||||
isOldestReleaseDeprecated: boolean
|
||||
oldestSupported: string
|
||||
nextDeprecationDate: string
|
||||
}
|
||||
export type MainContextT = {
|
||||
breadcrumbs: Record<string, BreadcrumbT>
|
||||
builtAssets: { main: { css: string; js: string } }
|
||||
expose: string
|
||||
activeProducts: Array<ProductT>
|
||||
currentProduct: ProductT
|
||||
currentLayoutName: string
|
||||
data: DataT
|
||||
airGap?: boolean
|
||||
error: string
|
||||
currentCategory?: string
|
||||
relativePath?: string
|
||||
enterpriseServerReleases: EnterpriseServerReleases
|
||||
currentLanguage: string
|
||||
languages: Record<string, LanguageItem>
|
||||
allVersions: Record<string, VersionItem>
|
||||
}
|
||||
|
||||
export const getMainContextFromRequest = (req: any): MainContextT => {
|
||||
return {
|
||||
builtAssets: req.context.builtAssets,
|
||||
expose: req.context.expose,
|
||||
breadcrumbs: req.context.breadcrumbs || {},
|
||||
activeProducts: req.context.activeProducts,
|
||||
currentProduct: req.context.productMap[req.context.currentProduct],
|
||||
currentLayoutName: req.context.currentLayoutName,
|
||||
error: req.context.error || '',
|
||||
data: {
|
||||
ui: req.context.site.data.ui,
|
||||
reusables: {
|
||||
enterprise_deprecation: req.context.site.data.reusables.enterprise_deprecation,
|
||||
policies: req.context.site.data.reusables.policies,
|
||||
},
|
||||
variables: {
|
||||
release_candidate: req.context.site.data.variables.release_candidate,
|
||||
},
|
||||
},
|
||||
airGap: req.context.AIRGAP || false,
|
||||
currentCategory: req.context.currentCategory || '',
|
||||
relativePath: req.context.page.relativePath,
|
||||
enterpriseServerReleases: req.context.enterpriseServerReleases,
|
||||
currentLanguage: req.context.currentLanguage,
|
||||
languages: Object.fromEntries(
|
||||
Object.entries(req.context.languages).map(([key, entry]: any) => {
|
||||
return [
|
||||
key,
|
||||
{
|
||||
name: entry.name,
|
||||
nativeName: entry.nativeName || '',
|
||||
code: entry.code,
|
||||
hreflang: entry.hreflang,
|
||||
},
|
||||
]
|
||||
})
|
||||
),
|
||||
allVersions: req.context.allVersions,
|
||||
}
|
||||
}
|
||||
|
||||
export const MainContext = createContext<MainContextT | null>(null)
|
||||
|
||||
export const useMainContext = (): MainContextT => {
|
||||
const context = useContext(MainContext)
|
||||
|
||||
if (!context) {
|
||||
throw new Error('"useMainContext" may only be used inside "MainContext.Provider"')
|
||||
}
|
||||
|
||||
return context
|
||||
}
|
||||
126
components/context/ProductLandingContext.tsx
Normal file
@@ -0,0 +1,126 @@
|
||||
import { createContext, useContext } from 'react'
|
||||
import pick from 'lodash/pick'
|
||||
|
||||
export type FeaturedLink = {
|
||||
title: string
|
||||
href: string
|
||||
intro?: string
|
||||
authors?: Array<string>
|
||||
hideIntro?: boolean
|
||||
date?: string
|
||||
}
|
||||
export type CodeExample = {
|
||||
title: string
|
||||
description: string
|
||||
languages: string // single comma separated string
|
||||
href: string
|
||||
tags: Array<string>
|
||||
}
|
||||
|
||||
export type ProductLandingContextT = {
|
||||
title: string
|
||||
introPlainText: string
|
||||
shortTitle: string
|
||||
intro: string
|
||||
beta_product: boolean
|
||||
// primaryAction: LinkButtonT
|
||||
// secondaryAction?: LinkButtonT
|
||||
introLinks: {
|
||||
quickstart?: string
|
||||
reference?: string
|
||||
overview?: string
|
||||
}
|
||||
product_video?: string
|
||||
// featuredLinks?: {
|
||||
// guides: Array<FeaturedLink>
|
||||
// popular: Array<FeaturedLink>
|
||||
// guideCards: Array<FeaturedLink>
|
||||
// }
|
||||
guideCards: Array<FeaturedLink>
|
||||
productCodeExamples: Array<CodeExample>
|
||||
productUserExamples: Array<{ username: string; description: string }>
|
||||
productCommunityExamples: Array<{ repo: string; description: string }>
|
||||
featuredArticles: Array<{
|
||||
label: string // Guides
|
||||
viewAllHref?: string // If provided, adds a "View All ->" to the header
|
||||
articles: Array<FeaturedLink>
|
||||
}>
|
||||
changelog: { label: string; prefix: string }
|
||||
changelogUrl?: string
|
||||
whatsNewChangelog?: Array<{ href: string; title: string; date: string }>
|
||||
}
|
||||
|
||||
export const ProductLandingContext = createContext<ProductLandingContextT | null>(null)
|
||||
|
||||
export const useProductLandingContext = (): ProductLandingContextT => {
|
||||
const context = useContext(ProductLandingContext)
|
||||
|
||||
if (!context) {
|
||||
throw new Error(
|
||||
'"useProductLandingContext" may only be used inside "ProductLandingContext.Provider"'
|
||||
)
|
||||
}
|
||||
|
||||
return context
|
||||
}
|
||||
|
||||
export const getProductLandingContextFromRequest = (req: any): ProductLandingContextT => {
|
||||
const { currentCategory, currentPath, data } = req.context
|
||||
return {
|
||||
...pick(req.context.page, [
|
||||
'title',
|
||||
'shortTitle',
|
||||
'introPlainText',
|
||||
'beta_product',
|
||||
'intro',
|
||||
'product_video',
|
||||
'changelog',
|
||||
]),
|
||||
whatsNewChangelog: req.context.whatsNewChangelog,
|
||||
changelogUrl: req.context.changelogUrl,
|
||||
|
||||
productCodeExamples: req.context.productCodeExamples || [],
|
||||
|
||||
productCommunityExamples: req.context.productCommunityExamples || [],
|
||||
|
||||
productUserExamples: (req.context.productUserExamples || []).map(
|
||||
({ user, description }: any) => ({
|
||||
username: user,
|
||||
description,
|
||||
})
|
||||
),
|
||||
|
||||
introLinks: Object.fromEntries(
|
||||
Object.entries(req.context.page.introLinks || {}).filter(([key, val]) => !!val)
|
||||
),
|
||||
|
||||
guideCards: (req.context.featuredLinks.guideCards || []).map((link: any) => {
|
||||
return {
|
||||
href: link.href,
|
||||
title: link.title,
|
||||
intro: link.intro,
|
||||
authors: link.page.authors || [],
|
||||
}
|
||||
}),
|
||||
|
||||
featuredArticles: Object.entries(req.context.featuredLinks)
|
||||
.filter(([key]) => {
|
||||
return key === 'guides' || key === 'popular'
|
||||
})
|
||||
.map(([key, links]: any) => {
|
||||
return {
|
||||
label: req.context.site.data.ui.toc[key],
|
||||
viewAllHref: key === 'guides' && !currentCategory ? `${currentPath}/${key}` : '',
|
||||
articles: links.map((link: any) => {
|
||||
return {
|
||||
hideIntro: key === 'popular',
|
||||
href: link.href,
|
||||
title: link.title,
|
||||
intro: link.intro,
|
||||
authors: link.page.authors || [],
|
||||
}
|
||||
}),
|
||||
}
|
||||
}),
|
||||
}
|
||||
}
|
||||
41
components/hooks/useTranslation.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import { useMainContext } from 'components/context/MainContext'
|
||||
import get from 'lodash/get'
|
||||
|
||||
// The idea of this component is to mimic a popular i18n library (i18next)
|
||||
// so that we can set ourselves up to transition to it (or a similar library) in the future
|
||||
export const useTranslation = (namespaces: string | Array<string>) => {
|
||||
const { data } = useMainContext()
|
||||
|
||||
// this can eventually be an object constructed from the input namespaces param above, but for now everything is already loaded
|
||||
const loadedData: any = data.ui
|
||||
|
||||
return {
|
||||
// The compiled string supports prefixing with a namespace such as `my-namespace:path.to.value`
|
||||
t: (strings: TemplateStringsArray | string, ...values: Array<any>) => {
|
||||
const key = typeof strings === 'string' ? strings : String.raw(strings, ...values)
|
||||
|
||||
const splitKey = key.split(':')
|
||||
if (splitKey.length > 2) {
|
||||
throw new Error('Multiple ":" not allowed in translation lookup path')
|
||||
}
|
||||
|
||||
if (splitKey.length === 2) {
|
||||
const [namespace, path] = splitKey
|
||||
return get(loadedData[namespace], path)
|
||||
}
|
||||
|
||||
const [path] = splitKey
|
||||
if (Array.isArray(namespaces)) {
|
||||
for (const namespace of namespaces) {
|
||||
const val = get(loadedData[namespace], path)
|
||||
if (val !== undefined) {
|
||||
return val
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
} else {
|
||||
return get(loadedData[namespaces], path)
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
12
components/hooks/useVersion.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
type VersionInfo = {
|
||||
currentVersion: string
|
||||
isEnterprise: boolean
|
||||
}
|
||||
const DEFAULT_VERSION = 'free-pro-team@latest'
|
||||
export const useVersion = (): VersionInfo => {
|
||||
const router = useRouter()
|
||||
const currentVersion = (router.query.versionId as string) || DEFAULT_VERSION
|
||||
return { currentVersion, isEnterprise: currentVersion.includes('enterprise') }
|
||||
}
|
||||
35
components/landing/CodeExampleCard.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import { RepoIcon } from '@primer/octicons-react'
|
||||
import { CodeExample } from 'components/context/ProductLandingContext'
|
||||
|
||||
type Props = {
|
||||
example: CodeExample
|
||||
}
|
||||
export const CodeExampleCard = ({ example }: Props) => {
|
||||
return (
|
||||
<a
|
||||
className="Box d-flex flex-column flex-justify-between height-full color-shadow-medium hover-shadow-large no-underline color-text-primary"
|
||||
href={`https://github.com/${example.href}`}
|
||||
>
|
||||
<div className="p-4">
|
||||
<h4>{example.title}</h4>
|
||||
<p className="mt-2 mb-4 color-text-tertiary">{example.description}</p>
|
||||
<div className="d-flex flex-wrap">
|
||||
{example.tags.map((tag) => {
|
||||
return (
|
||||
<span
|
||||
key={tag}
|
||||
className="IssueLabel color-text-inverse color-bg-info-inverse mr-2 mb-1"
|
||||
>
|
||||
{tag}
|
||||
</span>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<footer className="border-top p-4 color-text-secondary d-flex flex-items-center">
|
||||
<RepoIcon className="flex-shrink-0" />
|
||||
<span className="ml-2 text-mono text-small color-text-link">{example.href}</span>
|
||||
</footer>
|
||||
</a>
|
||||
)
|
||||
}
|
||||