Added contributors.

Added Copy Angular.
Added release info to icon page.
This commit is contained in:
Karsa
2023-05-08 18:18:33 +02:00
parent 79cb35634d
commit cd737236ae
11 changed files with 138 additions and 152 deletions
-131
View File
@@ -1,131 +0,0 @@
import crypto from 'crypto';
import fs from 'fs';
import path from 'path';
import { Contributor } from '../theme/types';
import fetch, { Headers } from 'node-fetch'
const IGNORE_COMMIT_MESSAGES = ['fork', 'optimize'];
function getContentHashOfFile(path) {
return new Promise((resolve, reject) => {
const hash = crypto.createHash('sha256');
const stream = fs.createReadStream(path);
stream.on('error', err => reject(err));
stream.on('data', chunk => hash.update(chunk));
stream.on('end', () => resolve(hash.digest('hex')));
});
}
const fetchCommitsOfIcon = async (name) =>{
try {
const headers = new Headers();
const token = process.env.GITHUB_TOKEN;
const username = process.env.GITHUB_USERNAME;
const password = process.env.GITHUB_API_KEY;
headers.set(
'Authorization',
token ? `Bearer ${token}` : `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}`,
);
const res = await fetch(
`https://api.github.com/repos/lucide-icons/lucide/commits?path=icons/${name}.svg`,
{
method: 'GET',
headers,
},
);
const data = await res.json();
return {
name,
commits: data,
};
} catch (error) {
throw new Error(error);
}
};
export const filterCommits = (commits) =>
commits.filter(({ commit }) =>
!IGNORE_COMMIT_MESSAGES.some(ignoreItem =>
commit.message.toLowerCase().includes(ignoreItem),
))
.map(({ author }) => ({
author: author && author.login ? author.login : null,
}))
.filter(({ author }, index, self) => self.findIndex((commit) => commit.author === author) === index);
const getIconHash = async (icon) => await getContentHashOfFile(path.join(process.cwd(), "../icons", `${icon}.svg`))
const iconCacheDir = path.join(process.cwd(),'.vitepress/cache/github-api');
const iconCache = (hash) => path.join(iconCacheDir, `${hash}.json`);
export async function checkIconCache(icon) {
const hash = await getIconHash(icon);
const cachePath = iconCache(hash);
if(fs.existsSync( cachePath )) {
const iconCache = fs.readFileSync(cachePath, "utf8");
return JSON.parse(iconCache)
}
return false
}
async function writeIconCache(icon, content) {
const hash = await getIconHash(icon);
const iconCachePath = iconCache(hash);
if (!fs.existsSync(iconCacheDir)){
fs.mkdirSync(iconCacheDir);
}
fs.writeFileSync(iconCachePath, JSON.stringify(content), 'utf-8');
}
export async function getContributors(icon): Promise<Contributor[]> {
try {
let iconCommits
const iconCache = await checkIconCache(icon);
if (iconCache) {
iconCommits = iconCache
} else {
const { commits } = await fetchCommitsOfIcon(icon);
writeIconCache(icon, commits)
iconCommits = commits
}
if (iconCommits && iconCommits.length) {
return filterCommits(iconCommits);
}
return [];
} catch (error) {
throw new Error(error);
}
}
export async function getAllContributors(icons) {
try {
const AllIconCommits = await Promise.all(icons.map(fetchCommitsOfIcon));
const filteredCommits = AllIconCommits.reduce((acc, { name, commits }) => {
if (commits && commits.length) {
acc[name] = filterCommits(commits)
}
return acc;
}, {});
return filteredCommits
} catch (error) {
console.error(error);
}
}
+4 -6
View File
@@ -2,8 +2,7 @@ import fs from "fs";
import path from "path";
import { parseSync } from "svgson";
import { IconNode } from "../../../packages/lucide-react/src/createLucideIcon";
import { IconEntity, Release } from "../theme/types";
import { getContributors } from "./fetchAllContributors";
import { IconEntity } from "../theme/types";
import { generateHashedKey } from "./helpers";
import releaseMeta from "../data/releaseMetaData.json";
@@ -26,7 +25,7 @@ export async function getData(name: string, { withChildKeys = false }: GetDataOp
const svgContent = fs.readFileSync(svgPath, "utf8");
const jsonPath = path.join(directory, `${name}.json`);
const jsonContent = fs.readFileSync(jsonPath, "utf8");
const { tags, categories } = JSON.parse(jsonContent);
const { tags, categories, contributors } = JSON.parse(jsonContent);
const iconNode = parseSync(svgContent).children.map(
(child) => {
@@ -40,14 +39,13 @@ export async function getData(name: string, { withChildKeys = false }: GetDataOp
}
) as IconNode
// const contributors = await getContributors(name);
return {
name,
tags,
categories,
// contributors,
iconNode,
contributors,
// contributors: ['ericfennis', 'karsa-mistmere'],
...(releaseMeta[name] ?? {})
};
}
@@ -6,6 +6,6 @@
<style scoped>
.container {
padding: 32px 32px 96px;
padding: 32px;
}
</style>
@@ -53,6 +53,28 @@ function copyVue() {
navigator.clipboard.writeText(code)
}
function copyAngular() {
let attrs = ['']
attrs.push(`name="${props.name}"`)
if (size.value && size.value !== 24) {
attrs.push(`[size]="${size.value}"`)
}
if (color.value && color.value !== 'currentColor') {
attrs.push(`color="${color.value}"`)
}
if (strokeWidth.value && strokeWidth.value !== 2) {
attrs.push(`[strokeWidth]="${strokeWidth.value}"`)
}
const code = `<lucide-icon${attrs.join(' ')}></lucide-icon>`
navigator.clipboard.writeText(code)
}
function confetti(e) {
animate.value = true;
@@ -72,6 +94,7 @@ function confetti(e) {
{ text: 'Copy JSX' , onClick: copyJSX },
{ text: 'Copy Vue' , onClick: copyVue },
{ text: 'Copy Svelte' , onClick: copyJSX },
{ text: 'Copy Angular' , onClick: copyAngular },
]"
/>
</template>
@@ -0,0 +1,78 @@
<script setup lang="ts">
import {IconEntity} from "../../types";
const props = defineProps<{
icon: IconEntity
}>()
</script>
<template>
<div class="contributors" v-if="props.icon.contributors?.length>0">
<h2 class="label">Contributors</h2>
<div class="avatar-group">
<a class="avatar" v-for="contributor in props.icon.contributors" :href="`https://github.com/${contributor}`" target="_blank" :data-name="contributor">
<img class="avatar-image" :alt="contributor" :src="`https://github.com/${contributor}.png?size=128`" />
</a>
</div>
</div>
</template>
<style scoped>
.contributors {
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-end;
gap: 24px;
}
h2.label {
font-size: 20px;
border: 0;
padding: 0;
margin: 0;
}
.avatar-group {
display: flex;
}
.avatar:not(:first-child) {
margin-left: -24px;
}
.avatar:before {
content: attr(data-name);
display: block;
font-size: 12px;
line-height: 20px;
margin-left: 27px;
transform: translate(-50%, -28px) scale(0.9);
font-weight: 400;
position: absolute;
background: var(--vp-c-brand-dark);
color: white;
z-index: 10;
padding: 2px 8px;
border-radius: 4px;
box-shadow: var(--vp-shadow-1);
opacity: 0;
pointer-events: none;
transition: cubic-bezier(0.19, 1, 0.22, 1) .2s;
transition-property: opacity, transform;
/* max-width: calc((32px * 2) + 56px); */
overflow: hidden;
white-space: nowrap;
word-break: break-word;
}
.avatar:hover:before {
opacity: 1;
transform: translate(-50%, -28px) scale(1);
}
.avatar-image {
width: 64px;
height: 64px;
border-radius: 50%;
border: 3px solid var(--vp-c-bg-elv);
}
.avatar:hover .avatar-image {
border: 3px solid var(--vp-c-bg-soft-mute);
}
</style>
@@ -9,7 +9,6 @@
font-size: 24px;
font-weight: 500;
line-height: 32px;
margin-bottom: 12px;
transition: background ease-in .15s;;
padding: 2px 8px;
border-radius: 8px;
@@ -3,6 +3,7 @@
import { computed, ref } from 'vue'
import createLucideIcon from 'lucide-vue-next/src/createLucideIcon';
import IconButton from '../base/IconButton.vue';
import IconContributors from './IconContributors.vue';
import IconPreview from './IconPreview.vue';
import { x, expand } from '../../../data/iconNodes'
import { useRouter } from 'vitepress';
@@ -47,7 +48,11 @@
:iconNode="props.icon.iconNode"
customizable
/>
<IconInfo :icon="props.icon" />
<IconInfo :icon="props.icon">
<template v-slot:footer>
<IconContributors :icon="props.icon" />
</template>
</IconInfo>
</div>
</div>
</Transition>
@@ -93,8 +98,8 @@
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
will-change: transform;
pointer-events: all;
height: 240px;
padding: 24px 32px;
height: 288px;
padding: 24px;
display: flex;
box-shadow: var(--vp-shadow-5);
}
@@ -134,6 +139,4 @@
opacity: 0;
}
</style>
@@ -40,7 +40,7 @@ const { page } = useData()
<CopySVGButton :name="icon.name" />
<CopyCodeButton :name="icon.name"/>
</div>
<slot name="footer" />
</div>
</template>
@@ -51,8 +51,10 @@ const { page } = useData()
gap: 8px;
margin-bottom: 24px;
}
.category {
text-transform: capitalize;
}
.icon-name {
margin-bottom: 12px;
}
</style>
@@ -95,6 +95,6 @@ watch(searchQueryThrottled, (searchString) => {
}
.bottom-page {
height: 200px;
height: 288px;
}
</style>
+1 -5
View File
@@ -4,16 +4,12 @@ export interface IconEntity {
name: string;
tags: string[];
categories: string[];
contributors: Contributor[];
contributors: string[];
iconNode: IconNode;
createdRelease?: Release;
changedRelease?: Release;
}
export interface Contributor {
author: string;
}
export interface Category {
name: string
title: string
+18
View File
@@ -12,7 +12,9 @@ import { computed } from 'vue'
import { useData } from 'vitepress'
import IconPreview from '../.vitepress/theme/components/icons/IconPreview.vue'
import IconInfo from '../.vitepress/theme/components/icons/IconInfo.vue'
import IconContributors from '../.vitepress/theme/components/icons/IconContributors.vue'
import CodeGroup from '../.vitepress/theme/components/base/CodeGroup.vue'
import Badge from '../.vitepress/theme/components/base/Badge.vue'
import VPButton from 'vitepress/dist/client/theme-default/components/VPButton.vue';
import { data } from './codeExamples.data'
import { camelCase, startCase } from 'lodash-es'
@@ -50,6 +52,11 @@ const codeExample = computed(() => data.codeExamples?.map(
/>
</CodeGroup>
</div>
<div class="release-info">
<Badge><strong>Created:</strong> v{{$params.createdRelease.version}}</Badge>
<Badge><strong>Last changed:</strong> v{{$params.changedRelease.version}}</Badge>
</div>
<IconContributors :icon="$params" />
</div>
<style module>
@@ -75,3 +82,14 @@ const codeExample = computed(() => data.codeExamples?.map(
}
}
</style>
<style scoped>
.release-info {
display: flex;
gap: 8px;
flex-direction: column;
align-items: flex-start;
}
.badge {
font-weight: 500;
}
</style>