├── .eslintignore ├── .github ├── dependabot.yml └── workflows │ ├── ci.yaml │ ├── deploy.yaml │ └── testdeploy.yaml ├── .gitignore ├── .husky ├── .gitignore └── pre-commit ├── .nvmrc ├── .prettierignore ├── .prettierrc.yaml ├── LICENSE ├── README.md ├── deployment ├── installtutorials.py └── requirements.txt ├── gatsby-config.js ├── gatsby-node.js ├── package-lock.json ├── package.json ├── src ├── components │ ├── callToAction.js │ ├── footer.js │ ├── header.js │ ├── instantsearch │ │ ├── hits.js │ │ ├── poweredBy.js │ │ ├── refinementList.js │ │ ├── searchBox.js │ │ └── virtualPrioritySort.js │ ├── layout.js │ ├── pageCover.js │ ├── resultCard.js │ ├── searchLayout.js │ └── seo.js ├── contributing │ └── index.md ├── pages │ ├── 404.js │ └── index.js ├── searchClient.js ├── styles │ ├── breakpoints.js │ └── globalStyles.js └── templates │ └── contribTemplate.js └── static ├── CNAME ├── Numfocus_stamp.png ├── astropy_favicon.ico ├── astropy_logo_notext.svg ├── dunlap-logo.png └── learn-astropy-logo.png /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .cache 3 | package-lock.json 4 | public 5 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "github-actions" # See documentation for possible values 9 | directory: ".github/workflows" # Location of package manifests 10 | schedule: 11 | interval: "monthly" 12 | groups: 13 | actions: 14 | patterns: 15 | - "*" 16 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | 'on': 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 15 | 16 | - name: Read .nvmrc 17 | id: node_version 18 | run: echo ::set-output name=NODE_VERSION::$(cat .nvmrc) 19 | 20 | - name: Set up node 21 | uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0 22 | with: 23 | node-version: ${{ steps.node_version.outputs.NODE_VERSION }} 24 | 25 | - name: Cache dependencies 26 | uses: actions/cache@d4323d4df104b026a6aa633fdb11d772146be0bf # v4.2.2 27 | with: 28 | path: ~/.npm 29 | key: ${{ runner.os }}-node-${{ steps.node_version.outputs.NODE_VERSION }}-${{ hashFiles('**/package-lock.json') }} 30 | restore-keys: | 31 | ${{ runner.os }}-node-${{ steps.node_version.outputs.NODE_VERSION }} 32 | 33 | - run: npm ci 34 | name: Install 35 | 36 | - run: npm run lint 37 | name: ESLint 38 | 39 | - run: npm run prettier 40 | name: Prettier 41 | 42 | - run: npm run build 43 | name: Build 44 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yaml: -------------------------------------------------------------------------------- 1 | name: Deploy 2 | 3 | 'on': 4 | push: 5 | branches: 6 | - main 7 | repository_dispatch: 8 | types: 9 | - 'tutorials-build' # triggered from astropy/astropy-tutorials 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 17 | 18 | - name: Read .nvmrc 19 | id: node_version 20 | run: echo ::set-output name=NODE_VERSION::$(cat .nvmrc) 21 | 22 | - name: Set up node 23 | uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0 24 | with: 25 | node-version: ${{ steps.node_version.outputs.NODE_VERSION }} 26 | 27 | - name: Cache dependencies 28 | uses: actions/cache@d4323d4df104b026a6aa633fdb11d772146be0bf # v4.2.2 29 | with: 30 | path: ~/.npm 31 | key: ${{ runner.os }}-node-${{ steps.node_version.outputs.NODE_VERSION }}-${{ hashFiles('**/package-lock.json') }} 32 | restore-keys: | 33 | ${{ runner.os }}-node-${{ steps.node_version.outputs.NODE_VERSION }} 34 | 35 | - run: npm ci 36 | name: Install 37 | 38 | - run: npm run build 39 | name: Build 40 | 41 | - name: Upload gatsby artifact 42 | uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1 43 | with: 44 | name: gatsby-build 45 | path: ./public 46 | 47 | deploy: 48 | runs-on: ubuntu-latest 49 | needs: build 50 | 51 | steps: 52 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 53 | 54 | - name: Set up Python 55 | uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5.4.0 56 | with: 57 | python-version: 3.12 58 | 59 | - name: Install Python dependencies 60 | run: | 61 | python -m pip install -U pip 62 | python -m pip install -r deployment/requirements.txt 63 | 64 | - name: Download gatsby artifact 65 | uses: actions/download-artifact@cc203385981b70ca67e1cc392babf9cc229d5806 # v4.1.9 66 | with: 67 | name: gatsby-build 68 | path: ./public 69 | 70 | - name: Download tutorials for tutorial dispatch event 71 | if: ${{ github.event == 'repository_dispatch' }} 72 | env: 73 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 74 | run: | 75 | python deployment/installtutorials.py \ 76 | --dest public/tutorials \ 77 | --tutorials-run ${{ github.event.client_payload.runid }} \ 78 | --tutorials-artifact ${{ github.event.client_payload.artifactName }} \ 79 | --tutorials-repo ${{ github.event.client_payload.repo }} 80 | 81 | - name: Download latest tutorials 82 | if: ${{ github.event != 'repository_dispatch' }} 83 | env: 84 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 85 | run: | 86 | python deployment/installtutorials.py --dest public/tutorials 87 | 88 | - name: Deploy to gh-pages 89 | uses: peaceiris/actions-gh-pages@4f9cc6602d3f66b9c108549d475ec49e8ef4d45e # v4.0.0 90 | with: 91 | github_token: ${{ secrets.GITHUB_TOKEN }} 92 | publish_dir: ./public 93 | 94 | - name: Index tutorials 95 | env: 96 | ALGOLIA_ID: ${{ secrets.ALGOLIA_ID }} 97 | ALGOLIA_KEY: ${{ secrets.ALGOLIA_KEY }} 98 | ALGOLIA_INDEX: ${{ secrets.ALGOLIA_INDEX }} 99 | run: | 100 | astropylibrarian index tutorial-site \ 101 | public/tutorials \ 102 | https://learn.astropy.org/tutorials 103 | -------------------------------------------------------------------------------- /.github/workflows/testdeploy.yaml: -------------------------------------------------------------------------------- 1 | name: Test deployment scripts 2 | 3 | 'on': 4 | pull_request: 5 | 6 | jobs: 7 | deploy: 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 12 | 13 | - name: Set up Python 14 | uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5.4.0 15 | with: 16 | python-version: 3.12 17 | 18 | - name: Install Python dependencies 19 | run: | 20 | python -m pip install -U pip 21 | python -m pip install -r deployment/requirements.txt 22 | 23 | - name: Check Astropy Librarian installation 24 | run: | 25 | astropylibrarian --help 26 | 27 | - name: Download latest tutorials 28 | env: 29 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 30 | run: | 31 | python deployment/installtutorials.py --dest public/tutorials 32 | 33 | - name: List tutorials 34 | run: | 35 | tree public 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .cache/ 3 | public 4 | -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx --no-install lint-staged && npx --no-install pretty-quick --staged 5 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 14.16.0 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .cache 3 | .github 4 | package.json 5 | package-lock.json 6 | public 7 | -------------------------------------------------------------------------------- /.prettierrc.yaml: -------------------------------------------------------------------------------- 1 | trailingComma: es5 2 | singleQuote: true 3 | printWidth: 80 4 | endOfLine: auto 5 | proseWrap: never 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2020, Astropy Developers 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Astropy Learn 2 | 3 | This repository hosts the homepage of the Astropy Learn project, https://learn.astropy.org, and serves the tutorial content from the [astropy-learn](https://github.com/astropy-learn) organization. The site itself is built with [Gatsby](https://www.gatsbyjs.com/) and the [Algolia](https://www.algolia.com) search service. Records for the Algolia database are curated and formatted by the [learn-astropy-librarian](https://github.com/astropy-learn/learn-astropy-librarian) app. 4 | 5 | ## Developer guide 6 | 7 | ### Initial set up 8 | 9 | Create a fork on https://github.com/astropy-learn/learn. 10 | 11 | ```bash 12 | npm install 13 | ``` 14 | 15 | ### Run a development server 16 | 17 | You can run a development server that will serve the site and reload as you develop the app: 18 | 19 | ```bash 20 | npm run develop 21 | ``` 22 | 23 | By default the app is hosted at http://localhost:8000. You can also interact with the GraphQL data layer by browsing 24 | 25 | ### Build for production 26 | 27 | ```bash 28 | npm run build 29 | ``` 30 | 31 | Preview the built site by running: 32 | 33 | ```bash 34 | npm run serve 35 | ``` 36 | 37 | ### Linting and autoformatting 38 | 39 | This app uses ESLint to lint JavaScript, which in turn runs Prettier to format JavaScript. The configuration is based on [wesbos/eslint-config-wesbos](https://github.com/wesbos/eslint-config-wesbos). 40 | 41 | A Git pre-commit hooks runs both ESLint and Prettier and automatically lints and reformats code before every commit. These hooks are run by [husky](https://typicode.github.io/husky/#/) and should already be installed when you ran `npm install`. 42 | 43 | To manually lint the code base: 44 | 45 | ```bash 46 | npm run lint 47 | ``` 48 | 49 | To also fix issues and format the code base: 50 | 51 | ```bash 52 | npm run lint:fix 53 | ``` 54 | 55 | Ideally your editor will also apply eslint/prettier on save, though these commands are handy as a fallback. 56 | 57 | ### About the node version 58 | 59 | This project is intended to be built with a Node.js version that's encoded in the [`.nvmrc`](./.nvmrc) file. To adopt this Node version, we recommend installing and using the [node version manager](https://github.com/nvm-sh/nvm). 60 | 61 | Then you can use the preferred node version by running `nvm` from the project root: 62 | 63 | ```sh 64 | nvm use 65 | ``` 66 | 67 | ### Additional resources for developers 68 | 69 | Learn more about Gatsby: 70 | 71 | - [Documentation](https://www.gatsbyjs.com/docs/?utm_source=starter&utm_medium=readme&utm_campaign=minimal-starter) 72 | - [Tutorials](https://www.gatsbyjs.com/tutorial/?utm_source=starter&utm_medium=readme&utm_campaign=minimal-starter) 73 | - [Guides](https://www.gatsbyjs.com/tutorial/?utm_source=starter&utm_medium=readme&utm_campaign=minimal-starter) 74 | - [API Reference](https://www.gatsbyjs.com/docs/api-reference/?utm_source=starter&utm_medium=readme&utm_campaign=minimal-starter) 75 | - [Plugin Library](https://www.gatsbyjs.com/plugins?utm_source=starter&utm_medium=readme&utm_campaign=minimal-starter) 76 | - [Cheat Sheet](https://www.gatsbyjs.com/docs/cheat-sheet/?utm_source=starter&utm_medium=readme&utm_campaign=minimal-starter) 77 | 78 | Learn more about Algolia: 79 | 80 | - [Documentation](https://www.algolia.com/doc/) 81 | - [React instantsearch](https://www.algolia.com/doc/guides/building-search-ui/what-is-instantsearch/react/) 82 | -------------------------------------------------------------------------------- /deployment/installtutorials.py: -------------------------------------------------------------------------------- 1 | """Install the built tutorials HTML from astropy/astropy-tutorials into the 2 | built Gatsby site. 3 | """ 4 | 5 | from __future__ import annotations 6 | 7 | import argparse 8 | import os 9 | from io import BytesIO 10 | from pathlib import Path 11 | from typing import Any, Dict, Optional 12 | from zipfile import ZipFile 13 | 14 | import requests 15 | from uritemplate import expand 16 | 17 | 18 | def parse_args() -> argparse.Namespace: 19 | parser = argparse.ArgumentParser( 20 | description=( 21 | 'Install the tutorials HTML artifact from ' 22 | 'astropy/astropy-tutorials into the build Gatsby site.\n\n' 23 | 'There are two usage modes:\n\n' 24 | '1. If --tutorials-run is set, get the tutorials from the ' 25 | 'corresponding workflow artifact.\n' 26 | '2. If --tutorials-run is not set, the workflow artifact from ' 27 | 'the most recent merge to the main branch is used.\n\n' 28 | ), 29 | formatter_class=argparse.RawDescriptionHelpFormatter 30 | ) 31 | parser.add_argument( 32 | '--dest', 33 | required=True, 34 | help="Directory where the tutorials are installed. This should be " 35 | "inside the Gatsby 'public' directory." 36 | ) 37 | parser.add_argument( 38 | '--tutorials-repo', 39 | default="astropy/astropy-tutorials", 40 | help='Tutorials repo slug (should be astropy/astropy-tutorials).' 41 | ) 42 | parser.add_argument( 43 | '--tutorials-artifact', 44 | default="rendered-tutorials", 45 | help='Name of the artifact from the tutorials repo.' 46 | ) 47 | parser.add_argument( 48 | '--tutorials-run', 49 | help='ID of the workflow run from the tutorials repo.' 50 | ) 51 | return parser.parse_args() 52 | 53 | 54 | def main() -> None: 55 | """The main script entrypoint.""" 56 | args = parse_args() 57 | 58 | github_token = os.getenv("GITHUB_TOKEN") 59 | if github_token is None: 60 | raise RuntimeError("Set the GITHUB_TOKEN environment variable") 61 | 62 | if args.tutorials_run is not None: 63 | artifact = TutorialsArtifact( 64 | repo=args.tutorials_repo, 65 | name=args.tutorials_artifact, 66 | run_id=args.tutorials_run, 67 | github_token=github_token 68 | ) 69 | else: 70 | artifact = TutorialsArtifact.from_latest( 71 | repo=args.tutorials_repo, 72 | name=args.tutorials_artifact, 73 | github_token=github_token 74 | ) 75 | 76 | artifact.install(args.dest) 77 | 78 | 79 | class TutorialsArtifact: 80 | """A tutorials build artifact that can be downloaded and installed into 81 | the site's build directory. 82 | 83 | Parameters 84 | ---------- 85 | repo : str 86 | The repository slug (astropy/astropy-tutorials). 87 | name : str 88 | The name of the workflow artifact with rendered tutorials. 89 | run_id : str 90 | The workflow run ID corresponding to the workflow artifact. 91 | github_token : str 92 | A GitHub token. 93 | """ 94 | 95 | def __init__( 96 | self, 97 | *, 98 | repo: str, 99 | name: str, 100 | run_id: str, 101 | github_token: str 102 | ) -> None: 103 | self.repo = repo 104 | self.name = name 105 | self.run_id = run_id 106 | self.github_token = github_token 107 | self._zip_path: Optional[Path] = None 108 | 109 | @classmethod 110 | def from_latest( 111 | cls, 112 | *, 113 | repo: str, 114 | name: str, 115 | github_token: str 116 | ) -> TutorialsArtifact: 117 | """Get the artifact from the latest run on the main branch. 118 | 119 | Parameters 120 | ---------- 121 | repo : str 122 | The repository slug (astropy/astropy-tutorials). 123 | name : str 124 | The name of the workflow artifact with rendered tutorials. 125 | run_id : str 126 | The workflow run ID corresponding to the workflow artifact. 127 | github_token : str 128 | A GitHub token. 129 | """ 130 | owner, repo_name = repo.split('/') 131 | url = expand( 132 | "https://api.github.com/repos/{owner}/{repo}/actions/runs", 133 | owner=owner, 134 | repo=repo_name 135 | ) 136 | headers = cls._make_github_headers(github_token) 137 | response = requests.get( 138 | url, 139 | headers=headers, 140 | params={"branch": "main", "event": "push", "status": "success"} 141 | ) 142 | response.raise_for_status() 143 | runs_data = response.json() 144 | first_run = runs_data["workflow_runs"][0] 145 | run_id = first_run["id"] 146 | return cls( 147 | repo=repo, 148 | name=name, 149 | run_id=run_id, 150 | github_token=github_token 151 | ) 152 | 153 | @staticmethod 154 | def _make_github_headers(github_token) -> Dict[str, str]: 155 | return { 156 | "Accept": "application/vnd.github.v3+json", 157 | "Authorization": f"Bearer {github_token}" 158 | } 159 | 160 | def _download_artifact(self) -> bytes: 161 | # Note that if there's a large number of artifacts (>30), we need 162 | # to iterate over all pages of the request. Generally this shouldn't 163 | # be necessary. 164 | artifacts_data = self._get_workflow_run_artifacts() 165 | for artifact in artifacts_data["artifacts"]: 166 | if artifact["name"] == self.name: 167 | download_url = artifact["archive_download_url"] 168 | response = requests.get( 169 | download_url, 170 | headers={"Authorization": f"Bearer {self.github_token}"} 171 | ) 172 | return response.content 173 | raise RuntimeError("Did not find artifact for download") 174 | 175 | def _get_workflow_run_artifacts(self, page=1) -> Dict[str, Any]: 176 | owner, repo = self.repo.split('/') 177 | uri = expand( 178 | "https://api.github.com/repos/{owner}/{repo}/actions/runs/" 179 | "{run_id}/artifacts", 180 | owner=owner, 181 | repo=repo, 182 | run_id=self.run_id, 183 | ) 184 | headers = TutorialsArtifact._make_github_headers(self.github_token) 185 | response = requests.get( 186 | uri, 187 | params={"page": str(page)}, 188 | headers=headers 189 | ) 190 | response.raise_for_status() 191 | return response.json() 192 | 193 | def install(self, destination_directory: str) -> None: 194 | """Download and install the artifact contents into the desination 195 | directory, which is created if it does not already exist. 196 | """ 197 | artifact_archive = self._download_artifact() 198 | bytes_stream = BytesIO(artifact_archive) 199 | zip_file = ZipFile(bytes_stream) 200 | zip_file.extractall(path=Path(destination_directory)) 201 | 202 | 203 | if __name__ == '__main__': 204 | main() 205 | -------------------------------------------------------------------------------- /deployment/requirements.txt: -------------------------------------------------------------------------------- 1 | requests 2 | uritemplate 3 | git+https://github.com/astropy-learn/learn-astropy-librarian.git#egg=astropy-librarian 4 | -------------------------------------------------------------------------------- /gatsby-config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | siteMetadata: { 3 | title: 'Learn Astropy', 4 | description: 5 | 'Astropy is a Python library for use in astronomy. Learn Astropy provides a portal to all of the Astropy educational material.', 6 | author: 'Astropy Project', 7 | twitter: '@astropy', 8 | siteUrl: 'https://learn.astropy.org', 9 | }, 10 | plugins: [ 11 | 'gatsby-plugin-styled-components', 12 | 'gatsby-plugin-react-helmet', 13 | { 14 | resolve: `gatsby-source-filesystem`, 15 | options: { 16 | name: `markdown-docs`, 17 | path: `${__dirname}/src/contributing`, 18 | }, 19 | }, 20 | `gatsby-transformer-remark`, 21 | ], 22 | }; 23 | -------------------------------------------------------------------------------- /gatsby-node.js: -------------------------------------------------------------------------------- 1 | exports.createPages = async ({ actions, graphql, reporter }) => { 2 | const { createPage } = actions; 3 | const contribTemplate = require.resolve(`./src/templates/contribTemplate.js`); 4 | const result = await graphql(` 5 | { 6 | allMarkdownRemark { 7 | edges { 8 | node { 9 | frontmatter { 10 | slug 11 | } 12 | } 13 | } 14 | } 15 | } 16 | `); 17 | // Handle errors 18 | if (result.errors) { 19 | reporter.panicOnBuild(`Error while running GraphQL query.`); 20 | return; 21 | } 22 | result.data.allMarkdownRemark.edges.forEach(({ node }) => { 23 | createPage({ 24 | path: node.frontmatter.slug, 25 | component: contribTemplate, 26 | context: { 27 | // additional data can be passed via context 28 | slug: node.frontmatter.slug, 29 | }, 30 | }); 31 | }); 32 | }; 33 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "learn-astropy", 3 | "version": "1.0.0", 4 | "private": true, 5 | "description": "Learn Astropy", 6 | "author": "Jonathan Sick", 7 | "keywords": [ 8 | "gatsby" 9 | ], 10 | "scripts": { 11 | "develop": "gatsby develop", 12 | "start": "gatsby develop", 13 | "build": "gatsby build", 14 | "serve": "gatsby serve", 15 | "clean": "gatsby clean", 16 | "lint": "eslint .", 17 | "lint:fix": "eslint . --fix", 18 | "prettier": "npx prettier . --check", 19 | "prettier:fix": "npm run prettier -- --write", 20 | "prepare": "husky install" 21 | }, 22 | "eslintConfig": { 23 | "extends": [ 24 | "airbnb", 25 | "prettier" 26 | ], 27 | "parser": "babel-eslint", 28 | "plugins": [ 29 | "html", 30 | "react-hooks" 31 | ], 32 | "rules": { 33 | "no-debugger": 0, 34 | "no-alert": 0, 35 | "no-await-in-loop": 0, 36 | "no-return-assign": [ 37 | "error", 38 | "except-parens" 39 | ], 40 | "no-restricted-syntax": [ 41 | 2, 42 | "ForInStatement", 43 | "LabeledStatement", 44 | "WithStatement" 45 | ], 46 | "no-unused-vars": [ 47 | 1, 48 | { 49 | "ignoreRestSiblings": true, 50 | "argsIgnorePattern": "res|next|^err" 51 | } 52 | ], 53 | "prefer-const": [ 54 | "error", 55 | { 56 | "destructuring": "all" 57 | } 58 | ], 59 | "arrow-body-style": [ 60 | 2, 61 | "as-needed" 62 | ], 63 | "no-unused-expressions": [ 64 | 2, 65 | { 66 | "allowTaggedTemplates": true 67 | } 68 | ], 69 | "no-param-reassign": [ 70 | 2, 71 | { 72 | "props": false 73 | } 74 | ], 75 | "no-console": 0, 76 | "import/prefer-default-export": 0, 77 | "import": 0, 78 | "func-names": 0, 79 | "space-before-function-paren": 0, 80 | "comma-dangle": 0, 81 | "max-len": 0, 82 | "import/extensions": 0, 83 | "no-underscore-dangle": 0, 84 | "consistent-return": 0, 85 | "react/display-name": 1, 86 | "react/no-array-index-key": 0, 87 | "react/react-in-jsx-scope": 0, 88 | "react/prefer-stateless-function": 0, 89 | "react/forbid-prop-types": 0, 90 | "react/no-unescaped-entities": 0, 91 | "jsx-a11y/accessible-emoji": 0, 92 | "jsx-a11y/label-has-associated-control": [ 93 | "error", 94 | { 95 | "assert": "either" 96 | } 97 | ], 98 | "react/require-default-props": 0, 99 | "react/jsx-filename-extension": [ 100 | 1, 101 | { 102 | "extensions": [ 103 | ".js", 104 | ".jsx" 105 | ] 106 | } 107 | ], 108 | "radix": 0, 109 | "no-shadow": [ 110 | 2, 111 | { 112 | "hoist": "all", 113 | "allow": [ 114 | "resolve", 115 | "reject", 116 | "done", 117 | "next", 118 | "err", 119 | "error" 120 | ] 121 | } 122 | ], 123 | "quotes": [ 124 | 2, 125 | "single", 126 | { 127 | "avoidEscape": true, 128 | "allowTemplateLiterals": true 129 | } 130 | ], 131 | "jsx-a11y/href-no-hash": "off", 132 | "jsx-a11y/anchor-is-valid": [ 133 | "warn", 134 | { 135 | "aspects": [ 136 | "invalidHref" 137 | ] 138 | } 139 | ], 140 | "react-hooks/rules-of-hooks": "error", 141 | "react-hooks/exhaustive-deps": "warn" 142 | } 143 | }, 144 | "lint-staged": { 145 | "*.js": "eslint" 146 | }, 147 | "dependencies": { 148 | "@fontsource/source-sans-pro": "^4.5.0", 149 | "algoliasearch": "^4.10.3", 150 | "babel-plugin-styled-components": "^1.13.2", 151 | "gatsby": "^3.15.0", 152 | "gatsby-image": "^3.10.0", 153 | "gatsby-plugin-react-helmet": "^4.10.0", 154 | "gatsby-plugin-styled-components": "^4.12.0", 155 | "gatsby-source-filesystem": "^3.10.0", 156 | "gatsby-transformer-remark": "^4.7.0", 157 | "instantsearch.css": "^7.4.5", 158 | "polished": "^4.1.3", 159 | "prop-types": "^15.7.2", 160 | "react": "^17.0.2", 161 | "react-dom": "^17.0.2", 162 | "react-helmet": "^6.1.0", 163 | "react-instantsearch-dom": "^6.12.1", 164 | "styled-components": "^5.3.1" 165 | }, 166 | "devDependencies": { 167 | "babel-eslint": "^10.1.0", 168 | "eslint": "^7.32.0", 169 | "eslint-config-airbnb": "^18.2.1", 170 | "eslint-config-prettier": "^8.3.0", 171 | "eslint-plugin-html": "^6.1.2", 172 | "eslint-plugin-import": "^2.23.4", 173 | "eslint-plugin-jsx-a11y": "^6.4.1", 174 | "eslint-plugin-react": "^7.24.0", 175 | "eslint-plugin-react-hooks": "^4.2.0", 176 | "husky": "^7.0.1", 177 | "lint-staged": "^11.1.1", 178 | "prettier": "^2.3.2", 179 | "pretty-quick": "^3.1.1" 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /src/components/callToAction.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import styled from 'styled-components'; 4 | import { Link } from 'gatsby'; 5 | 6 | const Button = styled.div` 7 | background-color: #fa743b; 8 | border-color: #fa743b; 9 | cursor: pointer; 10 | text-decoration: none; 11 | color: #ffffff; 12 | text-align: center; 13 | vertical-align: middle; 14 | padding: 0.375rem 0.75rem; 15 | font-size: 0.875rem; 16 | border-radius: 0.25rem; 17 | display: inline-block; 18 | `; 19 | 20 | /* 21 | * A call-to-action button that is a link to an internal page (using the 22 | * Gatsby Link API). 23 | */ 24 | export default function CallToActionLink({ children, to }) { 25 | return ( 26 | 27 | 28 | 29 | ); 30 | } 31 | 32 | CallToActionLink.propTypes = { 33 | children: PropTypes.node, 34 | to: PropTypes.string.isRequired, 35 | }; 36 | -------------------------------------------------------------------------------- /src/components/footer.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'gatsby'; 3 | import styled from 'styled-components'; 4 | 5 | import numfocusStamp from '../../static/Numfocus_stamp.png'; 6 | import dunlapLogo from '../../static/dunlap-logo.png'; 7 | 8 | const FullWidthContainer = styled.div` 9 | width: 100vw; 10 | position: relative; 11 | left: 50%; 12 | right: 50%; 13 | margin: var(--astropy-size-xl) -50vw 0; 14 | background-color: rgb(250, 250, 250); 15 | `; 16 | 17 | const StyledFooter = styled.footer` 18 | margin: 0 auto; 19 | max-width: var(--astropy-content-width); 20 | padding: var(--astropy-size-m) var(--astropy-size-s); 21 | 22 | h2 { 23 | font-size: var(--astropy-font-size-ml); 24 | font-weight: 400; 25 | } 26 | 27 | nav ul { 28 | list-style: none; 29 | padding-left: 0; 30 | } 31 | 32 | nav a { 33 | font-weight: 500; 34 | } 35 | 36 | nav ul li:first-child a { 37 | font-weight: 700; 38 | } 39 | 40 | .footer-content-layer { 41 | display: flex; 42 | flex-flow: row nowrap; 43 | justify-content: space-between; 44 | align-items: flex-start; 45 | margin: var(--astropy-size-m) 0; 46 | } 47 | 48 | .sponsors, 49 | .code-of-conduct { 50 | width: 24rem; 51 | } 52 | 53 | .code-of-conduct p { 54 | margin-bottom: 0; 55 | } 56 | 57 | .numfocusStamp { 58 | margin-top: var(--astropy-size-l); 59 | } 60 | 61 | .sponsors .numfocusStamp__image { 62 | width: 16rem; 63 | } 64 | 65 | .sponsors .dunlapLogo__image { 66 | margin-top: var(--astropy-size-m); 67 | width: 20rem; 68 | } 69 | 70 | .copyright { 71 | margin-top: var(--astropy-size-xl); 72 | } 73 | `; 74 | 75 | /* 76 | * Footer component (contained within a Layout component). 77 | */ 78 | export default function Footer() { 79 | return ( 80 | 81 | 82 | 92 |
93 |
94 |

Code of Conduct

95 |

96 | The Astropy project is committed to fostering an inclusive 97 | community. The community of participants in open source Astronomy 98 | projects is made up of members from around the globe with a 99 | diverse set of skills, personalities, and experiences. It is 100 | through these differences that our community experiences success 101 | and continued growth.{' '} 102 | 103 | Learn more. 104 | 105 |

106 |
107 | 128 |
129 |

130 | Copyright {new Date().getFullYear()} The Astropy Developers 131 |

132 |
133 |
134 | ); 135 | } 136 | -------------------------------------------------------------------------------- /src/components/header.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import { Link } from 'gatsby'; 4 | 5 | import logo from '../../static/learn-astropy-logo.png'; 6 | 7 | const HeaderContainer = styled.header` 8 | width: 100%; 9 | padding: var(--astropy-size-s) var(--astropy-size-m); 10 | margin: 0; 11 | background-color: var(--astropy-nav-header-color); 12 | color: var(--astropy-nav-header-text-color); 13 | 14 | display: flex; 15 | flex-direction: row; 16 | flex-wrap: nowrap; 17 | justify-content: flex-start; 18 | align-items: center; 19 | 20 | .learn-astropy-logo { 21 | width: 12rem; 22 | } 23 | 24 | .main-nav { 25 | display: flex; 26 | margin-left: 2rem; 27 | flex-direction: row 28 | flex-wrap: nowrap; 29 | justify-content: flex-start; 30 | align-items: flex-start; 31 | 32 | @media screen and (max-width: 600px) { 33 | margin-left: -1rem; 34 | } 35 | } 36 | 37 | .astropy-link { 38 | margin-left: auto; 39 | } 40 | 41 | a { 42 | color: var(--astropy-neutral-900); 43 | } 44 | 45 | a:hover { 46 | text-decoration: none; 47 | } 48 | 49 | @media screen and (max-width: 600px) { 50 | display: flex; 51 | flex-direction: column; 52 | flex-wrap: nowrap; 53 | justify-content: flex-start; 54 | align-items: flex-start; 55 | } 56 | `; 57 | 58 | const NavItem = styled.div` 59 | transition: all 0.2s ease-in-out; 60 | margin: 0 1em; 61 | border-bottom: 2px solid transparent; 62 | 63 | &:hover { 64 | border-bottom: 2px solid var(--astropy-primary-color); 65 | color: var(--astropy-primary-color); 66 | } 67 | 68 | .astropy-link { 69 | margin-left: auto; 70 | } 71 | 72 | @media screen and (max-width: 600px) { 73 | width: 100vw; 74 | display: flex; 75 | flex-direction: column; 76 | align-items: flex-start; 77 | padding-top: 10px; 78 | padding-left: 0.5rem; 79 | } 80 | `; 81 | 82 | /* 83 | * Header component that includes the logo, search bar, and navigation tabs. 84 | */ 85 | export default function NavHeader() { 86 | return ( 87 | <> 88 | 89 | 90 | Learn Astropy Homepage 95 | 96 | 101 | 102 | Astropy Project 103 | 104 | 105 | 106 | ); 107 | } 108 | -------------------------------------------------------------------------------- /src/components/instantsearch/hits.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Extension to the Algolia Hits widget that passes props through to individual 3 | * Hit components. 4 | */ 5 | 6 | import React from 'react'; 7 | import PropTypes from 'prop-types'; 8 | import styled from 'styled-components'; 9 | import { connectHits } from 'react-instantsearch-dom'; 10 | 11 | /** 12 | * Custom Hits component that passes props to individual Hit components. 13 | */ 14 | const Hits = ({ hits, hitComponent, className = '' }) => { 15 | const HitComponent = hitComponent; 16 | 17 | return ( 18 |
    19 | {hits.map((hit) => ( 20 |
  1. 21 | 22 |
  2. 23 | ))} 24 |
25 | ); 26 | }; 27 | 28 | const HitPropTypes = PropTypes.shape({ 29 | objectID: PropTypes.oneOfType([PropTypes.string, PropTypes.number]) 30 | .isRequired, 31 | }); 32 | 33 | Hits.propTypes = { 34 | hits: PropTypes.arrayOf(HitPropTypes.isRequired).isRequired, 35 | hitComponent: PropTypes.func.isRequired, 36 | className: PropTypes.string, 37 | }; 38 | 39 | /** 40 | * The Hits component, connected to Algolia instantsearch. 41 | */ 42 | const ConnectedHits = connectHits(Hits); 43 | 44 | /** 45 | * Styled components wrapper for ConnectedHits. 46 | */ 47 | export const StyledHits = styled(ConnectedHits)` 48 | display: block; 49 | margin: 0; 50 | padding: 0; 51 | list-style: none; 52 | 53 | .hits-item { 54 | width: 100%; 55 | margin-bottom: var(--astropy-size-m); 56 | padding: var(--astropy-size-s); 57 | border: 1px solid #ddd; 58 | border-radius: var(--astropy-border-radius-m); 59 | } 60 | `; 61 | -------------------------------------------------------------------------------- /src/components/instantsearch/poweredBy.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Styled version of the Algolia InstantSearch PoweredBy component. 3 | * 4 | * https://www.algolia.com/doc/api-reference/widgets/powered-by/react/ 5 | */ 6 | 7 | import styled from 'styled-components'; 8 | import { PoweredBy as BasePoweredBy } from 'react-instantsearch-dom'; 9 | 10 | /* PoweredBy Algolia InstantSearch widget that's styled. 11 | * 12 | * https://www.algolia.com/doc/api-reference/widgets/powered-by/react/ 13 | */ 14 | const PoweredBy = styled(BasePoweredBy)` 15 | margin-left: var(--astropy-size-m); 16 | .ais-PoweredBy-text { 17 | color: var(--astropy-text-color); 18 | margin-right: var(--astropy-size-s); 19 | } 20 | .ais-PoweredBy-logo path:last-of-type { 21 | fill: var(--algolia-primary-color); 22 | } 23 | `; 24 | 25 | export default PoweredBy; 26 | -------------------------------------------------------------------------------- /src/components/instantsearch/refinementList.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Styled version of the Algolia InstantSearch RefinementList component. 3 | * 4 | * https://www.algolia.com/doc/api-reference/widgets/refinement-list/react/ 5 | */ 6 | 7 | import styled from 'styled-components'; 8 | import { RefinementList as BaseRefinementList } from 'react-instantsearch-dom'; 9 | 10 | const RefinementList = styled(BaseRefinementList)` 11 | .ais-RefinementList-labelText { 12 | margin-left: var(--astropy-size-s); 13 | } 14 | `; 15 | 16 | export default RefinementList; 17 | -------------------------------------------------------------------------------- /src/components/instantsearch/searchBox.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Styled SearchBox that includes the PoweredBy widget. 3 | */ 4 | 5 | import React from 'react'; 6 | import styled from 'styled-components'; 7 | import { SearchBox as SearchBoxCore } from 'react-instantsearch-dom'; 8 | 9 | import PoweredBy from './poweredBy'; 10 | 11 | const SearchBoxContainer = styled.div` 12 | display: flex; /* Lay out box+powered by in line */ 13 | `; 14 | 15 | /* SearchBox Algolia InstantSearch widget that's styled. 16 | * https://www.algolia.com/doc/api-reference/widgets/search-box/react/ 17 | */ 18 | export const StyledSearchBoxCore = styled(SearchBoxCore)` 19 | flex: 1 1 0; 20 | `; 21 | 22 | const SearchBox = () => ( 23 | 24 | 25 | 26 | 27 | ); 28 | 29 | export default SearchBox; 30 | -------------------------------------------------------------------------------- /src/components/instantsearch/virtualPrioritySort.js: -------------------------------------------------------------------------------- 1 | /* 2 | * An extension of the SortBy component that toggles between priority and 3 | * relevance-based sorting whenever the user enters a query string. 4 | * 5 | * The component is "virtual" since there is not visual interaction; the 6 | * toggling happens automatically based on the search state. 7 | */ 8 | 9 | import React from 'react'; 10 | 11 | import PropTypes from 'prop-types'; 12 | 13 | import { connectStateResults, connectSortBy } from 'react-instantsearch-dom'; 14 | 15 | /* 16 | * The VirtualSortBy is a "virtual" SortBy component, meaning that it takes 17 | * the props of SortBy and can control sorting; but it does not render in the 18 | * UI. 19 | */ 20 | const VirtualSortBy = connectSortBy(() => null); 21 | 22 | const PrioritySortCore = ({ 23 | searchState, 24 | priorityRefinement, 25 | relevanceRefinement, 26 | }) => { 27 | const refinement = searchState.query 28 | ? relevanceRefinement 29 | : priorityRefinement; 30 | 31 | const items = [ 32 | { value: priorityRefinement, label: priorityRefinement }, 33 | { value: relevanceRefinement, label: relevanceRefinement }, 34 | ]; 35 | 36 | return ; 37 | }; 38 | 39 | PrioritySortCore.propTypes = { 40 | searchState: PropTypes.object, 41 | priorityRefinement: PropTypes.string, 42 | relevanceRefinement: PropTypes.string, 43 | }; 44 | 45 | const PrioritySort = connectStateResults(PrioritySortCore); 46 | 47 | export default PrioritySort; 48 | -------------------------------------------------------------------------------- /src/components/layout.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import styled from 'styled-components'; 4 | 5 | // Fonts from fontsource 6 | // https://github.com/fontsource/fontsource 7 | import '@fontsource/source-sans-pro/400.css'; 8 | import '@fontsource/source-sans-pro/400-italic.css'; 9 | import '@fontsource/source-sans-pro/700.css'; 10 | 11 | // Full Algolia instantsearch theme includes its reset 12 | import 'instantsearch.css/themes/satellite.css'; 13 | 14 | import GlobalStyles from '../styles/globalStyles'; 15 | import Header from './header'; 16 | import Footer from './footer'; 17 | 18 | /* 19 | * Layout wrapper div. 20 | * 21 | * Its main job is to provide a "sticky footer" so that the Footer component 22 | * stays at the bottom of the page and the Header/MainContent components 23 | * take up any excess space. See 24 | * https://css-tricks.com/couple-takes-sticky-footer/ 25 | */ 26 | const StyledLayout = styled.div` 27 | /* Flexbox for the sticky footer */ 28 | display: flex; 29 | flex-direction: column; 30 | min-height: 100vh; 31 | 32 | .upper-container { 33 | flex: 1 0 auto; 34 | } 35 | .sticky-footer-container { 36 | flex-shrink: 0; 37 | } 38 | `; 39 | 40 | const StyledMain = styled.main` 41 | margin: 0 auto; 42 | max-width: var(--astropy-content-width); 43 | padding: 0 var(--astropy-size-s); 44 | `; 45 | 46 | /* 47 | * The Layout component wraps the contents of every page. 48 | */ 49 | export default function Layout({ children }) { 50 | return ( 51 | <> 52 | 53 | 54 |
55 |
56 | {children} 57 |
58 |
59 |
60 |
61 |
62 | 63 | ); 64 | } 65 | 66 | Layout.propTypes = { 67 | children: PropTypes.node.isRequired, 68 | }; 69 | -------------------------------------------------------------------------------- /src/components/pageCover.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import styled from 'styled-components'; 4 | 5 | const FullWidthContainer = styled.div` 6 | width: 100vw; 7 | position: relative; 8 | left: 50%; 9 | right: 50%; 10 | margin: 0 -50vw; 11 | background-color: var(--astropy-nav-header-color); 12 | color: var(--astropy-nav-header-text-color); 13 | 14 | @media screen and (max-width: 600px) { 15 | padding: 20px; 16 | } 17 | `; 18 | 19 | const StyledPageCoverContent = styled.header` 20 | margin: 0 auto; 21 | max-width: var(--astropy-content-width); 22 | padding: var(--astropy-size-m) var(--astropy-size-s); 23 | 24 | p { 25 | font-size: var(--astropy-font-size-ml); 26 | } 27 | 28 | @media screen and (max-width: 600px) { 29 | margin: 0 auto; 30 | } 31 | `; 32 | 33 | /* 34 | * The page cover is meant to contain the title and lead paragraph of a 35 | * content page. 36 | */ 37 | export default function PageCover({ children }) { 38 | return ( 39 | 40 | {children} 41 | 42 | ); 43 | } 44 | 45 | PageCover.propTypes = { 46 | children: PropTypes.node.isRequired, 47 | }; 48 | -------------------------------------------------------------------------------- /src/components/resultCard.js: -------------------------------------------------------------------------------- 1 | /** 2 | * The ResultCard renders an algoliasearch hit. 3 | * 4 | * See https://www.algolia.com/doc/api-reference/widgets/hits/react/ 5 | */ 6 | 7 | import React from 'react'; 8 | import PropTypes from 'prop-types'; 9 | import styled from 'styled-components'; 10 | import { Snippet } from 'react-instantsearch-dom'; 11 | 12 | const ResultCardContainer = styled.div` 13 | .result-title { 14 | display: flex; 15 | flex: 1 1 0; 16 | align-items: center; 17 | margin-bottom: 1rem; 18 | } 19 | 20 | h2 { 21 | line-height: 1.1; 22 | margin: 0; 23 | font-size: var(--astropy-font-size-m); 24 | } 25 | 26 | a { 27 | text-decoration: none; 28 | 29 | &: hover { 30 | text-decoration: underline; 31 | } 32 | } 33 | 34 | .content-type-tag { 35 | background-color: var(--astropy-primary-color); 36 | border-radius: var(--astropy-border-radius-m); 37 | color: white; 38 | font-size: 0.6rem; 39 | font-weight: 700; 40 | text-transform: uppercase; 41 | letter-spacing: 0.05em; 42 | margin-right: var(--astropy-size-s); 43 | padding: 0.125rem var(--astropy-size-xs); 44 | } 45 | 46 | .root-title { 47 | font-size: var(--astropy-font-size-s); 48 | margin: -0.5rem 0 1rem; 49 | } 50 | 51 | .sidebyside { 52 | display: flex; 53 | } 54 | 55 | .sidebyside__image { 56 | margin-right: 1rem; 57 | flex: 0 0 8rem; 58 | } 59 | 60 | .sidebyside__image img { 61 | width: 100%; 62 | } 63 | 64 | .sidebyside__content { 65 | flex: 1 1 auto; 66 | 67 | font-size: var(--astropy-font-size-s); 68 | } 69 | 70 | .sidebyside__content *:first-child { 71 | margin-top: 0; 72 | } 73 | `; 74 | 75 | const StyledSnippetBlock = styled.blockquote` 76 | padding: 0.5rem 1rem; 77 | margin-left: 0; 78 | margin-right: 0; 79 | border-left: 4px solid #ddd; 80 | background: #eee; 81 | border-radius: var(--astropy-border-radius-s); 82 | 83 | &::before { 84 | content: '[…] '; 85 | opacity: 0.5; 86 | } 87 | 88 | &::after { 89 | content: '[…]'; 90 | opacity: 0.5; 91 | } 92 | `; 93 | 94 | const StyledSnippet = styled(Snippet)` 95 | span, 96 | ${({ tagName }) => tagName} { 97 | // more specific than Algolia theme 98 | font-size: var(--astropy-font-size-s); 99 | } 100 | 101 | ${({ tagName }) => tagName} { 102 | background: yellow; 103 | } 104 | `; 105 | 106 | const ResultCard = ({ hit }) => { 107 | let linkUrl; 108 | let title; 109 | 110 | if (hit.content_type === 'guide' && hit.importance > 1) { 111 | linkUrl = hit.url; 112 | title = hit.h1; 113 | } else { 114 | linkUrl = hit.root_url; 115 | title = hit.root_title; 116 | } 117 | 118 | return ( 119 | 120 |
121 | {hit.content_type} 122 | 123 |

{title}

124 |
125 |
126 | {hit.content_type === 'guide' && hit.importance > 1 && ( 127 |

128 | Inside{' '} 129 | 130 | {hit.root_title} 131 | 132 |

133 | )} 134 | 135 |
136 | {hit.thumbnail_url && ( 137 |
138 | 139 | 140 | 141 |
142 | )} 143 |
144 |

{hit.root_summary}

145 | {hit._snippetResult.content.matchLevel !== 'none' && ( 146 | 147 | {' '} 153 | 154 | )} 155 |
156 |
157 |
158 | ); 159 | }; 160 | 161 | ResultCard.propTypes = { 162 | hit: PropTypes.object.isRequired, 163 | }; 164 | 165 | export default ResultCard; 166 | -------------------------------------------------------------------------------- /src/components/searchLayout.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | import bp from '../styles/breakpoints'; 4 | 5 | export const SearchLayout = styled.div` 6 | grid-template-columns: 16rem 1fr; 7 | grid-template-rows: auto 1fr; 8 | grid-column-gap: 2rem; 9 | grid-row-gap: 2rem; 10 | margin-top: 4rem; 11 | 12 | /* 13 | * Use grid layout on bigger screens. 14 | */ 15 | @media only screen and (min-width: ${bp.phone}) { 16 | display: grid; 17 | } 18 | 19 | .search-box-area { 20 | grid-column: 2 / 3; 21 | grid-row: 1 / 2; 22 | 23 | @media screen and (max-width: 750px) { 24 | grid-column: 1 / 3; 25 | } 26 | } 27 | 28 | .search-refinements-area { 29 | grid-column: 1 / 2; 30 | grid-row: 1 / 3; 31 | 32 | margin-top: 1rem; 33 | @media only screen and (min-width: ${bp.phone}) { 34 | margin-top: 0; 35 | } 36 | @media screen and (max-width: 750px) { 37 | display: none; 38 | } 39 | } 40 | 41 | .search-results-area { 42 | grid-column: 2 / 3; 43 | grid-row: 2 / 3; 44 | 45 | @media screen and (max-width: 750px) { 46 | grid-column: 1 / 3; 47 | } 48 | } 49 | `; 50 | 51 | /* Styled component div around a refinement widget. 52 | * 53 | * This styling controls spacing and the heading styling 54 | */ 55 | export const SearchRefinementsSection = styled.div` 56 | margin-bottom: var(--astropy-size-l); 57 | 58 | h2 { 59 | margin-top: 0; 60 | font-size: var(--astropy-font-size-ml); 61 | } 62 | `; 63 | -------------------------------------------------------------------------------- /src/components/seo.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import Helmet from 'react-helmet'; 4 | import { useStaticQuery, graphql } from 'gatsby'; 5 | 6 | /* 7 | * SEO component that adds tags to the page's header using react-helmet. 8 | */ 9 | export default function SEO({ children, location, title, description, image }) { 10 | const { site } = useStaticQuery( 11 | graphql` 12 | query { 13 | site { 14 | siteMetadata { 15 | title 16 | description 17 | author 18 | siteUrl 19 | twitter 20 | } 21 | } 22 | } 23 | ` 24 | ); 25 | 26 | // The description can be overidden for individual pages via description 27 | // prop. 28 | const desc = description || site.siteMetadata.description; 29 | 30 | // The page's canonical URL 31 | const canonicalUrl = site.siteMetadata.siteUrl + location.pathname; 32 | 33 | return ( 34 | 35 | 36 | {title} 37 | {/* Favicon */} 38 | 39 | 40 | {/* General meta tags */} 41 | 42 | 43 | 44 | 45 | {/* Open Graph */} 46 | 47 | 52 | 53 | 58 | 59 | {/* Twitter card */} 60 | 65 | {children} 66 | 67 | ); 68 | } 69 | 70 | SEO.propTypes = { 71 | children: PropTypes.node, 72 | title: PropTypes.string.isRequired, 73 | description: PropTypes.string, 74 | location: PropTypes.shape({ 75 | pathname: PropTypes.string.isRequired, 76 | }), 77 | image: PropTypes.string, 78 | }; 79 | -------------------------------------------------------------------------------- /src/contributing/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'Contributing to Learn Astropy' 3 | slug: '/contributing/' 4 | --- 5 | 6 | We are always interested in incorporating new tutorials into Learn Astropy and the Astropy Tutorials series. We welcome tutorials covering astro-relevant topics; they do not need to use the Astropy package in order to be hosted or indexed here. If you have astronomy tutorials that you would like to contribute, or if you have a separate tutorial series that you would like indexed by the Learn Astropy website, see below. 7 | 8 | ## Overview 9 | 10 | Each tutorial is a [Jupyter notebook](https://jupyter.org/) file in a unique repository `tutorial--*` in the [astropy-learn organization](https://github.com/astropy-learn). For example, let's look at the [FITS-header](https://github.com/astropy-learn/tutorial--FITS-header/tree/main/) tutorial. The repository has a few files that authors should always write/amend when contributing a new tutorial or set of tutorials: 11 | 12 | - A single Jupyter notebook file that contains the text and code for the tutorial (it should follow the content guidelines below), 13 | - Any small data files used in the tutorial (in this case, a single FITS file), 14 | - A `requirements.txt` file that specifies the required packages to run the notebook, and 15 | - An `AUTHORS.md` file that lists the notebook authors. 16 | - If you are contributing multiple, thematically-linked notebooks, the `index.md` file should summarize the contents of the individual notebooks (it will be the first page in the 'jupyter book' containing all individual notebooks). Additionally each `.ipynb` filename should be preceded by a number and an underscore, in the order the notebooks should appear in the book, e.g.: 17 | ``` 18 | 1_intro-to-modeling 19 | 2_applying-model-to-data 20 | ``` 21 | 22 | The notebook file(s) are automatically run and converted into a static HTML page ([for example](https://learn.astropy.org/tutorials/FITS-header.html)), which is then displayed in the listing on the main tutorials webpage, http://tutorials.astropy.org. 23 | 24 | ## Content Guidelines 25 | 26 | ### Overview 27 | 28 | - Each tutorial should have 3–5 explicit [Learning Goals](http://tll.mit.edu/help/intended-learning-outcomes), demonstrate ~2–3 pieces of functionality relevant to astronomy, and contain 2–3 demonstrations of generic but commonly used functionality (e.g., `numpy`, `matplotlib`). 29 | - Each tutorial should roughly follow this progression: 30 | - _Input/Output_: read in some data (use [astroquery](https://astroquery.readthedocs.io/en/latest/) where possible to query real astronomical datasets) 31 | - _Analysis_: do something insightful / useful with the data 32 | - _Visualization_: make a pretty figure (use [astropy.visualization](http://docs.astropy.org/en/stable/visualization/) where possible) 33 | - The tutorials must be compatible with the versions supported by the last major release of the Astropy core package (i.e. Python >= 3.5) 34 | 35 | ### Template intro 36 | 37 | The first cell in every tutorial notebook is a markdown cell used for the title, author list, keywords, and summary. All of this information should be contained in a single cell and should adhere to the following format: 38 | 39 | ``` 40 | # Title name 41 | 42 | ## Authors 43 | Jane Smith (@GITHUB-ID, ORCID-ID), Jose Jones (@GITHUB-ID, ORCID-ID) 44 | 45 | ## Learning Goals 46 | * Query the ... dataset 47 | * Calculate ... 48 | * Display ... 49 | 50 | ## Keywords (please draw from [this list](https://github.com/astropy-learn/astropy-tutorials/blob/main/resources/keywords.md)) 51 | Example, example, example 52 | 53 | ## Companion Content 54 | Carroll & Ostlie 10.3, Binney & Tremaine 1.5 55 | 56 | ## Summary 57 | In this tutorial, we will download a data file, do something to it, and then 58 | visualize it. 59 | ``` 60 | 61 | ### Code 62 | 63 | - Demonstrate good commenting practice 64 | - Add comments to sections of code that use concepts not included in the Learning Goals 65 | - Demonstrate best practices of variable names 66 | - Variables should be all lower case with words separated by underscores 67 | - Variable names should be descriptive, e.g., `galaxy_mass`, `u_mag` 68 | - Use the print function explicitly to display information about variables 69 | - As much as possible, comply with [PEP8](https://www.python.org/dev/peps/pep-0008/). 70 | - As much as possible, comply with Jupyter notebook style guides - [STScI style guide](https://github.com/spacetelescope/style-guides/blob/master/guides/jupyter-notebooks.md) and [Official Coding Style](https://jupyter.readthedocs.io/en/latest/development_guide/coding_style.html). 71 | - Imports 72 | - Do not use `from package import *`; import packages, classes, and functions explicitly 73 | - Follow recommended package name abbreviations: 74 | - `import numpy as np` 75 | - `import matplotlib as mpl` 76 | - `import matplotlib.pyplot as plt` 77 | - `import astropy.units as u` 78 | - `import astropy.coordinates as coord` 79 | - `from astropy.io import fits` 80 | - Display figures inline using matplotlib's inline backend: 81 | - `%matplotlib inline # make plots display in notebooks` 82 | 83 | ### Narrative 84 | 85 | - Please read through the other tutorials to get a sense of the desired tone and length. 86 | - Use [Markdown formatting](http://jupyter-notebook.readthedocs.io/en/latest/examples/Notebook/Working%20With%20Markdown%20Cells.html) in text cells for formatting, links, latex, and code snippets. 87 | - Titles should be short yet descriptive and emphasize the learning goals of the tutorial. Try to make the title appeal to a broad audience and avoid referencing a specific instrument, catalog, or anything wavelength dependent. 88 | - List all authors' full names (comma separated) and link to GitHub profiles and/or [ORCID iD](https://orcid.org/) when relevant. 89 | - Include [Learning Goals](http://tll.mit.edu/help/intended-learning-outcomes) at the top as a bulleted list. 90 | - Include Keywords as a comma separated list of topics, packages, and functions demonstrated. 91 | - The first paragraph should give a brief overview of the entire tutorial including relevant astronomy concepts. 92 | - Use the first-person inclusive plural ("we"). For example, "We are going to make a plot which...", or "Above, we did it the hard way, but here is the easier way..." 93 | - Section headings should be in the imperative mood. For example, "Download the data." 94 | - Avoid extraneous words such as "obviously", "just", "simply", or "easily." For example, avoid phrases like "we just have to do this one thing." 95 | - Use `
Note
` for Notes and `
Warning
` for Warnings (Markdown supports raw HTML) 96 | 97 | ## Procedure for contributing a notebook or set of notebooks 98 | 99 | There are two methods for submitting a tutorial or set of thematically linked tutorials. 100 | 101 | ### Method 1: Provide a link 102 | 103 | - [Open an issue on the astropy-tutorials Github repo](https://github.com/astropy/astropy-tutorials/issues) with a link to your Jupyter notebook(s). 104 | 105 | Learn Astropy maintainers will download your notebook, test it, and edit the file if necessary to conform to the above style guide. When the tutorial is ready to be incorporated, maintainers will open a pull request on behalf of the tutorial authors. 106 | 107 | ### Method 2: Submit a pull request 108 | 109 | This process for contributing a tutorial involves the [GitHub fork](https://help.github.com/articles/working-with-forks/) and `git` workflow concepts [branch, push, pull request](https://help.github.com/articles/proposing-changes-to-your-work-with-pull-requests/). 110 | 111 | To contribute a new tutorial, first fork the [Astropy Learn tutorial template repository](https://github.com/astropy-learn/tutorial--template). Then clone your fork locally to your machine (replace `` with your GitHub username): 112 | 113 | ``` 114 | git clone git@github.com:/astropy-tutorials.git 115 | ``` 116 | 117 | Next, create a branch in your local repository with the name of the tutorial you'd like to contribute. Say we're adding a tutorial to demonstrate spectral line fitting -- we might call it "Spectral-Line-Fitting": 118 | 119 | ``` 120 | git checkout -b Spectral-Line-Fitting 121 | ``` 122 | 123 | Include the notebook `.ipynb` file(s) and any data files used by the notebook (see the 'Data files' section below). Update the `AUTHORS.md` file. Update the `requirements.txt` file with the Python packages the tutorial depends on and files. For example, if your tutorial requires `scipy` version 1.0 and `numpy` version 1.13 or greater, your `requirements.txt` file would look like: 124 | 125 | ``` 126 | scipy==1.0 127 | numpy>=1.13 128 | ``` 129 | 130 | Push the notebook and other files from your local branch up to your fork of the repository on GitHub (by default, named 'origin'): 131 | 132 | ``` 133 | git push origin Spectral-Line-Fitting 134 | ``` 135 | 136 | When the tutorial is ready for submission, [open a pull request](https://help.github.com/articles/creating-a-pull-request/) against the main [`tutorial--template` repository](https://github.com/astropy-learn/tutorial--template), and your submission will be reviewed. 137 | 138 | ## Data files 139 | 140 | If your tutorial includes large data files (where large means >~ 1 MB), including them in the tutorial's repository would drastically slow down cloning of the repository. Instead, for files < 10 MB, we encourage use of the `astropy.utils.download_files` function, and we will host data files on the http://data.astropy.org server (or you can do this directly by opening a PR at the https://github.com/astropy/astropy-data repository). Alternatively, if the file size is > 10 MB, the data should be hosted on Zenodo. To do the former, use the following procedure: 141 | 142 | - If contributing your notebook(s) via a pull request, include the data files (e.g., `tutorials/notebooks/My-tutorial-name/mydatafile.fits`). **IMPORTANT**: when you add or modify data files, make sure the only thing in that commit involves the data files. That is, do _not_ edit your notebook and add/change data files in the same commit. This will make it easier to remove the data files when your tutorial is merged. 143 | 144 | - To access your data files in the notebook, do something like this at the top of the notebook: 145 | 146 | ``` 147 | from astropy.utils.data import download_file 148 | 149 | tutorialpath = '' 150 | mydatafilename1 = download_file(tutorialpath + 'mydatafile1.fits', cache=True) 151 | mydatafilename2 = download_file(tutorialpath + 'mydatafile2.dat', cache=True) 152 | ``` 153 | 154 | And then use them like this: 155 | 156 | ``` 157 | fits.open(mydatafilename1) 158 | ... 159 | with open(mydatafilename2) as f: 160 | ... 161 | ``` 162 | 163 | If you do this, the only change necessary when merging your notebook will be to set `tutorialpath` to `'http://data.astropy.org/tutorials/My-tutorial-name/'`. 164 | 165 | For data files that are larger than 10 MB in size, we recommend hosting with Zenodo. To use this approach, follow these steps: 166 | 167 | - Sign up for an account at https://zenodo.org/ if you do not have one already. 168 | 169 | - Log in to Zenodo and perform a new upload. Follow the Zenodo instructions and complete all the required fields in order to have the data file(s) uploaded to their records. Once this is done you will have a link to share the data. 170 | 171 | - With the link to the data file record, which has the format `https://zenodo.org/api/records/:id`, an example HTTP GET request needed to retrieve the data using the Python package `requests` is shown below: 172 | 173 | ``` 174 | import requests 175 | r = requests.get("https://zenodo.org/api/records/1234) 176 | ``` 177 | 178 | To use the output as a locally stored file, you would first need to write the file contents to a file, for example: 179 | 180 | ``` 181 | with open('./some-data-file.fits', 'wb') as f: 182 | f.write(r.content) 183 | ``` 184 | -------------------------------------------------------------------------------- /src/pages/404.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Link } from 'gatsby'; 4 | import SEO from '../components/seo'; 5 | 6 | // styles 7 | const pageStyles = { 8 | color: '#232129', 9 | padding: '96px', 10 | fontFamily: '-apple-system, Roboto, sans-serif, serif', 11 | }; 12 | const headingStyles = { 13 | marginTop: 0, 14 | marginBottom: 64, 15 | maxWidth: 320, 16 | }; 17 | 18 | const paragraphStyles = { 19 | marginBottom: 48, 20 | }; 21 | const codeStyles = { 22 | color: '#8A6534', 23 | padding: 4, 24 | backgroundColor: '#FFF4DB', 25 | fontSize: '1.25rem', 26 | borderRadius: 4, 27 | }; 28 | 29 | // markup 30 | const NotFoundPage = ({ location }) => ( 31 |
32 | 33 |

Page not found

34 |

35 | Sorry{' '} 36 | 37 | 😔 38 | {' '} 39 | we couldn’t find what you were looking for. 40 |
41 | {process.env.NODE_ENV === 'development' ? ( 42 | <> 43 |
44 | Try creating a page in src/pages/. 45 |
46 | 47 | ) : null} 48 |
49 | Go home. 50 |

51 |
52 | ); 53 | 54 | export default NotFoundPage; 55 | 56 | NotFoundPage.propTypes = { 57 | location: PropTypes.shape({ 58 | pathname: PropTypes.string.isRequired, 59 | }), 60 | }; 61 | -------------------------------------------------------------------------------- /src/pages/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import { InstantSearch, Configure } from 'react-instantsearch-dom'; 5 | 6 | import Layout from '../components/layout'; 7 | import { 8 | SearchLayout, 9 | SearchRefinementsSection, 10 | } from '../components/searchLayout'; 11 | import SEO from '../components/seo'; 12 | import PageCover from '../components/pageCover'; 13 | import searchClient from '../searchClient'; 14 | import { StyledHits } from '../components/instantsearch/hits'; 15 | import RefinementList from '../components/instantsearch/refinementList'; 16 | import SearchBox from '../components/instantsearch/searchBox'; 17 | import PrioritySort from '../components/instantsearch/virtualPrioritySort'; 18 | import ResultCard from '../components/resultCard'; 19 | 20 | export default function IndexPage({ location }) { 21 | return ( 22 | 23 | 24 | 25 |

Learn Astropy

26 |

27 | Learn how to use Python for astronomy through tutorials and guides 28 | that cover Astropy and other packages in the astronomy Python 29 | ecosystem. 30 |

31 |
32 | 33 | 34 | 35 | 39 | 40 |
41 | 42 |
43 |
44 | 45 |

Format

46 | 47 |
48 | 49 |

Astropy packages

50 | 57 |
58 | 59 |

Python packages

60 | 67 |
68 | 69 |

Tasks

70 | 77 |
78 | 79 |

Science domains

80 | 87 |
88 |
89 |
90 | 91 |
92 |
93 |
94 |
95 | ); 96 | } 97 | 98 | IndexPage.propTypes = { 99 | location: PropTypes.shape({ 100 | pathname: PropTypes.string.isRequired, 101 | }), 102 | }; 103 | -------------------------------------------------------------------------------- /src/searchClient.js: -------------------------------------------------------------------------------- 1 | import algoliasearch from 'algoliasearch/lite'; 2 | 3 | // This is the Search-only API key 4 | const searchClient = algoliasearch( 5 | 'H6MWLDHTG5', 6 | '45fc58ab091520ba879eb8952b422c4e' 7 | ); 8 | 9 | export default searchClient; 10 | -------------------------------------------------------------------------------- /src/styles/breakpoints.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Breakpoints. 3 | * 4 | * We're using em units for media queries so that they scale with zooming. 5 | * Note that ems in media queries are always relative to the **default root 6 | * font size**, not the font-size set on :root. See Ch 5 of Scott Brown's 7 | * Flexible Typography. 8 | */ 9 | 10 | const bp = { 11 | phone: '24em', // maximum width of a phone (vertical) 12 | }; 13 | 14 | export default bp; 15 | -------------------------------------------------------------------------------- /src/styles/globalStyles.js: -------------------------------------------------------------------------------- 1 | import { createGlobalStyle } from 'styled-components'; 2 | 3 | import { normalize } from 'polished'; 4 | 5 | const GlobalStyles = createGlobalStyle` 6 | /* 7 | * CSS reset via normalize. 8 | */ 9 | ${normalize()} 10 | 11 | html { 12 | box-sizing: border-box; 13 | } 14 | 15 | /* 16 | * Inherit border-box sizing from html 17 | * https://css-tricks.com/inheriting-box-sizing-probably-slightly-better-best-practice/ 18 | */ 19 | *, 20 | *:before, 21 | *:after { 22 | box-sizing: inherit; 23 | } 24 | 25 | :root { 26 | /* 27 | * Reinforce that we're respecting the user's ability to set a default 28 | * font size. The rem unit now becomes relative to this. 29 | * Flexible Typesetting, Tim Brown, ch 2 and 4 30 | */ 31 | font-size: 1.1rem; 32 | 33 | /* 34 | * Design tokens: Color palette 35 | */ 36 | --astropy-primary-color: #fa743b; 37 | --astropy-neutral-100: #111111; 38 | --astropy-neutral-900: #ffffff; 39 | --algolia-primary-color: #182359; 40 | 41 | /* 42 | * Design tokens: Sizes 43 | */ 44 | --astropy-size-xxs: 0.125rem; 45 | --astropy-size-xs: 0.25rem; 46 | --astropy-size-s: 0.5rem; 47 | --astropy-size-m: 1rem; 48 | --astropy-size-ml: 1.2rem; 49 | --astropy-size-l: 2rem; 50 | --astropy-size-xl: 4rem; 51 | 52 | /* 53 | * Design tokens: font sizes 54 | */ 55 | --astropy-font-size-s: 0.8rem; 56 | --astropy-font-size-m: 1rem; 57 | --astropy-font-size-ml: 1.2rem; 58 | 59 | /* 60 | * Design tokens: border radii 61 | */ 62 | --astropy-border-radius-s: 0.125rem; 63 | --astropy-border-radius-m: 0.25rem; 64 | --astropy-border-radius-l: 0.5rem; 65 | 66 | /* 67 | * Applied colors 68 | */ 69 | --astropy-text-color: var(--astropy-neutral-100); 70 | --astropy-page-background-color: var(--astropy-neutral-900); 71 | --astropy-nav-header-color: var(--astropy-neutral-100); 72 | --astropy-nav-header-text-color: var(--astropy-neutral-900); 73 | 74 | /* 75 | * Applied sizes 76 | */ 77 | --astropy-content-width: 60em; 78 | } 79 | 80 | html, body { 81 | padding: 0; 82 | margin: 0; 83 | font-family: "Source Sans Pro", -apple-system, BlinkMacSystemFont, Segoe UI, 84 | Roboto, Oxygen, Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, 85 | sans-serif; 86 | line-height: 1.45; 87 | color: var(--astropy-text-color); 88 | background-color: var(--astropy-page-background-color); 89 | } 90 | 91 | a { 92 | color: var(--astropy-primary-color); 93 | font-weight: 700; 94 | text-decoration: none; 95 | } 96 | 97 | a:hover { 98 | text-decoration: solid underline var(--astropy-primary-color) 2px; 99 | } 100 | `; 101 | 102 | export default GlobalStyles; 103 | -------------------------------------------------------------------------------- /src/templates/contribTemplate.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Template for contributing/documentation pages that are sourced from 3 | * Markdown. 4 | */ 5 | 6 | import React from 'react'; 7 | import PropTypes from 'prop-types'; 8 | import { graphql } from 'gatsby'; 9 | 10 | import Layout from '../components/layout'; 11 | import SEO from '../components/seo'; 12 | 13 | export const pageQuery = graphql` 14 | query ($slug: String!) { 15 | markdownRemark(frontmatter: { slug: { eq: $slug } }) { 16 | html 17 | frontmatter { 18 | slug 19 | title 20 | } 21 | } 22 | } 23 | `; 24 | 25 | export default function Template({ data, location }) { 26 | const { markdownRemark } = data; // data.markdownRemark holds your post data 27 | const { frontmatter, html } = markdownRemark; 28 | 29 | return ( 30 | 31 | 32 | 33 |

{frontmatter.title}

34 | {/* eslint-disable react/no-danger */} 35 |
36 | {/* eslint-enable react/no-danger */} 37 | 38 | ); 39 | } 40 | 41 | Template.propTypes = { 42 | data: PropTypes.object.isRequired, 43 | location: PropTypes.shape({ 44 | pathname: PropTypes.string.isRequired, 45 | }), 46 | }; 47 | -------------------------------------------------------------------------------- /static/CNAME: -------------------------------------------------------------------------------- 1 | learn.astropy.org 2 | -------------------------------------------------------------------------------- /static/Numfocus_stamp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/astropy-learn/learn-astropy/8a19529d9b377f755036fa4b51f620f26e13448e/static/Numfocus_stamp.png -------------------------------------------------------------------------------- /static/astropy_favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/astropy-learn/learn-astropy/8a19529d9b377f755036fa4b51f620f26e13448e/static/astropy_favicon.ico -------------------------------------------------------------------------------- /static/astropy_logo_notext.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 20 | 22 | 24 | 28 | 32 | 33 | 36 | 40 | 41 | 43 | 47 | 51 | 52 | 56 | 62 | 67 | 72 | 76 | 77 | 87 | 88 | 94 | 99 | 104 | 108 | 109 | 119 | 120 | 130 | 131 | 155 | 157 | 158 | 160 | image/svg+xml 161 | 163 | 164 | 165 | 166 | 167 | 172 | 184 | 197 | 207 | 213 | 216 | 221 | 226 | 231 | 232 | 238 | 251 | 252 | 253 | -------------------------------------------------------------------------------- /static/dunlap-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/astropy-learn/learn-astropy/8a19529d9b377f755036fa4b51f620f26e13448e/static/dunlap-logo.png -------------------------------------------------------------------------------- /static/learn-astropy-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/astropy-learn/learn-astropy/8a19529d9b377f755036fa4b51f620f26e13448e/static/learn-astropy-logo.png --------------------------------------------------------------------------------