From 9336a9162390420e5a4ca6471d6a27d7e909ae09 Mon Sep 17 00:00:00 2001 From: Celestora Date: Thu, 7 Oct 2021 11:48:55 +0300 Subject: [PATCH] Add gifts Signed-off-by: Celestora --- Web/Models/Entities/Gift.php | 164 ++++++++++++++++++ Web/Models/Entities/GiftCategory.php | 155 +++++++++++++++++ .../Notifications/GiftNotification.php | 13 ++ Web/Models/Entities/User.php | 35 +++- Web/Models/Repositories/Gifts.php | 45 +++++ Web/Presenters/AdminPresenter.php | 158 ++++++++++++++++- Web/Presenters/GiftsPresenter.php | 131 ++++++++++++++ .../templates/@MilkshakeListView.xml | 110 ++++++------ Web/Presenters/templates/@layout.xml | 4 + Web/Presenters/templates/Admin/@layout.xml | 25 ++- Web/Presenters/templates/Admin/Gift.xml | 106 +++++++++++ .../templates/Admin/GiftCategories.xml | 56 ++++++ .../templates/Admin/GiftCategory.xml | 72 ++++++++ Web/Presenters/templates/Admin/Gifts.xml | 82 +++++++++ Web/Presenters/templates/Admin/Vouchers.xml | 12 +- Web/Presenters/templates/Gifts/Confirm.xml | 30 ++++ Web/Presenters/templates/Gifts/Menu.xml | 29 ++++ Web/Presenters/templates/Gifts/Pick.xml | 58 +++++++ Web/Presenters/templates/Gifts/UserGifts.xml | 43 +++++ Web/Presenters/templates/User/View.xml | 26 +++ .../components/notifications/9601/_18_20_.xml | 4 + Web/di.yml | 2 + Web/routes.yml | 14 ++ Web/static/css/style.css | 39 +++++ bootstrap.php | 29 ++-- data/modelCodes.json | 5 +- 26 files changed, 1367 insertions(+), 80 deletions(-) create mode 100644 Web/Models/Entities/Gift.php create mode 100644 Web/Models/Entities/GiftCategory.php create mode 100644 Web/Models/Entities/Notifications/GiftNotification.php create mode 100644 Web/Models/Repositories/Gifts.php create mode 100644 Web/Presenters/GiftsPresenter.php create mode 100644 Web/Presenters/templates/Admin/Gift.xml create mode 100644 Web/Presenters/templates/Admin/GiftCategories.xml create mode 100644 Web/Presenters/templates/Admin/GiftCategory.xml create mode 100644 Web/Presenters/templates/Admin/Gifts.xml create mode 100644 Web/Presenters/templates/Gifts/Confirm.xml create mode 100644 Web/Presenters/templates/Gifts/Menu.xml create mode 100644 Web/Presenters/templates/Gifts/Pick.xml create mode 100644 Web/Presenters/templates/Gifts/UserGifts.xml create mode 100644 Web/Presenters/templates/components/notifications/9601/_18_20_.xml diff --git a/Web/Models/Entities/Gift.php b/Web/Models/Entities/Gift.php new file mode 100644 index 00000000..fb9e0ba0 --- /dev/null +++ b/Web/Models/Entities/Gift.php @@ -0,0 +1,164 @@ +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); + } +} diff --git a/Web/Models/Entities/GiftCategory.php b/Web/Models/Entities/GiftCategory.php new file mode 100644 index 00000000..b008b9a5 --- /dev/null +++ b/Web/Models/Entities/GiftCategory.php @@ -0,0 +1,155 @@ +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)); + } +} diff --git a/Web/Models/Entities/Notifications/GiftNotification.php b/Web/Models/Entities/Notifications/GiftNotification.php new file mode 100644 index 00000000..cdbc67de --- /dev/null +++ b/Web/Models/Entities/Notifications/GiftNotification.php @@ -0,0 +1,13 @@ +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"); diff --git a/Web/Models/Repositories/Gifts.php b/Web/Models/Repositories/Gifts.php new file mode 100644 index 00000000..f36b82a5 --- /dev/null +++ b/Web/Models/Repositories/Gifts.php @@ -0,0 +1,45 @@ +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); + } +} diff --git a/Web/Presenters/AdminPresenter.php b/Web/Presenters/AdminPresenter.php index fe5dab25..98003e6b 100644 --- a/Web/Presenters/AdminPresenter.php +++ b/Web/Presenters/AdminPresenter.php @@ -1,19 +1,21 @@ 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 Recycle Bin."); + 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 $name."); + $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 { diff --git a/Web/Presenters/GiftsPresenter.php b/Web/Presenters/GiftsPresenter.php new file mode 100644 index 00000000..97980794 --- /dev/null +++ b/Web/Presenters/GiftsPresenter.php @@ -0,0 +1,131 @@ +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", "Подарок отправлен", "Вы отправили подарок " . $user->getFirstName() . " за " . $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); + } +} diff --git a/Web/Presenters/templates/@MilkshakeListView.xml b/Web/Presenters/templates/@MilkshakeListView.xml index 9cc4e389..4473e056 100644 --- a/Web/Presenters/templates/@MilkshakeListView.xml +++ b/Web/Presenters/templates/@MilkshakeListView.xml @@ -1,57 +1,61 @@ -
-
- {var data = is_array($iterator) ? $iterator : iterator_to_array($iterator)} - - {if sizeof($data) > 0} - - - - - - - - -
- {include preview, x => $dat} - - - -
- {include description, x => $dat} -
-
-
+{extends "@layout.xml"} + +{block wrap} +
+
+ {var data = is_array($iterator) ? $iterator : iterator_to_array($iterator)} -
- {include "components/paginator.xml", conf => (object) [ - "page" => $page, - "count" => $count, - "amount" => sizeof($data), - "perPage" => $perPage ?? OPENVK_DEFAULT_PER_PAGE, - ]} -
- {else} - {ifset customErrorMessage} - {include customErrorMessage} + {if sizeof($data) > 0} + + + + + + + + +
+ {include preview, x => $dat} + + + +
+ {include description, x => $dat} +
+
+
+ +
+ {include "components/paginator.xml", conf => (object) [ + "page" => $page, + "count" => $count, + "amount" => sizeof($data), + "perPage" => $perPage ?? OPENVK_DEFAULT_PER_PAGE, + ]} +
{else} - {include "components/nothing.xml"} - {/ifset} - {/if} -
- -
- {include actions} -
-
- {_"sort_randomly"} - {_"sort_up"} - {_"sort_down"} + {ifset customErrorMessage} + {include customErrorMessage} + {else} + {include "components/nothing.xml"} + {/ifset} + {/if} +
+ +
+ {include actions} +
+
-
\ No newline at end of file +{/block} diff --git a/Web/Presenters/templates/@layout.xml b/Web/Presenters/templates/@layout.xml index 8b8fe2be..999d7f21 100644 --- a/Web/Presenters/templates/@layout.xml +++ b/Web/Presenters/templates/@layout.xml @@ -268,6 +268,10 @@ + + {ifset bodyScripts} + {include bodyScripts} + {/ifset} diff --git a/Web/Presenters/templates/Admin/@layout.xml b/Web/Presenters/templates/Admin/@layout.xml index 2fb177fe..8b9ebd42 100644 --- a/Web/Presenters/templates/Admin/@layout.xml +++ b/Web/Presenters/templates/Admin/@layout.xml @@ -79,6 +79,11 @@ {_vouchers} +
  • + + Подарки + +
  • Настройки @@ -125,10 +130,24 @@
    + {ifset $flashMessage} + {var type = ["err" => "error", "warn" => "warning", "info" => "basic", "succ" => "success"][$flashMessage->type]} +
    +

    + {$flashMessage->title} +

    +

    {$flashMessage->msg|noescape}

    +
    + {/ifset} +
    -

    {include heading}

    + {ifset headingWrap} + {include headingWrap} + {else} +

    {include heading}

    + {/ifset}
    @@ -150,5 +169,9 @@ {script "js/node_modules/jquery/dist/jquery.min.js"} {script "js/node_modules/@atlassian/aui/dist/aui/aui-prototyping.js"} + + {ifset scripts} + {include scripts} + {/ifset} diff --git a/Web/Presenters/templates/Admin/Gift.xml b/Web/Presenters/templates/Admin/Gift.xml new file mode 100644 index 00000000..fd05037c --- /dev/null +++ b/Web/Presenters/templates/Admin/Gift.xml @@ -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} +
    +
    + + {if $form->id === 0} + + {else} + + + + + + + + {/if} +
    + +
    + + +
    +
    + + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + +
    + + + + +
    +
    + + + +
    +
    + + +
    +
    +
    +{/block} + +{block scripts} + +{/block} diff --git a/Web/Presenters/templates/Admin/GiftCategories.xml b/Web/Presenters/templates/Admin/GiftCategories.xml new file mode 100644 index 00000000..2dd41ad9 --- /dev/null +++ b/Web/Presenters/templates/Admin/GiftCategories.xml @@ -0,0 +1,56 @@ +{extends "@layout.xml"} + +{block title} + Наборы подарков +{/block} + +{block headingWrap} + + {_create} + + +

    Наборы подарков

    +{/block} + +{block content} +
    + {if sizeof($categories) > 0} + + + + + + + + +
    + {$cat->getName()} + {$cat->getName()} + + {ovk_proc_strtr($cat->getDescription(), 128)} + + + Редактировать + + + + Открыть + Открыть + +
    + {else} +
    +

    Наборов подарков нету. Чтобы создать подарок, создайте набор.

    +
    + {/if} + +
    + {var isLast = ((20 * (($_GET['p'] ?? 1) - 1)) + sizeof($categories)) < $count} + + ⭁ туда + + + ⭇ сюда + +
    +{/block} diff --git a/Web/Presenters/templates/Admin/GiftCategory.xml b/Web/Presenters/templates/Admin/GiftCategory.xml new file mode 100644 index 00000000..936a823f --- /dev/null +++ b/Web/Presenters/templates/Admin/GiftCategory.xml @@ -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} +
    +

    Общие настройки

    +
    +
    + + +
    +
    + + +
    Внутреннее название набора, которое будет использоваться, если не удаётся найти название на языке пользователя.
    +
    +
    + + +
    Внутреннее описание набора, которое будет использоваться, если не удаётся найти название на языке пользователя.
    +
    +
    + +

    Языко-зависимые настройки

    +
    + {foreach $form->languages as $locale => $data} + {continueIf $locale === "master"} + +
    + + +
    +
    + + +
    + {/foreach} +
    + +
    +
    + + +
    +
    +
    +{/block} diff --git a/Web/Presenters/templates/Admin/Gifts.xml b/Web/Presenters/templates/Admin/Gifts.xml new file mode 100644 index 00000000..c068e067 --- /dev/null +++ b/Web/Presenters/templates/Admin/Gifts.xml @@ -0,0 +1,82 @@ +{extends "@layout.xml"} + +{block title} + {$cat->getName()} +{/block} + +{block headingWrap} + + {_create} + + +

    Набор "{$cat->getName()}"

    +{/block} + +{block content} + {if sizeof($gifts) > 0} + + + + + + + + + + + + + + + + + + + + + +
    ПодарокИмяЦенаПодаренОграничениеСброс счётчика ограниченийДействия
    + {$gift->getName()} + + {$gift->getName()} + + бесплатный + + + {$gift->getPrice()} голосов + + {$gift->getUsages()} раз + + {if $gift->getLimit() === INF} + Отсутствует + {else} + Не более {$gift->getLimit()} дарений + {/if} + + {if !$gift->getLimitResetTime()} + Никогда + {else} + Последний раз в + {$gift->getLimitResetTime()->format("%a, %d %B %G")} + {/if} + + + Редактировать + +
    + {else} +
    +

    Подарков нету. Нажмите на красивую кнопку вверху, чтобы создать первый.

    +
    + {/if} + +
    + {var isLast = ((20 * (($_GET['p'] ?? 1) - 1)) + sizeof($gifts)) < $count} + + ⭁ туда + + + ⭇ сюда + +
    +{/block} diff --git a/Web/Presenters/templates/Admin/Vouchers.xml b/Web/Presenters/templates/Admin/Vouchers.xml index 7ab5a32e..2f28fe21 100644 --- a/Web/Presenters/templates/Admin/Vouchers.xml +++ b/Web/Presenters/templates/Admin/Vouchers.xml @@ -4,8 +4,12 @@ {_vouchers} {/block} -{block heading} - {_vouchers} +{block headingWrap} + + {_create} + + +

    {_vouchers}

    {/block} {block content} @@ -46,10 +50,6 @@
    - - {_create} - - {var isLast = ((20 * (($_GET['p'] ?? 1) - 1)) + sizeof($vouchers)) < $count} ⭁ туда diff --git a/Web/Presenters/templates/Gifts/Confirm.xml b/Web/Presenters/templates/Gifts/Confirm.xml new file mode 100644 index 00000000..d1b7c444 --- /dev/null +++ b/Web/Presenters/templates/Gifts/Confirm.xml @@ -0,0 +1,30 @@ +{extends "../@layout.xml"} + +{block title} + Подарить подарок +{/block} + +{block header} + {$user->getCanonicalName()} » + Выбор подарка » + Коллекции » + {$cat->getName(tr("__lang"))} » + Подтверждение +{/block} + +{block content} +
    + Подарок + +
    + +

    + + + + +
    +
    +{/block} diff --git a/Web/Presenters/templates/Gifts/Menu.xml b/Web/Presenters/templates/Gifts/Menu.xml new file mode 100644 index 00000000..2ca74486 --- /dev/null +++ b/Web/Presenters/templates/Gifts/Menu.xml @@ -0,0 +1,29 @@ +{extends "../@listView.xml"} + +{block title} + Выбрать подарок +{/block} + +{block header} + {$user->getCanonicalName()} » + Выбор подарка » + Коллекции +{/block} + +{* BEGIN ELEMENTS DESCRIPTION *} + +{block link|strip|stripHtml} + /gifts?act=menu&user={$user->getId()}&pack={$x->getId()} +{/block} + +{block preview} + {$x->getName(tr('__lang'))} +{/block} + +{block name} + {$x->getName(tr("__lang"))} +{/block} + +{block description} + {$x->getDescription(tr("__lang"))} +{/block} diff --git a/Web/Presenters/templates/Gifts/Pick.xml b/Web/Presenters/templates/Gifts/Pick.xml new file mode 100644 index 00000000..9e705796 --- /dev/null +++ b/Web/Presenters/templates/Gifts/Pick.xml @@ -0,0 +1,58 @@ +{extends "../@layout.xml"} + +{block title} + Выбрать подарок +{/block} + +{block header} + {$user->getCanonicalName()} » + Выбор подарка » + Коллекции » + {$cat->getName(tr("__lang"))} +{/block} + +{block content} +
    +
    + Подарок + + + {if $gift->isFree()} + бесплатный + {else} + {$gift->getPrice()} голосов + {/if} + + + + {if $gift->getUsagesLeft($thisUser) !== INF} + осталось {$gift->getUsagesLeft($thisUser)} штук + {/if}  + +
    +
    + +
    + {include "../components/paginator.xml", conf => (object) [ + "page" => $page, + "count" => $count, + "amount" => sizeof($gifts), + "perPage" => OPENVK_DEFAULT_PER_PAGE, + ]} +
    +{/block} + +{block bodyScripts} + +{/block} diff --git a/Web/Presenters/templates/Gifts/UserGifts.xml b/Web/Presenters/templates/Gifts/UserGifts.xml new file mode 100644 index 00000000..4fb20851 --- /dev/null +++ b/Web/Presenters/templates/Gifts/UserGifts.xml @@ -0,0 +1,43 @@ +{extends "../@listView.xml"} + +{block title} + Подарки {$user->getFirstName()} +{/block} + +{block header} + {$user->getCanonicalName()} » + Подарки +{/block} + +{* BEGIN ELEMENTS DESCRIPTION *} + +{block link|strip|stripHtml} + javascript:false +{/block} + +{block preview} + Подарок +{/block} + +{block name} + Подарок +{/block} + +{block description} + + + + + + + + + + + +
    Даритель: + + {$x->sender->getFullName()} + +
    Комментарий: {$x->caption}
    +{/block} diff --git a/Web/Presenters/templates/User/View.xml b/Web/Presenters/templates/User/View.xml index 5dd29366..63bf7377 100644 --- a/Web/Presenters/templates/User/View.xml +++ b/Web/Presenters/templates/User/View.xml @@ -84,6 +84,8 @@ {/if} + Подарить подарок + {var subStatus = $user->getSubscriptionStatus($thisUser)} {if $subStatus === 0}
    @@ -150,6 +152,30 @@ {/if}

    +
    +
    + {_gifts} +
    +
    +
    + {tr("gifts", $giftCount)} + +
    +
    +
    + {var hideInfo = $giftDescriptor->anon ? $thisUser->getId() !== $user->getId() : false} + + + {$hideInfo ? 'Подарок' : ($giftDescriptor->caption ?? 'Подарок')} + +
    +
    +
    +
    {var friendCount = $user->getFriendsCount()} diff --git a/Web/Presenters/templates/components/notifications/9601/_18_20_.xml b/Web/Presenters/templates/components/notifications/9601/_18_20_.xml new file mode 100644 index 00000000..aba064cf --- /dev/null +++ b/Web/Presenters/templates/components/notifications/9601/_18_20_.xml @@ -0,0 +1,4 @@ +{var gift = $notification->getModel(0)} +{var sender = $notification->getModel(1)} + +{$sender->getCanonicalName()} отправил вам {$notification->getDateTime()} подарок. diff --git a/Web/di.yml b/Web/di.yml index 5d239a2b..ca1b857f 100644 --- a/Web/di.yml +++ b/Web/di.yml @@ -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 diff --git a/Web/routes.yml b/Web/routes.yml index f5598b94..014cb358 100644 --- a/Web/routes.yml +++ b/Web/routes.yml @@ -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}" diff --git a/Web/static/css/style.css b/Web/static/css/style.css index 9556a1ed..dae2fad0 100644 --- a/Web/static/css/style.css +++ b/Web/static/css/style.css @@ -1306,4 +1306,43 @@ 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; } \ No newline at end of file diff --git a/bootstrap.php b/bootstrap.php index 518e2941..dcce4f95 100644 --- a/bootstrap.php +++ b/bootstrap.php @@ -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; @@ -108,17 +110,17 @@ function getLanguages(): array function isLanguageAvailable($lg): bool { - $lg_temp = false; - foreach(getLanguages() as $lang) { - if ($lang['code'] == $lg) $lg_temp = true; - } - return $lg_temp; + $lg_temp = false; + foreach(getLanguages() as $lang) { + if ($lang['code'] == $lg) $lg_temp = true; + } + return $lg_temp; } function getBrowsersLanguage(): array { - if ($_SERVER['HTTP_ACCEPT_LANGUAGE'] != null) return mb_split(",", mb_split(";", $_SERVER['HTTP_ACCEPT_LANGUAGE'])[0]); - else return array(); + if ($_SERVER['HTTP_ACCEPT_LANGUAGE'] != null) return mb_split(",", mb_split(";", $_SERVER['HTTP_ACCEPT_LANGUAGE'])[0]); + else return array(); } function eventdb(): ?DatabaseConnection @@ -196,12 +198,12 @@ return (function() { setlocale(LC_TIME, "POSIX"); - // TODO: Default language in config + // TODO: Default language in config if(Session::i()->get("lang") == null) { - $languages = array_reverse(getBrowsersLanguage()); - foreach($languages as $lg) { - if(isLanguageAvailable($lg)) setLanguage($lg); - } + $languages = array_reverse(getBrowsersLanguage()); + foreach($languages as $lg) { + if(isLanguageAvailable($lg)) setLanguage($lg); + } } if(empty($_SERVER["REQUEST_SCHEME"])) @@ -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); diff --git a/data/modelCodes.json b/data/modelCodes.json index 25a0ed4e..6ac105ee 100644 --- a/data/modelCodes.json +++ b/data/modelCodes.json @@ -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 -} \ No newline at end of file + "openvk\\Web\\Models\\Entities\\Video":19, + "openvk\\Web\\Models\\Entities\\Gift":20 +}