mirror of
https://github.com/kamranahmedse/developer-roadmap.git
synced 2026-03-13 02:01:57 +08:00
Compare commits
24 Commits
roadmap/de
...
best-pract
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bf7b1b02bd | ||
|
|
7182312a18 | ||
|
|
06fdfd780f | ||
|
|
3e56e83ece | ||
|
|
516c5fac3a | ||
|
|
525cad3189 | ||
|
|
c8d15f37dd | ||
|
|
17ad153583 | ||
|
|
b7237cc2dc | ||
|
|
4d0e0e3cd7 | ||
|
|
f50aefd5a5 | ||
|
|
1928b89d71 | ||
|
|
dbcf06244b | ||
|
|
035e6a7abf | ||
|
|
d88c87bf52 | ||
|
|
87a50af927 | ||
|
|
0558c56fce | ||
|
|
1406458583 | ||
|
|
26f36a05f2 | ||
|
|
ffa8de84a6 | ||
|
|
7fee35237a | ||
|
|
16eef91b30 | ||
|
|
7355818e49 | ||
|
|
d197b91c0f |
144
bin/best-practice-content.cjs
Normal file
144
bin/best-practice-content.cjs
Normal file
@@ -0,0 +1,144 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const CONTENT_DIR = path.join(__dirname, '../content');
|
||||
// Directory containing the best-practices
|
||||
const BEST_PRACTICE_CONTENT_DIR = path.join(__dirname, '../src/best-practices');
|
||||
const bestPracticeId = process.argv[2];
|
||||
|
||||
const allowedBestPracticeId = fs.readdirSync(BEST_PRACTICE_CONTENT_DIR);
|
||||
if (!bestPracticeId) {
|
||||
console.error('bestPractice is required');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!allowedBestPracticeId.includes(bestPracticeId)) {
|
||||
console.error(`Invalid best practice key ${bestPracticeId}`);
|
||||
console.error(`Allowed keys are ${allowedBestPracticeId.join(', ')}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Directory holding the best parctice content files
|
||||
const bestPracticeDirName = fs
|
||||
.readdirSync(BEST_PRACTICE_CONTENT_DIR)
|
||||
.find((dirName) => dirName.replace(/\d+-/, '') === bestPracticeId);
|
||||
|
||||
if (!bestPracticeDirName) {
|
||||
console.error('Best practice directory not found');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const bestPracticeDirPath = path.join(BEST_PRACTICE_CONTENT_DIR, bestPracticeDirName);
|
||||
const bestPracticeContentDirPath = path.join(
|
||||
BEST_PRACTICE_CONTENT_DIR,
|
||||
bestPracticeDirName,
|
||||
'content'
|
||||
);
|
||||
|
||||
// If best practice content already exists do not proceed as it would override the files
|
||||
if (fs.existsSync(bestPracticeContentDirPath)) {
|
||||
console.error(`Best Practice content already exists @ ${bestPracticeContentDirPath}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
function prepareDirTree(control, dirTree) {
|
||||
// Directories are only created for groups
|
||||
if (control.typeID !== '__group__') {
|
||||
return;
|
||||
}
|
||||
|
||||
// e.g. 104-testing-your-apps:other-options
|
||||
const controlName = control?.properties?.controlName || '';
|
||||
|
||||
// No directory for a group without control name
|
||||
if (!controlName || controlName.startsWith('check:') || controlName.startsWith('ext_link:')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// e.g. ['testing-your-apps', 'other-options']
|
||||
const dirParts = controlName.split(':');
|
||||
|
||||
// Nest the dir path in the dirTree
|
||||
let currDirTree = dirTree;
|
||||
dirParts.forEach((dirPart) => {
|
||||
currDirTree[dirPart] = currDirTree[dirPart] || {};
|
||||
currDirTree = currDirTree[dirPart];
|
||||
});
|
||||
|
||||
const childrenControls = control.children.controls.control;
|
||||
// No more children
|
||||
if (childrenControls.length) {
|
||||
childrenControls.forEach((childControl) => {
|
||||
prepareDirTree(childControl, dirTree);
|
||||
});
|
||||
}
|
||||
|
||||
return { dirTree };
|
||||
}
|
||||
|
||||
const bestPractice = require(path.join(__dirname, `../public/jsons/best-practices/${bestPracticeId}`));
|
||||
const controls = bestPractice.mockup.controls.control;
|
||||
|
||||
// Prepare the dir tree that we will be creating
|
||||
const dirTree = {};
|
||||
|
||||
controls.forEach((control) => {
|
||||
prepareDirTree(control, dirTree);
|
||||
});
|
||||
|
||||
/**
|
||||
* @param parentDir Parent directory in which directory is to be created
|
||||
* @param dirTree Nested dir tree to be created
|
||||
* @param filePaths The mapping from groupName to file path
|
||||
*/
|
||||
function createDirTree(parentDir, dirTree, filePaths = {}) {
|
||||
const childrenDirNames = Object.keys(dirTree);
|
||||
const hasChildren = childrenDirNames.length !== 0;
|
||||
|
||||
// @todo write test for this, yolo for now
|
||||
const groupName = parentDir
|
||||
.replace(bestPracticeContentDirPath, '') // Remove base dir path
|
||||
.replace(/(^\/)|(\/$)/g, '') // Remove trailing slashes
|
||||
.replaceAll('/', ':') // Replace slashes with `:`
|
||||
.replace(/:\d+-/, ':');
|
||||
|
||||
const humanizedGroupName = groupName
|
||||
.split(':')
|
||||
.pop()
|
||||
?.replaceAll('-', ' ')
|
||||
.replace(/^\w/, ($0) => $0.toUpperCase());
|
||||
|
||||
// If no children, create a file for this under the parent directory
|
||||
if (!hasChildren) {
|
||||
let fileName = `${parentDir}.md`;
|
||||
fs.writeFileSync(fileName, `# ${humanizedGroupName}`);
|
||||
|
||||
filePaths[groupName || 'home'] = fileName.replace(CONTENT_DIR, '');
|
||||
return filePaths;
|
||||
}
|
||||
|
||||
// There *are* children, so create the parent as a directory
|
||||
// and create `index.md` as the content file for this
|
||||
fs.mkdirSync(parentDir);
|
||||
|
||||
let readmeFilePath = path.join(parentDir, 'index.md');
|
||||
fs.writeFileSync(readmeFilePath, `# ${humanizedGroupName}`);
|
||||
|
||||
filePaths[groupName || 'home'] = readmeFilePath.replace(CONTENT_DIR, '');
|
||||
|
||||
// For each of the directory names, create a
|
||||
// directory inside the given directory
|
||||
childrenDirNames.forEach((dirName) => {
|
||||
createDirTree(
|
||||
path.join(parentDir, dirName),
|
||||
dirTree[dirName],
|
||||
filePaths
|
||||
);
|
||||
});
|
||||
|
||||
return filePaths;
|
||||
}
|
||||
|
||||
// Create directories and get back the paths for created directories
|
||||
createDirTree(bestPracticeContentDirPath, dirTree);
|
||||
console.log('Created best practice content directory structure');
|
||||
28
bin/readme.md
Normal file
28
bin/readme.md
Normal file
@@ -0,0 +1,28 @@
|
||||
## CLI Tools
|
||||
> A bunch of CLI scripts to make the development easier
|
||||
|
||||
## `roadmap-links.cjs`
|
||||
|
||||
Generates a list of all the resources links in any roadmap file.
|
||||
|
||||
## `compress-jsons.cjs`
|
||||
|
||||
Compresses all the JSON files in the `public/jsons` folder
|
||||
|
||||
## `roadmap-content.cjs`
|
||||
|
||||
This command is used to create the content folders and files for the interactivity of the roadmap. You can use the below command to generate the roadmap skeletons inside a roadmap directory:
|
||||
|
||||
```bash
|
||||
npm run roadmap-content [frontend|backend|devops|...]
|
||||
```
|
||||
|
||||
For the content skeleton to be generated, we should have proper grouping, and the group names in the project files. You can follow the steps listed below in order to add the meta information to the roadmap.
|
||||
|
||||
- Remove all the groups from the roadmaps through the project editor. Select all and press `cmd+shift+g`
|
||||
- Identify the boxes that should be clickable and group them together with `cmd+shift+g`
|
||||
- Assign the name to the groups.
|
||||
- Group names have the format of `[sort]-[slug]` e.g. `100-internet`. Each group name should start with a number starting from 100 which helps with sorting of the directories and the files. Groups at the same level have the sequential sorting information.
|
||||
- Each groups children have a separate group and have the name similar to `[sort]-[parent-slug]:[child-slug]` where sort refers to the sorting of the `child-slug` and not the parent. Also parent-slug does not need to have the sorting information as a part of slug e.g. if parent was `100-internet` the children would be `100-internet:how-does-the-internet-work`, `101-internet:what-is-http`, `102-internet:browsers`.
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
"upgrade": "ncu -u",
|
||||
"roadmap-links": "node bin/roadmap-links.cjs",
|
||||
"roadmap-content": "node bin/roadmap-content.cjs",
|
||||
"best-practice-content": "node bin/best-practice-content.cjs",
|
||||
"test:e2e": "playwright test"
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -24,7 +25,7 @@
|
||||
"node-html-parser": "^6.1.4",
|
||||
"npm-check-updates": "^16.6.2",
|
||||
"rehype-external-links": "^2.0.1",
|
||||
"roadmap-renderer": "^1.0.1",
|
||||
"roadmap-renderer": "^1.0.4",
|
||||
"tailwindcss": "^3.2.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
8
pnpm-lock.yaml
generated
8
pnpm-lock.yaml
generated
@@ -15,7 +15,7 @@ specifiers:
|
||||
prettier: ^2.8.3
|
||||
prettier-plugin-astro: ^0.7.2
|
||||
rehype-external-links: ^2.0.1
|
||||
roadmap-renderer: ^1.0.1
|
||||
roadmap-renderer: ^1.0.4
|
||||
tailwindcss: ^3.2.4
|
||||
|
||||
dependencies:
|
||||
@@ -26,7 +26,7 @@ dependencies:
|
||||
node-html-parser: 6.1.4
|
||||
npm-check-updates: 16.6.2
|
||||
rehype-external-links: 2.0.1
|
||||
roadmap-renderer: 1.0.1
|
||||
roadmap-renderer: 1.0.4
|
||||
tailwindcss: 3.2.4
|
||||
|
||||
devDependencies:
|
||||
@@ -4729,8 +4729,8 @@ packages:
|
||||
glob: 7.2.3
|
||||
dev: false
|
||||
|
||||
/roadmap-renderer/1.0.1:
|
||||
resolution: {integrity: sha512-f71DLNMfBNtwNwa5ffkXVRBL24loYJ7YMcyyeAUhbJMzEQYp9vWaArVGualylBIw95APy/UIgBZ9KuqiW1Y4UA==}
|
||||
/roadmap-renderer/1.0.4:
|
||||
resolution: {integrity: sha512-TS9jDZu/CzTqxv7QWnMZHgB89WzgLpaExXKcBIWQEKtXm9g9E45t7gijZst9qtRQ2E2+iplAxQz/eMuttq4wAQ==}
|
||||
dev: false
|
||||
|
||||
/roarr/2.15.4:
|
||||
|
||||
BIN
public/best-practices/frontend-performance.png
Normal file
BIN
public/best-practices/frontend-performance.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 378 KiB |
1
public/jsons/best-practices/frontend-performance.json
Normal file
1
public/jsons/best-practices/frontend-performance.json
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
BIN
public/pdfs/best-practices/frontend-performance.pdf
Normal file
BIN
public/pdfs/best-practices/frontend-performance.pdf
Normal file
Binary file not shown.
@@ -0,0 +1 @@
|
||||
# Analyse stylesheets complexity
|
||||
@@ -0,0 +1 @@
|
||||
# Analyze js for perf issues
|
||||
@@ -0,0 +1 @@
|
||||
# Avoid 404 files
|
||||
@@ -0,0 +1 @@
|
||||
# Avoid base64 images
|
||||
@@ -0,0 +1 @@
|
||||
# Avoid inline css
|
||||
@@ -0,0 +1 @@
|
||||
# Avoid multiple inline js snippets
|
||||
@@ -0,0 +1 @@
|
||||
# Bundlephobia
|
||||
@@ -0,0 +1 @@
|
||||
# Check dependency size
|
||||
@@ -0,0 +1 @@
|
||||
# Choose image format approprietly
|
||||
@@ -0,0 +1 @@
|
||||
# Chrome dev tools
|
||||
@@ -0,0 +1 @@
|
||||
# Compress your images
|
||||
@@ -0,0 +1 @@
|
||||
# Concatenate css single file
|
||||
@@ -0,0 +1 @@
|
||||
# Cookie size less 4096 bytes
|
||||
@@ -0,0 +1 @@
|
||||
# Enable compression
|
||||
@@ -0,0 +1 @@
|
||||
# Framework guides
|
||||
1
src/best-practices/frontend-performance/content/index.md
Normal file
1
src/best-practices/frontend-performance/content/index.md
Normal file
@@ -0,0 +1 @@
|
||||
#
|
||||
@@ -0,0 +1 @@
|
||||
# Inline critical css
|
||||
@@ -0,0 +1 @@
|
||||
# Keep cookie count below 20
|
||||
@@ -0,0 +1 @@
|
||||
# Keep dependencies up to date
|
||||
@@ -0,0 +1 @@
|
||||
# Keep ttfb less 1 3s
|
||||
@@ -0,0 +1 @@
|
||||
# Keep web font under 300k
|
||||
@@ -0,0 +1 @@
|
||||
# Lighthouse
|
||||
@@ -0,0 +1 @@
|
||||
# Load offscreen images lazily
|
||||
@@ -0,0 +1 @@
|
||||
# Make css files non blocking
|
||||
@@ -0,0 +1 @@
|
||||
# Minify css
|
||||
@@ -0,0 +1 @@
|
||||
# Minify html
|
||||
@@ -0,0 +1 @@
|
||||
# Minify your javascript
|
||||
@@ -0,0 +1 @@
|
||||
# Minimize http requests
|
||||
@@ -0,0 +1,3 @@
|
||||
# Avoid iframes
|
||||
|
||||
Use iframes only if you don't have any other technical possibility. Try to avoid iframes as much as you can. Iframes are not only bad for performance, but also for accessibility and usability. Iframes are also not indexed by search engines.
|
||||
@@ -0,0 +1 @@
|
||||
# Page load time below 3s
|
||||
@@ -0,0 +1 @@
|
||||
# Page speed insights
|
||||
@@ -0,0 +1 @@
|
||||
# Page weight below 1500
|
||||
@@ -0,0 +1 @@
|
||||
# Pre load urls where possible
|
||||
@@ -0,0 +1 @@
|
||||
# Prefer vector images
|
||||
@@ -0,0 +1 @@
|
||||
# Prevent flash text
|
||||
@@ -0,0 +1 @@
|
||||
# Recommended guides
|
||||
@@ -0,0 +1 @@
|
||||
# Remove unused css
|
||||
@@ -0,0 +1 @@
|
||||
# Serve exact size images
|
||||
@@ -0,0 +1 @@
|
||||
# Set width height images
|
||||
@@ -0,0 +1 @@
|
||||
# Squoosh ap
|
||||
@@ -0,0 +1 @@
|
||||
# Use cdn
|
||||
@@ -0,0 +1 @@
|
||||
# Use http cache headers
|
||||
@@ -0,0 +1 @@
|
||||
# Use https on your website
|
||||
@@ -0,0 +1 @@
|
||||
# Use non blocking javascript
|
||||
@@ -0,0 +1 @@
|
||||
# Use preconnect to load fonts
|
||||
@@ -0,0 +1 @@
|
||||
# Use same protocol
|
||||
@@ -0,0 +1 @@
|
||||
# Use service workers for caching
|
||||
@@ -0,0 +1 @@
|
||||
# Use woff2 font format
|
||||
@@ -0,0 +1 @@
|
||||
# Web page test
|
||||
@@ -0,0 +1,29 @@
|
||||
---
|
||||
jsonUrl: "/jsons/best-practices/frontend-performance.json"
|
||||
pdfUrl: "/pdfs/best-practices/frontend-performance.pdf"
|
||||
order: 1
|
||||
featuredTitle: "Frontend Performance"
|
||||
featuredDescription: "Frontend Performance Best Practices"
|
||||
isNew: true
|
||||
isUpcoming: false
|
||||
title: "Frontend Performance"
|
||||
description: "Detailed list of best practices to improve your frontend performance"
|
||||
dimensions:
|
||||
width: 968
|
||||
height: 1270.89
|
||||
schema:
|
||||
headline: "Frontend Performance Best Practices"
|
||||
description: "Detailed list of best practices to improve the frontend performance of your website. Each best practice carries further details and how to implement that best practice."
|
||||
imageUrl: "https://roadmap.sh/best-practices/frontend-performance.png"
|
||||
datePublished: "2023-01-23"
|
||||
dateModified: "2023-01-23"
|
||||
seo:
|
||||
title: "Frontend Performance Best Practices"
|
||||
description: "Detailed list of best practices to improve the frontend performance of your website. Each best practice carries further details and how to implement that best practice."
|
||||
keywords:
|
||||
- "frontend performance"
|
||||
- "frontend performance best practices"
|
||||
- "frontend performance checklist"
|
||||
- "frontend checklist"
|
||||
- "make performant frontends"
|
||||
---
|
||||
87
src/components/BestPracticeHeader.astro
Normal file
87
src/components/BestPracticeHeader.astro
Normal file
@@ -0,0 +1,87 @@
|
||||
---
|
||||
import BestPracticeHint from './BestPracticeHint.astro';
|
||||
import DownloadPopup from './DownloadPopup.astro';
|
||||
import Icon from './Icon.astro';
|
||||
import SubscribePopup from './SubscribePopup.astro';
|
||||
|
||||
export interface Props {
|
||||
title: string;
|
||||
description: string;
|
||||
bestPracticeId: string;
|
||||
isUpcoming?: boolean;
|
||||
}
|
||||
|
||||
const { title, description, bestPracticeId, isUpcoming = false } = Astro.props;
|
||||
const isBestPracticeReady = !isUpcoming;
|
||||
---
|
||||
|
||||
<DownloadPopup />
|
||||
<SubscribePopup />
|
||||
|
||||
<div class='border-b'>
|
||||
<div class='py-5 sm:py-12 container relative'>
|
||||
<div class='mt-0 mb-3 sm:mb-6'>
|
||||
<h1 class='text-2xl sm:text-4xl mb-0.5 sm:mb-2 font-bold'>
|
||||
{title}
|
||||
</h1>
|
||||
<p class='text-gray-500 text-sm sm:text-lg'>{description}</p>
|
||||
</div>
|
||||
|
||||
<div class='flex justify-between'>
|
||||
<div class='flex gap-1 sm:gap-2'>
|
||||
<a
|
||||
href='/best-practices'
|
||||
class='bg-gray-500 py-1.5 px-3 rounded-md text-white text-xs sm:text-sm font-medium hover:bg-gray-600'
|
||||
aria-label='Back to All Best Practices'
|
||||
>
|
||||
←<span class='hidden sm:inline'> All Best Practices</span>
|
||||
</a>
|
||||
|
||||
{
|
||||
isBestPracticeReady && (
|
||||
<button
|
||||
data-popup='download-popup'
|
||||
class='inline-flex items-center justify-center bg-yellow-400 py-1.5 px-3 text-xs sm:text-sm font-medium rounded-md hover:bg-yellow-500'
|
||||
aria-label='Download Best Practice'
|
||||
ga-category='Subscription'
|
||||
ga-action='Clicked Popup Opener'
|
||||
ga-label='Download Best Practice Popup'
|
||||
>
|
||||
<Icon icon='download' />
|
||||
<span class='hidden sm:inline ml-2'>Download</span>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
<button
|
||||
data-popup='subscribe-popup'
|
||||
class='inline-flex items-center justify-center bg-yellow-400 py-1.5 px-3 text-xs sm:text-sm font-medium rounded-md hover:bg-yellow-500'
|
||||
aria-label='Subscribe for Updates'
|
||||
ga-category='Subscription'
|
||||
ga-action='Clicked Popup Opener'
|
||||
ga-label='Subscribe Best Practice Popup'
|
||||
>
|
||||
<Icon icon='email' />
|
||||
<span class='ml-2'>Subscribe</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{
|
||||
isBestPracticeReady && (
|
||||
<a
|
||||
href={`https://github.com/kamranahmedse/developer-roadmap/issues/new?title=[Suggestion] ${title}`}
|
||||
target='_blank'
|
||||
class='inline-flex items-center justify-center bg-gray-500 text-white py-1.5 px-3 text-xs sm:text-sm font-medium rounded-md hover:bg-gray-600'
|
||||
aria-label='Suggest Changes'
|
||||
>
|
||||
<Icon icon='comment' class='h-3 w-3' />
|
||||
<span class='ml-2 hidden sm:inline'>Suggest Changes</span>
|
||||
<span class='ml-2 inline sm:hidden'>Suggest</span>
|
||||
</a>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
|
||||
<BestPracticeHint bestPracticeId={bestPracticeId} />
|
||||
</div>
|
||||
</div>
|
||||
20
src/components/BestPracticeHint.astro
Normal file
20
src/components/BestPracticeHint.astro
Normal file
@@ -0,0 +1,20 @@
|
||||
---
|
||||
export interface Props {
|
||||
bestPracticeId: string;
|
||||
}
|
||||
---
|
||||
|
||||
<div class='mt-4 sm:mt-7 border-0 sm:border rounded-md mb-0 sm:-mb-[65px]'>
|
||||
<!-- Desktop: Roadmap Resources - Alert -->
|
||||
<div class='hidden sm:flex justify-between px-2 bg-white items-center rounded-md p-1.5'>
|
||||
<p class='text-sm'>
|
||||
<span class='text-yellow-900 bg-yellow-200 py-0.5 px-1 text-xs rounded-sm font-medium uppercase mr-0.5'>Tip</span>
|
||||
Click the best practices for details and resources
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Mobile - Roadmap resources alert -->
|
||||
<p class='block sm:hidden text-sm border border-yellow-500 text-yellow-700 rounded-md py-1.5 px-2 bg-white relative'>
|
||||
Click the best practices for details and resources
|
||||
</p>
|
||||
</div>
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
import type { BreadcrumbItem } from '../lib/topic';
|
||||
import type { BreadcrumbItem } from '../lib/roadmap-topic';
|
||||
|
||||
export interface Props {
|
||||
breadcrumbs: BreadcrumbItem[];
|
||||
|
||||
@@ -5,7 +5,7 @@ import CaptchaFields from './Captcha/CaptchaFields.astro';
|
||||
|
||||
<Popup
|
||||
id='download-popup'
|
||||
title='Download Roadmap'
|
||||
title='Download'
|
||||
subtitle='Enter your email below to receive the download link.'
|
||||
>
|
||||
<form
|
||||
|
||||
@@ -9,11 +9,9 @@ export interface Props {
|
||||
const { featuredItems, heading } = Astro.props;
|
||||
---
|
||||
|
||||
<div class='py-4 sm:py-14 border-b border-b-slate-900 relative'>
|
||||
<div class='py-4 sm:py-14 border-b border-b-[#1e293c] relative'>
|
||||
<div class='container'>
|
||||
<h2
|
||||
class='hidden sm:flex absolute rounded-lg -top-[17px] left-1/2 -translate-x-1/2 bg-slate-900 py-1 px-3 border border-slate-900 text-md text-slate-400 font-regular'
|
||||
>
|
||||
<h2 class='hidden sm:flex absolute rounded-lg -top-[17px] left-1/2 -translate-x-1/2 bg-slate-900 py-1 px-3 border border-[#1e293c] text-md text-slate-400 font-regular'>
|
||||
{heading}
|
||||
</h2>
|
||||
|
||||
|
||||
29
src/components/FrameRenderer/FrameRenderer.astro
Normal file
29
src/components/FrameRenderer/FrameRenderer.astro
Normal file
@@ -0,0 +1,29 @@
|
||||
---
|
||||
import Loader from '../Loader.astro';
|
||||
import TopicOverlay from '../TopicOverlay/TopicOverlay.astro';
|
||||
import './FrameRenderer.css';
|
||||
|
||||
export interface Props {
|
||||
resourceType: 'roadmap' | 'best-practice';
|
||||
resourceId: string;
|
||||
jsonUrl: string;
|
||||
dimensions?: {
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
}
|
||||
|
||||
const { resourceId, resourceType, jsonUrl, dimensions = null } = Astro.props;
|
||||
---
|
||||
|
||||
<div
|
||||
id='resource-svg'
|
||||
style={dimensions ? `--aspect-ratio:${dimensions.width}/${dimensions.height}` : null}
|
||||
data-resource-type={resourceType}
|
||||
data-resource-id={resourceId}
|
||||
data-json-url={jsonUrl}
|
||||
>
|
||||
<Loader />
|
||||
</div>
|
||||
|
||||
<script src='./renderer.js'></script>
|
||||
94
src/components/FrameRenderer/FrameRenderer.css
Normal file
94
src/components/FrameRenderer/FrameRenderer.css
Normal file
@@ -0,0 +1,94 @@
|
||||
svg text tspan {
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
text-rendering: optimizeSpeed;
|
||||
}
|
||||
|
||||
code {
|
||||
background: #1e1e3f;
|
||||
color: #9efeff;
|
||||
padding: 3px 5px;
|
||||
font-size: 14px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
svg .clickable-group {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
svg .clickable-group:hover > [fill='rgb(65,53,214)'] {
|
||||
fill: #232381;
|
||||
stroke: #232381;
|
||||
}
|
||||
|
||||
svg .clickable-group:hover > [fill='rgb(255,255,0)'] {
|
||||
fill: #d6d700;
|
||||
}
|
||||
|
||||
svg .clickable-group:hover > [fill='rgb(255,229,153)'] {
|
||||
fill: #f3c950;
|
||||
}
|
||||
|
||||
svg .clickable-group:hover > [fill='rgb(153,153,153)'] {
|
||||
fill: #646464;
|
||||
}
|
||||
|
||||
svg .clickable-group:hover > [fill='rgb(255,255,255)'] {
|
||||
fill: #d7d7d7;
|
||||
}
|
||||
|
||||
svg .clickable-group:hover > [fill='rgb(255,255,221)'] {
|
||||
fill: #e5e5be;
|
||||
}
|
||||
|
||||
svg .clickable-group:hover > [fill='rgb(255,217,102)'] {
|
||||
fill: #d9b443;
|
||||
}
|
||||
|
||||
svg .done rect {
|
||||
fill: #cbcbcb !important;
|
||||
}
|
||||
|
||||
svg .done text {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
svg .clickable-group.done[data-group-id^='check:'] rect {
|
||||
fill: gray !important;
|
||||
stroke: gray;
|
||||
}
|
||||
|
||||
.clickable-group rect {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
/************************************
|
||||
Aspect ratio implementation
|
||||
*************************************/
|
||||
[style*='--aspect-ratio'] > :first-child {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
[style*='--aspect-ratio'] > img {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
@supports (--custom: property) {
|
||||
[style*='--aspect-ratio'] {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
[style*='--aspect-ratio']::before {
|
||||
content: '';
|
||||
display: block;
|
||||
/*noinspection CssUnresolvedCustomProperty*/
|
||||
padding-bottom: calc(100% / (var(--aspect-ratio)));
|
||||
}
|
||||
|
||||
[style*='--aspect-ratio'] > :first-child {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
@@ -1,25 +1,17 @@
|
||||
import { wireframeJSONToSVG } from 'roadmap-renderer';
|
||||
import { Topic } from './topic';
|
||||
import { Sharer } from './sharer';
|
||||
|
||||
/**
|
||||
* @typedef {{ roadmapId: string, jsonUrl: string }} RoadmapConfig
|
||||
*/
|
||||
|
||||
export class Roadmap {
|
||||
/**
|
||||
* @param {RoadmapConfig} config
|
||||
*/
|
||||
export class Renderer {
|
||||
constructor() {
|
||||
this.roadmapId = '';
|
||||
this.resourceId = '';
|
||||
this.resourceType = '';
|
||||
this.jsonUrl = '';
|
||||
|
||||
this.containerId = 'roadmap-svg';
|
||||
this.containerId = 'resource-svg';
|
||||
|
||||
this.init = this.init.bind(this);
|
||||
this.onDOMLoaded = this.onDOMLoaded.bind(this);
|
||||
this.fetchRoadmapSvg = this.fetchRoadmapSvg.bind(this);
|
||||
this.handleRoadmapClick = this.handleRoadmapClick.bind(this);
|
||||
this.jsonToSvg = this.jsonToSvg.bind(this);
|
||||
this.handleSvgClick = this.handleSvgClick.bind(this);
|
||||
this.prepareConfig = this.prepareConfig.bind(this);
|
||||
}
|
||||
|
||||
@@ -34,7 +26,8 @@ export class Roadmap {
|
||||
|
||||
const dataset = this.containerEl.dataset;
|
||||
|
||||
this.roadmapId = dataset.roadmapId;
|
||||
this.resourceType = dataset.resourceType;
|
||||
this.resourceId = dataset.resourceId;
|
||||
this.jsonUrl = dataset.jsonUrl;
|
||||
|
||||
return true;
|
||||
@@ -44,7 +37,7 @@ export class Roadmap {
|
||||
* @param { string } jsonUrl
|
||||
* @returns {Promise<SVGElement>}
|
||||
*/
|
||||
fetchRoadmapSvg(jsonUrl) {
|
||||
jsonToSvg(jsonUrl) {
|
||||
if (!jsonUrl) {
|
||||
console.error('jsonUrl not defined in frontmatter');
|
||||
return null;
|
||||
@@ -66,14 +59,14 @@ export class Roadmap {
|
||||
return;
|
||||
}
|
||||
|
||||
this.fetchRoadmapSvg(this.jsonUrl)
|
||||
this.jsonToSvg(this.jsonUrl)
|
||||
.then((svg) => {
|
||||
document.getElementById(this.containerId).replaceChildren(svg);
|
||||
})
|
||||
.catch(console.error);
|
||||
}
|
||||
|
||||
handleRoadmapClick(e) {
|
||||
handleSvgClick(e) {
|
||||
const targetGroup = e.target.closest('g') || {};
|
||||
const groupId = targetGroup.dataset ? targetGroup.dataset.groupId : '';
|
||||
if (!groupId) {
|
||||
@@ -82,11 +75,32 @@ export class Roadmap {
|
||||
|
||||
e.stopImmediatePropagation();
|
||||
|
||||
if (/^ext_link/.test(groupId)) {
|
||||
window.open(`https://${groupId.replace('ext_link:', '')}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (/^check:/.test(groupId)) {
|
||||
window.dispatchEvent(
|
||||
new CustomEvent(`${this.resourceType}.topic.toggle`, {
|
||||
detail: {
|
||||
topicId: groupId.replace('check:', ''),
|
||||
resourceType: this.resourceType,
|
||||
resourceId: this.resourceId,
|
||||
},
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove sorting prefix from groupId
|
||||
const normalizedGroupId = groupId.replace(/^\d+-/, '');
|
||||
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('topic.click', {
|
||||
new CustomEvent(`${this.resourceType}.topic.click`, {
|
||||
detail: {
|
||||
topicId: groupId,
|
||||
roadmapId: this.roadmapId,
|
||||
topicId: normalizedGroupId,
|
||||
resourceId: this.resourceId,
|
||||
},
|
||||
})
|
||||
);
|
||||
@@ -94,17 +108,9 @@ export class Roadmap {
|
||||
|
||||
init() {
|
||||
window.addEventListener('DOMContentLoaded', this.onDOMLoaded);
|
||||
window.addEventListener('click', this.handleRoadmapClick);
|
||||
window.addEventListener('click', this.handleSvgClick);
|
||||
}
|
||||
}
|
||||
|
||||
const roadmap = new Roadmap();
|
||||
roadmap.init();
|
||||
|
||||
// Initialize the topic loader
|
||||
const topic = new Topic();
|
||||
topic.init();
|
||||
|
||||
// Handles the share icons on the roadmap page
|
||||
const sharer = new Sharer();
|
||||
sharer.init();
|
||||
const renderer = new Renderer();
|
||||
renderer.init();
|
||||
32
src/components/GridItem.astro
Normal file
32
src/components/GridItem.astro
Normal file
@@ -0,0 +1,32 @@
|
||||
---
|
||||
import type { RoadmapFileType } from '../lib/roadmap';
|
||||
|
||||
export interface Props {
|
||||
url: string;
|
||||
title: string;
|
||||
description: string;
|
||||
isNew: boolean;
|
||||
}
|
||||
|
||||
const { url, title, description, isNew } = Astro.props;
|
||||
---
|
||||
|
||||
<a
|
||||
href={url}
|
||||
class='bg-gradient-to-r from-slate-900 to-amber-900 hover:from-stone-900 hover:to-stone-900 hover:bg-gray-100 flex flex-col p-2.5 sm:p-5 rounded-md sm:rounded-lg border border-gray-200 relative h-full'
|
||||
>
|
||||
<span
|
||||
class='font-regular sm:font-medium text-md sm:text-xl hover:text-gray-50 text-gray-200 sm:text-gray-100 mb-0 sm:mb-1.5'
|
||||
>
|
||||
{title}
|
||||
</span>
|
||||
<span class='text-sm leading-normal text-gray-400 hidden sm:block'>{description}</span>
|
||||
|
||||
{
|
||||
isNew && (
|
||||
<span class='absolute bottom-1 right-1 bg-yellow-300 text-yellow-900 text-xs font-medium px-1 sm:px-1.5 py-0.5 rounded-sm uppercase'>
|
||||
New
|
||||
</span>
|
||||
)
|
||||
}
|
||||
</a>
|
||||
@@ -1,31 +0,0 @@
|
||||
---
|
||||
import type { RoadmapFileType } from '../lib/roadmap';
|
||||
|
||||
export interface Props {
|
||||
roadmap: RoadmapFileType;
|
||||
}
|
||||
|
||||
const { roadmap } = Astro.props;
|
||||
const frontmatter = roadmap.frontmatter;
|
||||
---
|
||||
|
||||
<a
|
||||
href={`/${roadmap.id}`}
|
||||
class="bg-gradient-to-r from-slate-900 to-amber-900 hover:from-stone-900 hover:to-stone-900 hover:bg-gray-100 flex flex-col p-2.5 sm:p-5 rounded-md sm:rounded-lg border border-gray-200 relative h-full"
|
||||
>
|
||||
<span
|
||||
class="font-regular sm:font-medium text-md sm:text-xl hover:text-gray-50 text-gray-200 sm:text-gray-100 mb-0 sm:mb-1.5"
|
||||
>{frontmatter.title}</span
|
||||
>
|
||||
<span class="text-sm leading-normal text-gray-400 hidden sm:block"
|
||||
>{frontmatter.description}</span
|
||||
>
|
||||
|
||||
{
|
||||
frontmatter.isNew && (
|
||||
<span class="absolute bottom-1 right-1 bg-yellow-300 text-yellow-900 text-xs font-medium px-1 sm:px-1.5 py-0.5 rounded-sm uppercase">
|
||||
New
|
||||
</span>
|
||||
)
|
||||
}
|
||||
</a>
|
||||
@@ -1,53 +0,0 @@
|
||||
---
|
||||
import DownloadPopup from '../DownloadPopup.astro';
|
||||
import Loader from '../Loader.astro';
|
||||
import ShareIcons from '../ShareIcons.astro';
|
||||
import SubscribePopup from '../SubscribePopup.astro';
|
||||
import TopicOverlay from '../TopicOverlay.astro';
|
||||
import './InteractiveRoadmap.css';
|
||||
|
||||
export interface Props {
|
||||
roadmapId: string;
|
||||
description: string;
|
||||
jsonUrl: string;
|
||||
dimensions?: {
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
}
|
||||
|
||||
const { roadmapId, jsonUrl, dimensions = null, description } = Astro.props;
|
||||
---
|
||||
|
||||
<link
|
||||
rel='preload'
|
||||
href='/fonts/balsamiq.woff2'
|
||||
as='font'
|
||||
type='font/woff2'
|
||||
crossorigin
|
||||
slot='after-header'
|
||||
/>
|
||||
|
||||
<div class='bg-gray-50 py-4 sm:py-12'>
|
||||
<div class='max-w-[1000px] container relative'>
|
||||
<ShareIcons
|
||||
description={description}
|
||||
pageUrl={`https://roadmap.sh/${roadmapId}`}
|
||||
/>
|
||||
<DownloadPopup />
|
||||
<SubscribePopup />
|
||||
<TopicOverlay roadmapId={roadmapId} />
|
||||
<div
|
||||
id='roadmap-svg'
|
||||
style={dimensions
|
||||
? `--aspect-ratio:${dimensions.width}/${dimensions.height}`
|
||||
: null}
|
||||
data-roadmap-id={roadmapId}
|
||||
data-json-url={jsonUrl}
|
||||
>
|
||||
<Loader />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src='./roadmap.js'></script>
|
||||
@@ -1,86 +0,0 @@
|
||||
svg text tspan {
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
text-rendering: optimizeSpeed;
|
||||
}
|
||||
|
||||
code {
|
||||
background: #1e1e3f;
|
||||
color: #9efeff;
|
||||
padding: 3px 5px;
|
||||
font-size: 14px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
svg .clickable-group {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
svg .clickable-group:hover > [fill='rgb(65,53,214)'] {
|
||||
fill: #232381;
|
||||
stroke: #232381;
|
||||
}
|
||||
|
||||
svg .clickable-group:hover > [fill='rgb(255,255,0)'] {
|
||||
fill: #d6d700;
|
||||
}
|
||||
|
||||
svg .clickable-group:hover > [fill='rgb(255,229,153)'] {
|
||||
fill: #f3c950;
|
||||
}
|
||||
|
||||
svg .clickable-group:hover > [fill='rgb(153,153,153)'] {
|
||||
fill: #646464;
|
||||
}
|
||||
|
||||
svg .clickable-group:hover > [fill='rgb(255,255,255)'] {
|
||||
fill: #d7d7d7;
|
||||
}
|
||||
|
||||
svg .clickable-group:hover > [fill='rgb(255,255,221)'] {
|
||||
fill: #e5e5be;
|
||||
}
|
||||
|
||||
svg .clickable-group:hover > [fill='rgb(255,217,102)'] {
|
||||
fill: #d9b443;
|
||||
}
|
||||
|
||||
svg .done rect {
|
||||
fill: #cbcbcb !important;
|
||||
}
|
||||
|
||||
svg .done text {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
/************************************
|
||||
Aspect ratio implementation
|
||||
*************************************/
|
||||
[style*="--aspect-ratio"] > :first-child {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
[style*="--aspect-ratio"] > img {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
@supports (--custom:property) {
|
||||
[style*="--aspect-ratio"] {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
[style*="--aspect-ratio"]::before {
|
||||
content: "";
|
||||
display: block;
|
||||
/*noinspection CssUnresolvedCustomProperty*/
|
||||
padding-bottom: calc(100% / (var(--aspect-ratio)));
|
||||
}
|
||||
|
||||
[style*="--aspect-ratio"] > :first-child {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
5
src/components/MarkdownFile.astro
Normal file
5
src/components/MarkdownFile.astro
Normal file
@@ -0,0 +1,5 @@
|
||||
<div
|
||||
class='prose-blockquote:font-normal prose container prose-code:bg-transparent prose-h2:text-3xl prose-h2:mt-4 prose-h2:mb-2 prose-h3:mt-2 prose-img:mt-1'
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
@@ -1,22 +0,0 @@
|
||||
---
|
||||
import DownloadPopup from './DownloadPopup.astro';
|
||||
import SubscribePopup from './SubscribePopup.astro';
|
||||
|
||||
export interface Props {
|
||||
roadmapId: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
const { roadmapId, description } = Astro.props;
|
||||
---
|
||||
|
||||
<div class='bg-gray-50 py-2'>
|
||||
<div
|
||||
class='container prose prose-headings:mt-4 prose-headings:mb-2 prose-p:mb-0.5 relative prose-code:text-white'
|
||||
>
|
||||
<DownloadPopup />
|
||||
<SubscribePopup />
|
||||
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
@@ -14,6 +14,9 @@ import Icon from './Icon.astro';
|
||||
<li>
|
||||
<a href='/roadmaps' class='text-gray-400 hover:text-white'>Roadmaps</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href='/best-practices' class='text-gray-400 hover:text-white'>Best Practices</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href='/guides' class='text-gray-400 hover:text-white'>Guides</a>
|
||||
</li>
|
||||
@@ -48,6 +51,9 @@ import Icon from './Icon.astro';
|
||||
<li>
|
||||
<a href='/roadmaps' class='text-xl md:text-lg hover:text-blue-300'>Roadmaps</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href='/best-practices' class='text-xl md:text-lg hover:text-blue-300'>Best Practices</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href='/guides' class='text-xl md:text-lg hover:text-blue-300'>Guides</a>
|
||||
</li>
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
---
|
||||
import DownloadPopup from './DownloadPopup.astro';
|
||||
import Icon from './Icon.astro';
|
||||
import ResourcesAlert from './ResourcesAlert.astro';
|
||||
import RoadmapHint from './RoadmapHint.astro';
|
||||
import RoadmapNote from './RoadmapNote.astro';
|
||||
import SubscribePopup from './SubscribePopup.astro';
|
||||
import TopicSearch from './TopicSearch/TopicSearch.astro';
|
||||
import YouTubeAlert from './YouTubeAlert.astro';
|
||||
|
||||
@@ -20,6 +22,9 @@ const { title, description, roadmapId, isUpcoming = false, hasSearch = false, no
|
||||
const isRoadmapReady = !isUpcoming;
|
||||
---
|
||||
|
||||
<DownloadPopup />
|
||||
<SubscribePopup />
|
||||
|
||||
<div class='border-b'>
|
||||
<div class='py-5 sm:py-12 container relative'>
|
||||
<YouTubeAlert />
|
||||
@@ -104,7 +109,7 @@ const isRoadmapReady = !isUpcoming;
|
||||
</div>
|
||||
|
||||
<!-- Desktop: Roadmap Resources - Alert -->
|
||||
{hasTopics && <ResourcesAlert roadmapId={roadmapId} />}
|
||||
{hasTopics && <RoadmapHint roadmapId={roadmapId} />}
|
||||
|
||||
{hasSearch && <TopicSearch />}
|
||||
</div>
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
---
|
||||
import Icon from "./Icon.astro";
|
||||
|
||||
export interface Props {
|
||||
pageUrl: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
const { pageUrl, description } = Astro.props;
|
||||
|
||||
const twitterUrl = `https://twitter.com/intent/tweet?text=${description}&url=${pageUrl}`;
|
||||
const fbUrl = `https://www.facebook.com/sharer/sharer.php?quote=${description}&u=${pageUrl}`;
|
||||
const hnUrl = `https://news.ycombinator.com/submitlink?t=${description}&u=${pageUrl}`;
|
||||
const redditUrl = `https://www.reddit.com/submit?title=${description}&url=${pageUrl}`;
|
||||
---
|
||||
|
||||
<div
|
||||
class="absolute left-[-18px] top-[110px] h-full hidden"
|
||||
id="page-share-icons"
|
||||
>
|
||||
<div class="flex sticky top-[100px] flex-col gap-1.5">
|
||||
<a
|
||||
href={twitterUrl}
|
||||
target="_blank"
|
||||
class="text-gray-500 hover:text-gray-700"
|
||||
>
|
||||
<Icon icon="twitter" />
|
||||
</a>
|
||||
<a href={fbUrl} target="_blank" class="text-gray-500 hover:text-gray-700">
|
||||
<Icon icon="facebook" />
|
||||
</a>
|
||||
<a href={hnUrl} target="_blank" class="text-gray-500 hover:text-gray-700">
|
||||
<Icon icon="hackernews" />
|
||||
</a>
|
||||
<a
|
||||
href={redditUrl}
|
||||
target="_blank"
|
||||
class="text-gray-500 hover:text-gray-700"
|
||||
>
|
||||
<Icon icon="reddit" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
34
src/components/ShareIcons/ShareIcons.astro
Normal file
34
src/components/ShareIcons/ShareIcons.astro
Normal file
@@ -0,0 +1,34 @@
|
||||
---
|
||||
import Icon from '../Icon.astro';
|
||||
|
||||
export interface Props {
|
||||
pageUrl: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
const { pageUrl, description } = Astro.props;
|
||||
|
||||
const twitterUrl = `https://twitter.com/intent/tweet?text=${description}&url=${pageUrl}`;
|
||||
const fbUrl = `https://www.facebook.com/sharer/sharer.php?quote=${description}&u=${pageUrl}`;
|
||||
const hnUrl = `https://news.ycombinator.com/submitlink?t=${description}&u=${pageUrl}`;
|
||||
const redditUrl = `https://www.reddit.com/submit?title=${description}&url=${pageUrl}`;
|
||||
---
|
||||
|
||||
<div class='absolute left-[-18px] top-[110px] h-full hidden' id='page-share-icons'>
|
||||
<div class='flex sticky top-[100px] flex-col gap-1.5'>
|
||||
<a href={twitterUrl} target='_blank' class='text-gray-500 hover:text-gray-700'>
|
||||
<Icon icon='twitter' />
|
||||
</a>
|
||||
<a href={fbUrl} target='_blank' class='text-gray-500 hover:text-gray-700'>
|
||||
<Icon icon='facebook' />
|
||||
</a>
|
||||
<a href={hnUrl} target='_blank' class='text-gray-500 hover:text-gray-700'>
|
||||
<Icon icon='hackernews' />
|
||||
</a>
|
||||
<a href={redditUrl} target='_blank' class='text-gray-500 hover:text-gray-700'>
|
||||
<Icon icon='reddit' />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src='./sharer.js'></script>
|
||||
@@ -27,3 +27,6 @@ export class Sharer {
|
||||
window.addEventListener('scroll', this.onScroll, { passive: true });
|
||||
}
|
||||
}
|
||||
|
||||
const sharer = new Sharer();
|
||||
sharer.init();
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user