Compare commits

..

2 Commits

Author SHA1 Message Date
15a58e8ace
Merge pull request 'implement community basics' (!17) from feature/community-basics into master
All checks were successful
rvr-nextgen/pipeline/head This commit looks good
Reviewed-on: #17
2023-04-16 14:00:25 +02:00
d763c4344c
implement community basics
All checks were successful
rvr-nextgen/pipeline/pr-master This commit looks good
2023-04-16 13:57:54 +02:00
21 changed files with 844 additions and 15 deletions

View File

@ -0,0 +1,20 @@
CREATE TABLE `communities` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(255) NOT NULL,
`currency` varchar(3) NOT NULL,
`created` timestamp NOT NULL DEFAULT current_timestamp(),
PRIMARY KEY (`id`),
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;
CREATE TABLE `community_members` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`community_id` int(10) unsigned NOT NULL,
`user_id` int(10) unsigned DEFAULT NULL,
`owner` tinyint(1) NOT NULL DEFAULT 0,
PRIMARY KEY (`id`),
UNIQUE KEY `unique_user_for_community` (`community_id`, `user_id`),
KEY `community_id` (`community_id`),
KEY `user_id` (`user_id`),
CONSTRAINT `community_members_community_id` FOREIGN KEY (`community_id`) REFERENCES `communities` (`id`),
CONSTRAINT `community_members_user_id` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;

View File

@ -88,10 +88,14 @@ sub {
}
hr {
border: solid #bbbbbb 1px;
border: solid #bbbbcc 1px;
margin: 10px 0;
}
.normal {
font-weight: 400;
}
.bold {
font-weight: 700;
}
@ -418,13 +422,53 @@ div.buttonContainer>button {
div.box {
width: 576px;
background-color: #eeeeee;
background-color: #eeeef4;
border-radius: 3px;
margin: 10px auto;
padding: 10px;
box-sizing: border-box;
}
div.gridContainer {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
grid-gap: 10px;
}
div.gridContainer > div {
background-color: #eeeef4;
border-radius: 3px;
padding: 5px 10px;
}
table {
border-collapse: separate;
border-spacing: 0;
}
table.fullWidth {
width: 100%;
}
table th {
font-weight: bold;
}
table th, table td {
padding: 2px 0;
vertical-align: middle;
}
.choices__inner {
box-sizing: border-box;
}
@media screen and (max-width: 424px) {
div.gridContainer {
grid-template-columns: auto;
}
}
@media screen and (max-width: 599px) {
header h1 span {
display: none;

View File

@ -0,0 +1,62 @@
(function () {
const element = document.getElementById('new_member_user_id');
const choices = new Choices(element, {
noResultsText: 'No users found',
noChoicesText: 'Start typing to search users'
});
element.addEventListener('search', RVR.debounce(async function (e) {
RVR.httpRequest('GET', searchUserUrl.replace('QUERY', encodeURIComponent(e.detail.value)), function () {
choices.setChoices(this.response.results, 'value', 'label', true);
});
}));
element.addEventListener('choice', function () {
choices.setChoices([], 'value', 'label', true);
document.getElementById('new_member_button').disabled = false;
});
document.getElementById('new_member_button').addEventListener('click', function () {
document.getElementById('loading').style.visibility = 'visible';
let data = new FormData();
data.append('user_id', document.getElementById('new_member_user_id').value);
RVR.httpRequest('POST', newMemberUrl, function () {
window.location.reload();
}, data);
});
const ownerCheckboxesButtons = document.getElementsByClassName('member_owner');
for (const ownerCheckboxesButton of ownerCheckboxesButtons) {
ownerCheckboxesButton.addEventListener('change', function () {
document.getElementById('loading').style.visibility = 'visible';
let data = new FormData();
data.append('community_member_id', this.dataset.id);
data.append('owner', this.checked ? 1 : 0);
RVR.httpRequest('POST', editMemberUrl, function () {
document.getElementById('loading').style.visibility = 'hidden';
if (!this.response.success) {
ownerCheckboxesButton.checked = !ownerCheckboxesButton.checked;
}
}, data);
});
};
const deleteButtons = document.getElementsByClassName('delete_member');
for (const deleteButton of deleteButtons) {
deleteButton.addEventListener('click', function () {
document.getElementById('loading').style.visibility = 'visible';
let data = new FormData();
data.append('community_member_id', this.dataset.id);
RVR.httpRequest('POST', deleteMemberUrl, function () {
window.location.reload();
}, data);
});
};
})();

View File

@ -193,6 +193,14 @@ var RVR = {
form.onreset = function () {
form.elements.submit.disabled = true;
}
},
debounce: function(func, timeout = 300) {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => { func.apply(this, args); }, timeout);
};
}
};

57
public/static/package-lock.json generated Normal file
View File

@ -0,0 +1,57 @@
{
"requires": true,
"lockfileVersion": 1,
"dependencies": {
"@babel/runtime": {
"version": "7.21.0",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.21.0.tgz",
"integrity": "sha512-xwII0//EObnq89Ji5AKYQaRYiW/nZ3llSv29d49IuxPhKbtJoLP+9QUUZ4nVragQVtaVGeZrpB+ZtG/Pdy/POw==",
"requires": {
"regenerator-runtime": "^0.13.11"
}
},
"choices.js": {
"version": "10.2.0",
"resolved": "https://registry.npmjs.org/choices.js/-/choices.js-10.2.0.tgz",
"integrity": "sha512-8PKy6wq7BMjNwDTZwr3+Zry6G2+opJaAJDDA/j3yxvqSCnvkKe7ZIFfIyOhoc7htIWFhsfzF9tJpGUATcpUtPg==",
"requires": {
"deepmerge": "^4.2.2",
"fuse.js": "^6.6.2",
"redux": "^4.2.0"
}
},
"deepmerge": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
"integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="
},
"fuse.js": {
"version": "6.6.2",
"resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-6.6.2.tgz",
"integrity": "sha512-cJaJkxCCxC8qIIcPBF9yGxY0W/tVZS3uEISDxhYIdtk8OL93pe+6Zj7LjCqVV4dzbqcriOZ+kQ/NE4RXZHsIGA=="
},
"leaflet": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.3.tgz",
"integrity": "sha512-iB2cR9vAkDOu5l3HAay2obcUHZ7xwUBBjph8+PGtmW/2lYhbLizWtG7nTeYht36WfOslixQF9D/uSIzhZgGMfQ=="
},
"leaflet.markercluster": {
"version": "1.5.3",
"resolved": "https://registry.npmjs.org/leaflet.markercluster/-/leaflet.markercluster-1.5.3.tgz",
"integrity": "sha512-vPTw/Bndq7eQHjLBVlWpnGeLa3t+3zGiuM7fJwCkiMFq+nmRuG3RI3f7f4N4TDX7T4NpbAXpR2+NTRSEGfCSeA=="
},
"redux": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz",
"integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==",
"requires": {
"@babel/runtime": "^7.9.2"
}
},
"regenerator-runtime": {
"version": "0.13.11",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
"integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg=="
}
}
}

View File

@ -1,6 +1,7 @@
{
"dependencies": {
"leaflet": "^1.6.0",
"leaflet.markercluster": "^1.4.1"
"leaflet.markercluster": "^1.4.1",
"choices.js": "^10.2.0"
}
}

View File

@ -0,0 +1,216 @@
<?php namespace RVR\Controller;
use DateTime;
use RVR\PersistentData\Model\Community;
use RVR\PersistentData\Model\CommunityMember;
use RVR\PersistentData\Model\User;
use RVR\Repository\CommunityRepository;
use RVR\Repository\CommunityMemberRepository;
use RVR\Repository\UserRepository;
use SokoWeb\Interfaces\Authorization\ISecured;
use SokoWeb\Interfaces\Request\IRequest;
use SokoWeb\Interfaces\Response\IContent;
use SokoWeb\PersistentData\PersistentDataManager;
use SokoWeb\Response\HtmlContent;
use SokoWeb\Response\JsonContent;
class CommunityController implements ISecured
{
private IRequest $request;
private PersistentDataManager $pdm;
private UserRepository $userRepository;
private CommunityRepository $communityRepository;
private CommunityMemberRepository $communityMemberRepository;
public function __construct(IRequest $request)
{
$this->request = $request;
$this->pdm = new PersistentDataManager();
$this->userRepository = new UserRepository();
$this->communityRepository = new CommunityRepository();
$this->communityMemberRepository = new CommunityMemberRepository();
}
public function authorize(): bool
{
return $this->request->user() !== null;
}
public function getCommunityHome(): ?IContent
{
if (!$this->checkPermission($this->request->query('communityId'), false, $community, $ownCommunityMember)) {
return null;
}
return new HtmlContent('communities/community', [
'community' => $community,
'members' => $this->getMembers($community),
'currencyNames' => [],
'upcomingEvents' => [],
'editPermission' => $ownCommunityMember->getOwner()
]);
}
public function getCommunityNew(): IContent
{
return new HtmlContent('communities/community_edit');
}
public function getCommunityEdit(): ?IContent
{
if (!$this->checkPermission($this->request->query('communityId'), true, $community, $ownCommunityMember)) {
return null;
}
return new HtmlContent('communities/community_edit', [
'community' => $community
]);
}
public function getMembersEdit(): ?IContent
{
if (!$this->checkPermission($this->request->query('communityId'), true, $community, $ownCommunityMember)) {
return null;
}
return new HtmlContent('communities/community_members', [
'community' => $community,
'members' => $this->getMembers($community)
]);
}
private function getMembers(Community $community): array
{
$members = iterator_to_array($this->communityMemberRepository->getAllByCommunity($community, true));
usort($members, function($a, $b) {
return strnatcmp($a->getUser()->getDisplayName(), $b->getUser()->getDisplayName());
});
return $members;
}
public function newMember(): ?IContent
{
if (!$this->checkPermission($this->request->query('communityId'), true, $community, $ownCommunityMember)) {
return null;
}
$user = $this->userRepository->getById($this->request->post('user_id'));
$communityMember = new CommunityMember();
$communityMember->setCommunity($community);
$communityMember->setUser($user);
$this->pdm->saveToDb($communityMember);
return new JsonContent(['success' => true]);
}
public function editMember(): ?IContent
{
if (!$this->checkPermission($this->request->query('communityId'), true, $community, $ownCommunityMember)) {
return null;
}
$communityMember = $this->communityMemberRepository->getById($this->request->post('community_member_id'));
if ($communityMember->getUserId() === $this->request->user()->getUniqueId()) {
return new JsonContent([
'error' => ['errorText' => 'Own user cannot be edited.']
]);
}
$communityMember->setOwner($this->request->post('owner'));
$this->pdm->saveToDb($communityMember);
return new JsonContent(['success' => true]);
}
public function deleteMember(): ?IContent
{
if (!$this->checkPermission($this->request->query('communityId'), true, $community, $ownCommunityMember)) {
return null;
}
$communityMember = $this->communityMemberRepository->getById($this->request->post('community_member_id'));
if ($communityMember->getUserId() === $this->request->user()->getUniqueId()) {
return new JsonContent([
'error' => ['errorText' => 'Own user cannot be deleted.']
]);
}
$this->pdm->deleteFromDb($communityMember);
return new JsonContent(['success' => true]);
}
public function saveCommunity(): ?IContent
{
$communityId = $this->request->query('communityId');
if ($communityId){
if (!$this->checkPermission($communityId, true, $community, $ownCommunityMember)) {
return null;
}
} else {
$community = new Community();
}
$name = $this->request->post('name');
$currency = $this->request->post('currency');
if (strlen($name) === 0 || strlen($currency) === 0 || strlen($currency) > 3) {
return new JsonContent([
'error' => ['errorText' => 'Please fill all required fields!']
]);
}
$community->setName($name);
$community->setCurrency($currency);
if (!$communityId) {
$community->setCreatedDate(new DateTime());
}
$this->pdm->saveToDb($community);
if (!$communityId) {
/**
* @var User $user
*/
$user = $this->request->user();
$communityMember = new CommunityMember();
$communityMember->setCommunity($community);
$communityMember->setUser($user);
$communityMember->setOwner(true);
$this->pdm->saveToDb($communityMember);
}
return new JsonContent([
'redirect' => ['target' => '/' . \Container::$routeCollection->getRoute('community')->generateLink(['communityId' => $community->getId()])]
]);
}
private function checkPermission(
int $communityId,
bool $needToBeOwner,
?Community &$community,
?CommunityMember &$ownCommunityMember): bool
{
$community = $this->communityRepository->getById($communityId);
if ($community === null) {
return false;
}
/**
* @var User $user
*/
$user = $this->request->user();
$ownCommunityMember = $this->communityMemberRepository->getByCommunityAndUser($community, $user);
if ($ownCommunityMember === null || ($needToBeOwner && !$ownCommunityMember->getOwner())) {
return false;
}
return true;
}
}

View File

@ -1,18 +1,22 @@
<?php namespace RVR\Controller;
use RVR\PersistentData\Model\User;
use RVR\Repository\CommunityMemberRepository;
use SokoWeb\Interfaces\Authorization\ISecured;
use SokoWeb\Interfaces\Request\IRequest;
use SokoWeb\Interfaces\Response\IContent;
use SokoWeb\Response\HtmlContent;
use SokoWeb\Response\JsonContent;
class HomeController implements ISecured
{
private IRequest $request;
private CommunityMemberRepository $communityMemberRepository;
public function __construct(IRequest $request)
{
$this->request = $request;
$this->communityMemberRepository = new CommunityMemberRepository();
}
public function authorize(): bool
@ -20,8 +24,25 @@ class HomeController implements ISecured
return $this->request->user() !== null;
}
public function getIndex(): IContent
public function getHome(): IContent
{
return new HtmlContent('index');
/**
* @var User $user
*/
$user = $this->request->user();
$ownCommunityMembers = $this->communityMemberRepository->getAllByUser($user, true);
$communities = [];
foreach ($ownCommunityMembers as $ownCommunityMember) {
$communities[] = $ownCommunityMember->getCommunity();
}
usort($communities, function($a, $b) {
return strnatcmp($a->getName(), $b->getName());
});
return new HtmlContent('home', [
'communities' => $communities,
'upcomingEvents' => []
]);
}
}

View File

@ -0,0 +1,42 @@
<?php namespace RVR\Controller;
use RVR\Repository\UserRepository;
use SokoWeb\Interfaces\Authorization\ISecured;
use SokoWeb\Interfaces\Request\IRequest;
use SokoWeb\Interfaces\Response\IContent;
use SokoWeb\Response\JsonContent;
class UserSearchController implements ISecured
{
private IRequest $request;
private UserRepository $userRepository;
public function __construct(IRequest $request)
{
$this->request = $request;
$this->userRepository = new UserRepository();
}
public function authorize(): bool
{
return $this->request->user() !== null;
}
public function searchUser(): IContent
{
$users = iterator_to_array($this->userRepository->searchByName($this->request->query('q')));
usort($users, function($a, $b) {
return strnatcmp($a->getDisplayName(), $b->getDisplayName());
});
$results = [];
foreach ($users as $user) {
$results[] = ['value' => $user->getId(), 'label' => $user->getFullDisplayName()];
}
return new JsonContent([
'results' => $results
]);
}
}

View File

@ -0,0 +1,57 @@
<?php namespace RVR\PersistentData\Model;
use DateTime;
use SokoWeb\PersistentData\Model\Model;
class Community extends Model
{
protected static string $table = 'communities';
protected static array $fields = ['name', 'currency', 'created'];
private string $name = '';
private string $currency = '';
private DateTime $created;
public function setName(string $name): void
{
$this->name = $name;
}
public function setCurrency(string $currency): void
{
$this->currency = $currency;
}
public function setCreatedDate(DateTime $created): void
{
$this->created = $created;
}
public function setCreated(string $created): void
{
$this->created = new DateTime($created);
}
public function getName(): string
{
return $this->name;
}
public function getCurrency(): string
{
return $this->currency;
}
public function getCreatedDate(): DateTime
{
return $this->created;
}
public function getCreated(): string
{
return $this->created->format('Y-m-d H:i:s');
}
}

View File

@ -0,0 +1,72 @@
<?php namespace RVR\PersistentData\Model;
use SokoWeb\PersistentData\Model\Model;
class CommunityMember extends Model
{
protected static string $table = 'community_members';
protected static array $fields = ['community_id', 'user_id', 'owner'];
protected static array $relations = ['community' => Community::class, 'user' => User::class];
private ?Community $community = null;
private ?int $communityId = null;
private ?User $user = null;
private ?int $userId = null;
private bool $owner = false;
public function setCommunity(Community $community): void
{
$this->community = $community;
}
public function setCommunityId(int $communityId): void
{
$this->communityId = $communityId;
}
public function setUser(User $user): void
{
$this->user = $user;
}
public function setUserId(int $userId): void
{
$this->userId = $userId;
}
public function setOwner(bool $owner): void
{
$this->owner = $owner;
}
public function getCommunity(): ?Community
{
return $this->community;
}
public function getCommunityId(): ?int
{
return $this->communityId;
}
public function getUser(): ?User
{
return $this->user;
}
public function getUserId(): ?int
{
return $this->userId;
}
public function getOwner(): bool
{
return $this->owner;
}
}

View File

@ -171,6 +171,11 @@ class User extends Model implements IUser
return $this->nickname ?: $this->fullName;
}
public function getFullDisplayName(): string
{
return $this->nickname ? $this->nickname . ' (' . $this->fullName . ')' : $this->fullName;
}
public function checkPassword(string $password): bool
{
if ($this->password === null) {

View File

@ -0,0 +1,48 @@
<?php namespace RVR\Repository;
use Generator;
use RVR\PersistentData\Model\Community;
use RVR\PersistentData\Model\CommunityMember;
use RVR\PersistentData\Model\User;
use SokoWeb\Database\Query\Select;
use SokoWeb\PersistentData\PersistentDataManager;
class CommunityMemberRepository
{
private PersistentDataManager $pdm;
public function __construct()
{
$this->pdm = new PersistentDataManager();
}
public function getById(int $id): ?CommunityMember
{
return $this->pdm->selectFromDbById($id, CommunityMember::class);
}
public function getAllByCommunity(Community $community, bool $useRelations = false): Generator
{
$select = new Select(\Container::$dbConnection);
$select->where('community_id', '=', $community->getId());
yield from $this->pdm->selectMultipleFromDb($select, CommunityMember::class, $useRelations);
}
public function getAllByUser(User $user, bool $useRelations = false): Generator
{
$select = new Select(\Container::$dbConnection);
$select->where('user_id', '=', $user->getId());
yield from $this->pdm->selectMultipleFromDb($select, CommunityMember::class, $useRelations);
}
public function getByCommunityAndUser(Community $community, User $user) : ?CommunityMember
{
$select = new Select(\Container::$dbConnection);
$select->where('community_id', '=', $community->getId());
$select->where('user_id', '=', $user->getId());
return $this->pdm->selectFromDb($select, CommunityMember::class);
}
}

View File

@ -0,0 +1,19 @@
<?php namespace RVR\Repository;
use RVR\PersistentData\Model\Community;
use SokoWeb\PersistentData\PersistentDataManager;
class CommunityRepository
{
private PersistentDataManager $pdm;
public function __construct()
{
$this->pdm = new PersistentDataManager();
}
public function getById(int $id): ?Community
{
return $this->pdm->selectFromDbById($id, Community::class);
}
}

View File

@ -1,5 +1,6 @@
<?php namespace RVR\Repository;
use Generator;
use SokoWeb\Interfaces\Repository\IUserRepository;
use SokoWeb\Database\Query\Select;
use RVR\PersistentData\Model\User;
@ -51,4 +52,14 @@ class UserRepository implements IUserRepository
return $this->pdm->selectFromDb($select, User::class);
}
public function searchByName(string $name): Generator
{
$select = new Select(\Container::$dbConnection);
$select->where('full_name', 'LIKE', '%' . $name . '%');
$select->orWhere('nickname', 'LIKE', '%' . $name . '%');
$select->limit(10);
yield from $this->pdm->selectMultipleFromDb($select, User::class);
}
}

View File

@ -0,0 +1,50 @@
@extends(templates/layout_normal)
@section(main)
<h2><?= $community->getName() ?> <span class="small">[<a href="/<?= Container::$routeCollection->getRoute('community-edit')->generateLink(['communityId' => $community->getId()]) ?>">edit</a>]</span></h2>
<div class="gridContainer marginTop">
<div>
<h3 class="marginBottom">Members</h3>
<?php foreach ($members as $member): ?>
<p><?= $member->getUser()->getDisplayName() ?></p>
<?php endforeach; ?>
<?php if ($editPermission): ?>
<hr>
<p><a href="/<?= Container::$routeCollection->getRoute('community-members')->generateLink(['communityId' => $community->getId()]) ?>">Edit members</a></p>
<?php endif; ?>
</div>
<div>
<h3 class="marginBottom">Currencies</h3>
<p>Main currency: <b><?= $community->getCurrency() ?></b></p>
<p>Further currencies: <b><?= implode(', ', $currencyNames) ?></b></p>
</div>
<div>
<h3 class="marginBottom">Upcoming events</h3>
<?php if (count($upcomingEvents) > 0): ?>
<?php foreach ($upcomingEvents as $event): ?>
<!-- todo -->
<?php endforeach; ?>
<?php else: ?>
<p>There is no upcoming event.</p>
<?php endif; ?>
</div>
<div>
<h3 class="marginBottom">Finances</h3>
<table class="fullWidth">
<tr>
<td>You owe</td>
<td style="text-align: right; color: red;">0 <?= $community->getCurrency() ?></td>
</tr>
<tr>
<td>You're owed</td>
<td style="text-align: right; color: green;">0 <?= $community->getCurrency() ?></td>
</tr>
<tr>
<td>Your balance</td>
<td style="text-align: right;">0 <?= $community->getCurrency() ?></td>
</tr>
</table>
</div>
</div>
@endsection

View File

@ -0,0 +1,20 @@
@extends(templates/layout_normal)
@section(main)
<h2><?= isset($community) ? $community->getName() . ' - Edit' : 'New community' ?></h2>
<div class="box">
<?php
$formAction = isset($community) ?
Container::$routeCollection->getRoute('community-edit-action')->generateLink(['communityId' => $community->getId()]) :
Container::$routeCollection->getRoute('community-new-action')->generateLink();
?>
<form id="communityForm" action="/<?= $formAction ?>" method="post" data-redirect-on-success="true">
<input type="text" class="text big fullWidth" name="name" placeholder="Name" value="<?= isset($community) ? $community->getName() : '' ?>" required>
<input type="text" class="text big fullWidth marginTop" name="currency" value="<?= isset($community) ? $community->getCurrency() : '' ?>" placeholder="Default currency" maxlength="3" required>
<p id="accountFormError" class="formError justify marginTop"></p>
<div class="right marginTop">
<button type="submit" name="submit"><?= isset($community) ? 'Save' : 'Create' ?></button>
</div>
</form>
</div>
@endsection

View File

@ -0,0 +1,42 @@
@css(node_modules/choices.js/public/assets/styles/choices.min.css)
@js(node_modules/choices.js/public/assets/scripts/choices.js)
@js(js/communities/community_members.js)
@extends(templates/layout_normal)
@section(main)
<h2><?= $community->getName() ?> - Edit members</h2>
<div class="box">
<table class="fullWidth">
<thead>
<tr>
<th style="width: 50%;"></th>
<th style="width: 25%; text-align: center;">Owner</th>
<th style="width: 25%;"></th>
</tr>
</thead>
<?php foreach ($members as $member): ?>
<?php $editable = $member->getUserId() !== Container::$request->user()->getUniqueId(); ?>
<tr>
<td><?= $member->getUser()->getDisplayName() ?></td>
<td style="text-align: center;"><input type="checkbox" class="member_owner" data-id="<?= $member->getId() ?>" <?= $member->getOwner() ? 'checked' : '' ?> <?= !$editable ? 'disabled' : '' ?> /></td>
<td style="text-align: right;"><button type="button" class="small red delete_member" data-id="<?= $member->getId() ?>" <?= !$editable ? 'disabled' : '' ?>>Delete</button></td>
</tr>
<?php endforeach; ?>
<tr>
<td><select type="text" id="new_member_user_id"></td>
<td></td>
<td style="text-align: right;"><button type="button" class="small" id="new_member_button" disabled>Add</button></td>
</tr>
</table>
</div>
@endsection
@section(pageScript)
<script>
var searchUserUrl = '/<?= Container::$routeCollection->getRoute('searchUser')->generateLink(['q' => 'QUERY']) ?>';
var newMemberUrl = '/<?= Container::$routeCollection->getRoute('community-members-new')->generateLink(['communityId' => $community->getId()]) ?>';
var editMemberUrl = '/<?= Container::$routeCollection->getRoute('community-members-edit')->generateLink(['communityId' => $community->getId()]) ?>';
var deleteMemberUrl = '/<?= Container::$routeCollection->getRoute('community-members-delete')->generateLink(['communityId' => $community->getId()]) ?>';
</script>
@endsection

28
views/home.php Normal file
View File

@ -0,0 +1,28 @@
@extends(templates/layout_normal)
@section(main)
<div class="gridContainer">
<div>
<h3 class="marginBottom">Communities</h3>
<?php if (count($communities) > 0): ?>
<?php foreach ($communities as $community): ?>
<p><a href="/<?= Container::$routeCollection->getRoute('community')->generateLink(['communityId' => $community->getId()]) ?>"><?= $community->getName() ?></a></p>
<?php endforeach; ?>
<?php else: ?>
<p>You have no community.</p>
<?php endif; ?>
<hr>
<p><a href="/<?= Container::$routeCollection->getRoute('community-new')->generateLink() ?>">New community</a></p>
</div>
<div>
<h3 class="marginBottom">Upcoming events</h3>
<?php if (count($upcomingEvents) > 0): ?>
<?php foreach ($upcomingEvents as $event): ?>
<!-- todo -->
<?php endforeach; ?>
<?php else: ?>
<p>There is no upcoming event.</p>
<?php endif; ?>
</div>
</div>
@endsection

View File

@ -1,8 +0,0 @@
@extends(templates/layout_normal)
@section(main)
<h2><?= $_ENV['APP_NAME'] ?></h2>
<div class="box">
<p>This is the new <?= $_ENV['APP_NAME'] ?>.</p>
</div>
@endsection

16
web.php
View File

@ -12,7 +12,7 @@ if (!empty($_ENV['DEV'])) {
Container::$routeCollection = new SokoWeb\Routing\RouteCollection();
Container::$routeCollection->get('index', '', [RVR\Controller\HomeController::class, 'getIndex']);
Container::$routeCollection->get('home', '', [RVR\Controller\HomeController::class, 'getHome']);
Container::$routeCollection->get('startSession', 'startSession.json', [RVR\Controller\HomeController::class, 'startSession']);
Container::$routeCollection->group('login', function (SokoWeb\Routing\RouteCollection $routeCollection) {
$routeCollection->get('login', '', [RVR\Controller\LoginController::class, 'getLoginForm']);
@ -41,6 +41,20 @@ Container::$routeCollection->group('account', function (SokoWeb\Routing\RouteCol
$routeCollection->get('account.googleAuthenticate', 'googleAuthenticate', [RVR\Controller\UserController::class, 'getGoogleAuthenticateRedirect']);
$routeCollection->get('account.googleAuthenticate-action', 'googleAuthenticate/code', [RVR\Controller\UserController::class, 'authenticateWithGoogle']);
});
Container::$routeCollection->get('searchUser', 'searchUser', [RVR\Controller\UserSearchController::class, 'searchUser']);
Container::$routeCollection->group('communities', function (SokoWeb\Routing\RouteCollection $routeCollection) {
$routeCollection->get('community-new', 'new', [RVR\Controller\CommunityController::class, 'getCommunityNew']);
$routeCollection->post('community-new-action', 'new', [RVR\Controller\CommunityController::class, 'saveCommunity']);
$routeCollection->group('{communityId}', function (SokoWeb\Routing\RouteCollection $routeCollection) {
$routeCollection->get('community', '', [RVR\Controller\CommunityController::class, 'getCommunityHome']);
$routeCollection->get('community-edit', 'edit', [RVR\Controller\CommunityController::class, 'getCommunityEdit']);
$routeCollection->post('community-edit-action', 'edit', [RVR\Controller\CommunityController::class, 'saveCommunity']);
$routeCollection->get('community-members', 'members', [RVR\Controller\CommunityController::class, 'getMembersEdit']);
$routeCollection->post('community-members-new', 'newMember', [RVR\Controller\CommunityController::class, 'newMember']);
$routeCollection->post('community-members-edit', 'editMember', [RVR\Controller\CommunityController::class, 'editMember']);
$routeCollection->post('community-members-delete', 'deleteMember', [RVR\Controller\CommunityController::class, 'deleteMember']);
});
});
Container::$sessionHandler = new SokoWeb\Session\DatabaseSessionHandler();