├── src ├── server │ ├── config │ │ ├── const.ts │ │ └── index.ts │ ├── utils │ │ ├── uuid.ts │ │ ├── isDateOld.ts │ │ ├── axiosInstance.ts │ │ ├── extractParamFromUrl.ts │ │ └── logger.ts │ ├── db │ │ └── redis-vercel-kv.ts │ └── services │ │ └── dataManager.service.ts ├── app │ ├── favicon.ico │ ├── api │ │ ├── company │ │ │ ├── route.ts │ │ │ └── [company] │ │ │ │ └── route.ts │ │ ├── health │ │ │ └── route.ts │ │ ├── repositories │ │ │ └── route.ts │ │ └── route.ts │ ├── layout.tsx │ ├── globals.css │ ├── companies │ │ ├── page.tsx │ │ └── [company] │ │ │ └── page.tsx │ └── page.tsx ├── utils │ ├── breakpoints.ts │ ├── axios.util.ts │ └── extractParamFromUrl.ts ├── declarations.d.ts ├── components │ ├── Header │ │ ├── types.d.ts │ │ ├── Filters │ │ │ ├── FilterButton.tsx │ │ │ ├── Filter.tsx │ │ │ └── Filters.tsx │ │ ├── constants.ts │ │ ├── PageTitle.tsx │ │ └── SocialLinks.tsx │ ├── PageContainer.tsx │ ├── Icons │ │ ├── IssueIcon.tsx │ │ ├── TimeIcon.tsx │ │ ├── ReposIcon.tsx │ │ └── OrgIcon.tsx │ ├── MainContent │ │ ├── ReposList │ │ │ ├── ReposList.tsx │ │ │ ├── Language.tsx │ │ │ ├── Project.tsx │ │ │ └── langColors.ts │ │ ├── CompaniesList.tsx │ │ ├── Company.tsx │ │ ├── ReadmePreview.tsx │ │ └── HelpModalContent.tsx │ ├── LoadingSpinner.tsx │ ├── UnderConstruction │ │ └── UnderConstruction.tsx │ ├── Modal.tsx │ └── ReadmePanel.tsx ├── types │ ├── next.d.ts │ └── index.type.ts ├── middleware.ts ├── hooks │ ├── useWindowSize.ts │ └── useMarkdown.ts └── parser-plugins │ └── markedEmoji.ts ├── public ├── favicon.ico ├── vercel.svg └── next.svg ├── .hintrc ├── postcss.config.js ├── .prettierrc.json ├── env.example ├── .github └── workflows │ └── main.yml ├── .gitignore ├── next.config.js ├── .eslintrc.json ├── tailwind.config.js ├── tsconfig.json ├── LICENSE ├── package.json ├── CONTRIBUTING.md ├── README.md └── CODE_OF_CONDUCT.md /src/server/config/const.ts: -------------------------------------------------------------------------------- 1 | export const ROLES = { ADMIN: 'admin', USER: 'user' }; 2 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yonatanmgr/opensource-il-site/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yonatanmgr/opensource-il-site/HEAD/src/app/favicon.ico -------------------------------------------------------------------------------- /.hintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "development" 4 | ], 5 | "hints": { 6 | "disown-opener": "off" 7 | } 8 | } -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {} 5 | } 6 | }; 7 | -------------------------------------------------------------------------------- /src/utils/breakpoints.ts: -------------------------------------------------------------------------------- 1 | export const breakpoints = { 2 | medium: 768, 3 | large: 992, 4 | xLarge: 1200 5 | }; 6 | -------------------------------------------------------------------------------- /src/server/utils/uuid.ts: -------------------------------------------------------------------------------- 1 | import { v4 as uuidv4 } from 'uuid'; 2 | 3 | export default function getUuid(): string { 4 | return uuidv4(); 5 | } 6 | -------------------------------------------------------------------------------- /src/declarations.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.module.css'; 2 | declare module '*.module.scss' { 3 | const content: Record; 4 | export default content; 5 | } 6 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "trailingComma": "none", 4 | "singleQuote": true, 5 | "printWidth": 80, 6 | "tabWidth": 2, 7 | "plugins": ["prettier-plugin-tailwindcss"], 8 | "endOfLine": "auto" 9 | } 10 | -------------------------------------------------------------------------------- /src/server/utils/isDateOld.ts: -------------------------------------------------------------------------------- 1 | export function isDateOlderThanDays(inputDate: Date, days: number) { 2 | // Calculate the date that was 'days' days ago 3 | const daysAgo = new Date(); 4 | daysAgo.setDate(daysAgo.getDate() - days); 5 | 6 | return inputDate < daysAgo; 7 | } 8 | -------------------------------------------------------------------------------- /env.example: -------------------------------------------------------------------------------- 1 | NODE_ENV=development 2 | PORT=5000 3 | SECRET_KEY=addVar 4 | LOG_FORMAT=dev 5 | GITHUB_READ_ONLY=github api token 6 | MONGODB_URI=add mongo uri 7 | KV_URL=vercel KV (redis) 8 | KV_REST_API_URL=vercel KV (redis) 9 | KV_REST_API_TOKEN=vercel KV (redis) 10 | KV_REST_API_READ_ONLY_TOKEN=vercel KV (redis) 11 | TRIGGER_DATA_REFETCH=vercel KV (redis) -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Hourly cron job 2 | on: 3 | schedule: 4 | - cron: '0 */4 * * *' 5 | jobs: 6 | cron: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Hourly cron job 10 | run: | 11 | curl --request GET \ 12 | --url 'https://opensource-il.vercel.app/api?key=${{ secrets.UPDATE_PASSWORD }}' 13 | -------------------------------------------------------------------------------- /src/components/Header/types.d.ts: -------------------------------------------------------------------------------- 1 | import { sortButtonsTexts } from './constants'; 2 | 3 | export type SortTypes = keyof typeof sortButtonsTexts; 4 | export type AllSortTypes = 5 | (typeof sortButtonsTexts)[keyof typeof sortButtonsTexts]['buttons'][number]['action']; 6 | export interface TitleAndSocialLinkProps { 7 | setView: (view: Views) => void; 8 | view: string; 9 | companyName?: string; 10 | onResetPage?: () => void; 11 | } 12 | -------------------------------------------------------------------------------- /src/components/PageContainer.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | type Props = { 4 | children: React.ReactNode; 5 | }; 6 | 7 | /** 8 | * a generic page wrapper 9 | * 10 | * @param {Props} { children } 11 | * @return {React.ReactNode} 12 | */ 13 | function PageContainer({ children }: Props) { 14 | return ( 15 |
16 | {children} 17 |
18 | ); 19 | } 20 | 21 | export default PageContainer; 22 | -------------------------------------------------------------------------------- /src/components/Icons/IssueIcon.tsx: -------------------------------------------------------------------------------- 1 | export default function TimeIcon() { 2 | return ( 3 | <> 4 | 16 | 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /src/types/next.d.ts: -------------------------------------------------------------------------------- 1 | import type { NextRequest } from 'next/server'; 2 | 3 | /** 4 | * A NextRequest type extention to be used project wide 5 | * 6 | * 7 | * @param {NextRequest} request - the default consumption. 8 | * @example 9 | * ```ts 10 | * export function middleware(request: NextRequestExtended) { 11 | * request.uuid = getUuid(); 12 | * return response; 13 | * } 14 | * ``` 15 | * @see {@link ./src/types/next.d.ts | Request Extended Documentation} 16 | */ 17 | interface NextRequestExtended extends NextRequest { 18 | uuid: string; 19 | } 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | .vscode 4 | # dependencies 5 | /node_modules 6 | /.pnp 7 | .pnp.js 8 | 9 | .env 10 | .env.* 11 | # testing 12 | /coverage 13 | 14 | # next.js 15 | /.next/ 16 | /out/ 17 | 18 | # production 19 | /build 20 | 21 | # misc 22 | .DS_Store 23 | *.pem 24 | 25 | # debug 26 | npm-debug.log* 27 | yarn-debug.log* 28 | yarn-error.log* 29 | 30 | # local env files 31 | .env*.local 32 | 33 | # vercel 34 | .vercel 35 | 36 | # typescript 37 | *.tsbuildinfo 38 | next-env.d.ts 39 | -------------------------------------------------------------------------------- /src/server/utils/axiosInstance.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { logger } from './logger'; 3 | 4 | export const axiosInstance = axios.create({}); 5 | 6 | axiosInstance.interceptors.request.use((config) => { 7 | logger.info('Request sent', config); 8 | return config; 9 | }); 10 | 11 | axiosInstance.interceptors.response.use( 12 | (response) => { 13 | logger.info('🚀 ~ file: axiosInstance.tsx:14 ~ response:', response); 14 | return response; 15 | }, 16 | (error) => { 17 | logger.error('🚀 ~ file: axiosInstance.tsx:17 ~ error:', error); 18 | return Promise.reject(error); 19 | } 20 | ); 21 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/Header/Filters/FilterButton.tsx: -------------------------------------------------------------------------------- 1 | interface FilterButtonProps { 2 | onClick: () => void; 3 | text: string; 4 | isActive: boolean; 5 | } 6 | 7 | export const FilterButton = ({ 8 | onClick, 9 | text, 10 | isActive 11 | }: FilterButtonProps) => { 12 | return ( 13 | 21 | ); 22 | }; 23 | -------------------------------------------------------------------------------- /src/components/Icons/TimeIcon.tsx: -------------------------------------------------------------------------------- 1 | export default function TimeIcon() { 2 | return ( 3 | <> 4 | גרסה אחרונה: 5 | 16 | 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | experimental: { 4 | appDir: true 5 | }, 6 | reactStrictMode: true, 7 | env: { github_read_only: process.env.GITHUB_READ_ONLY }, 8 | images: { 9 | remotePatterns: [ 10 | { 11 | protocol: 'https', 12 | hostname: 'opengraph.githubassets.com', 13 | port: '', 14 | pathname: '/**' 15 | }, 16 | { 17 | protocol: 'https', 18 | hostname: '**.githubusercontent.com', 19 | port: '', 20 | pathname: '/**' 21 | } 22 | ] 23 | } 24 | }; 25 | 26 | module.exports = nextConfig; 27 | -------------------------------------------------------------------------------- /src/app/api/company/route.ts: -------------------------------------------------------------------------------- 1 | import { logger } from '@/server/utils/logger'; 2 | import getUuid from '@/server/utils/uuid'; 3 | import { NextResponse } from 'next/server'; 4 | import { fetchAllCompanies } from '@/server/services/dataManager.service'; 5 | 6 | export const dynamic = 'force-dynamic'; 7 | 8 | export async function GET() { 9 | const uuid = getUuid(); 10 | const companies = await fetchAllCompanies(); 11 | const payload = { 12 | uuid, 13 | companies, 14 | success: companies?.length ? true : false 15 | }; 16 | logger.info('request company', { 17 | uuid, 18 | success: companies?.length ? true : false, 19 | companies: companies?.length 20 | }); 21 | return NextResponse.json(payload); 22 | } 23 | -------------------------------------------------------------------------------- /src/app/api/health/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from 'next/server'; 2 | // utils 3 | import { logger } from '@/server/utils/logger'; 4 | import getUuid from '@/server/utils/uuid'; 5 | import { getRedisVal, setRedisVal } from '@/server/db/redis-vercel-kv'; 6 | import { NODE_ENV } from '@/server/config'; 7 | 8 | export const dynamic = 'force-dynamic'; 9 | 10 | export async function GET() { 11 | await setRedisVal('test', getUuid()); 12 | 13 | const data = { 14 | requestId: getUuid(), 15 | uptime: process.uptime(), 16 | message: 'Ok', 17 | date: new Date(), 18 | apiRedis: NODE_ENV, 19 | redis: await getRedisVal('test') 20 | }; 21 | logger.info('/api/health', data); 22 | return NextResponse.json(data); 23 | } 24 | -------------------------------------------------------------------------------- /src/components/Header/constants.ts: -------------------------------------------------------------------------------- 1 | export const sortButtonsTexts = { 2 | lastCommit: { 3 | title: 'זמן גרסה אחרונה', 4 | buttons: [ 5 | { text: '▲', action: 'lastCommitReverse' }, 6 | { text: '▼', action: 'lastCommit' } 7 | ] 8 | }, 9 | stars: { 10 | title: 'כמות כוכבים', 11 | buttons: [ 12 | { text: '▲', action: 'starsReverse' }, 13 | { text: '▼', action: 'stars' } 14 | ] 15 | }, 16 | issues: { 17 | title: 'כמות Issues פתוחים', 18 | buttons: [ 19 | { text: '▲', action: 'issuesReverse' }, 20 | { text: '▼', action: 'issues' } 21 | ] 22 | }, 23 | default: { 24 | title: 'איפוס', 25 | buttons: [{ text: 'איפוס', action: 'default' }] 26 | } 27 | } as const; 28 | -------------------------------------------------------------------------------- /src/app/api/repositories/route.ts: -------------------------------------------------------------------------------- 1 | import { logger } from '@/server/utils/logger'; 2 | import getUuid from '@/server/utils/uuid'; 3 | import { NextResponse } from 'next/server'; 4 | import { fetchAllRepositories } from '@/server/services/dataManager.service'; 5 | 6 | export const dynamic = 'force-dynamic'; 7 | 8 | export async function GET() { 9 | const uuid = getUuid(); 10 | const repositories = await fetchAllRepositories(); 11 | const payload = { 12 | uuid, 13 | repositories, 14 | success: repositories?.length ? true : false 15 | }; 16 | logger.info('request repository', { 17 | uuid, 18 | success: repositories?.length ? true : false, 19 | repositories: repositories?.length 20 | }); 21 | return NextResponse.json(payload); 22 | } 23 | -------------------------------------------------------------------------------- /src/components/MainContent/ReposList/ReposList.tsx: -------------------------------------------------------------------------------- 1 | import Project from '@/components/MainContent/ReposList/Project'; 2 | import { DataProps } from '@/types/index.type'; 3 | 4 | export default function ReposList(props: { 5 | showData: DataProps[]; 6 | setReadme: (readme: string) => void; 7 | }) { 8 | return ( 9 |
13 | {props.showData.map((project) => { 14 | return ( 15 | 20 | ); 21 | })} 22 |
23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /src/components/MainContent/CompaniesList.tsx: -------------------------------------------------------------------------------- 1 | import { CompanyProps } from '@/types/index.type'; 2 | import Company from './Company'; 3 | 4 | export default function CompaniesList(props: { 5 | companies: CompanyProps[]; 6 | setComp: (company: CompanyProps) => void; 7 | }) { 8 | return ( 9 |
13 | {props.companies.map((comp) => { 14 | return ( 15 | 22 | ); 23 | })} 24 |
25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /src/components/LoadingSpinner.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | 4 | type Props = { show: boolean }; 5 | 6 | function LoadingSpinner({ show }: Props) { 7 | const modalRoot = React.useRef(null); 8 | 9 | React.useEffect(() => { 10 | if (document) { 11 | modalRoot.current = document?.getElementById('modal-root'); 12 | } 13 | }, []); 14 | 15 | if (!modalRoot.current || !show) { 16 | return null; 17 | } 18 | 19 | return ReactDOM.createPortal( 20 |
21 |
22 |
, 23 | modalRoot.current 24 | ); 25 | } 26 | 27 | export default LoadingSpinner; 28 | -------------------------------------------------------------------------------- /src/server/db/redis-vercel-kv.ts: -------------------------------------------------------------------------------- 1 | import { kv } from '@vercel/kv'; 2 | import { logger } from '../utils/logger'; 3 | 4 | export async function setRedisVal(key: string, value: unknown) { 5 | const payload = JSON.stringify({ data: value }); 6 | return await kv.set(key, payload); 7 | } 8 | 9 | export async function getRedisVal(key: string) { 10 | try { 11 | const payload: { data: unknown } | null = await kv.get(key); 12 | if (!payload?.data) return null; 13 | // if (!payload?.data) throw new Error('error fetching data'); 14 | return payload.data; 15 | } catch (error) { 16 | logger.warn('payload is null', error); 17 | return null; 18 | } 19 | } 20 | 21 | export async function getAllRedisKeys() { 22 | return await kv.keys('*'); 23 | } 24 | 25 | export async function delRedisKey(key: string) { 26 | return await kv.del(key); 27 | } 28 | -------------------------------------------------------------------------------- /src/server/config/index.ts: -------------------------------------------------------------------------------- 1 | import { logger } from '../utils/logger'; 2 | 3 | type ConfigVars = { 4 | NODE_ENV: string; 5 | PORT: string; 6 | SECRET_KEY: string; 7 | LOG_FORMAT: string; 8 | GITHUB_READ_ONLY: string; 9 | MONGODB_URI: string; 10 | TRIGGER_DATA_REFETCH: string; 11 | }; 12 | 13 | export const { 14 | NODE_ENV, 15 | PORT, 16 | GITHUB_READ_ONLY, 17 | MONGODB_URI, 18 | SECRET_KEY, 19 | LOG_FORMAT, 20 | TRIGGER_DATA_REFETCH 21 | } = process.env as ConfigVars; 22 | 23 | const testVars: ConfigVars = { 24 | NODE_ENV, 25 | PORT, 26 | SECRET_KEY, 27 | LOG_FORMAT, 28 | GITHUB_READ_ONLY, 29 | MONGODB_URI, 30 | TRIGGER_DATA_REFETCH 31 | }; 32 | 33 | for (const key in testVars) { 34 | if (!key) { 35 | logger.error('🚀 ~ file: index.ts:34 ~ key:', { 36 | key, 37 | var: testVars[key as keyof ConfigVars] 38 | }); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true, 5 | "node": true 6 | }, 7 | "extends": [ 8 | "next/core-web-vitals", 9 | "eslint:recommended", 10 | "plugin:react/recommended", 11 | "plugin:@typescript-eslint/recommended", 12 | "plugin:prettier/recommended" 13 | ], 14 | "overrides": [], 15 | "parser": "@typescript-eslint/parser", 16 | "parserOptions": { 17 | "project": "./tsconfig.json", 18 | "ecmaVersion": "latest", 19 | "sourceType": "module" 20 | }, 21 | "plugins": ["react", "@typescript-eslint", "prettier"], 22 | "rules": { 23 | "@next/next/no-html-link-for-pages": ["error", "./src/app"], 24 | "@typescript-eslint/no-explicit-any": "off", 25 | "react/react-in-jsx-scope": "off", 26 | "prettier/prettier": [ 27 | "warn", 28 | { 29 | "endOfLine": "auto" 30 | } 31 | ] 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/middleware.ts: -------------------------------------------------------------------------------- 1 | // import getUuid from '@/utils/uuid'; 2 | import { NextResponse } from 'next/server'; 3 | 4 | // This function can be marked `async` if using `await` inside 5 | export function middleware() { 6 | // request.uuid = getUuid(); 7 | // response.headers.set('uuid', getUuid()); 8 | // response.cookies?.set('uuid', getUuid()); 9 | // console.log( 10 | // '🚀 ~ file: middleware.ts:15 ~ response.cookies:', 11 | // response.cookies 12 | // ); 13 | 14 | return NextResponse.next(); 15 | } 16 | 17 | // See "Matching Paths" below to learn more 18 | export const config = { 19 | matcher: [ 20 | /* 21 | * Match all request paths except for the ones starting with: 22 | * - _next/static (static files) 23 | * - _next/image (image optimization files) 24 | * - favicon.ico (favicon file) 25 | */ 26 | '/((?!_next/static|_next/image|favicon.ico).*)' 27 | ] 28 | }; 29 | -------------------------------------------------------------------------------- /src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import Head from 'next/head'; 2 | import './globals.css'; 3 | import { Analytics } from '@vercel/analytics/react'; 4 | import UnderConstruction from '@/components/UnderConstruction/UnderConstruction'; 5 | export const metadata = { 6 | title: 'קוד פתוח ישראלי', 7 | description: 'Open Source Community Israel', 8 | viewport: 'width=device-width, initial-scale=1' 9 | }; 10 | 11 | export default function RootLayout({ 12 | children 13 | }: { 14 | children: React.ReactNode; 15 | }) { 16 | return ( 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | {children} 25 | 26 | 27 | 28 | 29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: [ 4 | './app/**/*.{js,ts,jsx,tsx}', 5 | './pages/**/*.{js,ts,jsx,tsx}', 6 | './components/**/*.{js,ts,jsx,tsx}', 7 | 8 | // Or if using `src` directory: 9 | './src/**/*.{js,ts,jsx,tsx}' 10 | ], 11 | theme: { 12 | extend: { 13 | boxShadow: { 14 | '3xl': '0px 0px 30px hsl(222, 50%, 30%)', 15 | '4xl': '0px 0px 30px hsla(1, 0%, 0%, 0.4)' 16 | }, 17 | colors: { 18 | myblue: 'hsl(222, 50%, 30%)', 19 | mylightblue: 'hsl(222, 50%, 40%)', 20 | mydarkblue: 'hsl(222, 65%, 10%)', 21 | readmedark: 'hsl(222, 28%, 7%)', 22 | buttonhover: 'hsl(222, 63%, 15%)', 23 | buttonactive: 'hsl(222, 63%, 20%)', 24 | gradientfrom: 'hsla(222, 52%, 15%)', 25 | gradientto: 'hsl(222, 61%, 11%)' 26 | } 27 | } 28 | }, 29 | plugins: [] 30 | }; 31 | -------------------------------------------------------------------------------- /src/components/MainContent/ReposList/Language.tsx: -------------------------------------------------------------------------------- 1 | import { langsColors } from './langColors'; 2 | 3 | export default function LangPill(props: { name: string; size: number }) { 4 | return ( 5 |
12 | {props.name}{' '} 13 | 14 | ({props.size}%) 15 | 16 |
17 | ); 18 | } 19 | 20 | const setContrast = (hex: string) => { 21 | const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); 22 | if (result) 23 | return (parseInt(result[1], 16) * 299 + 24 | parseInt(result[2], 16) * 587 + 25 | parseInt(result[3], 16) * 114) / 26 | 1000 > 27 | 125 28 | ? 'black' 29 | : 'white'; 30 | }; 31 | -------------------------------------------------------------------------------- /src/app/api/company/[company]/route.ts: -------------------------------------------------------------------------------- 1 | import { Organization } from './../../../../types/index.type'; 2 | import { extractLastParamFromUrl } from '@/server/utils/extractParamFromUrl'; 3 | import { logger } from '@/server/utils/logger'; 4 | import getUuid from '@/server/utils/uuid'; 5 | import { NextRequest, NextResponse } from 'next/server'; 6 | import { fetchCompany } from '@/server/services/dataManager.service'; 7 | 8 | export const dynamic = 'force-dynamic'; 9 | 10 | export async function GET(req: NextRequest) { 11 | const uuid = getUuid(); 12 | const companyId = extractLastParamFromUrl(req); 13 | if (!companyId) { 14 | logger.error('missing companyId', { companyId }); 15 | return NextResponse.json({ error: 'no valid companyid' }); 16 | } 17 | const company = await fetchCompany(companyId); 18 | const payload = { 19 | uuid, 20 | company: company || null, 21 | companyName: company?.organization?.name || '', 22 | success: !!company 23 | }; 24 | logger.info('request company', payload.companyName); 25 | return NextResponse.json(payload); 26 | } 27 | -------------------------------------------------------------------------------- /src/utils/axios.util.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | // 4 | const BASE_URL = `/api/`; 5 | const axiosInstance = axios.create({ 6 | baseURL: BASE_URL 7 | }); 8 | 9 | axiosInstance.interceptors.request.use((config) => { 10 | // const token = LocalStorageService.get(TOKEN_LS_KEY); 11 | // if (token) { 12 | // config.headers = config.headers || {}; 13 | // (config.headers as any)['Authorization'] = `Bearer ${token}`; 14 | // } 15 | 16 | return config; 17 | }); 18 | 19 | axiosInstance.interceptors.response.use( 20 | (response) => { 21 | return response; 22 | }, 23 | (error) => { 24 | console.log('🚀 ~ file: HttpService.ts:39 ~ error:', error); 25 | // if (error.response.status === 401) { 26 | // // dispatch the logout action 27 | // store.dispatch(logoutAction()) 28 | // LocalStorageService.delete(TOKEN_LS_KEY) 29 | // LocalStorageService.delete(ROLE_LS_KEY) 30 | // LocalStorageService.delete(USERNAME_LS_KEY) 31 | // } 32 | return Promise.reject(error); 33 | } 34 | ); 35 | 36 | export default axiosInstance; 37 | -------------------------------------------------------------------------------- /src/hooks/useWindowSize.ts: -------------------------------------------------------------------------------- 1 | import { breakpoints } from '@/utils/breakpoints'; 2 | import { useEffect, useState } from 'react'; 3 | 4 | interface WindowSize { 5 | width: number; 6 | height: number; 7 | } 8 | 9 | const getWindowSize = (isBrowser: boolean): WindowSize => { 10 | return { 11 | width: isBrowser ? window.innerWidth : 0, 12 | height: isBrowser ? window.innerHeight : 0 13 | }; 14 | }; 15 | 16 | export const useWindowSize = () => { 17 | const isBrowser = typeof window !== 'undefined'; 18 | const [windowSize, setWindowSize] = useState( 19 | getWindowSize(isBrowser) 20 | ); 21 | 22 | useEffect(() => { 23 | const handler = () => { 24 | setWindowSize(getWindowSize(isBrowser)); 25 | }; 26 | window?.addEventListener('resize', handler); 27 | return () => window?.removeEventListener('resize', handler); 28 | }, [isBrowser]); 29 | 30 | return { 31 | isMediumUp: windowSize.width > breakpoints.medium, 32 | isLargeUp: windowSize.width > breakpoints.large, 33 | isXLargeUp: windowSize.width > breakpoints.xLarge 34 | }; 35 | }; 36 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "emitDecoratorMetadata": true, 8 | "strict": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "experimentalDecorators": true, 11 | "noEmit": true, 12 | "esModuleInterop": true, 13 | "module": "esnext", 14 | "moduleResolution": "node", 15 | "resolveJsonModule": true, 16 | "isolatedModules": true, 17 | "jsx": "preserve", 18 | "incremental": true, 19 | "strictPropertyInitialization": false, 20 | "plugins": [ 21 | { 22 | "name": "next" 23 | } 24 | ], 25 | "paths": { 26 | "@/*": ["./src/*"] 27 | } 28 | }, 29 | "include": [ 30 | "next-env.d.ts", 31 | "**/*.ts", 32 | "**/*.tsx", 33 | ".next/types/**/*.ts", 34 | "src/declarations.d.ts", 35 | "src/server/types/**/*.d.ts", 36 | "src/server/types/*.d.ts", 37 | "next.config.js", 38 | "postcss.config.js", 39 | "tailwind.config.js" 40 | ], 41 | "exclude": ["node_modules"] 42 | } -------------------------------------------------------------------------------- /src/app/api/route.ts: -------------------------------------------------------------------------------- 1 | import { TRIGGER_DATA_REFETCH } from '@/server/config'; 2 | import { getRedisVal, setRedisVal } from '@/server/db/redis-vercel-kv'; 3 | import { mainDataFetch } from '@/server/services/dataManager.service'; 4 | import { extractSearchParams } from '@/server/utils/extractParamFromUrl'; 5 | import { logger } from '@/server/utils/logger'; 6 | import { NextRequest, NextResponse } from 'next/server'; 7 | 8 | export const dynamic = 'force-dynamic'; 9 | 10 | export async function GET(req: NextRequest) { 11 | const searchParams = extractSearchParams(req); 12 | let isRefetch = false; 13 | if (searchParams?.refetch && searchParams.refetch === TRIGGER_DATA_REFETCH) { 14 | isRefetch = true; 15 | } 16 | const data = await mainDataFetch(isRefetch); 17 | await setRedisVal('test', new Date().toISOString()); 18 | const r = await getRedisVal('test'); 19 | logger.info('api/', { 20 | successRedis: !!r, 21 | dataSuccess: data?.success, 22 | date: new Date(), 23 | isRefetch 24 | }); 25 | return NextResponse.json({ 26 | refetch: isRefetch, 27 | api: 'online', 28 | data 29 | }); 30 | } 31 | -------------------------------------------------------------------------------- /src/components/Icons/ReposIcon.tsx: -------------------------------------------------------------------------------- 1 | import { TitleAndSocialLinkProps } from '../Header/types'; 2 | 3 | export default function ReposIcon({ setView, view }: TitleAndSocialLinkProps) { 4 | return ( 5 | 6 | 21 | 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Yonatan Magier 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/server/utils/extractParamFromUrl.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest } from 'next/server'; 2 | import { logger } from './logger'; 3 | 4 | export function extractLastParamFromUrl(req: NextRequest) { 5 | try { 6 | if (!req.url) { 7 | throw new Error('no req.url'); 8 | } 9 | const urlObj = new URL(req.url); 10 | return urlObj.pathname.split('/').pop(); 11 | } catch (error) { 12 | logger.error( 13 | '🚀 ~ file: extractParamFromUrl.ts:13 ~ extractParamFromUrl ~ error:', 14 | error 15 | ); 16 | return null; 17 | } 18 | } 19 | 20 | export function extractSearchParams(req: NextRequest) { 21 | try { 22 | if (!req.url) { 23 | throw new Error('no req.url'); 24 | } 25 | const urlObj = new URL(req.url); 26 | 27 | return Array.from(urlObj.searchParams.entries()).reduce( 28 | (acc: any, item: string[]) => { 29 | acc[item[0]] = item[1]; 30 | return acc; 31 | }, 32 | {} 33 | ); 34 | } catch (error) { 35 | logger.error( 36 | '🚀 ~ file: extractParamFromUrl.ts:13 ~ extractParamFromUrl ~ error:', 37 | error 38 | ); 39 | return null; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/components/MainContent/Company.tsx: -------------------------------------------------------------------------------- 1 | import { CompanyProps } from '@/types/index.type'; 2 | import Image from 'next/image'; 3 | 4 | export default function Project(props: { 5 | name: string; 6 | login: string; 7 | avatar: string; 8 | setComp: (company: CompanyProps) => unknown; 9 | }) { 10 | const url = `https://www.github.com/${props.login}`; 11 | 12 | return ( 13 |
14 | { 16 | props.setComp({ 17 | name: props.name, 18 | login: props.login, 19 | avatar: props.avatar 20 | }); 21 | }} 22 | draggable={false} 23 | className="aspect-square rounded-xl border border-myblue transition group-hover:scale-105 group-hover:shadow-3xl group-active:scale-95" 24 | width={120} 25 | height={120} 26 | src={props.avatar} 27 | alt={props.name} 28 | /> 29 | 30 | 36 | {props.name} 37 | 38 | 39 |
40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /src/types/index.type.ts: -------------------------------------------------------------------------------- 1 | export type ReademGithubCompany = { name: string }; 2 | export type ReademGithubProject = { name: string; description: string }; 3 | 4 | export type Organization = { 5 | name: string; 6 | avatarUrl: string; 7 | login: string; 8 | repositories: { 9 | nodes: RepoProps[]; 10 | }; 11 | }; 12 | 13 | export type RepoProps = { 14 | openIssues: { 15 | totalCount: number; 16 | }; 17 | stargazerCount: number; 18 | nameWithOwner: string; 19 | languages: { 20 | totalSize: number; 21 | edges: { 22 | size: number; 23 | node: { 24 | name: string; 25 | }; 26 | }[]; 27 | }; 28 | openGraphImageUrl: string; 29 | description: string | null; 30 | defaultBranchRef: { 31 | target: { 32 | committedDate: string; 33 | }; 34 | }; 35 | }; 36 | 37 | export type Views = 'repos' | 'companies'; 38 | 39 | export type DataProps = { 40 | id: string; 41 | image: string; 42 | owner: string; 43 | name: string; 44 | description: string; 45 | lastCommit: string; 46 | stars: number; 47 | issuesCount: number; 48 | languages: { 49 | name: string; 50 | size: number; 51 | }[]; 52 | totalSize: number; 53 | }; 54 | 55 | export type CompanyProps = { 56 | name: string; 57 | login: string; 58 | avatar: string; 59 | }; 60 | -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/UnderConstruction/UnderConstruction.tsx: -------------------------------------------------------------------------------- 1 | const UnderConstruction = () => { 2 | return ( 3 |
4 | 30 |
31 | ); 32 | }; 33 | 34 | export default UnderConstruction; 35 | -------------------------------------------------------------------------------- /src/parser-plugins/markedEmoji.ts: -------------------------------------------------------------------------------- 1 | import type { marked } from 'marked'; 2 | 3 | interface MarkedEmojiOptions { 4 | emojis: Record; 5 | } 6 | 7 | export function markedEmoji(options: MarkedEmojiOptions) { 8 | if (!options.emojis) { 9 | throw new Error('Must provide emojis to markedEmoji'); 10 | } 11 | 12 | const start = (src: string) => { 13 | return src.indexOf(':'); 14 | }; 15 | 16 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 17 | const tokenizer = (src: string, tokens: marked.Token[]) => { 18 | const rule = /^:(.+?):/; 19 | const match = rule.exec(src); 20 | if (!match) return; 21 | const name = match[1]; 22 | const emoji = options.emojis[name]; 23 | if (!emoji) return; 24 | return { 25 | type: 'emoji', 26 | raw: match[0], 27 | name, 28 | emoji 29 | }; 30 | }; 31 | 32 | const renderer = (token: NonNullable>) => { 33 | return `${token.emoji}`; 34 | }; 35 | 36 | const extensions = [ 37 | { 38 | name: 'emoji', 39 | level: 'inline', 40 | start, 41 | tokenizer, 42 | // This coercion is necessary because marked's type definitions are bad. 43 | renderer: renderer as (token: marked.Tokens.Generic) => string 44 | } 45 | ]; 46 | return { 47 | extensions 48 | }; 49 | } 50 | -------------------------------------------------------------------------------- /src/components/Modal.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import React, { useEffect, useRef } from 'react'; 3 | import ReactDOM from 'react-dom'; 4 | 5 | type ModalProps = { 6 | setShow: (arg0: boolean) => unknown; 7 | show: boolean; 8 | children: React.ReactNode; 9 | }; 10 | 11 | export default function Modal({ setShow, show, children }: ModalProps) { 12 | const modalRoot = useRef(null); 13 | 14 | useEffect(() => { 15 | if (document) { 16 | modalRoot.current = document?.getElementById('modal-root'); 17 | } 18 | }, []); 19 | 20 | if (!modalRoot.current || !show) { 21 | return null; 22 | } 23 | 24 | return ReactDOM.createPortal( 25 |
setShow(false)}> 26 |
27 |
31 | 38 | {children} 39 |
40 |
41 |
, 42 | modalRoot.current 43 | ); 44 | } 45 | -------------------------------------------------------------------------------- /src/utils/extractParamFromUrl.ts: -------------------------------------------------------------------------------- 1 | export function parseLastParamFromUrl(pathname: string) { 2 | try { 3 | if (!pathname?.length || !pathname.includes('/')) { 4 | throw new Error('invalid pathname: ' + pathname); 5 | } 6 | const lastParam = pathname.split('/').pop(); 7 | return lastParam?.includes('?') ? lastParam.split('?')[0] : lastParam; 8 | } catch (error) { 9 | console.log( 10 | '🚀 ~ file: extractParamFromUrl.ts:11 ~ extractLastParamFromUrl ~ error:', 11 | error 12 | ); 13 | 14 | return null; 15 | } 16 | } 17 | 18 | export function parseSearchParams(searchParams: string) { 19 | try { 20 | if (!searchParams.length || !searchParams.includes('=')) { 21 | throw new Error('invalid searchParams: ' + searchParams); 22 | } 23 | const queryParamEntries = searchParams.split('&'); 24 | 25 | return queryParamEntries.reduce((acc: any, item: string) => { 26 | if (!item.includes('=')) { 27 | console.warn( 28 | '🚀 ~ file: extractParamFromUrl.ts:27 ~ returnqueryParamEntries.reduce ~ item ~ invalid query param:', 29 | item 30 | ); 31 | throw new Error( 32 | 'extractParamFromUrl - reducer got invalid query param: ' + 33 | item + 34 | ' searchParams: ' + 35 | searchParams 36 | ); 37 | } 38 | const entry = item.split('='); 39 | acc[entry[0]] = entry[1]; 40 | return acc; 41 | }, {}); 42 | } catch (error) { 43 | console.error( 44 | '🚀 ~ file: extractParamFromUrl.ts:13 ~ extractParamFromUrl ~ error:', 45 | error 46 | ); 47 | return null; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/components/Icons/OrgIcon.tsx: -------------------------------------------------------------------------------- 1 | import { TitleAndSocialLinkProps } from '../Header/types'; 2 | 3 | export default function OrgIcon({ setView, view }: TitleAndSocialLinkProps) { 4 | return ( 5 | 6 | 21 | 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "opensource-il-site", 3 | "version": "0.1.0", 4 | "private": true, 5 | "engines": { 6 | "node": "^18.0.0" 7 | }, 8 | "scripts": { 9 | "dev": "next dev", 10 | "build": "next build", 11 | "start": "next start", 12 | "lint": "next lint" 13 | }, 14 | "dependencies": { 15 | "@types/marked": "^4.3.0", 16 | "@types/node": "18.15.3", 17 | "@types/react": "18.0.28", 18 | "@types/react-dom": "18.0.11", 19 | "@vercel/analytics": "^0.1.11", 20 | "@vercel/kv": "^0.2.1", 21 | "axios": "^1.4.0", 22 | "eslint-config-next": "13.2.4", 23 | "eslint-config-standard-with-typescript": "^34.0.1", 24 | "gemoji": "^8.1.0", 25 | "github-markdown-css": "^5.2.0", 26 | "html-react-parser": "^3.0.15", 27 | "marked": "^5.0.2", 28 | "moment": "^2.29.4", 29 | "next": "13.2.4", 30 | "octokit": "^2.0.14", 31 | "react": "18.2.0", 32 | "react-dom": "18.2.0", 33 | "redis": "^4.6.6", 34 | "sass": "^1.60.0", 35 | "showdown": "^2.1.0", 36 | "typescript": "5.0.2", 37 | "uuid": "^9.0.0", 38 | "winston": "^3.8.2", 39 | "winston-mongodb": "^5.1.1" 40 | }, 41 | "devDependencies": { 42 | "@types/redis": "^4.0.11", 43 | "@types/uuid": "^9.0.1", 44 | "@typescript-eslint/eslint-plugin": "^5.59.5", 45 | "@typescript-eslint/parser": "^5.59.5", 46 | "autoprefixer": "^10.4.14", 47 | "eslint": "^8.40.0", 48 | "eslint-config-prettier": "^8.8.0", 49 | "eslint-plugin-prettier": "^4.2.1", 50 | "eslint-plugin-react": "^7.32.2", 51 | "postcss": "^8.4.21", 52 | "prettier": "^2.8.8", 53 | "prettier-plugin-tailwindcss": "^0.2.8", 54 | "tailwindcss": "^3.2.7" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/components/Header/PageTitle.tsx: -------------------------------------------------------------------------------- 1 | import SocialLinks from './SocialLinks'; 2 | import { TitleAndSocialLinkProps } from './types'; 3 | 4 | export default function PageTitle({ 5 | setView, 6 | view, 7 | companyName, 8 | onResetPage 9 | }: TitleAndSocialLinkProps) { 10 | const currentView = { 11 | repos: ( 12 | 13 | {companyName ? ( 14 | <> 15 | {companyName} 16 | / פרויקטים 17 | 18 | ) : ( 19 | 'פרויקטי קוד פתוח ישראלים' 20 | )} 21 | 22 | ), 23 | companies: חברות ישראליות בקוד פתוח 24 | }[view]; 25 | return ( 26 |
30 |
36 | {companyName && view == 'repos' && ( 37 | 45 | )} 46 | {currentView} 47 |
48 | 49 |
50 | ); 51 | } 52 | -------------------------------------------------------------------------------- /src/server/utils/logger.ts: -------------------------------------------------------------------------------- 1 | import winston, { transports } from 'winston'; 2 | import 'winston-mongodb'; 3 | 4 | const loggerInstance = winston.createLogger({ 5 | format: winston.format.json(), 6 | transports: [ 7 | new transports.Console({ level: 'info' }), 8 | new transports.Console({ level: 'warn', stderrLevels: ['warn'] }), 9 | new transports.MongoDB({ 10 | db: process.env.MONGODB_URI as string, 11 | options: { 12 | useNewUrlParser: true, 13 | useUnifiedTopology: true 14 | }, 15 | dbName: 'opensource-logs', 16 | collection: 'logs', 17 | label: 'vercel', 18 | level: 'info', 19 | metaKey: 'meta' 20 | }) 21 | ] 22 | }); 23 | 24 | /** 25 | * A Logger type extension to be used project wide. 26 | * 27 | * @property {Function} info - Logs informational messages. 28 | * @property {Function} warn - Logs warning messages. 29 | * @property {Function} error - Logs error messages. 30 | * @example 31 | * ```ts 32 | * const log: AppLogger = logger; 33 | * log.info("Informational message"); 34 | * log.info({hello:"world"}); 35 | * log.warn("Warning message"); 36 | * log.error("Error message"); 37 | * ``` 38 | */ 39 | type AppLoggerMethod = (message: string, data?: unknown) => void; 40 | 41 | interface AppLogger { 42 | info: AppLoggerMethod; 43 | warn: AppLoggerMethod; 44 | error: AppLoggerMethod; 45 | } 46 | 47 | const logger: AppLogger = { 48 | info: (message: string, data = {}) => { 49 | loggerInstance.info('info', { message, meta: { data } }); 50 | }, 51 | warn: (message: string, data = {}) => { 52 | loggerInstance.warn('warn', { message, meta: { data } }); 53 | }, 54 | error: (message: string, data = {}) => { 55 | loggerInstance.error('error', { message, meta: { data } }); 56 | } 57 | }; 58 | 59 | export { logger }; 60 | -------------------------------------------------------------------------------- /src/components/Header/Filters/Filter.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { sortButtonsTexts } from '../constants'; 3 | import { AllSortTypes, SortTypes } from '../types'; 4 | import { useWindowSize } from '@/hooks/useWindowSize'; 5 | 6 | interface FilterProps { 7 | sortType: string; 8 | activeSortType: AllSortTypes | undefined; 9 | onSortChange: (sortType: SortTypes) => void; 10 | setShouldShowFilters: (arg0: boolean) => unknown; 11 | } 12 | 13 | export default function Filter({ 14 | sortType, 15 | activeSortType, 16 | onSortChange, 17 | setShouldShowFilters 18 | }: FilterProps) { 19 | const { isMediumUp } = useWindowSize(); 20 | 21 | const isActive = sortButtonsTexts[sortType as SortTypes].buttons?.some( 22 | (tp: any) => tp.action === activeSortType 23 | ); 24 | const [activeIndex, setActiveIndex] = useState(0); 25 | 26 | return ( 27 |
{ 33 | setActiveIndex(() => (activeIndex === 1 ? 0 : 1)); 34 | onSortChange( 35 | sortButtonsTexts[sortType as SortTypes].buttons[activeIndex] 36 | .action as SortTypes 37 | ); 38 | if (!isMediumUp) { 39 | setShouldShowFilters(false); 40 | } 41 | }} 42 | > 43 | 44 | {sortButtonsTexts[sortType as SortTypes].title} 45 | 46 |
47 | {isActive && } 48 |
49 |
50 | ); 51 | } 52 | -------------------------------------------------------------------------------- /src/app/globals.css: -------------------------------------------------------------------------------- 1 | @import 'https://fonts.googleapis.com/css2?family=Rubik:wght@300;400;500;600;700;800;900&display=swap'; 2 | /* we can use these lines instead if we want to support general dark / light modes 3 | @import url("https://cdnjs.cloudflare.com/ajax/libs/github-markdown-css/5.2.0/github-markdown-dark.css") (prefers-color-scheme: dark); 4 | @import url("https://cdnjs.cloudflare.com/ajax/libs/github-markdown-css/5.2.0/github-markdown-light.css") (prefers-color-scheme: light); */ 5 | @import url('https://cdnjs.cloudflare.com/ajax/libs/github-markdown-css/5.2.0/github-markdown-dark.css'); 6 | 7 | /* try to edit img in readme 8 | It seems that there is no reference to this in the github-markdown css file. I added a global setting that would give a max-height of 8rem and it looks much better.*/ 9 | 10 | .markdown-body img { 11 | max-height: 25rem; 12 | } 13 | 14 | @tailwind base; 15 | @tailwind components; 16 | @tailwind utilities; 17 | 18 | @layer base { 19 | 20 | .markdown-body ul, 21 | ol, 22 | menu { 23 | list-style: revert; 24 | } 25 | } 26 | 27 | @layer utilities { 28 | .no-scrollbar::-webkit-scrollbar { 29 | display: none; 30 | } 31 | 32 | .no-scrollbar { 33 | -ms-overflow-style: none; 34 | scrollbar-width: none; 35 | } 36 | } 37 | 38 | .markdown-body { 39 | padding: 45px; 40 | } 41 | 42 | a>img, 43 | p>img { 44 | display: inline; 45 | } 46 | 47 | .arrow { 48 | border: solid #fff; 49 | border-width: 0 2px 2px 0; 50 | display: inline-block; 51 | padding: 2px; 52 | } 53 | 54 | .up { 55 | -webkit-transform: rotate(-135deg); 56 | transform: rotate(-135deg); 57 | } 58 | 59 | .down { 60 | -webkit-transform: rotate(45deg); 61 | transform: rotate(45deg); 62 | } 63 | 64 | /* we can use these lines instead if we want to support general dark / light modes 65 | @media (prefers-color-scheme: dark){ 66 | [href$="#gh-light-mode-only"] { 67 | display: none; 68 | } 69 | } 70 | 71 | @media (prefers-color-scheme: light){ 72 | [href$="#gh-dark-mode-only"] { 73 | display: none; 74 | } 75 | } 76 | */ 77 | [href$='#gh-light-mode-only'] { 78 | display: none; 79 | } 80 | -------------------------------------------------------------------------------- /src/hooks/useMarkdown.ts: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import { marked } from 'marked'; 3 | import { markedEmoji } from '../parser-plugins/markedEmoji'; 4 | import { nameToEmoji } from 'gemoji'; 5 | import { DataProps } from '@/types/index.type'; 6 | 7 | marked.use( 8 | { 9 | // This silences warnings about deprecated options, which are enabled by default for some reason, taken from: 10 | // https://github.com/markedjs/marked/issues/2793#issuecomment-1532386286 11 | mangle: false, 12 | headerIds: false 13 | }, 14 | // This is a custom plugin that adds support for emojis, taken from the source code of the marked-emoji package: https://www.npmjs.com/package/marked-emoji 15 | // and modified to our needs. 16 | markedEmoji({ 17 | emojis: nameToEmoji 18 | }) 19 | ); 20 | 21 | export default function useMarkdown(readme?: string) { 22 | const [readMe, setReadme] = useState(readme || ''); 23 | const [isReadmeLoading, setIsReadmeLoading] = useState(false); 24 | 25 | const parseMarkdown = (markdown: string) => { 26 | return marked.parse(markdown); 27 | }; 28 | 29 | const fetchMarkedDown = async (repo: DataProps) => { 30 | const readmeUrl = `https://api.github.com/repos/${repo.owner}/${repo.name}/readme`; 31 | try { 32 | setIsReadmeLoading(true); 33 | let res = await fetch(readmeUrl); 34 | let data = await res.json(); 35 | if (data?.message === 'Not Found') { 36 | throw new Error('data fetch for repo failed'); 37 | } 38 | res = await fetch(data.download_url); 39 | data = await res.text(); 40 | const text = data.replace(``, ''); 41 | const html = parseMarkdown(text); 42 | setReadme(html); 43 | } catch (error) { 44 | console.error( 45 | '🚀 ~ file: useMarkdown.ts:35 ~ fetchMarkedDown ~ error:', 46 | error 47 | ); 48 | setReadme(/*html*/ ` 49 |
50 | Readme fetching failed for -
${readmeUrl} 51 |
`); 52 | } finally { 53 | setIsReadmeLoading(false); 54 | } 55 | }; 56 | return { 57 | parseMarkdown, 58 | fetchMarkedDown, 59 | isReadmeLoading, 60 | readMe 61 | }; 62 | } 63 | -------------------------------------------------------------------------------- /src/components/ReadmePanel.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import parse from 'html-react-parser'; 3 | 4 | export default function ReadmePanel(props: { 5 | readmePreview: string; 6 | loading: boolean; 7 | }) { 8 | const [content, setContent] = useState(props.readmePreview); 9 | 10 | const readmeLoadingSpinner = ` 11 |
12 | 16 |
17 | `; 18 | 19 | useEffect(() => { 20 | props.loading 21 | ? setContent(readmeLoadingSpinner) 22 | : setContent(props.readmePreview); 23 | // eslint-disable-next-line react-hooks/exhaustive-deps 24 | }, [props.loading, props.readmePreview]); 25 | 26 | return ( 27 | 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /src/components/MainContent/ReadmePreview.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import parse from 'html-react-parser'; 3 | 4 | export default function ReadmePreview(props: { 5 | readmePreview: string; 6 | loading: boolean; 7 | }) { 8 | const [content, setContent] = useState(props.readmePreview); 9 | 10 | const readmeLoadingSpinner = ` 11 |
12 | 16 |
17 | `; 18 | 19 | useEffect(() => { 20 | props.loading 21 | ? setContent(readmeLoadingSpinner) 22 | : setContent(props.readmePreview); 23 | // eslint-disable-next-line react-hooks/exhaustive-deps 24 | }, [props.loading, props.readmePreview]); 25 | 26 | return ( 27 | 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Contributing Guide 2 | Welcome! We are glad you are interested in contributing to this project❤️. 3 | 4 | ## Introduction 5 | This project was created to bring together all Israeli open source projects in one repository. This improves collaboration and visibility and also gives you many open source projects to contribute to. 6 | 7 | ## Ways to contribute 8 | We are open to contributions of all types, which include: 9 | - New features 10 | - CI/CD 11 | - Bug fixes 12 | - Documentation 13 | - Web design 14 | - Testing 15 | - Community support and management 16 | 17 | If you have more ideas on how you can contribute to this project, please feel free to reach out to us on [Discord](https://discord.gg/a2VyCjRk2M). 18 | 19 | ## How to contribute 20 | To contribute to this project, follow these steps: 21 | - **Navigate** to the ‘Issues’ tab to see already opened issues or visit our [website](https://opensource-il.vercel.app/) to get ideas on what to improve. 22 | - To open a new issue, **Click** on the ‘New issue’ button on the top right (Ensure this issue has not been raised by someone). 23 | - To work on an issue, **after it has been assigned to you**, **Fork** the project on GitHub and **Clone** it to your local computer. 24 | - **Navigate** to your code editor. 25 | - **Open** your terminal, then **Type** `npm i` to install all the dependencies for this project. 26 | - **Type** `npm run dev` to confirm installation was successful. 27 | - **Click** on the local host URL in your terminal to run the project. 28 | - Make the fixes, then **Open** your terminal. 29 | - **Type** `git add .` or navigate to the file path just edited. 30 | - **Type** `git commit -m ‘Your commit message’` 31 | - **Type** `git pull` to be up to date with the remote repository, then `git push`. 32 | - **Reload** your forked repo, and **Click** ‘Create pull request’ 33 | - **Click** on ‘Compare across fork’ 34 | - **Type** in details in the input field, and **Click** ‘Create pull request’ 35 | - Your pull request will be reviewed by our maintainers. 36 | 37 | > **Please note**: Before working on a new bug or a feature, open an issue, then discuss your interest to be assigned to the issue by tagging the maintainers @yonatanmgr and @urielofir in the comment box. When assigned to the issue, you may fork the repository, work on the issue, and create a pull request. 38 | 39 | For no-code contributions, please reach out to us on [Discord](https://discord.gg/a2VyCjRk2M). 40 | 41 | ## Get Involved 42 | We encourage you to get involved in our community’s project. You can do this by: 43 | - Joining our [Discord](https://discord.gg/a2VyCjRk2M) community and introducing yourself in the *#newcomers* channel, asking questions, and helping to answer questions. 44 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### English (Hebrew Follows) 2 | 3 | Welcome to the Open Source Israeli website repository 4 | 5 | Want to get started contributing to open source and not sure where to begin? You've come to the right place 6 | Open Source Israeli website scrapes [awesome-opensource-israel](https://github.com/lirantal/awesome-opensource-israel) github and displays every open source Israeli projects. 7 | 8 | How to contribute to this project: 9 | Prequsties: You must have [Node.js](https://nodejs.org/en/download) and NPM installed on your machine 10 | 11 | 1. Fork the project to create your own copy of the repository at github (by pressing `fork` button) 12 | 2. Navigate to some directory where you plan the project to be cloned 13 | 3. Clone the project by running `git clone https://github.com//opensource-il-site.git` in terminal 14 | 4. Navigate to project directory 15 | 5. `cd opensource-il-site` 16 | 6. Install the project's dependecies by running `npm i` command in the terminal 17 | 7. Make sure the installation was successful by using the `npm run dev` in the terminal 18 | 19 | Want to contribute to the Open Source Israeli website? Visited the site and still unsure where to begin? Facing an issue? 20 | Drop by the [Discord community](https://discordapp.com/channels/1089589164707684443/1102155816750022657) and leave a comment 21 | 22 | ### עברית 23 | ברוכים הבאים למאגר של אתר הקוד הפתוח הישראלי 24 | 25 | רוצים לתרום לקוד פתוח ולא בטוחים כיצד להתחיל? הגעתם למקום הנכון 26 | אתר הקוד הפתוח הישראלי סורק את המאגר של פרויקט הגיטהאב [awesome-opensource-israel](https://github.com/lirantal/awesome-opensource-israel) 27 | ומציג את פרוייקטי הקוד הפתוח הישראלים 28 | 29 | רוצים לתרום לאתר הקוד הפתוח הישראלי? ביקרתם באתר ועדיין לא בטוחים כיצד להתחיל? נתקלתם בבעיה? 30 | קפצו [לקהילת הדיסקורד](https://discordapp.com/channels/1089589164707684443/1102155816750022657) וכתבו הודעה 31 | 32 | כיצד לתרום לפרויקט זה: 33 | דרישות מקדימות: 34 | [Node.js](https://nodejs.org/en/download) ו-npm גרסה של 35 | מותקנות על המערכת 36 | 37 | 1. צרו העתק משלכם של מאגר הפרויקט באמצעות Fork 38 | 2. צרו תיקיה מקומית על מחשבכם שאליה תעתיקו את הפרויקט 39 | 3. העתיקו את קישור ה- "Clone" URL של הפרויקט: https://github.com/*משתמש_הגיט_שלכם*/opensource-il-site.git 40 | 4. פתחו את הטרמינל בתוך התיקייה שיצרתם 41 | 5. כתבו את הפקודה `git clone https://github.com/<שם מתשמש>/opensource-il-site.git` בתוך הטרמינל 42 | 6. התקינו את התלויות של הפרויקט באמצעות הפקודה `npm i` בתוך הטרמינל 43 | 7. ודאו שההתקנה הצליחה על ידי הרצת הפקודה `npm run dev` בטרמינל 44 | 45 | ### Projects 46 | 47 | ![image](https://user-images.githubusercontent.com/31913495/227768041-20ab7d33-d88f-45ff-99b4-53b974af7e0e.png) 48 | 49 | ### Companies 50 | 51 | ![image](https://user-images.githubusercontent.com/31913495/227767963-64fbba35-1ecb-4964-8adb-32786bafa273.png) 52 | -------------------------------------------------------------------------------- /src/components/MainContent/HelpModalContent.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReposIcon from '../Icons/ReposIcon'; 3 | import OrgIcon from '../Icons/OrgIcon'; 4 | import { Views } from '@/types/index.type'; 5 | 6 | type Props = { 7 | handleModalClick: ( 8 | event: React.MouseEvent 9 | ) => void; 10 | setView: React.Dispatch>; 11 | view: string; 12 | }; 13 | 14 | function HelpModalContent({ handleModalClick, setView, view }: Props) { 15 | return ( 16 |
handleModalClick(e)} 20 | > 21 |

ברוכים הבאים!

22 |

23 | באתר זה תוכלו למצוא פרויקטי קוד פתוח ישראליים וחברות ישראליות המתחזקות 24 | ספריות קוד פתוח, לקרוא על הפרויקטים ולמצוא את הפרויקט הבא (ואולי גם 25 | הראשון 😉) אליו תוכלו לתרום. 26 |

27 |

28 | במסך המאגרים ( 29 | ), לחיצה על "הצג מסננים", תפתח בפניכם מספר אפשרויות סינון 30 | שיעזרו לכם למצוא את הפרויקט האידיאלי עבורכם: זמן גרסה אחרון,{' '} 31 | כמות כוכבים ו- 32 | כמות Issues פתוחים. בנוסף, תוכלו לסנן את כל הפרויקטים המוצגים לפי 33 | שפת התכנות שלהם וכך לדייק את חיפושיכם לפרויקטים המתאימים לכם ביותר. 34 |

35 |

36 | בלחיצה על כפתור החברות ( ), 37 | יוצגו בפניכם עשרות חברות ישראליות המתחזקות ספריות קוד פתוח. בעוד שלחיצה 38 | על שם החברה יוביל לדף הבית שלה ב-GitHub, לחיצה על סמליל החברה יפתח 39 | בפניכם את כל מאגרי הקוד הפתוח הציבוריים שלה, אליהם תוכלו להצטרף. 40 |

41 |

42 | לחיצה על הקישור ל-GitHub בחלקו העליון של הדף, תוביל אתכם למאגר{' '} 43 | 49 | awesome-opensource-israel 50 | 51 | , ממנו נמשכים המאגרים והארגונים המוצגים באתר זה. 52 |

53 |

54 | פרויקט נוסף אליו תוכלו לתרום קוד הוא{' '} 55 | 61 | אתר זה ממש 62 | 63 | ! מוזמנים להצטרף לפיתוח, להוסיף תכולות ולסייע בתיקון תקלות - וכך לעזור 64 | לבנות בית לקוד הפתוח בישראל. 65 |

66 |

67 | נוצר ע"י יונתן מגר, 2023. ממשיך להתקיים{' '} 68 | 74 | בזכותכם 75 | 76 | . 77 |

78 |
79 | ); 80 | } 81 | 82 | export default HelpModalContent; 83 | -------------------------------------------------------------------------------- /src/app/companies/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import SocialLinks from '@/components/Header/SocialLinks'; 3 | import LoadingSpinner from '@/components/LoadingSpinner'; 4 | import CompaniesList from '@/components/MainContent/CompaniesList'; 5 | import ReadmePanel from '@/components/ReadmePanel'; 6 | import PageContainer from '@/components/PageContainer'; 7 | import { useRouter } from 'next/navigation'; 8 | import { CompanyProps } from '@/types/index.type'; 9 | import { useEffect, useState } from 'react'; 10 | 11 | function CompanyNavbar() { 12 | const router = useRouter(); 13 | 14 | return ( 15 |
19 |
20 | חברות ישראליות בקוד פתוח 21 |
22 | 23 | { 25 | if (view === 'repos') router.push('/'); 26 | }} 27 | view={'companies'} 28 | /> 29 |
30 | ); 31 | } 32 | 33 | const COMPANIES_READ_ME_PLACEHOLDER = `

בחרו בחברה מהרשימה כדי להיכנס לרשימת ה-Repositories שלה,

או לחצו על שם החברה כדי לראות את עמוד ה-GitHub שלה!

`; 34 | 35 | export default function CompanyPage() { 36 | const [companies, setCompanies] = useState([]); 37 | const [isLoading, setLoading] = useState(false); 38 | const router = useRouter(); 39 | 40 | useEffect(() => { 41 | fetchCompanies(); 42 | }, []); 43 | useEffect(() => { 44 | setLoading(companies.length ? false : true); 45 | }, [companies]); 46 | 47 | const fetchCompanies = async () => { 48 | const res = await fetch('/api/company'); 49 | const data = await res.json(); 50 | 51 | setCompanies( 52 | data.companies 53 | .filter( 54 | (company: { organization: { [key: string]: string } }) => 55 | company.organization?.name?.length && 56 | company.organization?.avatarUrl?.length 57 | ) 58 | .map( 59 | ({ organization }: { organization: { [key: string]: string } }) => { 60 | return { 61 | name: organization.name, 62 | login: organization.login, 63 | avatar: organization.avatarUrl 64 | }; 65 | } 66 | ) 67 | ); 68 | }; 69 | 70 | const onSelectCompany = (company: CompanyProps): void => { 71 | console.log('🚀 ~ file: page.tsx:38 ~ onSelectCompany ~ company:', company); 72 | // redirect to company page 73 | router.push( 74 | `/companies/${company.login}?name=${company.name}&avatarUrl=${company.avatar}` 75 | ); 76 | }; 77 | return ( 78 | 79 | 80 | 81 |
85 | 86 | 90 |
91 |
92 | ); 93 | } 94 | -------------------------------------------------------------------------------- /src/components/Header/Filters/Filters.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import { sortButtonsTexts } from '../constants'; 3 | import { AllSortTypes, SortTypes } from '../types'; 4 | import { useWindowSize } from '@/hooks/useWindowSize'; 5 | import Filter from './Filter'; 6 | 7 | interface FiltersProps { 8 | handleSortChange: (sortType: SortTypes) => void; 9 | setSelectedLang: (lang: string) => void; 10 | langs: string[]; 11 | activeSortType: AllSortTypes | undefined; 12 | selectedLang: string; 13 | } 14 | 15 | export default function Filters({ 16 | langs, 17 | setSelectedLang, 18 | handleSortChange, 19 | activeSortType, 20 | selectedLang 21 | }: FiltersProps) { 22 | const { isMediumUp } = useWindowSize(); 23 | const [shouldShowFilters, setShouldShowFilters] = useState(true); 24 | 25 | // Hides filters on medium or smaller screens 26 | useEffect(() => { 27 | setShouldShowFilters(isMediumUp); 28 | }, [isMediumUp]); 29 | 30 | return ( 31 |
35 | 45 | {shouldShowFilters && ( 46 | <> 47 | {Object.keys(sortButtonsTexts).map((sortType) => { 48 | if (sortType === 'default') return; 49 | return ( 50 | 57 | ); 58 | })} 59 | 77 | 86 | 87 | )} 88 |
89 | ); 90 | } 91 | -------------------------------------------------------------------------------- /src/components/MainContent/ReposList/Project.tsx: -------------------------------------------------------------------------------- 1 | import moment from 'moment'; 2 | import 'moment/locale/he'; 3 | import LangPill from './Language'; 4 | import Image from 'next/image'; 5 | import TimeIcon from '@/components/Icons/TimeIcon'; 6 | import IssueIcon from '@/components/Icons/IssueIcon'; 7 | import { DataProps } from '@/types/index.type'; 8 | 9 | type ProjectProps = { 10 | setReadme: (name: string) => void; 11 | repo: DataProps; 12 | }; 13 | 14 | const starSvg = ( 15 | 25 | ); 26 | 27 | function nFormatter(num: number) { 28 | if (num >= 1000) { 29 | return (num / 1000).toFixed(1).replace(/\.0$/, '') + 'K'; 30 | } 31 | return num; 32 | } 33 | 34 | export default function Project({ repo, setReadme }: ProjectProps) { 35 | const url = `https://www.github.com/${repo.owner}/${repo.name}`; 36 | 37 | return ( 38 |
setReadme(url)} 41 | > 42 | 49 | {repo.name} 56 | 57 |
58 |
59 | {repo.owner}/ 60 | {repo.name} 61 |
62 | 63 |
64 | 65 | {nFormatter(repo.stars)} 66 | 67 | {starSvg} 68 | 69 | {' '} 70 | · 71 | 72 | {repo.issuesCount} 73 | 74 | {repo.issuesCount == 1 ? 'Open Issue' : 'Open Issues'} 75 | 76 | {' '} 77 | · 78 | 79 | 80 | {moment(repo.lastCommit) 81 | .locale('he') 82 | .calendar() 83 | .replace('האחרון ', '')} 84 | 85 |
86 |
87 | {repo.languages ? ( 88 | repo.languages 89 | .filter((lang) => lang.name != 'Dockerfile') 90 | .map((lang) => ( 91 | 96 | )) 97 | ) : ( 98 | <> 99 | )} 100 |
101 |
102 |
103 | ); 104 | } 105 | -------------------------------------------------------------------------------- /src/components/Header/SocialLinks.tsx: -------------------------------------------------------------------------------- 1 | import OrgIcon from '../Icons/OrgIcon'; 2 | import ReposIcon from '../Icons/ReposIcon'; 3 | import { TitleAndSocialLinkProps } from './types'; 4 | 5 | export default function SocialLinks({ 6 | setView, 7 | view 8 | }: TitleAndSocialLinkProps) { 9 | return ( 10 | 58 | ); 59 | } 60 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | @yonmgr on Discord or yonatanmgr@gmail.com. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /src/app/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import Filters from '@/components/Header/Filters/Filters'; 3 | import SocialLinks from '@/components/Header/SocialLinks'; 4 | import { AllSortTypes } from '@/components/Header/types'; 5 | import LoadingSpinner from '@/components/LoadingSpinner'; 6 | import ReposList from '@/components/MainContent/ReposList/ReposList'; 7 | import PageContainer from '@/components/PageContainer'; 8 | import ReadmePanel from '@/components/ReadmePanel'; 9 | import useMarkdown from '@/hooks/useMarkdown'; 10 | import type { DataProps, RepoProps } from '@/types/index.type'; 11 | import { useRouter } from 'next/navigation'; 12 | import { useEffect, useMemo, useState } from 'react'; 13 | function ReposNavbar() { 14 | const router = useRouter(); 15 | 16 | return ( 17 |
21 |
22 | פרויקטי קוד פתוח ישראלים 23 |
24 | 25 | { 27 | if (view === 'companies') router.push('/companies'); 28 | }} 29 | view={'repos'} 30 | /> 31 |
32 | ); 33 | } 34 | 35 | const DEFAULT_READ_ME_PLACEHOLDER = `
בחרו ב-Repository מהרשימה כדי לקרוא את קובץ ה-README שלו!
`; 36 | 37 | const sortByLastCommit = (b: DataProps, a: DataProps) => 38 | a.lastCommit < b.lastCommit ? -1 : a.lastCommit > b.lastCommit ? 1 : 0; 39 | 40 | export default function RepositoryPage() { 41 | const [isLoading, setLoading] = useState(false); 42 | const [selectedLang, setSelectedLang] = useState(''); 43 | const [repositories, setRepositories] = useState([]); 44 | const [showData, setShowData] = useState([]); 45 | const [currentRepo, setCurrentRepo] = useState(''); 46 | const { fetchMarkedDown, readMe, isReadmeLoading } = useMarkdown( 47 | DEFAULT_READ_ME_PLACEHOLDER 48 | ); 49 | const [activeSortType, setSortFunction] = useState< 50 | AllSortTypes | undefined 51 | >(); 52 | 53 | useEffect(() => { 54 | setLoading(true); 55 | fetchRepos(); 56 | // eslint-disable-next-line react-hooks/exhaustive-deps 57 | }, []); 58 | 59 | const fetchRepos = async () => { 60 | const res = await fetch('/api/repositories'); 61 | const data: any /*{ repository: RepoProps }[]*/ = await res.json(); 62 | console.log('🚀 ~ file: page.tsx:93 ~ fetchRepos ~ data:', data); 63 | 64 | const organizedData = data.repositories 65 | .filter((proj: RepoProps) => proj !== null) 66 | .map((proj: { repository: RepoProps }) => { 67 | const repo = proj.repository; 68 | 69 | const nameWithOwner = repo.nameWithOwner; 70 | const image = repo.openGraphImageUrl; 71 | const description = repo.description ?? ''; 72 | const lastCommit = repo.defaultBranchRef 73 | ? repo.defaultBranchRef.target.committedDate 74 | : '1970-01-01T00:00:00Z'; 75 | const stargazerCount = repo.stargazerCount; 76 | const issuesCount = repo.openIssues.totalCount; 77 | const languages = repo.languages.edges.map((lang) => ({ 78 | name: lang.node.name, 79 | size: lang.size 80 | })); 81 | const totalSize = repo.languages.totalSize; 82 | 83 | return { 84 | id: crypto.randomUUID(), 85 | image: image, 86 | owner: nameWithOwner.split('/')[0], 87 | name: nameWithOwner.split('/')[1], 88 | description: description, 89 | lastCommit: lastCommit, 90 | stars: stargazerCount, 91 | issuesCount: issuesCount, 92 | languages: languages, 93 | totalSize: totalSize 94 | }; 95 | }); 96 | 97 | setRepositories(organizedData.sort(sortByLastCommit)); 98 | setShowData(organizedData.sort(sortByLastCommit)); 99 | setLoading(false); 100 | }; 101 | 102 | const allLangs = useMemo(() => { 103 | return showData.reduce((allLangs: string[], repo: DataProps) => { 104 | if (repo.languages) { 105 | repo.languages.forEach((lang) => { 106 | if (!allLangs.includes(lang.name) && lang.name != 'Dockerfile') 107 | allLangs.push(lang.name); 108 | }); 109 | } 110 | return allLangs.sort(); 111 | }, []); 112 | }, [showData]); 113 | 114 | const onSetReadMe = async (readme: string) => { 115 | if (currentRepo !== readme) { 116 | const foundReadme = showData.find( 117 | (repo) => `https://www.github.com/${repo.owner}/${repo.name}` === readme 118 | ); 119 | 120 | // nav to repo page 121 | setCurrentRepo( 122 | `https://www.github.com/${foundReadme?.owner}/${foundReadme?.name}` 123 | ); 124 | 125 | if (foundReadme) { 126 | fetchMarkedDown(foundReadme); 127 | } 128 | } 129 | }; 130 | 131 | const handleSortChange = (sortType: AllSortTypes) => { 132 | let sorted; 133 | switch (sortType) { 134 | case 'lastCommit': 135 | sorted = [...showData].sort((b: DataProps, a: DataProps) => 136 | a.lastCommit < b.lastCommit ? -1 : a.lastCommit > b.lastCommit ? 1 : 0 137 | ); 138 | break; 139 | case 'lastCommitReverse': 140 | sorted = [...showData].sort((a: DataProps, b: DataProps) => 141 | a.lastCommit < b.lastCommit ? -1 : a.lastCommit > b.lastCommit ? 1 : 0 142 | ); 143 | break; 144 | case 'stars': 145 | sorted = [...showData].sort( 146 | (b: DataProps, a: DataProps) => a.stars - b.stars 147 | ); 148 | break; 149 | case 'starsReverse': 150 | sorted = [...showData].sort( 151 | (a: DataProps, b: DataProps) => a.stars - b.stars 152 | ); 153 | break; 154 | case 'issues': 155 | sorted = [...showData].sort( 156 | (b: DataProps, a: DataProps) => a.issuesCount - b.issuesCount 157 | ); 158 | break; 159 | case 'issuesReverse': 160 | sorted = [...showData].sort( 161 | (a: DataProps, b: DataProps) => a.issuesCount - b.issuesCount 162 | ); 163 | break; 164 | case 'default': 165 | sorted = [...showData].sort(sortByLastCommit); 166 | break; 167 | default: 168 | sorted = [...showData]; 169 | break; 170 | } 171 | setShowData(sorted); 172 | setSortFunction(sortType); 173 | }; 174 | 175 | const dataForDisplay = useMemo(() => { 176 | return selectedLang === '' 177 | ? showData 178 | : showData.filter((repo: DataProps) => 179 | repo.languages.find( 180 | (language: { name: string }) => language.name == selectedLang 181 | ) 182 | ); 183 | }, [showData, selectedLang]); 184 | 185 | if (!repositories && !isLoading) return

Error loading data

; 186 | 187 | return ( 188 | 189 | 190 | 191 | 198 |
202 | {' '} 203 | 204 |
205 |
206 | ); 207 | } 208 | -------------------------------------------------------------------------------- /src/app/companies/[company]/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import Filters from '@/components/Header/Filters/Filters'; 3 | import SocialLinks from '@/components/Header/SocialLinks'; 4 | import type { AllSortTypes } from '@/components/Header/types'; 5 | import LoadingSpinner from '@/components/LoadingSpinner'; 6 | import ReposList from '@/components/MainContent/ReposList/ReposList'; 7 | import PageContainer from '@/components/PageContainer'; 8 | import ReadmePanel from '@/components/ReadmePanel'; 9 | import useMarkdown from '@/hooks/useMarkdown'; 10 | import type { DataProps, Organization, RepoProps } from '@/types/index.type'; 11 | import { 12 | parseLastParamFromUrl, 13 | parseSearchParams 14 | } from '@/utils/extractParamFromUrl'; 15 | import { usePathname, useRouter, useSearchParams } from 'next/navigation'; 16 | import React, { useEffect, useMemo, useState } from 'react'; 17 | 18 | const DEFAULT_READ_ME_PLACEHOLDER = `
בחרו ב-Repository מהרשימה כדי לקרוא את קובץ ה-README שלו!
`; 19 | 20 | function CompanyNavbar({ companyName }: { companyName: string | undefined }) { 21 | const router = useRouter(); 22 | 23 | return ( 24 |
28 |
29 | 37 |   38 | {companyName}  39 | / פרויקטים 40 |
41 | 42 | { 44 | if (view === 'repos') router.push('/'); 45 | }} 46 | view={'companies'} 47 | /> 48 |
49 | ); 50 | } 51 | 52 | const sortByLastCommit = (b: DataProps, a: DataProps) => 53 | a.lastCommit < b.lastCommit ? -1 : a.lastCommit > b.lastCommit ? 1 : 0; 54 | 55 | function CompanyRepositories() { 56 | const pathname = usePathname(); 57 | const searchParams = useSearchParams(); 58 | 59 | const [isLoading, setLoading] = useState(false); 60 | const [companyRepos, setCompaniesRepos] = useState([]); 61 | const [showData, setShowData] = useState([]); 62 | const [company, setCompany] = useState(); 63 | const [currentDisplayedRepo, setCurrentRepo] = useState(''); 64 | const [activeSortType, setSortFunction] = useState< 65 | AllSortTypes | undefined 66 | >(); 67 | const [selectedLang, setSelectedLang] = useState(''); 68 | const { fetchMarkedDown, readMe, isReadmeLoading } = useMarkdown( 69 | DEFAULT_READ_ME_PLACEHOLDER 70 | ); 71 | const [companyName, setCompanyName] = useState(''); 72 | 73 | const onSetReadMe = async (readme: string) => { 74 | if (currentDisplayedRepo !== readme) { 75 | const foundReadme = companyRepos.find( 76 | (repo) => `https://www.github.com/${repo.owner}/${repo.name}` === readme 77 | ); 78 | 79 | // for if check above to prevent refetch of active readme 80 | setCurrentRepo( 81 | `https://www.github.com/${foundReadme?.owner}/${foundReadme?.name}` 82 | ); 83 | 84 | if (foundReadme) { 85 | fetchMarkedDown(foundReadme); 86 | } 87 | } 88 | }; 89 | 90 | const fetchCompanyRepos = async (company: string) => { 91 | setLoading(true); 92 | const res = await fetch('/api/company/' + company); 93 | const data: { company: { organization: Organization } } = await res.json(); 94 | setCompany(data.company.organization); 95 | 96 | setCompaniesRepos( 97 | (data.company.organization.repositories.nodes as RepoProps[]) 98 | .map((repo) => { 99 | const nameWithOwner = repo.nameWithOwner; 100 | const image = repo.openGraphImageUrl; 101 | const description = repo.description ?? ''; 102 | const lastCommit = repo.defaultBranchRef 103 | ? repo.defaultBranchRef.target.committedDate 104 | : '1970-01-01T00:00:00Z'; 105 | const stargazerCount = repo.stargazerCount; 106 | const issuesCount = repo.openIssues.totalCount; 107 | const languages = repo.languages.edges.map((lang) => ({ 108 | name: lang.node.name, 109 | size: lang.size 110 | })); 111 | const totalSize = repo.languages.totalSize; 112 | 113 | return { 114 | id: crypto.randomUUID(), 115 | image, 116 | owner: nameWithOwner.split('/')[0], 117 | name: nameWithOwner.split('/')[1], 118 | description, 119 | lastCommit, 120 | stars: stargazerCount, 121 | issuesCount, 122 | languages, 123 | totalSize 124 | }; 125 | }) 126 | .filter((repo: DataProps) => repo.name != '.github') 127 | .sort(sortByLastCommit) 128 | ); 129 | setLoading(false); 130 | }; 131 | 132 | const handleSortChange = (sortType: AllSortTypes) => { 133 | let sorted; 134 | switch (sortType) { 135 | case 'lastCommit': 136 | sorted = [...companyRepos].sort((b: DataProps, a: DataProps) => 137 | a.lastCommit < b.lastCommit ? -1 : a.lastCommit > b.lastCommit ? 1 : 0 138 | ); 139 | break; 140 | case 'lastCommitReverse': 141 | sorted = [...companyRepos].sort((a: DataProps, b: DataProps) => 142 | a.lastCommit < b.lastCommit ? -1 : a.lastCommit > b.lastCommit ? 1 : 0 143 | ); 144 | break; 145 | case 'stars': 146 | sorted = [...companyRepos].sort( 147 | (b: DataProps, a: DataProps) => a.stars - b.stars 148 | ); 149 | break; 150 | case 'starsReverse': 151 | sorted = [...companyRepos].sort( 152 | (a: DataProps, b: DataProps) => a.stars - b.stars 153 | ); 154 | break; 155 | case 'issues': 156 | sorted = [...companyRepos].sort( 157 | (b: DataProps, a: DataProps) => a.issuesCount - b.issuesCount 158 | ); 159 | break; 160 | case 'issuesReverse': 161 | sorted = [...companyRepos].sort( 162 | (a: DataProps, b: DataProps) => a.issuesCount - b.issuesCount 163 | ); 164 | break; 165 | case 'default': 166 | sorted = [...companyRepos].sort(sortByLastCommit); 167 | break; 168 | default: 169 | sorted = [...companyRepos]; 170 | break; 171 | } 172 | setShowData(sorted); 173 | setSortFunction(sortType); 174 | }; 175 | 176 | const allLangs = useMemo(() => { 177 | return companyRepos.reduce((allLangs: string[], repo: DataProps) => { 178 | if (repo.languages) { 179 | repo.languages.forEach((lang) => { 180 | if (!allLangs.includes(lang.name) && lang.name != 'Dockerfile') 181 | allLangs.push(lang.name); 182 | }); 183 | } 184 | return allLangs.sort(); 185 | }, []); 186 | }, [companyRepos]); 187 | 188 | useEffect(() => { 189 | setShowData(companyRepos.sort(sortByLastCommit)); 190 | }, [company, companyRepos]); 191 | 192 | useEffect(() => { 193 | console.log( 194 | '🚀 ~ file: page.tsx:11 ~ useEffect ~ {pathname ,searchParams}:', 195 | { pathname, searchParams: searchParams.toString() } 196 | ); 197 | 198 | const companyLogin = parseLastParamFromUrl(pathname); 199 | if (companyLogin) setCompanyName(companyLogin); 200 | 201 | // const searchParamObj = parseSearchParams(searchParams.toString()); 202 | if (companyLogin) fetchCompanyRepos(companyLogin); 203 | 204 | // if (pathname && pathname.split('/')) fetchCompanyRepos(pathname); 205 | // You can now use the current URL 206 | }, [pathname, searchParams]); 207 | 208 | const dataForDisplay = useMemo(() => { 209 | return selectedLang === '' 210 | ? showData 211 | : showData.filter((repo: DataProps) => 212 | repo.languages.find( 213 | (language: { name: string }) => language.name == selectedLang 214 | ) 215 | ); 216 | }, [showData, selectedLang]); 217 | 218 | return ( 219 | 220 | 221 | 222 | 229 |
233 | 234 | 235 |
236 |
237 | ); 238 | } 239 | 240 | export default CompanyRepositories; 241 | -------------------------------------------------------------------------------- /src/server/services/dataManager.service.ts: -------------------------------------------------------------------------------- 1 | import { logger } from '../utils/logger'; 2 | import { ReademGithubCompany, ReademGithubProject } from '@/types/index.type'; 3 | import { getRedisVal, setRedisVal } from '../db/redis-vercel-kv'; 4 | 5 | export const JSON_DATA_STORE_KEY = 'jsonData'; 6 | export const COMPANIES_STORE_KEY = 'companies'; 7 | export const PROJECTS_STORE_KEY = 'projects'; 8 | 9 | const langsToListRegex = /^\s?#{3}([^#{3}]+?)\n([^]+?)(?=^\s?#{3}[^#{3}])/gm; 10 | const splitProjectRegex = /\[(.+)\]\((.+)\) - (.+)/; 11 | const splitCompanyRegex = /\[(.+)\]\((.+)\)/; 12 | const cleanBadgesRegex = /!\[(.+)\]\(.+\)/; 13 | const findListItemRegex = /(?<=\* ).*/gm; 14 | const projectsTitleRegex = 15 | /(?:^|\n)## Projects by main language\s?[^\n]*\n(.*?)(?=\n##?\s|$)/gs; 16 | const compsTitleRegex = /(?:^|\n)## Companies\s?[^\n]*\n(.*?)(?=\n##?\s|$)/gs; 17 | 18 | const headersList = { 19 | Accept: '*/*', 20 | Authorization: 'bearer ' + process.env.GITHUB_READ_ONLY, 21 | 'Content-Type': 'application/json' 22 | }; 23 | 24 | export function githubMdParser(rawReadmeFile: string) { 25 | try { 26 | const allProjects: string[] = []; 27 | const allComps: string[] = []; 28 | 29 | const compsStr = (rawReadmeFile.match(compsTitleRegex) as string[])[0]; 30 | const langsStr = (rawReadmeFile.match(projectsTitleRegex) as string[])[0]; 31 | 32 | const compsList = compsStr.match(findListItemRegex); 33 | const allLanguages = langsStr.match(langsToListRegex); 34 | 35 | compsList?.forEach((company) => { 36 | allComps.push(company.match(splitCompanyRegex) as unknown as string); 37 | }); 38 | 39 | allLanguages?.forEach((lang) => { 40 | allProjects.push(lang.match(findListItemRegex) as unknown as string); 41 | }); 42 | 43 | const parsedAllComps = allComps 44 | .map((company: string) => { 45 | if (company[2].split('/').includes('github.com')) { 46 | return { name: company[2].replace('https://github.com/', '') }; 47 | } 48 | }) 49 | .filter( 50 | (company: ReademGithubCompany | undefined) => company != undefined 51 | ); 52 | 53 | const parsedAllProjects = allProjects 54 | .flat() 55 | .map((projectStr: string) => { 56 | const res = projectStr.match(splitProjectRegex); 57 | 58 | // Checking if rawReadmeFile exists and if it is a GitHub url 59 | if (res && res[2].split('/').includes('github.com')) { 60 | // Checking if rawReadmeFile is a repo. Else, add to companies 61 | if (res[2].split('/').length > 4) { 62 | const name = res[2].replace('https://github.com/', ''); 63 | return { 64 | name: name, 65 | description: res[3].replace(cleanBadgesRegex, '') 66 | }; 67 | } else { 68 | parsedAllComps.push({ name: res[2].split('/')[3] }); 69 | } 70 | } 71 | }) 72 | .filter( 73 | (project: ReademGithubProject | undefined) => project != undefined 74 | ); 75 | 76 | return { 77 | success: true, 78 | allComps: parsedAllComps as unknown as ReademGithubCompany[], 79 | allProjects: parsedAllProjects as unknown as ReademGithubProject[], 80 | allLanguages 81 | }; 82 | } catch (error) { 83 | logger.error( 84 | '🚀 ~ file: githubMdParser.ts:5 ~ githubMdParser ~ error:', 85 | error 86 | ); 87 | return { success: false, allComps: [], allProjects: [], allLanguages: [] }; 88 | } 89 | } 90 | 91 | export async function fetchGithubMd() { 92 | 'use server'; 93 | const readmeUrl = 94 | 'https://raw.githubusercontent.com/lirantal/awesome-opensource-israel/master/README.md'; 95 | 96 | try { 97 | const repsonse = await fetch(readmeUrl).then((res) => res.text()); 98 | return repsonse; 99 | } catch (error) { 100 | logger.error( 101 | '🚀 ~ file: githubMdParser.ts:93 ~ fetchGithubMd ~ error:', 102 | error 103 | ); 104 | 105 | return null; 106 | } 107 | } 108 | 109 | async function getCompany(company: { name: string }) { 110 | const gqlBody: { query: string; variables: { login: string } } = { 111 | query: `query ($login: String!) { 112 | organization(login: $login) { 113 | name 114 | avatarUrl 115 | login 116 | repositories( 117 | first: 100 118 | isLocked: false 119 | isFork: false 120 | privacy: PUBLIC 121 | orderBy: {direction: DESC, field: STARGAZERS} 122 | ) { 123 | nodes { 124 | openIssues: issues(states:OPEN) { 125 | totalCount 126 | } 127 | stargazerCount 128 | nameWithOwner 129 | languages(first: 3, orderBy: {field: SIZE, direction: DESC}) { 130 | totalSize 131 | edges { 132 | size 133 | node { 134 | name 135 | } 136 | } 137 | } 138 | openGraphImageUrl 139 | description 140 | defaultBranchRef { 141 | target { 142 | ... on Commit { 143 | committedDate 144 | } 145 | } 146 | } 147 | } 148 | } 149 | } 150 | }`, 151 | variables: { login: company.name } 152 | }; 153 | 154 | return await fetch('https://api.github.com/graphql', { 155 | method: 'POST', 156 | mode: 'cors', 157 | headers: headersList, 158 | body: JSON.stringify(gqlBody) 159 | }) 160 | .then((res) => { 161 | const gqlResult = res.json(); 162 | logger.info( 163 | '🚀 ~ file: dataManager.service.ts:162 ~ getCompany ~ gqlResult:', 164 | { 165 | company, 166 | gqlResult, 167 | gqlResultText: res.text() 168 | } 169 | ); 170 | return gqlResult; 171 | }) 172 | .catch((err) => logger.error('gqlErr for:' + company.name, err)); 173 | } 174 | 175 | async function getProject(project: { name: string }) { 176 | const gqlBody = { 177 | query: `query ($repoOwner: String!, $repoName: String!) { 178 | repository(owner: $repoOwner, name: $repoName) { 179 | openIssues: issues(states:OPEN) { 180 | totalCount 181 | } 182 | stargazerCount 183 | nameWithOwner 184 | languages(first: 3, orderBy: {field: SIZE, direction: DESC}) { 185 | totalSize 186 | edges { 187 | size 188 | node { 189 | name 190 | } 191 | } 192 | } 193 | openGraphImageUrl 194 | description 195 | defaultBranchRef { 196 | target { 197 | ... on Commit { 198 | committedDate 199 | } 200 | } 201 | } 202 | } 203 | } 204 | `, 205 | variables: { 206 | repoOwner: project.name.split('/')[0], 207 | repoName: project.name.split('/')[1] 208 | } 209 | }; 210 | 211 | return await fetch('https://api.github.com/graphql', { 212 | method: 'POST', 213 | mode: 'cors', 214 | headers: headersList, 215 | body: JSON.stringify(gqlBody) 216 | }) 217 | .then((res) => { 218 | const gqlResult = res.json(); 219 | logger.info( 220 | '🚀 ~ file: dataManager.service.ts:212 ~ getProject ~ gqlResult:', 221 | { 222 | project, 223 | gqlResult, 224 | gqlResultText: res.text() 225 | } 226 | ); 227 | return gqlResult; 228 | }) 229 | .catch((err) => logger.error('gqlErr for:' + project.name, err)); 230 | } 231 | 232 | export async function fetchProjects( 233 | allProjects: { name: string; description: string }[] 234 | ) { 235 | const requests: any[] = []; 236 | const results: any[] = []; 237 | 238 | allProjects.forEach((project: { name: string; description: string }) => { 239 | requests.push(getProject(project)); 240 | }); 241 | 242 | return await new Promise((resolve) => { 243 | Promise.all(requests) 244 | .then((proms) => proms.forEach((p) => results.push(p.data))) 245 | .then(() => resolve(results)); 246 | }); 247 | 248 | // const allResponses = await Promise.allSettled(requests); 249 | 250 | // for (const response of allResponses) { 251 | // if (response.status === 'fulfilled') { 252 | // console.log( 253 | // '🚀 ~ file: dataManager.service.ts:288 ~ response:', 254 | // response 255 | // ); 256 | // results.push(response.value.data); 257 | // } else { 258 | // console.error(response.reason); 259 | // } 260 | // } 261 | 262 | // return results; 263 | } 264 | 265 | export async function fetchComps(allComps: { name: string }[]) { 266 | const requests: any[] = []; 267 | const results: any[] = []; 268 | 269 | allComps.forEach((company: { name: string }) => { 270 | requests.push(getCompany(company)); 271 | }); 272 | return await new Promise((resolve) => { 273 | Promise.all(requests) 274 | .then((proms) => proms.forEach((p) => results.push(p.data))) 275 | .then(() => resolve(results)); 276 | }); 277 | } 278 | 279 | const storeNonNulls = (arr: unknown[]) => arr.filter((n) => !!n); 280 | 281 | export async function mainDataFetch(resetData?: boolean) { 282 | let isFromDB = false; 283 | try { 284 | logger.info('Initiating search for existing store in memory...'); 285 | if (resetData) { 286 | await setRedisVal(JSON_DATA_STORE_KEY, ''); 287 | } 288 | const dbData: any = await getRedisVal(JSON_DATA_STORE_KEY); 289 | if (dbData?.allComps?.length) { 290 | const { 291 | success, 292 | allComps, 293 | allGqlProjects, 294 | allGqlCompanies, 295 | allLanguages, 296 | createDate 297 | } = dbData; 298 | isFromDB = true; 299 | return { 300 | success, 301 | allComps, 302 | projects: allGqlProjects, 303 | companies: allGqlCompanies, 304 | allLanguages, 305 | isFromDB, 306 | createDate 307 | }; 308 | } 309 | 310 | logger.info('Fetching from github...'); 311 | const result = await fetchGithubMd(); 312 | if (!result) { 313 | throw new Error('Failed to fetch from github readme!'); 314 | } 315 | const { success, allComps, allLanguages, allProjects } = 316 | githubMdParser(result); 317 | if ( 318 | !success || 319 | !allComps.length || 320 | !allLanguages?.length || 321 | !allProjects.length 322 | ) { 323 | throw new Error('Error parsing readme from github'); 324 | } 325 | 326 | const allGqlCompanies: any = await fetchComps(allComps); 327 | if (!allGqlCompanies.length) { 328 | throw new Error('Error fetching GQL companies'); 329 | } 330 | const allGqlProjects: any = await fetchProjects(allProjects); 331 | if (!allGqlProjects.length) { 332 | throw new Error('Error fetching GQL projects'); 333 | } 334 | 335 | const saveResult = await setRedisVal(JSON_DATA_STORE_KEY, { 336 | success, 337 | allComps, 338 | allGqlProjects: storeNonNulls(allGqlProjects), 339 | allGqlCompanies: storeNonNulls(allGqlCompanies), 340 | allLanguages, 341 | createDate: new Date() 342 | }); 343 | 344 | if (saveResult !== 'OK') { 345 | throw new Error('store in KV REDIS failed!'); 346 | } 347 | 348 | logger.info('Setting to memory...'); 349 | 350 | return { 351 | success, 352 | allComps, 353 | projects: storeNonNulls(allGqlProjects), 354 | companies: storeNonNulls(allGqlCompanies), 355 | allLanguages, 356 | saveResult, 357 | isFromDB, 358 | createDate: new Date() 359 | }; 360 | } catch (error) { 361 | logger.error( 362 | '🚀 ~ file: githubMdParser.ts:235 ~ CronSingleTon ~ getData ~ error:', 363 | error 364 | ); 365 | } 366 | } 367 | 368 | export async function fetchCompany(companyId: string) { 369 | try { 370 | await mainDataFetch(); 371 | const companies = await mainDataFetch().then((data) => data?.companies); 372 | const target = companies.find( 373 | (company: any) => company?.organization?.login === companyId 374 | ); 375 | return target !== -1 ? target : null; 376 | } catch (error) { 377 | logger.error( 378 | '🚀 ~ file: dataManager.service.ts:397 ~ fetchCompany ~ error:', 379 | error 380 | ); 381 | } 382 | } 383 | 384 | export async function fetchAllCompanies() { 385 | try { 386 | return await mainDataFetch().then((data) => data?.companies); 387 | } catch (error) { 388 | logger.error( 389 | '🚀 ~ file: dataManager.service.ts:416 ~ fetchAllCompanies ~ error:', 390 | error 391 | ); 392 | } 393 | } 394 | 395 | export async function fetchAllRepositories() { 396 | try { 397 | return await mainDataFetch().then((data) => data?.projects); 398 | } catch (error) { 399 | logger.error( 400 | '🚀 ~ file: dataManager.service.ts:429 ~ fetchAllRepositories ~ error:', 401 | error 402 | ); 403 | } 404 | } 405 | -------------------------------------------------------------------------------- /src/components/MainContent/ReposList/langColors.ts: -------------------------------------------------------------------------------- 1 | export const langsColors: { [key: string]: string } = { 2 | '1CEnterprise': '#814CCC', 3 | '2-DimensionalArray': '#38761D', 4 | '4D': '#004289', 5 | ABAP: '#E8274B', 6 | ABAPCDS: '#555e25', 7 | ActionScript: '#882B0F', 8 | Ada: '#02f88c', 9 | AdblockFilterList: '#800000', 10 | AdobeFontMetrics: '#fa0f00', 11 | Agda: '#315665', 12 | AGSScript: '#B9D9FF', 13 | AIDL: '#34EB6B', 14 | AL: '#3AA2B5', 15 | Alloy: '#64C800', 16 | AlpineAbuild: '#0D597F', 17 | AltiumDesigner: '#A89663', 18 | AMPL: '#E6EFBB', 19 | AngelScript: '#C7D7DC', 20 | AntBuildSystem: '#A9157E', 21 | Antlers: '#ff269e', 22 | ANTLR: '#9DC3FF', 23 | ApacheConf: '#d12127', 24 | Apex: '#1797c0', 25 | APIBlueprint: '#2ACCA8', 26 | APL: '#5A8164', 27 | ApolloGuidanceComputer: '#0B3D91', 28 | AppleScript: '#101F1F', 29 | Arc: '#aa2afe', 30 | AsciiDoc: '#73a0c5', 31 | ASL: '#888888', 32 | 'ASP.NET': '#9400ff', 33 | AspectJ: '#a957b0', 34 | Assembly: '#6E4C13', 35 | Astro: '#ff5a03', 36 | Asymptote: '#ff0000', 37 | ATS: '#1ac620', 38 | Augeas: '#9CC134', 39 | AutoHotkey: '#6594b9', 40 | AutoIt: '#1C3552', 41 | AvroIDL: '#0040FF', 42 | Awk: '#c30e9b', 43 | Ballerina: '#FF5000', 44 | BASIC: '#ff0000', 45 | Batchfile: '#C1F12E', 46 | Beef: '#a52f4e', 47 | Befunge: '#888888', 48 | Berry: '#15A13C', 49 | BibTeX: '#778899', 50 | Bicep: '#519aba', 51 | Bikeshed: '#5562ac', 52 | Bison: '#6A463F', 53 | BitBake: '#00bce4', 54 | Blade: '#f7523f', 55 | BlitzBasic: '#00FFAE', 56 | BlitzMax: '#cd6400', 57 | Bluespec: '#12223c', 58 | Boo: '#d4bec1', 59 | Boogie: '#c80fa0', 60 | Brainfuck: '#2F2530', 61 | BrighterScript: '#66AABB', 62 | Brightscript: '#662D91', 63 | Browserslist: '#ffd539', 64 | C: '#555555', 65 | 'C#': '#178600', 66 | 'C++': '#f34b7d', 67 | C2hsHaskell: '#888888', 68 | CabalConfig: '#483465', 69 | Cadence: '#00ef8b', 70 | Cairo: '#ff4a48', 71 | CameLIGO: '#3be133', 72 | CAPCDS: '#0092d1', 73 | "Cap'nProto": '#c42727', 74 | CartoCSS: '#888888', 75 | Ceylon: '#dfa535', 76 | Chapel: '#8dc63f', 77 | Charity: '#888888', 78 | ChucK: '#3f8000', 79 | Circom: '#707575', 80 | Cirru: '#ccccff', 81 | Clarion: '#db901e', 82 | Clarity: '#5546ff', 83 | ClassicASP: '#6a40fd', 84 | Clean: '#3F85AF', 85 | Click: '#E4E6F3', 86 | CLIPS: '#00A300', 87 | Clojure: '#db5855', 88 | ClosureTemplates: '#0d948f', 89 | CloudFirestoreSecurityRules: '#FFA000', 90 | CMake: '#DA3434', 91 | COBOL: '#888888', 92 | CodeQL: '#140f46', 93 | CoffeeScript: '#244776', 94 | ColdFusion: '#ed2cd6', 95 | ColdFusionCFC: '#ed2cd6', 96 | COLLADA: '#F1A42B', 97 | CommonLisp: '#3fb68b', 98 | CommonWorkflowLanguage: '#B5314C', 99 | ComponentPascal: '#B0CE4E', 100 | Cool: '#888888', 101 | Coq: '#d0b68c', 102 | Crystal: '#000100', 103 | CSON: '#244776', 104 | Csound: '#1a1a1a', 105 | CsoundDocument: '#1a1a1a', 106 | CsoundScore: '#1a1a1a', 107 | CSS: '#563d7c', 108 | CSV: '#237346', 109 | Cuda: '#3A4E3A', 110 | CUE: '#5886E1', 111 | Curry: '#531242', 112 | CWeb: '#00007a', 113 | Cycript: '#888888', 114 | Cypher: '#34c0eb', 115 | Cython: '#fedf5b', 116 | D: '#ba595e', 117 | Dafny: '#FFEC25', 118 | DarcsPatch: '#8eff23', 119 | Dart: '#00B4AB', 120 | DataWeave: '#003a52', 121 | DebianPackageControlFile: '#D70751', 122 | DenizenScript: '#FBEE96', 123 | Dhall: '#dfafff', 124 | DIGITALCommandLanguage: '#888888', 125 | DirectX3DFile: '#aace60', 126 | DM: '#447265', 127 | Dockerfile: '#384d54', 128 | Dogescript: '#cca760', 129 | Dotenv: '#e5d559', 130 | DTrace: '#888888', 131 | Dylan: '#6c616e', 132 | E: '#ccce35', 133 | Earthly: '#2af0ff', 134 | Easybuild: '#069406', 135 | eC: '#913960', 136 | EcereProjects: '#913960', 137 | ECL: '#8a1267', 138 | ECLiPSe: '#001d9d', 139 | Ecmarkup: '#eb8131', 140 | EditorConfig: '#fff1f2', 141 | Eiffel: '#4d6977', 142 | EJS: '#a91e50', 143 | Elixir: '#6e4a7e', 144 | Elm: '#60B5CC', 145 | Elvish: '#55BB55', 146 | ElvishTranscript: '#55BB55', 147 | EmacsLisp: '#c065db', 148 | EmberScript: '#FFF4F3', 149 | EQ: '#a78649', 150 | Erlang: '#B83998', 151 | Euphoria: '#FF790B', 152 | 'F#': '#b845fc', 153 | 'F*': '#572e30', 154 | Factor: '#636746', 155 | Fancy: '#7b9db4', 156 | Fantom: '#14253c', 157 | Faust: '#c37240', 158 | Fennel: '#fff3d7', 159 | FIGletFont: '#FFDDBB', 160 | FilebenchWML: '#F6B900', 161 | Filterscript: '#888888', 162 | fish: '#4aae47', 163 | Fluent: '#ffcc33', 164 | FLUX: '#88ccff', 165 | Forth: '#341708', 166 | Fortran: '#4d41b1', 167 | FortranFreeForm: '#4d41b1', 168 | FreeBasic: '#141AC9', 169 | FreeMarker: '#0050b2', 170 | Frege: '#00cafe', 171 | Futhark: '#5f021f', 172 | 'G-code': '#D08CF2', 173 | GameMakerLanguage: '#71b417', 174 | GAML: '#FFC766', 175 | GAMS: '#f49a22', 176 | GAP: '#0000cc', 177 | GCCMachineDescription: '#FFCFAB', 178 | GDB: '#888888', 179 | GDScript: '#355570', 180 | GEDCOM: '#003058', 181 | 'Gemfile.lock': '#701516', 182 | Gemini: '#ff6900', 183 | Genero: '#63408e', 184 | GeneroForms: '#d8df39', 185 | Genie: '#fb855d', 186 | Genshi: '#951531', 187 | GentooEbuild: '#9400ff', 188 | GentooEclass: '#9400ff', 189 | GerberImage: '#d20b00', 190 | Gherkin: '#5B2063', 191 | GitAttributes: '#F44D27', 192 | GitConfig: '#F44D27', 193 | GitRevisionList: '#F44D27', 194 | Gleam: '#ffaff3', 195 | GLSL: '#5686a5', 196 | Glyph: '#c1ac7f', 197 | Gnuplot: '#f0a9f0', 198 | Go: '#00ADD8', 199 | GoChecksums: '#00ADD8', 200 | GoModule: '#00ADD8', 201 | GodotResource: '#355570', 202 | Golo: '#88562A', 203 | Gosu: '#82937f', 204 | Grace: '#615f8b', 205 | Gradle: '#02303a', 206 | GrammaticalFramework: '#ff0000', 207 | GraphQL: '#e10098', 208 | 'Graphviz(DOT)': '#2596be', 209 | Groovy: '#4298b8', 210 | GroovyServerPages: '#4298b8', 211 | GSC: '#FF6800', 212 | Hack: '#878787', 213 | Haml: '#ece2a9', 214 | Handlebars: '#f7931e', 215 | HAProxy: '#106da9', 216 | Harbour: '#0e60e3', 217 | Haskell: '#5e5086', 218 | Haxe: '#df7900', 219 | HCL: '#844FBA', 220 | HiveQL: '#dce200', 221 | HLSL: '#aace60', 222 | HOCON: '#9ff8ee', 223 | HolyC: '#ffefaf', 224 | hoon: '#00b171', 225 | HTML: '#e34c26', 226 | 'HTML+ECR': '#2e1052', 227 | 'HTML+EEX': '#6e4a7e', 228 | 'HTML+ERB': '#701516', 229 | 'HTML+PHP': '#4f5d95', 230 | 'HTML+Razor': '#512be4', 231 | HTTP: '#005C9C', 232 | HXML: '#f68712', 233 | Hy: '#7790B2', 234 | HyPhy: '#888888', 235 | IDL: '#a3522f', 236 | Idris: '#b30000', 237 | IgnoreList: '#000000', 238 | IGORPro: '#0000cc', 239 | ImageJMacro: '#99AAFF', 240 | Imba: '#16cec6', 241 | Inform7: '#888888', 242 | INI: '#d1dbe0', 243 | Ink: '#888888', 244 | InnoSetup: '#264b99', 245 | Io: '#a9188d', 246 | Ioke: '#078193', 247 | Isabelle: '#FEFE00', 248 | IsabelleROOT: '#FEFE00', 249 | J: '#9EEDFF', 250 | Janet: '#0886a5', 251 | JARManifest: '#b07219', 252 | Jasmin: '#d03600', 253 | Java: '#b07219', 254 | JavaProperties: '#2A6277', 255 | JavaServerPages: '#2A6277', 256 | JavaScript: '#f1e05a', 257 | 'JavaScript+ERB': '#f1e05a', 258 | JCL: '#d90e09', 259 | JestSnapshot: '#15c213', 260 | JetBrainsMPS: '#21D789', 261 | JFlex: '#DBCA00', 262 | Jinja: '#a52a22', 263 | Jison: '#56b3cb', 264 | JisonLex: '#56b3cb', 265 | Jolie: '#843179', 266 | jq: '#c7254e', 267 | JSON: '#292929', 268 | JSONwithComments: '#292929', 269 | JSON5: '#267CB9', 270 | JSONiq: '#40d47e', 271 | JSONLD: '#0c479c', 272 | Jsonnet: '#0064bd', 273 | Julia: '#a270ba', 274 | JupyterNotebook: '#DA5B0B', 275 | 'Jupyter Notebook': '#DA5B0B', 276 | Just: '#384d54', 277 | KaitaiStruct: '#773b37', 278 | KakouneScript: '#6f8042', 279 | KerboScript: '#41adf0', 280 | KiCadLayout: '#2f4aab', 281 | KiCadLegacyLayout: '#2f4aab', 282 | KiCadSchematic: '#2f4aab', 283 | Kotlin: '#A97BFF', 284 | KRL: '#28430A', 285 | kvlang: '#1da6e0', 286 | LabVIEW: '#fede06', 287 | Lark: '#2980B9', 288 | Lasso: '#999999', 289 | Latte: '#f2a542', 290 | Lean: '#888888', 291 | Less: '#1d365d', 292 | Lex: '#DBCA00', 293 | LFE: '#4C3023', 294 | LigoLANG: '#0e74ff', 295 | LilyPond: '#9ccc7c', 296 | Limbo: '#888888', 297 | Liquid: '#67b8de', 298 | LiterateAgda: '#315665', 299 | LiterateCoffeeScript: '#244776', 300 | LiterateHaskell: '#5e5086', 301 | LiveScript: '#499886', 302 | LLVM: '#185619', 303 | Logos: '#888888', 304 | Logtalk: '#295b9a', 305 | LOLCODE: '#cc9900', 306 | LookML: '#652B81', 307 | LoomScript: '#888888', 308 | LSL: '#3d9970', 309 | Lua: '#000080', 310 | M: '#888888', 311 | M4: '#888888', 312 | M4Sugar: '#888888', 313 | Macaulay2: '#d8ffff', 314 | Makefile: '#427819', 315 | Mako: '#7e858d', 316 | Markdown: '#083fa1', 317 | Marko: '#42bff2', 318 | Mask: '#f97732', 319 | Mathematica: '#dd1100', 320 | MATLAB: '#e16737', 321 | Max: '#c4a79c', 322 | MAXScript: '#00a6a6', 323 | mcfunction: '#E22837', 324 | Mercury: '#ff2b2b', 325 | Mermaid: '#ff3670', 326 | Meson: '#007800', 327 | Metal: '#8f14e9', 328 | MiniD: '#888888', 329 | MiniYAML: '#ff1111', 330 | Mint: '#02b046', 331 | Mirah: '#c7a938', 332 | mIRCScript: '#3d57c3', 333 | MLIR: '#5EC8DB', 334 | Modelica: '#de1d31', 335 | 'Modula-2': '#10253f', 336 | 'Modula-3': '#223388', 337 | ModuleManagementSystem: '#888888', 338 | Monkey: '#888888', 339 | MonkeyC: '#8D6747', 340 | Moocode: '#888888', 341 | MoonScript: '#ff4585', 342 | Motoko: '#fbb03b', 343 | Motorola68KAssembly: '#005daa', 344 | Move: '#4a137a', 345 | MQL4: '#62A8D6', 346 | MQL5: '#4A76B8', 347 | MTML: '#b7e1f4', 348 | MUF: '#888888', 349 | mupad: '#244963', 350 | Mustache: '#724b3b', 351 | Myghty: '#888888', 352 | nanorc: '#2d004d', 353 | Nasal: '#1d2c4e', 354 | NASL: '#888888', 355 | NCL: '#28431f', 356 | Nearley: '#990000', 357 | Nemerle: '#3d3c6e', 358 | nesC: '#94B0C7', 359 | NetLinx: '#0aa0ff', 360 | 'NetLinx+ERB': '#747faa', 361 | NetLogo: '#ff6375', 362 | NewLisp: '#87AED7', 363 | Nextflow: '#3ac486', 364 | Nginx: '#009639', 365 | Nim: '#ffc200', 366 | Nit: '#009917', 367 | Nix: '#7e7eff', 368 | NPMConfig: '#cb3837', 369 | NSIS: '#888888', 370 | Nu: '#c9df40', 371 | NumPy: '#9C8AF9', 372 | Nunjucks: '#3d8137', 373 | NWScript: '#111522', 374 | 'OASv2-json': '#85ea2d', 375 | 'OASv2-yaml': '#85ea2d', 376 | 'OASv3-json': '#85ea2d', 377 | 'OASv3-yaml': '#85ea2d', 378 | 'Objective-C': '#438eff', 379 | 'Objective-C++': '#6866fb', 380 | 'Objective-J': '#ff0c5a', 381 | ObjectScript: '#424893', 382 | OCaml: '#3be133', 383 | Odin: '#60AFFE', 384 | Omgrofl: '#cabbff', 385 | ooc: '#b0b77e', 386 | Opa: '#888888', 387 | Opal: '#f7ede0', 388 | OpenPolicyAgent: '#7d9199', 389 | 'Open Policy Agent': '#7d9199', 390 | OpenAPISpecificationv2: '#85ea2d', 391 | OpenAPISpecificationv3: '#85ea2d', 392 | OpenCL: '#ed2e2d', 393 | OpenEdgeABL: '#5ce600', 394 | OpenQASM: '#AA70FF', 395 | OpenRCrunscript: '#888888', 396 | OpenSCAD: '#e5cd45', 397 | OptionList: '#476732', 398 | Org: '#77aa99', 399 | Ox: '#888888', 400 | Oxygene: '#cdd0e3', 401 | Oz: '#fab738', 402 | P4: '#7055b5', 403 | Pan: '#cc0000', 404 | Papyrus: '#6600cc', 405 | Parrot: '#f3ca0a', 406 | ParrotAssembly: '#888888', 407 | ParrotInternalRepresentation: '#888888', 408 | Pascal: '#E3F171', 409 | Pawn: '#dbb284', 410 | PDDL: '#0d00ff', 411 | 'PEG.js': '#234d6b', 412 | Pep8: '#C76F5B', 413 | Perl: '#0298c3', 414 | PHP: '#4F5D95', 415 | PicoLisp: '#6067af', 416 | PigLatin: '#fcd7de', 417 | Pike: '#005390', 418 | PlantUML: '#fbbd16', 419 | PLpgSQL: '#336790', 420 | PLSQL: '#dad8d8', 421 | PogoScript: '#d80074', 422 | Polar: '#ae81ff', 423 | Pony: '#888888', 424 | Portugol: '#f8bd00', 425 | PostCSS: '#dc3a0c', 426 | PostScript: '#da291c', 427 | 'POV-RaySDL': '#6bac65', 428 | PowerBuilder: '#8f0f8d', 429 | PowerShell: '#012456', 430 | Prisma: '#0c344b', 431 | Processing: '#0096D8', 432 | Procfile: '#3B2F63', 433 | Prolog: '#74283c', 434 | Promela: '#de0000', 435 | PropellerSpin: '#7fa2a7', 436 | Pug: '#a86454', 437 | Puppet: '#302B6D', 438 | PureBasic: '#5a6986', 439 | PureScript: '#1D222D', 440 | Pyret: '#ee1e10', 441 | Python: '#3572A5', 442 | Pythonconsole: '#3572A5', 443 | Pythontraceback: '#3572A5', 444 | q: '#0040cd', 445 | 'Q#': '#fed659', 446 | QMake: '#888888', 447 | QML: '#44a51c', 448 | QtScript: '#00b841', 449 | Quake: '#882233', 450 | R: '#198CE7', 451 | Racket: '#3c5caa', 452 | Ragel: '#9d5200', 453 | Raku: '#0000fb', 454 | RAML: '#77d9fb', 455 | Rascal: '#fffaa0', 456 | RDoc: '#701516', 457 | REALbasic: '#888888', 458 | Reason: '#ff5847', 459 | ReasonLIGO: '#ff5847', 460 | Rebol: '#358a5b', 461 | RecordJar: '#0673ba', 462 | Red: '#f50000', 463 | Redcode: '#888888', 464 | RegularExpression: '#009a00', 465 | "Ren'Py": '#ff7f7f', 466 | RenderScript: '#888888', 467 | ReScript: '#ed5051', 468 | reStructuredText: '#141414', 469 | REXX: '#d90e09', 470 | Ring: '#2D54CB', 471 | Riot: '#A71E49', 472 | RMarkdown: '#198ce7', 473 | RobotFramework: '#00c0b5', 474 | Roff: '#ecdebe', 475 | RoffManpage: '#ecdebe', 476 | Rouge: '#cc0088', 477 | RouterOSScript: '#DE3941', 478 | RPC: '#888888', 479 | RPGLE: '#2BDE21', 480 | Ruby: '#701516', 481 | RUNOFF: '#665a4e', 482 | Rust: '#dea584', 483 | Sage: '#888888', 484 | SaltStack: '#646464', 485 | SAS: '#B34936', 486 | Sass: '#a53b70', 487 | Scala: '#c22d40', 488 | Scaml: '#bd181a', 489 | Scenic: '#fdc700', 490 | Scheme: '#1e4aec', 491 | Scilab: '#ca0f21', 492 | SCSS: '#c6538c', 493 | sed: '#64b970', 494 | Self: '#0579aa', 495 | ShaderLab: '#222c37', 496 | Shell: '#89e051', 497 | ShellCheckConfig: '#cecfcb', 498 | ShellSession: '#888888', 499 | Shen: '#120F14', 500 | Sieve: '#888888', 501 | SimpleFileVerification: '#C9BFED', 502 | Singularity: '#64E6AD', 503 | Slash: '#007eff', 504 | Slice: '#003fa2', 505 | Slim: '#2b2b2b', 506 | Smali: '#888888', 507 | Smalltalk: '#596706', 508 | Smarty: '#f0c040', 509 | Smithy: '#c44536', 510 | SmPL: '#c94949', 511 | SMT: '#888888', 512 | Snakemake: '#419179', 513 | Solidity: '#AA6746', 514 | SourcePawn: '#f69e1d', 515 | SPARQL: '#0C4597', 516 | SQF: '#3F3F3F', 517 | SQL: '#e38c00', 518 | SQLPL: '#e38c00', 519 | Squirrel: '#800000', 520 | SRecodeTemplate: '#348a34', 521 | Stan: '#b2011d', 522 | StandardML: '#dc566d', 523 | Starlark: '#76d275', 524 | Stata: '#1a5f91', 525 | STL: '#373b5e', 526 | StringTemplate: '#3fb34f', 527 | Stylus: '#ff6347', 528 | SubRipText: '#9e0101', 529 | SugarSS: '#2fcc9f', 530 | SuperCollider: '#46390b', 531 | Svelte: '#ff3e00', 532 | SVG: '#ff9900', 533 | Sway: '#dea584', 534 | Swift: '#F05138', 535 | SWIG: '#888888', 536 | SystemVerilog: '#DAE1C2', 537 | Talon: '#333333', 538 | Tcl: '#e4cc98', 539 | Tcsh: '#888888', 540 | Terra: '#00004c', 541 | TeX: '#3D6117', 542 | Textile: '#ffe7ac', 543 | TextMateProperties: '#df66e4', 544 | Thrift: '#D12127', 545 | TIProgram: '#A0AA87', 546 | TLA: '#4b0079', 547 | TOML: '#9c4221', 548 | TSQL: '#e38c00', 549 | TSV: '#237346', 550 | TSX: '#3178c6', 551 | Turing: '#cf142b', 552 | Twig: '#c1d026', 553 | TXL: '#0178b8', 554 | TypeScript: '#3178c6', 555 | UnifiedParallelC: '#4e3617', 556 | Unity3DAsset: '#222c37', 557 | UnixAssembly: '#888888', 558 | Uno: '#9933cc', 559 | UnrealScript: '#a54c4d', 560 | UrWeb: '#ccccee', 561 | V: '#4f87c4', 562 | Vala: '#a56de2', 563 | ValveDataFormat: '#f26025', 564 | VBA: '#867db1', 565 | VBScript: '#15dcdc', 566 | VCL: '#148AA8', 567 | VelocityTemplateLanguage: '#507cff', 568 | Verilog: '#b2b7f8', 569 | VHDL: '#adb2cb', 570 | VimHelpFile: '#199f4b', 571 | VimScript: '#199f4b', 572 | 'Vim Script': '#199f4b', 573 | VimSnippet: '#199f4b', 574 | 'VisualBasic.NET': '#945db7', 575 | 'VisualBasic6.0': '#2c6353', 576 | Volt: '#1F1F1F', 577 | Vue: '#41b883', 578 | Vyper: '#2980b9', 579 | wdl: '#42f1f4', 580 | WebOntologyLanguage: '#5b70bd', 581 | WebAssembly: '#04133b', 582 | WebIDL: '#888888', 583 | Whiley: '#d5c397', 584 | Wikitext: '#fc5757', 585 | WindowsRegistryEntries: '#52d5ff', 586 | wisp: '#7582D1', 587 | WitcherScript: '#ff0000', 588 | Wollok: '#a23738', 589 | WorldofWarcraftAddonData: '#f7e43f', 590 | Wren: '#383838', 591 | X10: '#4B6BEF', 592 | xBase: '#403a40', 593 | XC: '#99DA07', 594 | XML: '#0060ac', 595 | XMLPropertyList: '#0060ac', 596 | Xojo: '#81bd41', 597 | Xonsh: '#285EEF', 598 | XProc: '#888888', 599 | XQuery: '#5232e7', 600 | XS: '#888888', 601 | XSLT: '#EB8CEB', 602 | Xtend: '#24255d', 603 | Yacc: '#4B6C4B', 604 | YAML: '#cb171e', 605 | YARA: '#220000', 606 | YASnippet: '#32AB90', 607 | Yul: '#794932', 608 | ZAP: '#0d665e', 609 | Zeek: '#888888', 610 | ZenScript: '#00BCD1', 611 | Zephir: '#118f9e', 612 | Zig: '#ec915c', 613 | ZIL: '#dc75e5', 614 | Zimpl: '#d67711' 615 | }; 616 | --------------------------------------------------------------------------------