Add gifts

Signed-off-by: Celestora <kitsuruko@gmail.com>
This commit is contained in:
Celestora 2021-10-07 11:48:55 +03:00
parent 255d70e974
commit 9336a91623
26 changed files with 1367 additions and 80 deletions

View file

@ -0,0 +1,164 @@
<?php declare(strict_types=1);
namespace openvk\Web\Models\Entities;
use openvk\Web\Util\DateTime;
use openvk\Web\Models\RowModel;
use openvk\Web\Models\Entities\User;
use Nette\Utils\{Image, ImageException};
class Gift extends RowModel
{
const IMAGE_MAXSIZE = 131072;
const IMAGE_BINARY = 0;
const IMAGE_BASE64 = 1;
const IMAGE_URL = 2;
const PERIOD_IGNORE = 0;
const PERIOD_SET = 1;
const PERIOD_SET_IF_NONE = 2;
protected $tableName = "gifts";
function getName(): string
{
return $this->getRecord()->internal_name;
}
function getPrice(): int
{
return $this->getRecord()->price;
}
function getUsages(): int
{
return $this->getRecord()->usages;
}
function getUsagesBy(User $user, ?int $since = NULL): int
{
$sent = $this->getRecord()
->related("gift_user_relations.gift")
->where("sender", $user->getId())
->where("sent >= ?", $since ?? $this->getRecord()->limit_period ?? 0);
return sizeof($sent);
}
function getUsagesLeft(User $user): float
{
if($this->getLimit() === INF)
return INF;
return max(0, $this->getLimit() - $this->getUsagesBy($user));
}
function getImage(int $type = 0): /* ?binary */ string
{
switch($type) {
default:
case static::IMAGE_BINARY:
return $this->getRecord()->image ?? "";
break;
case static::IMAGE_BASE64:
return "data:image/png;base64," . base64_encode($this->getRecord()->image ?? "");
break;
case static::IMAGE_URL:
return "/gift" . $this->getId() . "_" . $this->getUpdateDate()->timestamp() . ".png";
break;
}
}
function getLimit(): float
{
$limit = $this->getRecord()->limit;
return !$limit ? INF : (float) $limit;
}
function getLimitResetTime(): ?DateTime
{
return is_null($t = $this->getRecord()->limit_period) ? NULL : new DateTime($t);
}
function getUpdateDate(): DateTime
{
return new DateTime($this->getRecord()->updated);
}
function canUse(User $user): bool
{
return $this->getUsagesLeft($user) > 0;
}
function isFree(): bool
{
return $this->getPrice() === 0;
}
function used(): void
{
$this->stateChanges("usages", $this->getUsages() + 1);
$this->save();
}
function setName(string $name): void
{
$this->stateChanges("internal_name", $name);
}
function setImage(string $file): bool
{
$imgBlob;
try {
$image = Image::fromFile($file);
$image->resize(512, 512, Image::SHRINK_ONLY);
$imgBlob = $image->toString(Image::PNG);
} catch(ImageException $ex) {
return false;
}
if(strlen($imgBlob) > (2**24 - 1)) {
return false;
} else {
$this->stateChanges("updated", time());
$this->stateChanges("image", $imgBlob);
}
return true;
}
function setLimit(?float $limit = NULL, int $periodBehaviour = 0): void
{
$limit ??= $this->getLimit();
$limit = $limit === INF ? NULL : (int) $limit;
$this->stateChanges("limit", $limit);
if(!$limit) {
$this->stateChanges("limit_period", NULL);
return;
}
switch($periodBehaviour) {
default:
case static::PERIOD_IGNORE:
break;
case static::PERIOD_SET:
$this->stateChanges("limit_period", time());
break;
case static::PERIOD_SET_IF_NONE:
if(is_null($this->getRecord()) || is_null($this->getRecord()->limit_period))
$this->stateChanges("limit_period", time());
break;
}
}
function delete(bool $softly = true): void
{
$this->getRecord()->related("gift_relations.gift")->delete();
parent::delete($softly);
}
}

View file

@ -0,0 +1,155 @@
<?php declare(strict_types=1);
namespace openvk\Web\Models\Entities;
use Chandler\Database\DatabaseConnection as DB;
use openvk\Web\Models\Repositories\Gifts;
use openvk\Web\Models\RowModel;
class GiftCategory extends RowModel
{
protected $tableName = "gift_categories";
private function getLocalization(string $language): object
{
return $this->getRecord()
->related("gift_categories_locales.category")
->where("language", $language);
}
private function createLocalizationIfNotExists(string $language): void
{
if(!is_null($this->getLocalization($language)->fetch()))
return;
DB::i()->getContext()->table("gift_categories_locales")->insert([
"category" => $this->getId(),
"language" => $language,
"name" => "Sample Text",
"description" => "Sample Text",
]);
}
function getSlug(): string
{
return \Transliterator::createFromRules(
":: Any-Latin;"
. ":: NFD;"
. ":: [:Nonspacing Mark:] Remove;"
. ":: NFC;"
. ":: [:Punctuation:] Remove;"
. ":: Lower();"
. "[:Separator:] > '-'"
)->transliterate($this->getName());
}
function getThumbnailURL(): string
{
$primeGift = iterator_to_array($this->getGifts(1, 1))[0];
$serverUrl = ovk_scheme(true) . $_SERVER["SERVER_NAME"];
if(!$primeGift)
return "$serverUrl/assets/packages/static/openvk/img/camera_200.png";
return $primeGift->getImage(Gift::IMAGE_URL);
}
function getName(string $language = "_", bool $returnNull = false): ?string
{
$loc = $this->getLocalization($language)->fetch();
if(!$loc) {
if($returnNull)
return NULL;
return $language === "_" ? "Unlocalized" : $this->getName();
}
return $loc->name;
}
function getDescription(string $language = "_", bool $returnNull = false): ?string
{
$loc = $this->getLocalization($language)->fetch();
if(!$loc) {
if($returnNull)
return NULL;
return $language === "_" ? "Unlocalized" : $this->getDescription();
}
return $loc->description;
}
function getGifts(int $page = -1, ?int $perPage = NULL, &$count = nullptr): \Traversable
{
$gifts = $this->getRecord()->related("gift_relations.category");
if($page !== -1) {
$count = $gifts->count();
$gifts = $gifts->page($page, $perPage ?? OPENVK_DEFAULT_PER_PAGE);
}
foreach($gifts as $rel)
yield (new Gifts)->get($rel->gift);
}
function isMagical(): bool
{
return !is_null($this->getRecord()->autoquery);
}
function hasGift(Gift $gift): bool
{
$rels = $this->getRecord()->related("gift_relations.category");
return $rels->where("gift", $gift->getId())->count() > 0;
}
function addGift(Gift $gift): void
{
if($this->hasGift($gift))
return;
DB::i()->getContext()->table("gift_relations")->insert([
"category" => $this->getId(),
"gift" => $gift->getId(),
]);
}
function removeGift(Gift $gift): void
{
if(!$this->hasGift($gift))
return;
DB::i()->getContext()->table("gift_relations")->where([
"category" => $this->getId(),
"gift" => $gift->getId(),
])->delete();
}
function setName(string $language, string $name): void
{
$this->createLocalizationIfNotExists($language);
$this->getLocalization($language)->update([
"name" => $name,
]);
}
function setDescription(string $language, string $description): void
{
$this->createLocalizationIfNotExists($language);
$this->getLocalization($language)->update([
"description" => $description,
]);
}
function setAutoQuery(?array $query = NULL): void
{
if(is_null($query)) {
$this->stateChanges("autoquery", NULL);
return;
}
$allowedColumns = ["price", "usages"];
if(array_diff_key($query, array_flip($allowedColumns)))
throw new \LogicException("Invalid query");
$this->stateChanges("autoquery", serialize($query));
}
}

View file

@ -0,0 +1,13 @@
<?php declare(strict_types=1);
namespace openvk\Web\Models\Entities\Notifications;
use openvk\Web\Models\Entities\{User, Gift};
final class GiftNotification extends Notification
{
protected $actionCode = 9601;
function __construct(User $receiver, User $sender, Gift $gift, ?string $comment)
{
parent::__construct($receiver, $gift, $sender, time(), $comment ?? "");
}
}

View file

@ -3,8 +3,8 @@ namespace openvk\Web\Models\Entities;
use openvk\Web\Themes\{Themepack, Themepacks};
use openvk\Web\Util\DateTime;
use openvk\Web\Models\RowModel;
use openvk\Web\Models\Entities\{Photo, Message, Correspondence};
use openvk\Web\Models\Repositories\{Users, Clubs, Albums, Notifications};
use openvk\Web\Models\Entities\{Photo, Message, Correspondence, Gift};
use openvk\Web\Models\Repositories\{Users, Clubs, Albums, Gifts, Notifications};
use Nette\Database\Table\ActiveRow;
use Chandler\Database\DatabaseConnection;
use Chandler\Security\User as ChandlerUser;
@ -474,6 +474,25 @@ class User extends RowModel
return sizeof($this->getRecord()->related("event_turnouts.user"));
}
function getGifts(int $page = 1, ?int $perPage = NULL): \Traversable
{
$gifts = $this->getRecord()->related("gift_user_relations.receiver")->order("sent DESC")->page($page, $perPage ?? OPENVK_DEFAULT_PER_PAGE);
foreach($gifts as $rel) {
yield (object) [
"sender" => (new Users)->get($rel->sender),
"gift" => (new Gifts)->get($rel->gift),
"caption" => $rel->comment,
"anon" => $rel->anonymous,
"sent" => new DateTime($rel->sent),
];
}
}
function getGiftCount(): int
{
return sizeof($this->getRecord()->related("gift_user_relations.receiver"));
}
function getSubscriptionStatus(User $user): int
{
$subbed = !is_null($this->getRecord()->related("subscriptions.follower")->where([
@ -544,6 +563,18 @@ class User extends RowModel
return !is_null($this->getPendingPhoneVerification());
}
function gift(User $sender, Gift $gift, ?string $comment = NULL, bool $anonymous = false): void
{
DatabaseConnection::i()->getContext()->table("gift_user_relations")->insert([
"sender" => $sender->getId(),
"receiver" => $this->getId(),
"gift" => $gift->getId(),
"comment" => $comment,
"anonymous" => $anonymous,
"sent" => time(),
]);
}
function ban(string $reason): void
{
$subs = DatabaseConnection::i()->getContext()->table("subscriptions");

View file

@ -0,0 +1,45 @@
<?php declare(strict_types=1);
namespace openvk\Web\Models\Repositories;
use Chandler\Database\DatabaseConnection;
use openvk\Web\Models\Entities\{Gift, GiftCategory};
class Gifts
{
private $context;
private $gifts;
private $cats;
function __construct()
{
$this->context = DatabaseConnection::i()->getContext();
$this->gifts = $this->context->table("gifts");
$this->cats = $this->context->table("gift_categories");
}
function get(int $id): ?Gift
{
$gift = $this->gifts->get($id);
if(!$gift)
return NULL;
return new Gift($gift);
}
function getCat(int $id): ?GiftCategory
{
$cat = $this->cats->get($id);
if(!$cat)
return NULL;
return new GiftCategory($cat);
}
function getCategories(int $page, ?int $perPage = NULL, &$count = nullptr): \Traversable
{
$cats = $this->cats->where("deleted", false);
$count = $cats->count();
$cats = $cats->page($page, $perPage ?? OPENVK_DEFAULT_PER_PAGE);
foreach($cats as $cat)
yield new GiftCategory($cat);
}
}

View file

@ -1,19 +1,21 @@
<?php declare(strict_types=1);
namespace openvk\Web\Presenters;
use openvk\Web\Models\Entities\{Voucher, User};
use openvk\Web\Models\Repositories\{Users, Clubs, Vouchers};
use openvk\Web\Models\Entities\{Voucher, Gift, GiftCategory, User};
use openvk\Web\Models\Repositories\{Users, Clubs, Vouchers, Gifts};
final class AdminPresenter extends OpenVKPresenter
{
private $users;
private $clubs;
private $vouchers;
private $gifts;
function __construct(Users $users, Clubs $clubs, Vouchers $vouchers)
function __construct(Users $users, Clubs $clubs, Vouchers $vouchers, Gifts $gifts)
{
$this->users = $users;
$this->clubs = $clubs;
$this->vouchers = $vouchers;
$this->gifts = $gifts;
parent::__construct();
}
@ -159,6 +161,156 @@ final class AdminPresenter extends OpenVKPresenter
exit;
}
function renderGiftCategories(): void
{
$this->template->act = $this->queryParam("act") ?? "list";
$this->template->categories = iterator_to_array($this->gifts->getCategories((int) ($this->queryParam("p") ?? 1), NULL, $this->template->count));
}
function renderGiftCategory(string $slug, int $id): void
{
$cat;
$gen = false;
if($id !== 0) {
$cat = $this->gifts->getCat($id);
if(!$cat)
$this->notFound();
else if($cat->getSlug() !== $slug)
$this->redirect("/admin/gifts/" . $cat->getSlug() . "." . $id . ".meta", static::REDIRECT_TEMPORARY);
} else {
$gen = true;
$cat = new GiftCategory;
}
$this->template->form = (object) [];
$this->template->form->id = $id;
$this->template->form->languages = [];
foreach(getLanguages() as $language) {
$language = (object) $language;
$this->template->form->languages[$language->code] = (object) [];
$this->template->form->languages[$language->code]->name = $gen ? "" : ($cat->getName($language->code, true) ?? "");
$this->template->form->languages[$language->code]->description = $gen ? "" : ($cat->getDescription($language->code, true) ?? "");
}
$this->template->form->languages["master"] = (object) [
"name" => $gen ? "Unknown Name" : $cat->getName(),
"description" => $gen ? "" : $cat->getDescription(),
];
if($_SERVER["REQUEST_METHOD"] !== "POST")
return;
if($gen) {
$cat->setAutoQuery(NULL);
$cat->save();
}
$cat->setName("_", $this->postParam("name_master"));
$cat->setDescription("_", $this->postParam("description_master"));
foreach(getLanguages() as $language) {
$code = $language["code"];
if(!empty($this->postParam("name_$code") ?? NULL))
$cat->setName($code, $this->postParam("name_$code"));
if(!empty($this->postParam("description_$code") ?? NULL))
$cat->setDescription($code, $this->postParam("description_$code"));
}
$this->redirect("/admin/gifts/" . $cat->getSlug() . "." . $cat->getId() . ".meta", static::REDIRECT_TEMPORARY);
}
function renderGifts(string $catSlug, int $catId): void
{
$cat = $this->gifts->getCat($catId);
if(!$cat)
$this->notFound();
else if($cat->getSlug() !== $catSlug)
$this->redirect("/admin/gifts/" . $cat->getSlug() . "." . $catId . "/", static::REDIRECT_TEMPORARY);
$this->template->cat = $cat;
$this->template->gifts = iterator_to_array($cat->getGifts((int) ($this->queryParam("p") ?? 1), NULL, $this->template->count));
}
function renderGift(int $id): void
{
$gift = $this->gifts->get($id);
$act = $this->queryParam("act") ?? "edit";
switch($act) {
case "delete":
$this->assertNoCSRF();
if(!$gift)
$this->notFound();
$gift->delete();
$this->flashFail("succ", "Gift moved successfully", "This gift will now be in <b>Recycle Bin</b>.");
break;
case "copy":
case "move":
$this->assertNoCSRF();
if(!$gift)
$this->notFound();
$catFrom = $this->gifts->getCat((int) ($this->queryParam("from") ?? 0));
$catTo = $this->gifts->getCat((int) ($this->queryParam("to") ?? 0));
if(!$catFrom || !$catTo || !$catFrom->hasGift($gift))
$this->badRequest();
if($act === "move")
$catFrom->removeGift($gift);
$catTo->addGift($gift);
$name = $catTo->getName();
$this->flash("succ", "Gift moved successfully", "This gift will now be in <b>$name</b>.");
$this->redirect("/admin/gifts/" . $catTo->getSlug() . "." . $catTo->getId() . "/", static::REDIRECT_TEMPORARY);
break;
default:
case "edit":
$gen = false;
if(!$gift) {
$gen = true;
$gift = new Gift;
}
$this->template->form = (object) [];
$this->template->form->id = $id;
$this->template->form->name = $gen ? "New Gift (1)" : $gift->getName();
$this->template->form->price = $gen ? 0 : $gift->getPrice();
$this->template->form->usages = $gen ? 0 : $gift->getUsages();
$this->template->form->limit = $gen ? -1 : ($gift->getLimit() === INF ? -1 : $gift->getLimit());
$this->template->form->pic = $gen ? NULL : $gift->getImage(Gift::IMAGE_URL);
if($_SERVER["REQUEST_METHOD"] !== "POST")
return;
$limit = $this->postParam("limit") ?? $this->template->form->limit;
$limit = $limit == "-1" ? INF : (float) $limit;
$gift->setLimit($limit, is_null($this->postParam("reset_limit")) ? Gift::PERIOD_SET_IF_NONE : Gift::PERIOD_SET);
$gift->setName($this->postParam("name"));
$gift->setPrice((int) $this->postParam("price"));
$gift->setUsages((int) $this->postParam("usages"));
if(isset($_FILES["pic"]) && $_FILES["pic"]["error"] === UPLOAD_ERR_OK) {
if(!$gift->setImage($_FILES["pic"]["tmp_name"]))
$this->flashFail("err", "Не удалось сохранить подарок", "Изображение подарка кривое.");
} else if($gen) {
# If there's no gift pic but it's newly created
$this->flashFail("err", "Не удалось сохранить подарок", "Пожалуйста, загрузите изображение подарка.");
}
$gift->save();
if($gen && !is_null($cat = $this->postParam("_cat"))) {
$cat = $this->gifts->getCat((int) $cat);
if(!is_null($cat))
$cat->addGift($gift);
}
$this->redirect("/admin/gifts/id" . $gift->getId(), static::REDIRECT_TEMPORARY);
}
}
function renderFiles(): void
{

View file

@ -0,0 +1,131 @@
<?php declare(strict_types=1);
namespace openvk\Web\Presenters;
use openvk\Web\Models\Repositories\{Gifts, Users};
use openvk\Web\Models\Entities\Notifications\GiftNotification;
final class GiftsPresenter extends OpenVKPresenter
{
private $gifts;
private $users;
function __construct(Gifts $gifts, Users $users)
{
$this->gifts = $gifts;
$this->users = $users;
}
function renderUserGifts(int $user): void
{
$this->assertUserLoggedIn();
$user = $this->users->get($user);
if(!$user)
$this->notFound();
$this->template->user = $user;
$this->template->page = $page = (int) ($this->queryParam("p") ?? 1);
$this->template->count = $user->getGiftCount();
$this->template->iterator = $user->getGifts($page);
$this->template->hideInfo = $this->user->id !== $user->getId();
}
function renderGiftMenu(): void
{
$user = $this->users->get((int) ($this->queryParam("user") ?? 0));
if(!$user)
$this->notFound();
$this->template->page = $page = (int) ($this->queryParam("p") ?? 1);
$cats = $this->gifts->getCategories($page, NULL, $this->template->count);
$this->template->user = $user;
$this->template->iterator = $cats;
$this->template->_template = "Gifts/Menu.xml";
}
function renderGiftList(): void
{
$user = $this->users->get((int) ($this->queryParam("user") ?? 0));
$cat = $this->gifts->getCat((int) ($this->queryParam("pack") ?? 0));
if(!$user || !$cat)
$this->flashFail("err", "Не удалось подарить", "Пользователь или набор не существуют.");
$this->template->page = $page = (int) ($this->queryParam("p") ?? 1);
$gifts = $cat->getGifts($page, null, $this->template->count);
$this->template->user = $user;
$this->template->cat = $cat;
$this->template->gifts = iterator_to_array($gifts);
$this->template->_template = "Gifts/Pick.xml";
}
function renderConfirmGift(): void
{
$user = $this->users->get((int) ($this->queryParam("user") ?? 0));
$gift = $this->gifts->get((int) ($this->queryParam("elid") ?? 0));
$cat = $this->gifts->getCat((int) ($this->queryParam("pack") ?? 0));
if(!$user || !$cat || !$gift || !$cat->hasGift($gift))
$this->flashFail("err", "Не удалось подарить", "Не удалось подтвердить права на подарок.");
if(!$gift->canUse($this->user->identity))
$this->flashFail("err", "Не удалось подарить", "У вас больше не осталось таких подарков.");
$coinsLeft = $this->user->identity->getCoins() - $gift->getPrice();
if($coinsLeft < 0)
$this->flashFail("err", "Не удалось подарить", "Ору нищ не пук.");
$this->template->_template = "Gifts/Confirm.xml";
if($_SERVER["REQUEST_METHOD"] !== "POST") {
$this->template->user = $user;
$this->template->cat = $cat;
$this->template->gift = $gift;
return;
}
$comment = empty($c = $this->postParam("comment")) ? NULL : $c;
$notification = new GiftNotification($user, $this->user->identity, $gift, $comment);
$notification->emit();
$this->user->identity->setCoins($coinsLeft);
$user->gift($this->user->identity, $gift, $comment, !is_null($this->postParam("anonymous")));
$gift->used();
$this->flash("succ", "Подарок отправлен", "Вы отправили подарок <b>" . $user->getFirstName() . "</b> за " . $gift->getPrice() . " голосов.");
$this->redirect($user->getURL(), static::REDIRECT_TEMPORARY);
}
function renderStub(): void
{
$this->assertUserLoggedIn();
$act = $this->queryParam("act");
switch($act) {
case "pick":
$this->renderGiftMenu();
break;
case "menu":
$this->renderGiftList();
break;
case "confirm":
$this->renderConfirmGift();
break;
default:
$this->notFound();
}
}
function renderGiftImage(int $id, int $timestamp): void
{
$gift = $this->gifts->get($id);
if(!$gift)
$this->notFound();
$image = $gift->getImage();
header("Cache-Control: no-transform, immutable");
header("Content-Length: " . strlen($image));
header("Content-Type: image/png");
exit($image);
}
}

View file

@ -1,4 +1,7 @@
<div class="ovk-lw-container">
{extends "@layout.xml"}
{block wrap}
<div class="ovk-lw-container">
<div class="ovk-lw--list">
{var data = is_array($iterator) ? $iterator : iterator_to_array($iterator)}
@ -54,4 +57,5 @@
<a href="?C=M;O=A" class="profile_link">{_"sort_down"}</a>
</div>
</div>
</div>
</div>
{/block}

View file

@ -268,6 +268,10 @@
<script n:if="OPENVK_ROOT_CONF['openvk']['telemetry']['plausible']['enable']"
async defer data-domain="{php echo OPENVK_ROOT_CONF['openvk']['telemetry']['plausible']['domain']}"
src="{php echo OPENVK_ROOT_CONF['openvk']['telemetry']['plausible']['server']}js/plausible.js"></script>
{ifset bodyScripts}
{include bodyScripts}
{/ifset}
</body>
</html>

View file

@ -79,6 +79,11 @@
{_vouchers}
</a>
</li>
<li>
<a href="/admin/gifts">
Подарки
</a>
</li>
</ul>
<div class="aui-nav-heading">
<strong>Настройки</strong>
@ -125,10 +130,24 @@
</nav>
</div>
<section class="aui-page-panel-content">
{ifset $flashMessage}
{var type = ["err" => "error", "warn" => "warning", "info" => "basic", "succ" => "success"][$flashMessage->type]}
<div class="aui-message aui-message-{$type}" style="margin-bottom: 15px;">
<p class="title">
<strong>{$flashMessage->title}</strong>
</p>
<p>{$flashMessage->msg|noescape}</p>
</div>
{/ifset}
<header class="aui-page-header">
<div class="aui-page-header-inner">
<div class="aui-page-header-main">
{ifset headingWrap}
{include headingWrap}
{else}
<h1>{include heading}</h1>
{/ifset}
</div>
</div>
</header>
@ -150,5 +169,9 @@
{script "js/node_modules/jquery/dist/jquery.min.js"}
{script "js/node_modules/@atlassian/aui/dist/aui/aui-prototyping.js"}
<script>AJS.tabs.setup();</script>
{ifset scripts}
{include scripts}
{/ifset}
</body>
</html>

View file

@ -0,0 +1,106 @@
{extends "@layout.xml"}
{block title}
{if $form->id === 0}
Новый подарок
{else}
Подарок "{$form->name}"
{/if}
{/block}
{block heading}
{include title}
{/block}
{block content}
<form class="aui" method="POST" enctype="multipart/form-data">
<div class="field-group">
<label for="avatar">
Изображение
<span n:if="$form->id === 0" class="aui-icon icon-required"></span>
</label>
{if $form->id === 0}
<input type="file" name="pic" accept="image/jpeg,image/png,image/gif,image/webp" required="required" />
{else}
<span id="avatar" class="aui-avatar aui-avatar-project aui-avatar-xlarge">
<span class="aui-avatar-inner">
<img id="pic" src="{$form->pic}" style="object-fit: cover;"></img>
</span>
</span>
<input style="display: none;" id="picInput" type="file" name="pic" accept="image/jpeg,image/png,image/gif,image/webp" />
<div class="description">
<a id="picChange" href="javascript:false">Заменить изображение?</a>
</div>
{/if}
</div>
<div class="field-group">
<label for="id">
ID
</label>
<input class="text long-field" type="number" id="id" disabled="disabled" value="{$form->id}" />
</div>
<div class="field-group">
<label for="putin">
Использований
</label>
<input class="text long-field" type="number" id="putin" disabled="disabled" value="{$form->usages}" />
<div n:if="$form->usages > 0" class="description">
<a href="javascript:$('#putin').value(0);">Обнулить?</a>
</div>
</div>
<div class="field-group">
<label for="name">
Внутренее имя
<span class="aui-icon icon-required"></span>
</label>
<input class="text long-field" type="text" id="name" name="name" value="{$form->name}" />
</div>
<div class="field-group">
<label for="price">
Цена
<span class="aui-icon icon-required"></span>
</label>
<input class="text long-field" type="number" id="price" name="price" min="0" value="{$form->price}" />
</div>
<div class="field-group">
<label for="limit">
Ограничение
<span class="aui-icon icon-required"></span>
</label>
<input class="text long-field" type="number" min="-1" id="limit" name="limit" value="{$form->limit}" />
</div>
<fieldset class="group">
<legend></legend>
<div class="checkbox" resolved="">
<input n:attr="disabled => $form->id === 0, checked => $form->id === 0" class="checkbox" type="checkbox" name="reset_limit" id="reset_limit" />
<span class="aui-form-glyph"></span>
<label for="reset_limit">Сбросить счётчик ограничений</label>
</div>
</fieldset>
<input n:if="$form->id === 0" type="hidden" name="_cat" value="{$_GET['cat'] ?? 1}" />
<div class="buttons-container">
<div class="buttons">
<input type="hidden" name="hash" value="{$csrfToken}" />
<input class="aui-button aui-button-primary submit" type="submit" value="{_save}">
</div>
</div>
</form>
{/block}
{block scripts}
<script>
const TRANS_GIF = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7";
$("#picChange").click(_ => $("#picInput").click());
$("#picInput").bind("change", e => {
if(typeof e.target.files[0] === "undefined")
$("#pic").prop("src", URL.createObjectURL(TRANS_GIF));
$("#pic").prop("src", URL.createObjectURL(e.target.files[0]));
});
</script>
{/block}

View file

@ -0,0 +1,56 @@
{extends "@layout.xml"}
{block title}
Наборы подарков
{/block}
{block headingWrap}
<a style="float: right;" class="aui-button aui-button-primary" href="/admin/gifts/new.0.meta">
{_create}
</a>
<h1>Наборы подарков</h1>
{/block}
{block content}
<div id="spacer" style="height: 8px;"></div>
{if sizeof($categories) > 0}
<table class="aui aui-table-list">
<tbody>
<tr n:foreach="$categories as $cat">
<td style="vertical-align: middle;">
<span class="aui-icon aui-icon-small aui-iconfont-folder-filled">{$cat->getName()}</span>
{$cat->getName()}
</td>
<td style="vertical-align: middle;">
{ovk_proc_strtr($cat->getDescription(), 128)}
</td>
<td style="vertical-align: middle; text-align: right;">
<a class="aui-button aui-button-primary" href="/admin/gifts/{$cat->getSlug()}.{$cat->getId()}.meta">
<span class="aui-icon aui-icon-small aui-iconfont-new-edit">Редактировать</span>
</a>
<a class="aui-button" href="/admin/gifts/{$cat->getSlug()}.{$cat->getId()}/">
<span class="aui-icon aui-icon-small aui-iconfont-gallery">Открыть</span>
Открыть
</a>
</td>
</tr>
</tbody>
</table>
{else}
<center>
<p>Наборов подарков нету. Чтобы создать подарок, создайте набор.</p>
</center>
{/if}
<div align="right">
{var isLast = ((20 * (($_GET['p'] ?? 1) - 1)) + sizeof($categories)) < $count}
<a n:if="($_GET['p'] ?? 1) > 1" class="aui-button" href="?act={$act}&p={($_GET['p'] ?? 1) - 1}">
⭁ туда
</a>
<a n:if="$isLast" class="aui-button" href="?act={$act}&p={($_GET['p'] ?? 1) + 1}">
⭇ сюда
</a>
</div>
{/block}

View file

@ -0,0 +1,72 @@
{extends "@layout.xml"}
{block title}
{if $form->id === 0}
Создать набор подарков
{else}
{$form->languages["master"]->name}
{/if}
{/block}
{block heading}
{include title}
{/block}
{block content}
<form class="aui" method="POST">
<h3>Общие настройки</h3>
<fieldset>
<div class="field-group">
<label for="id">
ID
</label>
<input class="text long-field" type="number" id="id" name="id" disabled value="{$form->id}" />
</div>
<div class="field-group">
<label for="name_master">
Наименование
<span class="aui-icon icon-required"></span>
</label>
<input class="text long-field" type="text" id="name_master" name="name_master" value="{$form->languages['master']->name}" />
<div class="description">Внутреннее название набора, которое будет использоваться, если не удаётся найти название на языке пользователя.</div>
</div>
<div class="field-group">
<label for="description_master">
Описание
<span class="aui-icon icon-required"></span>
</label>
<input class="text long-field" type="text" id="description_master" name="description_master" value="{$form->languages['master']->description}" />
<div class="description">Внутреннее описание набора, которое будет использоваться, если не удаётся найти название на языке пользователя.</div>
</div>
</fieldset>
<h3>Языко-зависимые настройки</h3>
<fieldset>
{foreach $form->languages as $locale => $data}
{continueIf $locale === "master"}
<div class="field-group">
<label for="name_{$locale}">
Наименование
<img src="/assets/packages/static/openvk/img/flags/{$locale}.gif" alt="{$locale}" />
</label>
<input class="text long-field" type="text" id="name_{$locale}" name="name_{$locale}" value="{$data->name}" />
</div>
<div class="field-group">
<label for="description_{$locale}">
Описание
<img src="/assets/packages/static/openvk/img/flags/{$locale}.gif" alt="{$locale}" />
</label>
<input class="text long-field" type="text" id="description_{$locale}" name="description_{$locale}" value="{$data->description}" />
</div>
{/foreach}
</fieldset>
<div class="buttons-container">
<div class="buttons">
<input type="hidden" name="hash" value="{$csrfToken}" />
<input class="aui-button aui-button-primary submit" type="submit" value="{_save}">
</div>
</div>
</form>
{/block}

View file

@ -0,0 +1,82 @@
{extends "@layout.xml"}
{block title}
{$cat->getName()}
{/block}
{block headingWrap}
<a style="float: right;" class="aui-button aui-button-primary" href="/admin/gifts/id0?act=edit&cat={$cat->getId()}">
{_create}
</a>
<h1>Набор "{$cat->getName()}"</h1>
{/block}
{block content}
{if sizeof($gifts) > 0}
<table class="aui aui-table-list">
<thead>
<th>Подарок</th>
<th>Имя</th>
<th>Цена</th>
<th>Подарен</th>
<th>Ограничение</th>
<th>Сброс счётчика ограничений</th>
<th>Действия</th>
</thead>
<tbody>
<tr n:foreach="$gifts as $gift">
<td style="vertical-align: middle; width: 0px;">
<img style="max-width: 32px;" src="{$gift->getImage(2)}" alt="{$gift->getName()}" />
</td>
<td style="vertical-align: middle;">
{$gift->getName()}
<span n:if="$gift->isFree()" class="aui-lozenge aui-lozenge-subtle aui-lozenge-success">
бесплатный
</span>
</td>
<td style="vertical-align: middle;">
{$gift->getPrice()} голосов
</td>
<td style="vertical-align: middle;">
{$gift->getUsages()} раз
</td>
<td style="vertical-align: middle;">
{if $gift->getLimit() === INF}
Отсутствует
{else}
Не более {$gift->getLimit()} дарений
{/if}
</td>
<td style="vertical-align: middle;">
{if !$gift->getLimitResetTime()}
Никогда
{else}
Последний раз в
{$gift->getLimitResetTime()->format("%a, %d %B %G")}
{/if}
</td>
<td style="vertical-align: middle; text-align: right;">
<a class="aui-button aui-button-primary" href="/admin/gifts/id{$gift->getId()}">
<span class="aui-icon aui-icon-small aui-iconfont-new-edit">Редактировать</span>
</a>
</td>
</tr>
</tbody>
</table>
{else}
<center>
<p>Подарков нету. Нажмите на красивую кнопку вверху, чтобы создать первый.</p>
</center>
{/if}
<div align="right">
{var isLast = ((20 * (($_GET['p'] ?? 1) - 1)) + sizeof($gifts)) < $count}
<a n:if="($_GET['p'] ?? 1) > 1" class="aui-button" href="?p={($_GET['p'] ?? 1) - 1}">
⭁ туда
</a>
<a n:if="$isLast" class="aui-button" href="?p={($_GET['p'] ?? 1) + 1}">
⭇ сюда
</a>
</div>
{/block}

View file

@ -4,8 +4,12 @@
{_vouchers}
{/block}
{block heading}
{_vouchers}
{block headingWrap}
<a style="float: right;" class="aui-button aui-button-primary" href="/admin/vouchers/id0">
{_create}
</a>
<h1>{_vouchers}</h1>
{/block}
{block content}
@ -46,10 +50,6 @@
<br/>
<div align="right">
<a style="float: left;" class="aui-button aui-button-primary" href="/admin/vouchers/id0">
{_create}
</a>
{var isLast = ((20 * (($_GET['p'] ?? 1) - 1)) + sizeof($vouchers)) < $count}
<a n:if="($_GET['p'] ?? 1) > 1" class="aui-button" href="?p={($_GET['p'] ?? 1) - 1}">
⭁ туда

View file

@ -0,0 +1,30 @@
{extends "../@layout.xml"}
{block title}
Подарить подарок
{/block}
{block header}
<a href="{$user->getURL()}">{$user->getCanonicalName()}</a> »
<a href="/gifts?act=pick&user={$user->getId()}">Выбор подарка</a> »
<a href="/gifts?act=pick&user={$user->getId()}">Коллекции</a> »
<a href="/gifts?act=menu&user={$user->getId()}&pack={$cat->getId()}">{$cat->getName(tr("__lang"))}</a> »
Подтверждение
{/block}
{block content}
<center>
<img class="gift_confirm_pic" style="max-width: 256px;" src="{$gift->getImage(2)}" alt="Подарок" />
<form style="width: 65%;" method="POST">
<textarea name="comment" style="resize: vertical; height: 65px;" placeholder="Ваше сообщение, которое будет доставлено вместе с подарком"></textarea>
<br/><br/>
<input type="hidden" name="hash" value="{$csrfToken}" />
<input type="submit" value="Отправить" class="button" />
<label>
<input type="checkbox" name="anonymous"> Анонимно
</label>
</form>
</center>
{/block}

View file

@ -0,0 +1,29 @@
{extends "../@listView.xml"}
{block title}
Выбрать подарок
{/block}
{block header}
<a href="{$user->getURL()}">{$user->getCanonicalName()}</a> »
<a href="/gifts?act=pick&user={$user->getId()}">Выбор подарка</a> »
Коллекции
{/block}
{* BEGIN ELEMENTS DESCRIPTION *}
{block link|strip|stripHtml}
/gifts?act=menu&user={$user->getId()}&pack={$x->getId()}
{/block}
{block preview}
<img src="{$x->getThumbnailURL()}" width="75" alt="{$x->getName(tr('__lang'))}" />
{/block}
{block name}
{$x->getName(tr("__lang"))}
{/block}
{block description}
{$x->getDescription(tr("__lang"))}
{/block}

View file

@ -0,0 +1,58 @@
{extends "../@layout.xml"}
{block title}
Выбрать подарок
{/block}
{block header}
<a href="{$user->getURL()}">{$user->getCanonicalName()}</a> »
<a href="/gifts?act=pick&user={$user->getId()}">Выбор подарка</a> »
<a href="/gifts?act=pick&user={$user->getId()}">Коллекции</a> »
{$cat->getName(tr("__lang"))}
{/block}
{block content}
<div class="gift_grid">
<div n:foreach="$gifts as $gift" n:class="gift_sel, !$gift->canUse($thisUser) ? disabled" data-gift="{$gift->getId()}">
<img class="gift_pic" src="{$gift->getImage(2)}" alt="Подарок" />
<strong class="gift_price">
{if $gift->isFree()}
бесплатный
{else}
{$gift->getPrice()} голосов
{/if}
</strong>
<strong class="gift_limit">
{if $gift->getUsagesLeft($thisUser) !== INF}
осталось {$gift->getUsagesLeft($thisUser)} штук
{/if}&nbsp;
</strong>
</div>
</div>
<div style="padding: 8px;">
{include "../components/paginator.xml", conf => (object) [
"page" => $page,
"count" => $count,
"amount" => sizeof($gifts),
"perPage" => OPENVK_DEFAULT_PER_PAGE,
]}
</div>
{/block}
{block bodyScripts}
<script>
$(".gift_sel").click(function() {
let el = $(this);
if(el.hasClass("disabled"))
return false;
let link = "/gifts?act=confirm&user={$user->getId()}&pack={$cat->getId()}&elid=";
let gift = el.data("gift");
window.location.assign(link + gift);
});
</script>
{/block}

View file

@ -0,0 +1,43 @@
{extends "../@listView.xml"}
{block title}
Подарки {$user->getFirstName()}
{/block}
{block header}
<a href="{$user->getURL()}">{$user->getCanonicalName()}</a> »
Подарки
{/block}
{* BEGIN ELEMENTS DESCRIPTION *}
{block link|strip|stripHtml}
javascript:false
{/block}
{block preview}
<img src="{$x->gift->getImage(2)}" width="75" alt="Подарок" />
{/block}
{block name}
Подарок
{/block}
{block description}
<table class="ugc-table" n:if="$hideInfo ? !$x->anon : true">
<tbody>
<tr>
<td><span class="nobold">Даритель: </span></td>
<td>
<a href="{$x->sender->getURL()}">
{$x->sender->getFullName()}
</a>
</td>
</tr>
<tr n:if="!empty($x->caption)">
<td><span class="nobold">Комментарий: </span></td>
<td>{$x->caption}</td>
</tr>
</tbody>
</table>
{/block}

View file

@ -84,6 +84,8 @@
</a>
{/if}
<a href="/gifts?act=pick&user={$user->getId()}" class="profile_link">Подарить подарок</a>
{var subStatus = $user->getSubscriptionStatus($thisUser)}
{if $subStatus === 0}
<form action="/setSub/user" method="post">
@ -150,6 +152,30 @@
{/if}
</div>
<br />
<div n:if="($giftCount = $user->getGiftCount()) > 0">
<div class="content_title_expanded" onclick="hidePanel(this, {$giftCount});">
{_gifts}
</div>
<div>
<div class="content_subtitle">
{tr("gifts", $giftCount)}
<div style="float:right;">
<a href="/gifts{$user->getId()}">{_all_title}</a>
</div>
</div>
<div class="ovk-avView">
<div class="ovk-avView--el" n:foreach="$user->getGifts(1, 3) as $giftDescriptor">
{var hideInfo = $giftDescriptor->anon ? $thisUser->getId() !== $user->getId() : false}
<a href="{$hideInfo ? 'javascript:false' : $giftDescriptor->sender->getURL()}">
<img class="ava"
src="{$giftDescriptor->gift->getImage(2)}"
alt="{$hideInfo ? 'Подарок' : ($giftDescriptor->caption ?? 'Подарок')}" />
</a>
</div>
</div>
</div>
</div>
<div n:if="$user->getFriendsCount() > 0 && $user->getPrivacyPermission('friends.read', $thisUser ?? NULL)">
{var friendCount = $user->getFriendsCount()}

View file

@ -0,0 +1,4 @@
{var gift = $notification->getModel(0)}
{var sender = $notification->getModel(1)}
<a href="{$sender->getURL()}"><b>{$sender->getCanonicalName()}</b></a> отправил вам {$notification->getDateTime()} подарок.

View file

@ -17,6 +17,7 @@ services:
- openvk\Web\Presenters\NotificationPresenter
- openvk\Web\Presenters\SupportPresenter
- openvk\Web\Presenters\AdminPresenter
- openvk\Web\Presenters\GiftsPresenter
- openvk\Web\Presenters\MessengerPresenter
- openvk\Web\Presenters\ThemepacksPresenter
- openvk\Web\Presenters\VKAPIPresenter
@ -34,4 +35,5 @@ services:
- openvk\Web\Models\Repositories\TicketComments
- openvk\Web\Models\Repositories\IPs
- openvk\Web\Models\Repositories\Vouchers
- openvk\Web\Models\Repositories\Gifts
- openvk\Web\Models\Repositories\ContentSearchRepository

View file

@ -199,6 +199,12 @@ routes:
handler: "About->invite"
- url: "/away.php"
handler: "Away->away"
- url: "/gift{num}_{num}.png"
handler: "Gifts->giftImage"
- url: "/gifts{num}"
handler: "Gifts->userGifts"
- url: "/gifts"
handler: "Gifts->stub"
- url: "/admin"
handler: "Admin->index"
- url: "/admin/users"
@ -213,6 +219,14 @@ routes:
handler: "Admin->vouchers"
- url: "/admin/vouchers/id{num}"
handler: "Admin->voucher"
- url: "/admin/gifts"
handler: "Admin->giftCategories"
- url: "/admin/gifts/id{num}"
handler: "Admin->gift"
- url: "/admin/gifts/{slug}.{num}.meta"
handler: "Admin->giftCategory"
- url: "/admin/gifts/{slug}.{num}/"
handler: "Admin->gifts"
- url: "/admin/ban.pl/{num}"
handler: "Admin->quickBan"
- url: "/admin/warn.pl/{num}"

View file

@ -1307,3 +1307,42 @@ body.scrolled .toTop:hover {
.knowledgeBaseArticle ul {
color: unset;
}
.gift_grid {
width: 100%;
display: grid;
grid-template-columns: 1fr 1fr 1fr 1fr;
}
.gift_sel {
display: grid;
box-sizing: border-box;
padding: 15px 8px;
justify-items: center;
cursor: pointer;
border-radius: 10px;
}
.gift_pic {
max-width: 70%;
}
.gift_sel:hover {
background-color: #f1f1f1;
}
.gift_sel.disabled:hover {
cursor: not-allowed;
}
.gift_sel > .gift_price, .gift_sel > .gift_limit {
visibility: hidden;
}
.gift_sel:hover > .gift_price, .gift_sel:hover > .gift_limit {
visibility: unset;
}
.gift_sel.disabled {
opacity: .5;
}

View file

@ -62,8 +62,10 @@ function tr(string $stringId, ...$variables): string
{
$localizer = Localizator::i();
$lang = Session::i()->get("lang", "ru");
$output = $localizer->_($stringId, $lang);
if($stringId === "__lang")
return $lang;
$output = $localizer->_($stringId, $lang);
if(sizeof($variables) > 0) {
if(gettype($variables[0]) === "integer") {
$numberedStringId = NULL;
@ -213,6 +215,7 @@ return (function() {
else
$ver = "Build 15";
define("nullptr", NULL);
define("OPENVK_VERSION", "Altair Preview ($ver)", false);
define("OPENVK_DEFAULT_PER_PAGE", 10, false);
define("__OPENVK_ERROR_CLOCK_IN_FUTURE", "Server clock error: FK1200-DTF", false);

View file

@ -16,5 +16,6 @@
"openvk\\Web\\Models\\Entities\\Ticket":16,
"openvk\\Web\\Models\\Entities\\TicketComment":17,
"openvk\\Web\\Models\\Entities\\User":18,
"openvk\\Web\\Models\\Entities\\Video":19
"openvk\\Web\\Models\\Entities\\Video":19,
"openvk\\Web\\Models\\Entities\\Gift":20
}