Skip to content

Commit 55bcb24

Browse files
authored
Merge pull request #283 from php-school/contributors
Add contributors list
2 parents ba42824 + d698a19 commit 55bcb24

File tree

8 files changed

+168
-2
lines changed

8 files changed

+168
-2
lines changed

.env.dist

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,5 @@ CACHE.ENABLE=false
99
DISPLAY_ERRORS=true
1010
GITHUB_CLIENT_ID=
1111
GITHUB_CLIENT_SECRET=
12+
GITHUB_TOKEN=
1213
DEV_MODE=true

app/config.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
use Doctrine\ORM\ORMSetup;
1212
use Doctrine\ORM\Tools\Console\ConsoleRunner;
1313
use Doctrine\ORM\Tools\Console\EntityManagerProvider\SingleManagerProvider;
14+
use Github\AuthMethod;
1415
use Github\Client;
1516
use League\CommonMark\Extension\CommonMarkCoreExtension;
1617
use League\CommonMark\Extension\ExternalLink\ExternalLinkExtension;
@@ -50,6 +51,7 @@
5051
use PhpSchool\Website\Action\SubmitWorkshop;
5152
use PhpSchool\Website\Action\TrackDownloads;
5253
use PhpSchool\Website\Blog\Generator;
54+
use PhpSchool\Website\Command\SyncContributors;
5355
use PhpSchool\Website\Online\CloudWorkshopRepository;
5456
use PhpSchool\Website\Online\Command\DownloadComposerPackageList;
5557
use PhpSchool\Website\Online\Middleware\ExerciseRunnerRateLimiter;
@@ -112,6 +114,7 @@
112114
$app->command('create-admin-user name email password', CreateAdminUser::class);
113115
$app->command('generate-blog', GenerateBlog::class);
114116
$app->command('download-composer-packages', DownloadComposerPackageList::class);
117+
$app->command('sync-contributors', SyncContributors::class);
115118

116119
ConsoleRunner::addCommands($app, new SingleManagerProvider($c->get(EntityManagerInterface::class)));
117120

@@ -189,6 +192,9 @@
189192
DownloadComposerPackageList::class => function (ContainerInterface $c): DownloadComposerPackageList {
190193
return new DownloadComposerPackageList($c->get('guzzle.packagist'), $c->get(LoggerInterface::class));
191194
},
195+
SyncContributors::class => function (ContainerInterface $c): SyncContributors {
196+
return new SyncContributors($c->get(Client::class), $c->get(LoggerInterface::class));
197+
},
192198

193199
TrackDownloads::class => function (ContainerInterface $c): TrackDownloads {
194200
return new TrackDownloads($c->get(WorkshopRepository::class), $c->get(WorkshopInstallRepository::class));
@@ -212,6 +218,13 @@
212218
);
213219
},
214220

221+
Client::class => function (ContainerInterface $c): Client {
222+
$client = new Client();
223+
$client->authenticate($c->get('config')['github']['token'], AuthMethod::ACCESS_TOKEN);
224+
225+
return $client;
226+
},
227+
215228
Github::class => function (ContainerInterface $c): Github {
216229
return new Github([
217230
'clientId' => $c->get('config')['github']['clientId'],
@@ -582,6 +595,7 @@ public function parse($markdown): string
582595
'github' => [
583596
'clientId' => $_ENV['GITHUB_CLIENT_ID'],
584597
'clientSecret' => $_ENV['GITHUB_CLIENT_SECRET'],
598+
'token' => $_ENV['GITHUB_TOKEN'],
585599
],
586600

587601
'jwtSecret' => $_ENV['JWT_SECRET'],
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
<script setup>
2+
import { ChatBubbleLeftRightIcon } from "@heroicons/vue/24/solid";
3+
import Modal from "../Online/ModalDialog.vue";
4+
import { onMounted, ref } from "vue";
5+
6+
const { open } = defineProps({
7+
open: Boolean,
8+
});
9+
10+
const emit = defineEmits(["close"]);
11+
12+
const contributors = ref([]);
13+
onMounted(async () => {
14+
const response = await fetch("/api/contributors");
15+
contributors.value = await response.json();
16+
});
17+
</script>
18+
19+
<template>
20+
<Teleport to="body">
21+
<Transition
22+
enter-active-class="transition-opacity duration-100 ease-in"
23+
leave-active-class="transition-opacity duration-200 ease-in"
24+
enter-from-class="opacity-0"
25+
enter-to-class="opacity-100"
26+
leave-from-class="opacity-100"
27+
leave-to-class="opacity-0"
28+
>
29+
<Modal :scroll-content="true" size="md" max-height="max-h-[calc(5/6*100%)]" v-if="open" @close="emit('close')">
30+
<template #header>
31+
<div class="flex items-center">
32+
<ChatBubbleLeftRightIcon class="mr-2 h-6 w-6 text-pink-500" />
33+
<h3 class="mt-0 pt-0 font-mono text-base font-semibold text-white lg:text-xl">Contributors</h3>
34+
</div>
35+
</template>
36+
<template #body>
37+
<p class="mb-4 text-justify text-sm italic text-white">The PHP School project is made possible by the following contributors. Thank you for your hard work and dedication to the project!</p>
38+
<ul role="list" class="divide-y divide-gray-800">
39+
<li v-for="contributor in contributors" :key="contributor.username" class="flex justify-between gap-x-6 py-2">
40+
<div class="flex min-w-0 gap-x-3">
41+
<img class="h-8 w-8 flex-none rounded-full bg-gray-800" :src="contributor.profilePic" alt="" />
42+
<div class="min-w-0 flex-auto">
43+
<a :href="contributor.profile" target="_blank" class="text-sm leading-6 text-white">{{ contributor.username }}</a>
44+
</div>
45+
</div>
46+
<div class="hidden shrink-0 sm:flex sm:flex-col sm:items-end">
47+
<p class="mt-1 text-xs leading-5 text-pink-500">
48+
<b>{{ contributor.contributions }}</b>
49+
Commit{{ contributor.contributions > 1 ? "s" : "" }}
50+
</p>
51+
</div>
52+
</li>
53+
</ul>
54+
</template>
55+
</Modal>
56+
</Transition>
57+
</Teleport>
58+
</template>

assets/components/Website/SiteFooter.vue

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@ import GitHubIcon from "../Icons/GitHubIcon.vue";
33
import Logo from "./SiteLogo.vue";
44
import { ref } from "vue";
55
import JoinSlack from "./JoinSlack.vue";
6+
import Contributors from "./ProjectContributors.vue";
67
78
const currentYear = new Date().getFullYear();
89
910
const slackModalOpen = ref(false);
11+
const contributorModalOpen = ref(false);
1012
</script>
1113
<template>
1214
<footer class="bg-cyan-600 font-sans text-sm text-white">
@@ -37,7 +39,7 @@ const slackModalOpen = ref(false);
3739
<div class="mb-4 ml-0 flex-grow sm:mb-0 sm:ml-6">
3840
<h4 class="mb-2 font-work-sans text-lg font-bold capitalize not-italic text-white">The Creators</h4>
3941
<ul class="list-none">
40-
<li class="py-1"><router-link to="/events" class="decoration-pink-600 decoration-2 underline-offset-4 hover:underline">About Us</router-link></li>
42+
<li class="py-1"><span @click="contributorModalOpen = true" class="cursor-pointer decoration-pink-600 decoration-2 underline-offset-4 hover:underline">Contributors</span></li>
4143
<li class="py-1"><a href="mailto:[email protected]" class="decoration-pink-600 decoration-2 underline-offset-4 hover:underline">Email Us</a></li>
4244
<li class="py-1"><a href="https://github.com/php-school" target="_blank" class="decoration-pink-600 decoration-2 underline-offset-4 hover:underline">GitHub</a></li>
4345
</ul>
@@ -67,5 +69,6 @@ const slackModalOpen = ref(false);
6769
</div>
6870
</div>
6971
<JoinSlack :open="slackModalOpen" @close="slackModalOpen = false" />
72+
<Contributors v-if="contributorModalOpen" :open="contributorModalOpen" @close="contributorModalOpen = false" />
7073
</footer>
7174
</template>

public/index.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
use PhpSchool\Website\Action\Admin\Workshop\View;
1717
use PhpSchool\Website\Action\BlogPost;
1818
use PhpSchool\Website\Action\BlogPosts;
19+
use PhpSchool\Website\Action\Contributors;
1920
use PhpSchool\Website\Action\Events;
2021
use PhpSchool\Website\Action\Online\ComposerPackageAdd;
2122
use PhpSchool\Website\Action\Online\ComposerPackageSearch;
@@ -69,6 +70,7 @@
6970
$app->get('/api/events', Events::class);
7071
$app->get('/api/posts', BlogPosts::class);
7172
$app->post('/api/slack-invite', SlackInvite::class);
73+
$app->get('/api/contributors', Contributors::class);
7274

7375
$app
7476
->group('/api/admin', function (RouteCollectorProxy $group) {

src/Action/Contributors.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpSchool\Website\Action;
6+
7+
use Psr\Http\Message\ResponseInterface as Response;
8+
use Psr\Http\Message\ServerRequestInterface as Request;
9+
10+
class Contributors
11+
{
12+
use JsonUtils;
13+
public function __invoke(Request $request, Response $response): Response
14+
{
15+
/** @var array<string, array{username: string, contributions: int, profilePic: string, profile: string}> $data */
16+
$data = json_decode((string) file_get_contents(__DIR__ . '/../../var/contributors.json'));
17+
return $this->withJson($data, $response);
18+
}
19+
}

src/Action/JsonUtils.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ private function jsonSuccess(Response $response): Response
2020
}
2121

2222
/**
23-
* @param array<string, mixed> $json
23+
* @param array<string|int, mixed> $json
2424
*/
2525
private function withJson(array $json, Response $response, int $status = 200): Response
2626
{

src/Command/SyncContributors.php

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpSchool\Website\Command;
6+
7+
use Psr\Log\LoggerInterface;
8+
use Symfony\Component\Console\Output\OutputInterface;
9+
10+
class SyncContributors
11+
{
12+
private const CONTRIBUTORS_FILE_LOCATION = __DIR__ . '/../../var/contributors.json';
13+
14+
public function __construct(private \Github\Client $client, private LoggerInterface $logger) {}
15+
16+
public function __invoke(OutputInterface $output): void
17+
{
18+
try {
19+
$contributors = $this->downloadContributors();
20+
} catch (\Exception $e) {
21+
$this->logger->error('Could not download contributors. Error: ' . $e->getMessage());
22+
return;
23+
}
24+
25+
file_put_contents(self::CONTRIBUTORS_FILE_LOCATION, json_encode($contributors, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR));
26+
27+
$output->writeln('<info>Contributors synced</info>');
28+
}
29+
30+
/**
31+
* @return array<int, array{
32+
* username: string,
33+
* contributions: int,
34+
* profilePic: string,
35+
* profile: string
36+
* }>
37+
*/
38+
private function downloadContributors(): array
39+
{
40+
$repositories = $this->client->user()->repositories('php-school');
41+
42+
$contributors = [];
43+
44+
foreach ($repositories as $repository) {
45+
$result = $this->client->repositories()->contributors('php-school', $repository['name']);
46+
47+
if (!is_array($result)) {
48+
continue;
49+
}
50+
51+
foreach ($result as $contributor) {
52+
if (!isset($contributors[$contributor['login']])) {
53+
$contributors[$contributor['login']] = [
54+
'username' => $contributor['login'],
55+
'contributions' => (int) $contributor['contributions'],
56+
'profilePic' => $contributor['avatar_url'],
57+
'profile' => $contributor['html_url'],
58+
];
59+
} else {
60+
$contributors[$contributor['login']]['contributions'] += (int) $contributor['contributions'];
61+
}
62+
}
63+
}
64+
65+
usort($contributors, fn(array $a, array $b) => $b['contributions'] <=> $a['contributions']);
66+
67+
return $contributors;
68+
}
69+
}

0 commit comments

Comments
 (0)