Skip to content

Commit

Permalink
feat: voting through github
Browse files Browse the repository at this point in the history
  • Loading branch information
RobertBrunhage committed Dec 5, 2024
1 parent 48946f4 commit 1fbc685
Show file tree
Hide file tree
Showing 5 changed files with 313 additions and 1 deletion.
221 changes: 221 additions & 0 deletions .github/workflows/create-discussions.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
name: Create GitHub Discussions

on:
pull_request:
types: [opened]
paths:
- 'src/content/apps/**'
workflow_dispatch:
inputs:
createForExisting:
description: 'Create discussions for existing apps'
required: true
default: 'true'
type: boolean

jobs:
create-discussion:
runs-on: ubuntu-latest
permissions:
discussions: write
pull-requests: read
contents: read
steps:
- uses: actions/checkout@v4

- name: Create Discussion for PR
if: github.event_name == 'pull_request'
uses: actions/github-script@v7
with:
script: |
const query = `query($owner: String!, $repo: String!) {
repository(owner: $owner, name: $repo) {
discussionCategories(first: 10) {
nodes {
id
name
}
}
}
}`;
const { repository } = await github.graphql(query, {
owner: context.repo.owner,
repo: context.repo.repo
});
let category = repository.discussionCategories.nodes.find(c => c.name === "app-votes");
if (!category) {
throw new Error("The 'app-votes' category does not exist. Please create it manually in the repository.");
}
const { data: files } = await github.rest.pulls.listFiles({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: context.issue.number
});
const appFiles = files.filter(file =>
file.status === 'added' &&
file.filename.startsWith('src/content/apps/') &&
!file.filename.endsWith('_template.md')
);
for (const file of appFiles) {
const content = Buffer.from((await github.rest.repos.getContent({
owner: context.repo.owner,
repo: context.repo.repo,
path: file.filename,
ref: context.payload.pull_request.head.sha
})).data.content, 'base64').toString();
const frontmatterMatch = content.match(/---\n([\s\S]*?)\n---/);
if (!frontmatterMatch) continue;
const frontmatter = frontmatterMatch[1];
const appData = {};
frontmatter.split('\n').forEach(line => {
const match = line.match(/^(\w+):\s*(?:"([^"]*)"|'([^']*)'|([^"'\s].*))\s*$/);
if (match) {
appData[match[1]] = match[2] || match[3] || match[4];
}
});
try {
if (!appData.name || !appData.author) {
console.log(`Skipping file ${file.filename || file.path}: Missing required fields in frontmatter`);
continue;
}
const query = `query($searchQuery: String!) {
search(query: $searchQuery, type: DISCUSSION, first: 10) {
nodes {
... on Discussion {
title
}
}
}
}`;
const searchResult = await github.graphql(query, {
searchQuery: `repo:${context.repo.owner}/${context.repo.repo} "${appData.name} by ${appData.author}" in:title`
});
const existingDiscussion = searchResult.search.nodes.find(node =>
node.title === `Vote: ${appData.name} by ${appData.author}`
);
if (!existingDiscussion) {
await github.rest.discussions.create({
owner: context.repo.owner,
repo: context.repo.repo,
title: `Vote: ${appData.name} by ${appData.author}`,
body: `🗳️ **Vote for this app by giving it a 👍 reaction!**\n\n${appData.description}`,
category_id: category.id
});
console.log(`Created discussion for ${appData.name}`);
} else {
console.log(`Discussion already exists for ${appData.name}`);
}
} catch (error) {
console.error(`Error processing ${file.filename || file.path}:`, error);
continue;
}
}
- name: Create Discussions for Existing Apps
if: github.event_name == 'workflow_dispatch' && github.event.inputs.createForExisting == 'true'
uses: actions/github-script@v7
with:
script: |
const query = `query($owner: String!, $repo: String!) {
repository(owner: $owner, name: $repo) {
discussionCategories(first: 10) {
nodes {
id
name
}
}
}
}`;
const { repository } = await github.graphql(query, {
owner: context.repo.owner,
repo: context.repo.repo
});
let category = repository.discussionCategories.nodes.find(c => c.name === "app-votes");
if (!category) {
throw new Error("The 'app-votes' category does not exist. Please create it manually in the repository.");
}
const { data: contents } = await github.rest.repos.getContent({
owner: context.repo.owner,
repo: context.repo.repo,
path: 'src/content/apps'
});
for (const file of contents) {
if (file.type !== 'file' || file.name.endsWith('_template.md')) continue;
const content = Buffer.from((await github.rest.repos.getContent({
owner: context.repo.owner,
repo: context.repo.repo,
path: file.path
})).data.content, 'base64').toString();
const frontmatterMatch = content.match(/---\n([\s\S]*?)\n---/);
if (!frontmatterMatch) continue;
const frontmatter = frontmatterMatch[1];
const appData = {};
frontmatter.split('\n').forEach(line => {
const match = line.match(/^(\w+):\s*(?:"([^"]*)"|'([^']*)'|([^"'\s].*))\s*$/);
if (match) {
appData[match[1]] = match[2] || match[3] || match[4];
}
});
try {
if (!appData.name || !appData.author) {
console.log(`Skipping file ${file.filename || file.path}: Missing required fields in frontmatter`);
continue;
}
const query = `query($searchQuery: String!) {
search(query: $searchQuery, type: DISCUSSION, first: 10) {
nodes {
... on Discussion {
title
}
}
}
}`;
const searchResult = await github.graphql(query, {
searchQuery: `repo:${context.repo.owner}/${context.repo.repo} "${appData.name} by ${appData.author}" in:title`
});
const existingDiscussion = searchResult.search.nodes.find(node =>
node.title === `Vote: ${appData.name} by ${appData.author}`
);
if (!existingDiscussion) {
await github.rest.discussions.create({
owner: context.repo.owner,
repo: context.repo.repo,
title: `Vote: ${appData.name} by ${appData.author}`,
body: `🗳️ **Vote for this app by giving it a 👍 reaction!**\n\n${appData.description}`,
category_id: category.id
});
console.log(`Created discussion for ${appData.name}`);
} else {
console.log(`Discussion already exists for ${appData.name}`);
}
} catch (error) {
console.error(`Error processing ${file.filename || file.path}:`, error);
continue;
}
}
59 changes: 59 additions & 0 deletions .github/workflows/update-votes.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
name: Update Vote Counts

on:
schedule:
- cron: '0 */6 * * *' # Run every 6 hours
workflow_dispatch: # Allow manual triggers

jobs:
update-votes:
runs-on: ubuntu-latest
permissions:
contents: write
discussions: read
steps:
- uses: actions/checkout@v4

- name: Update Vote Counts
uses: actions/github-script@v7
with:
script: |
const fs = require('fs').promises;
const path = require('path');
const yaml = require('js-yaml');
// Get all discussions in the Flutter of the Year category
const discussions = await github.paginate(github.rest.discussions.listForRepo, {
owner: context.repo.owner,
repo: context.repo.repo,
category_id: process.env.DISCUSSION_CATEGORY_ID
});
// Create votes.json with the current vote counts
const votes = {};
for (const discussion of discussions) {
if (discussion.title.startsWith('Vote: ')) {
const appName = discussion.title.replace('Vote: ', '').split(' by ')[0];
// Count thumbs up reactions
const reactions = await github.rest.reactions.listForDiscussion({
owner: context.repo.owner,
repo: context.repo.repo,
discussion_number: discussion.number
});
votes[appName] = reactions.data.filter(r => r.content === '+1').length;
}
}
// Write the votes to a JSON file
await fs.writeFile(
'src/data/votes.json',
JSON.stringify(votes, null, 2)
);
// Commit and push the changes
const date = new Date().toISOString();
await exec.exec('git', ['config', 'user.name', 'github-actions[bot]']);
await exec.exec('git', ['config', 'user.email', 'github-actions[bot]@users.noreply.github.com']);
await exec.exec('git', ['add', 'src/data/votes.json']);
await exec.exec('git', ['commit', '-m', `Update vote counts - ${date}`]);
await exec.exec('git', ['push']);
1 change: 1 addition & 0 deletions src/data/votes.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{}
18 changes: 17 additions & 1 deletion src/pages/index.astro
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,18 @@
import { getCollection } from 'astro:content';
import { Image } from 'astro:assets';
import Layout from '../layouts/Layout.astro';
import { getVotes, type VoteData } from '../utils/votes';
const currentYear = new Date().getFullYear();
const allApps = await getCollection('apps');
const votes: VoteData = getVotes();
// Sort apps by vote count (descending)
const sortedApps = allApps.sort((a, b) => {
const votesA = votes[a.data.name] || 0;
const votesB = votes[b.data.name] || 0;
return votesB - votesA;
});
---

Expand Down Expand Up @@ -51,7 +60,7 @@ const allApps = await getCollection('apps');
<!-- Apps Grid -->
<div class="container mx-auto px-4 py-8">
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{allApps.map((app) => (
{sortedApps.map((app) => (
<div class="bg-white/10 backdrop-blur-lg rounded-xl overflow-hidden transform hover:scale-105 transition-transform duration-300 border border-white/20">
<div class="p-6">
<!-- App Screenshot -->
Expand Down Expand Up @@ -89,6 +98,13 @@ const allApps = await getCollection('apps');
</a>
))}
</div>
<div class="flex justify-between items-start mb-2">
<h3 class="text-xl font-bold">{app.data.name}</h3>
<div class="flex items-center space-x-1 bg-white/10 px-2 py-1 rounded-full">
<span class="text-blue-300">👍</span>
<span class="text-sm font-medium">{votes[app.data.name] || 0}</span>
</div>
</div>
</div>
</div>
))}
Expand Down
15 changes: 15 additions & 0 deletions src/utils/votes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export interface VoteData {
[appName: string]: number;
}

export function getVotes(): VoteData {
try {
// During build time, this file will be created by GitHub Actions
// We import it as a module to get the data
const votes = import.meta.glob('/src/data/votes.json', { eager: true });
return Object.values(votes)[0] as VoteData || {};
} catch (error) {
console.error('Error reading votes:', error);
return {};
}
}

0 comments on commit 1fbc685

Please sign in to comment.