mirror of
https://github.com/lucide-icons/lucide.git
synced 2026-11-14 13:58:21 +00:00
Added contributors.
Added Copy Angular. Added release info to icon page.
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user