creating sets works now

added videos to sets is missing
migration is missing
This commit is contained in:
overflowerror 2021-02-21 19:52:37 +01:00
parent c10ad8aedc
commit 5607fed149
16 changed files with 787 additions and 14 deletions

View file

@ -8,17 +8,3 @@
font-weight: bold;
color: lightgrey;
}
.addButton {
position: absolute;
right: 2%;
top: calc(90vh - 3vw);
width: 3vw;
}
.addButton button {
position: fixed;
height: 3vw;
width: 3vw;
font-size: 1.4vw;
}

View file

@ -70,3 +70,17 @@ a.btn.disabled {
pointer-events: none;
cursor: pointer;
}
.addButton {
position: absolute;
right: 2%;
top: calc(90vh - 3vw);
width: 3vw;
}
.addButton button {
position: fixed;
height: 3vw;
width: 3vw;
font-size: 1.4vw;
}

21
public/css/sets.css Normal file
View file

@ -0,0 +1,21 @@
.rename {
white-space: nowrap;
}
.rename input[type=text] {
width: 20%;
display: inline-block;
}
.rename button {
display: inline-block;
}
.videos .list img {
height: 40px;
}
.list {
max-height: 400px;
overflow-y: scroll;
}

24
public/js/search.js Normal file
View file

@ -0,0 +1,24 @@
function search(input, elements, contents) {
for (let i = 0; i < contents.length; i++) {
console.log(contents[i])
contents[i] = contents[i].toLowerCase();
}
input.onkeyup = function () {
let tokens = input.value;
tokens = tokens.toLowerCase();
tokens = tokens.split(" ");
for (let i = 0; i < contents.length; i++) {
let okay = true;
for (let j = 0; j < tokens.length; j++) {
if (contents[i].indexOf(tokens[j]) < 0) {
okay = false;
break;
}
}
elements[i].style.display = okay ? "block" : "none";
}
}
}

View file

@ -4,13 +4,16 @@
namespace App\Controller;
use App\Entity\Set;
use App\Entity\User;
use App\Entity\Video;
use App\Entity\VideoLink;
use App\Form\SetType;
use App\Form\VideoLinkType;
use App\Form\VideoType;
use App\Mapper\CustomUuidMapper;
use App\Service\LoggingService;
use App\Service\SetService;
use App\Service\UserService;
use App\Service\VideoLinkService;
use App\Service\VideoService;
@ -28,10 +31,13 @@ class DashboardController extends AbstractController
{
public const DELETE_VIDEO_CSRF_TOKEN_ID = "delete-video";
public const DELETE_LINK_CSRF_TOKEN_ID = "delete-link";
public const DELETE_SET_CSRF_TOKEN_ID = "delete-set";
public const EDIT_SET_CSRF_TOKEN_ID = "edit-set";
private $userService;
private $videoService;
private $videoLinkService;
private $setService;
private $loggingService;
private $uuidMapper;
@ -40,6 +46,7 @@ class DashboardController extends AbstractController
UserService $userService,
VideoService $videoService,
VideoLinkService $videoLinkService,
SetService $setService,
LoggingService $loggingService,
CustomUuidMapper $uuidMapper
)
@ -47,6 +54,7 @@ class DashboardController extends AbstractController
$this->userService = $userService;
$this->videoService = $videoService;
$this->videoLinkService = $videoLinkService;
$this->setService = $setService;
$this->loggingService = $loggingService;
$this->uuidMapper = $uuidMapper;
}
@ -375,4 +383,154 @@ class DashboardController extends AbstractController
"form" => $form->createView()
]);
}
/**
* @Route("/sets", name="app_sets")
*/
public function showSets(): Response
{
if (!$this->isGranted(User::ROLE_USER)) {
// not logged in
return $this->redirectToRoute("app_login");
}
$user = $this->userService->getLoggedInUser();
$sets = $this->setService->getAll($user);
foreach ($sets as $set) {
$set->setCustomId($this->uuidMapper->toString($set->getId()));
}
return $this->render("dashboard/sets.html.twig", [
"sets" => $sets
]);
}
/**
* @Route("/sets/create", name="app_create_set")
*/
public function createSet(Request $request): Response
{
if (!$this->isGranted(User::ROLE_USER)) {
// not logged in
return $this->redirectToRoute("app_login");
}
$set = new Set();
$form = $this->createForm(SetType::class, $set);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$set = $form->getData();
$this->setService->add($set);
return $this->redirectToRoute("app_sets");
}
return $this->render("dashboard/set-new.html.twig", [
"form" => $form->createView()
]);
}
/**
* @Route("/sets/delete", name="app_delete_set", methods={"POST"})
*/
public function deleteSet(Request $request): Response
{
$token = $request->request->get("csrfToken");
$setId = $request->request->get("setId");
if (!$this->isCsrfTokenValid(self::DELETE_SET_CSRF_TOKEN_ID, $token)) {
throw new AccessDeniedHttpException();
}
if (!$setId) {
throw new BadRequestHttpException();
}
try {
$setId = $this->uuidMapper->fromString($setId);
} catch (ConversionException $e) {
throw new BadRequestHttpException();
}
$set = $this->setService->get($setId);
if ($set == null || $set->getCreator() != $this->userService->getLoggedInUser()) {
throw new AccessDeniedHttpException();
}
$this->setService->delete($set);
return $this->redirectToRoute("app_sets");
}
/**
* @Route("/sets/{setId}", name="app_edit_set")
*/
public function editSet($setId, Request $request): Response
{
if (!$this->isGranted(User::ROLE_USER)) {
// not logged in
return $this->redirectToRoute("app_login");
}
try {
$setId = $this->uuidMapper->fromString($setId);
} catch (ConversionException $e) {
throw new BadRequestHttpException();
}
$set = $this->setService->get($setId);
$form = $this->createForm(SetType::class, $set);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$set = $form->getData();
$this->setService->update($set);
}
$set->setCustomId($this->uuidMapper->toString($set->getId()));
return $this->render("dashboard/set-edit.html.twig", [
"form" => $form->createView(),
"set" => $set
]);
}
/**
* @Route("/sets/{setId}/add", name="app_edit_set_add")
*/
public function editSetAdd($setId, Request $request): Response
{
if (!$this->isGranted(User::ROLE_USER)) {
// not logged in
return $this->redirectToRoute("app_login");
}
try {
$setId = $this->uuidMapper->fromString($setId);
} catch (ConversionException $e) {
throw new BadRequestHttpException();
}
$set = $this->setService->get($setId);
$user = $this->userService->getLoggedInUser();
$videos = $this->videoService->getVideos($user);
$videos = array_udiff($videos, $set->getVideos()->getValues(), function ($a, $b) {
return $a->getId()->compareTo($b->getId());
});
foreach ($videos as $video) {
$video->setCustomId($this->uuidMapper->toString($video->getId()));
}
return $this->render("dashboard/set-edit-add.html.twig", [
"set" => $set,
"videos" => $videos
]);
}
}

135
src/Entity/Set.php Normal file
View file

@ -0,0 +1,135 @@
<?php
namespace App\Entity;
use App\Repository\SetRepository;
use DateTimeImmutable;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Ramsey\Uuid\Doctrine\UuidGenerator;
use Ramsey\Uuid\UuidInterface;
/**
* @ORM\Entity(repositoryClass=SetRepository::class)
* @ORM\Table(name="`set`")
*/
class Set
{
/**
* @ORM\Id
* @ORM\Column(type="uuid", unique=true)
* @ORM\GeneratedValue(strategy="CUSTOM")
* @ORM\CustomIdGenerator(class=UuidGenerator::class)
*/
private $id;
private $customId;
/**
* @ORM\ManyToMany(targetEntity=Video::class, inversedBy="sets")
*/
private $videos;
/**
* @ORM\Column(type="string", length=255)
*/
private $name;
/**
* @ORM\Column(type="datetime_immutable")
*/
private $created;
/**
* @ORM\ManyToOne(targetEntity=User::class)
* @ORM\JoinColumn(nullable=false)
*/
private $creator;
public function __construct()
{
$this->videos = new ArrayCollection();
}
public function getId(): ?UuidInterface
{
return $this->id;
}
/**
* @return Collection|Video[]
*/
public function getVideos(): Collection
{
return $this->videos;
}
public function addVideo(Video $video): self
{
if (!$this->videos->contains($video)) {
$this->videos[] = $video;
}
return $this;
}
public function removeVideo(Video $video): self
{
$this->videos->removeElement($video);
return $this;
}
public function getName(): ?string
{
return $this->name;
}
public function setName(string $name): self
{
$this->name = $name;
return $this;
}
public function getCreated(): ?DateTimeImmutable
{
return $this->created;
}
public function setCreated(): self
{
$this->created = new DateTimeImmutable();
return $this;
}
public function getCreator(): ?User
{
return $this->creator;
}
public function setCreator(?User $creator): self
{
$this->creator = $creator;
return $this;
}
public function getCustomId(): string
{
return $this->customId;
}
public function setCustomId($customId): self
{
$this->customId = $customId;
return $this;
}
public function clearVideos()
{
$this->videos = [];
return $this;
}
}

View file

@ -5,6 +5,7 @@ namespace App\Entity;
use App\Repository\VideoRepository;
use DateTimeImmutable;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Ramsey\Uuid\Doctrine\UuidGenerator;
use Ramsey\Uuid\UuidInterface;
@ -73,9 +74,15 @@ class Video
private $views = 0;
/**
* @ORM\ManyToMany(targetEntity=Set::class, mappedBy="videos")
*/
private $sets;
public function __construct()
{
$this->videoLinks = new ArrayCollection();
$this->sets = new ArrayCollection();
}
public function getId(): ?UuidInterface
@ -243,4 +250,31 @@ class Video
$this->views = $views;
return $this;
}
/**
* @return Collection|Set[]
*/
public function getSets(): Collection
{
return $this->sets;
}
public function addSet(Set $set): self
{
if (!$this->sets->contains($set)) {
$this->sets[] = $set;
$set->addVideo($this);
}
return $this;
}
public function removeSet(Set $set): self
{
if ($this->sets->removeElement($set)) {
$set->removeVideo($this);
}
return $this;
}
}

View file

@ -64,6 +64,11 @@ class VideoLink
*/
private $comment;
/**
* @ORM\ManyToOne(targetEntity=Set::class)
*/
private $sets;
public function getId(): ?UuidInterface
{
return $this->id;
@ -207,4 +212,16 @@ class VideoLink
return true;
}
public function getSets(): ?Set
{
return $this->sets;
}
public function setSets(?Set $sets): self
{
$this->sets = $sets;
return $this;
}
}

32
src/Form/SetType.php Normal file
View file

@ -0,0 +1,32 @@
<?php
namespace App\Form;
use App\Entity\Set;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class SetType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, $options): void
{
$builder
->add("name", TextType::class, [
"required" => true
])
->add("submit", SubmitType::class);
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
"data_class" => Set::class,
]);
}
}

View file

@ -0,0 +1,67 @@
<?php
namespace App\Repository;
use App\Entity\Set;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @method Set|null find($id, $lockMode = null, $lockVersion = null)
* @method Set|null findOneBy(array $criteria, array $orderBy = null)
* @method Set[] findAll()
* @method Set[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
*/
class SetRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, Set::class);
}
// /**
// * @return Set[] Returns an array of Set objects
// */
/*
public function findByExampleField($value)
{
return $this->createQueryBuilder('s')
->andWhere('s.exampleField = :val')
->setParameter('val', $value)
->orderBy('s.id', 'ASC')
->setMaxResults(10)
->getQuery()
->getResult()
;
}
*/
/*
public function findOneBySomeField($value): ?Set
{
return $this->createQueryBuilder('s')
->andWhere('s.exampleField = :val')
->setParameter('val', $value)
->getQuery()
->getOneOrNullResult()
;
}
*/
public function save(Set $set)
{
$this->_em->persist($set);
$this->_em->flush();
}
public function delete(Set $set)
{
$this->_em->remove($set);
$this->_em->flush();
}
public function update($set)
{
$this->_em->flush();
}
}

View file

@ -0,0 +1,57 @@
<?php
namespace App\Service;
use App\Entity\Set;
use App\Entity\User;
use App\Repository\SetRepository;
use Ramsey\Uuid\UuidInterface;
class SetService
{
private $userService;
private $setRepository;
public function __construct(
UserService $userService,
SetRepository $setRepository
)
{
$this->userService = $userService;
$this->setRepository = $setRepository;
}
public function getAll(User $user): array
{
return $this->setRepository->findByCreator($user);
}
public function add(Set $set)
{
$set->setCreated();
$set->setCreator($this->userService->getLoggedInUser());
$set->clearVideos();
$this->setRepository->save($set);
}
public function get(UuidInterface $setId): ?Set
{
return $this->setRepository->findOneById($setId);
}
public function delete(Set $set)
{
$this->setRepository->delete($set);
}
public function update($set)
{
$this->setRepository->update($set);
}
}

View file

@ -48,6 +48,11 @@
aria-current="page"
href="{{ path("app_links") }}">Links</a>
</li>
<li class="nav-item">
<a class="nav-link {% if route_name == "app_sets" %} active {% endif %}"
aria-current="page"
href="{{ path("app_sets") }}">Sets</a>
</li>
{% endif %}
{% if is_granted('ROLE_ADMIN') %}
<li class="nav-item">

View file

@ -0,0 +1,47 @@
{% extends 'base.html.twig' %}
{% block title %}Sets{% endblock %}
{% block stylesheets %}
<link rel="stylesheet" href="{{ asset("css/sets.css") }}">
{% endblock %}
{% block javascripts %}
<script src="{{ asset("js/search.js") }}"></script>
<script>
(function () {
let elements = document.getElementsByClassName("video");
let contents = [];
for (let i = 0; i < elements.length; i++) {
contents.push(elements[i].attributes["data-content"].textContent);
}
search(document.getElementById("search-bar"), elements, contents);
})();
</script>
{% endblock %}
{% block body %}
<div class="bg-light shadow-5">
<div class="videos">
<div class="search">
<input id="search-bar" type="text" placeholder="Search" class="form-control">
</div>
<div class="list">
<ul>
{% for video in videos %}
<li class="video" data-content="{{ video.name }}">
<a class="dropdown-item" href="#">
<img alt="Thumbnail" src="{{ path("app_watch_thumbnail", {
linkId: constant("App\\Controller\\WatchController::OWNER_LINK_ID"),
videoId: video.customId
}) }}"/>
{{ video.name }}
</a>
</li>
{% endfor %}
</ul>
</div>
<button
</div>
</div>
{% endblock %}

View file

@ -0,0 +1,83 @@
{% extends 'base.html.twig' %}
{% block title %}Sets{% endblock %}
{% block stylesheets %}
<link rel="stylesheet" href="{{ asset("css/sets.css") }}">
{% endblock %}
{% block body %}
<div class="bg-light shadow-5">
<h2>Edit Set</h2>
<div class="rename">
{{ form_start(form) }}
{{ form_widget(form.name) }}
{{ form_widget(form.submit, { 'label': 'rename' }) }}
{{ form_end(form) }}
</div>
<div>
{% set editCsrfToken = csrf_token(constant("App\\Controller\\DashboardController::EDIT_SET_CSRF_TOKEN_ID")) %}
<div class="links bg-light shadow-5">
<table class="table table-hover">
<tr>
<td>#</td>
<th>
Video
</th>
<th>
</th>
</tr>
{% for video in set.videos %}
<tr>
<td>
{{ loop.index }}
</td>
<td>
<a href="{{ path("app_watch_page", {
linkId: constant("App\\Controller\\WatchController::OWNER_LINK_ID"),
videoId: video.customId
}) }}">
<img alt="Thumbnail" src="{{ path("app_watch_thumbnail", {
linkId: constant("App\\Controller\\WatchController::OWNER_LINK_ID"),
videoId: video.customId
}) }}"/>
{{ video.name }}
</a>
</td>
<td class="no-wrap">
<div class="btn-group" role="group">
<form action="" method="POST">
<input type="hidden" name="videoId" value="{{ video.customId }}">
<input type="hidden" name="csrfToken" value="{{ editCsrfToken }}">
<button class="btn btn-link dropdown-toggle deleteToggle" style="color: red;"
id="{{ video.customId }}-deleteDropDown"
data-mdb-toggle="dropdown"
aria-expanded="false"
onclick="">
<i class="fas fa-trash-alt"></i>
</button>
<div class="dropdown-menu dropdown-menu-end deleteConfirm"
id="{{ video.customId }}-deleteDropDownMenu"
aria-labelledby="{{ video.customId }}-deleteDropDown">
Do you really want to delete this video from this set?<br/>
<button class="btn btn-link deleteButton">YES</button>
<button class="btn btn-primary" onclick="removeClass('show', [
'#{{ video.customId }}-deleteDropDown',
'#{{ video.customId }}-deleteDropDownMenu'
]); return false;">NO
</button>
</div>
</form>
</div>
</td>
</tr>
{% endfor %}
</table>
</div>
</div>
<div class="addButton">
<a type="button" class="btn btn-primary btn-floating"
href="{{ path("app_edit_set_add", {"setId": set.customId}) }}">
<i class="fas fa-plus"></i>
</a>
</div>
{% endblock %}

View file

@ -0,0 +1,10 @@
{% extends 'base.html.twig' %}
{% block title %}Sets{% endblock %}
{% block stylesheets %}
<link rel="stylesheet" href="{{ asset("css/sets.css") }}">
{% endblock %}
{% block body %}
{{ form(form) }}
{% endblock %}

View file

@ -0,0 +1,83 @@
{% extends 'base.html.twig' %}
{% block title %}Sets{% endblock %}
{% block stylesheets %}
<link rel="stylesheet" href="{{ asset("css/sets.css") }}">
{% endblock %}
{% block body %}
{% if sets | length == 0 %}
<div class="no-content">
No sets yet.<br/>
Go <a href="{{ path("app_create_set") }}">here</a> to add a new set.
</div>
{% else %}
{% set deleteCsrfToken = csrf_token(constant("App\\Controller\\DashboardController::DELETE_SET_CSRF_TOKEN_ID")) %}
<div class="bg-light shadow-5">
<table class="table table-hover">
<tr>
<td></td>
<th>
Name
</th>
<th>
Created
</th>
<th>
# Videos
</th>
<th>
</th>
</tr>
{% for set in sets %}
<tr>
<td></td>
<td>
{{ set.name }}
</td>
<td>
{{ set.created | date("Y-m-d") }}
</td>
<td>
{{ set.videos | length }}
</td>
<td class="no-wrap">
<div class="btn-group" role="group">
<a class="btn btn-link" href="{{ path("app_edit_set", {"setId": set.customId}) }}">
<i class="fas fa-cog"></i>
</a>
<form action="{{ path("app_delete_set") }}" method="POST">
<input type="hidden" name="setId" value="{{ set.customId }}">
<input type="hidden" name="csrfToken" value="{{ deleteCsrfToken }}">
<button class="btn btn-link dropdown-toggle deleteToggle" style="color: red;"
id="{{ set.customId }}-deleteDropDown"
data-mdb-toggle="dropdown"
aria-expanded="false"
onclick="">
<i class="fas fa-trash-alt"></i>
</button>
<div class="dropdown-menu dropdown-menu-end deleteConfirm"
id="{{ set.customId }}-deleteDropDownMenu"
aria-labelledby="{{ set.customId }}-deleteDropDown">
Do you really want to delete this set?<br/>
<button class="btn btn-link deleteButton">YES</button>
<button class="btn btn-primary" onclick="removeClass('show', [
'#{{ set.customId }}-deleteDropDown',
'#{{ set.customId }}-deleteDropDownMenu'
]); return false;">NO
</button>
</div>
</form>
</div>
</td>
</tr>
{% endfor %}
</table>
</div>
<div class="addButton">
<a type="button" class="btn btn-primary btn-floating" href="{{ path("app_create_set") }}">
<i class="fas fa-plus"></i>
</a>
</div>
{% endif %}
{% endblock %}