[TASK] Initial implementation

This commit is contained in:
Sebastian Fischer 2025-01-21 20:22:57 +01:00
commit 1c5fd77759
58 changed files with 6547 additions and 0 deletions

3
.gitattributes vendored Normal file
View File

@ -0,0 +1,3 @@
/Build export-ignore
/.gitattributes export-ignore
/.gitignore export-ignore

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
.idea/
Build/

85
Build/.stylelintrc Normal file
View File

@ -0,0 +1,85 @@
{
"plugins": [
"stylelint-order"
],
"rules": {
"at-rule-empty-line-before": [
"always",
{
"except": [
"blockless-after-same-name-blockless",
"first-nested"
],
"ignore": [
"after-comment"
]
}
],
"block-no-empty": true,
"color-hex-length": "short",
"color-no-invalid-hex": true,
"comment-empty-line-before": [
"always",
{
"except": [
"first-nested"
],
"ignore": [
"stylelint-commands"
]
}
],
"comment-no-empty": true,
"comment-whitespace-inside": "always",
"custom-property-empty-line-before": "never",
"declaration-block-no-duplicate-properties": true,
"declaration-block-no-redundant-longhand-properties": true,
"declaration-block-no-shorthand-property-overrides": true,
"declaration-block-single-line-max-declarations": 1,
"declaration-empty-line-before": "never",
"font-family-no-duplicate-names": true,
"function-calc-no-unspaced-operator": true,
"function-linear-gradient-no-nonstandard-direction": true,
"function-name-case": "lower",
"keyframe-declaration-no-important": true,
"length-zero-no-unit": true,
"media-feature-name-no-unknown": true,
"no-empty-source": true,
"no-invalid-double-slash-comments": true,
"property-no-unknown": true,
"rule-empty-line-before": [
"always-multi-line",
{
"except": [
"first-nested"
],
"ignore": [
"after-comment"
]
}
],
"selector-pseudo-class-no-unknown": true,
"selector-pseudo-element-colon-notation": "single",
"selector-pseudo-element-no-unknown": true,
"selector-type-case": "lower",
"selector-type-no-unknown": [
true,
{
"ignore": [
"custom-elements"
]
}
],
"shorthand-property-no-redundant-values": true,
"string-no-newline": true,
"unit-no-unknown": true,
"order/order": [
"custom-properties",
"declarations"
],
"order/properties-order": [
"width",
"height"
]
}
}

15
Build/Makefile Normal file
View File

@ -0,0 +1,15 @@
.PHONY: install
install:
ddev npm install
.PHONY: build
build:
ddev npm run build:css
.PHONY: compile
compile:
ddev npm run compile:css
.PHONY: watch
watch:
ddev npm run watch:css

View File

@ -0,0 +1,8 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
.shadow-component {
-webkit-box-shadow: var(--typo3-component-box-shadow);
box-shadow: var(--typo3-component-box-shadow);
}

3523
Build/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

29
Build/package.json Normal file
View File

@ -0,0 +1,29 @@
{
"name": "ew_bloggy",
"description": "Bundles all resources for the usage",
"repository": "https://gitea.fischer.im/evoWeb/ew_bloggy.git",
"readme": "../README.md",
"homepage": "https://www.evoweb.de/",
"author": "Sebastian Fischer",
"version": "0.0.1",
"license": "GPL-2.0-or-later",
"engines": {
"node": ">=22.0.0 <23.0.0",
"npm": ">=10.0.0"
},
"type": "module",
"devDependencies": {
"css-minify": "^2.1.0",
"stylelint": "^16.13.2",
"stylelint-order": "^6.0.4",
"tailwindcss": "^3.4.17"
},
"scripts": {
"build:css": "npm run lint:css && npm run compile:css && npm run minify:css && npm run remove:css",
"lint:css": "stylelint Sources/Css/*.css",
"compile:css": "tailwindcss -i ./Sources/Css/pagelayout.css -o ../Resources/Public/Css/pagelayout.css",
"minify:css": "css-minify -f ../Resources/Public/Css/pagelayout.css -o ../Resources/Public/Css/",
"remove:css": "rm ../Resources/Public/Css/pagelayout.css",
"watch:css": "tailwindcss -i ./Sources/Css/pagelayout.css -o ../Resources/Public/Css/pagelayout.css --watch"
}
}

35
Build/tailwind.config.js Normal file
View File

@ -0,0 +1,35 @@
/** @type {import('tailwindcss').Config} */
export default {
prefix: 'tw-',
corePlugins: {
preflight: false,
},
content: [
'../Resources/Private/Templates/PageLayout/*.html'
],
plugins: [],
theme: {
extend: {
colors: {
component: {
bg: 'var(--typo3-component-bg)',
},
},
borderRadius: {
component: 'var(--typo3-component-border-radius)',
},
borderWidth: {
12: '12px',
},
gridTemplateColumns: {
component: 'repeat(auto-fit, minmax(150px, 1fr))',
},
margin: {
component: 'var(--typo3-spacing, 1em) auto',
},
spacing: {
19: '4.75rem',
}
},
},
}

View File

@ -0,0 +1,87 @@
<?php
declare(strict_types=1);
/*
* This file is developed by evoWeb.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*/
namespace Evoweb\EwBloggy\Backend\View;
use Evoweb\EwBloggy\Constants;
use Evoweb\EwBloggy\Domain\Model\Post;
use Evoweb\EwBloggy\Domain\Repository\PostRepository;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use TYPO3\CMS\Core\Type\Bitmask\Permission;
use TYPO3\CMS\Core\View\ViewFactoryData;
use TYPO3\CMS\Core\View\ViewFactoryInterface;
use TYPO3\CMS\Fluid\View\FluidViewAdapter;
readonly class PostHeaderRenderer
{
public function __construct(
private ViewFactoryInterface $viewFactory,
private PostRepository $postRepository,
) {
}
public function render(ServerRequestInterface $request): string
{
$pageUid = (int)($request->getParsedBody()['id'] ?? $request->getQueryParams()['id'] ?? 0);
$pageInfo = $this->getPageRecord($pageUid);
if ($pageInfo === null || $pageInfo['doktype'] !== Constants::DOKTYPE_BLOG_POST) {
return '';
}
$post = $this->getPostByUid($pageUid);
$view = $this->getView($request);
$view->assign('post', $post);
return $view->render();
}
protected function getPostByUid(int $pageUid): Post
{
$query = $this->postRepository->createQuery();
$querySettings = $query->getQuerySettings();
$querySettings->setIgnoreEnableFields(true);
$querySettings->setRespectStoragePage(false);
$this->postRepository->setDefaultQuerySettings($querySettings);
return $this->postRepository->findOneBy(['uid' => $pageUid]);
}
protected function getPageRecord(int $pageUid): ?array
{
$pagePermsClause = $this->getBackendUser()?->getPagePermsClause(Permission::PAGE_SHOW) ?? '';
return BackendUtility::getRecord('pages', $pageUid, 'doktype', $pagePermsClause);
}
protected function getView(ServerRequestInterface $request): FluidViewAdapter
{
$viewFactoryData = new ViewFactoryData(
['EXT:ew_bloggy/Resources/Private/Templates'],
['EXT:ew_bloggy/Resources/Private/Partials'],
['EXT:ew_bloggy/Resources/Private/Layouts'],
request: $request,
);
/** @var FluidViewAdapter $view */
$view = $this->viewFactory->create($viewFactoryData);
$view->getRenderingContext()->setControllerName('PageLayout');
$view->getRenderingContext()->setControllerAction('Header');
return $view;
}
protected function getBackendUser(): ?BackendUserAuthentication
{
return $GLOBALS['BE_USER'] ?? null;
}
}

23
Classes/Constants.php Normal file
View File

@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
/*
* This file is developed by evoWeb.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*/
namespace Evoweb\EwBloggy;
class Constants
{
public const DOKTYPE_BLOG_POST = 137;
public const CATEGORY_TYPE_BLOG = 100;
}

View File

@ -0,0 +1,126 @@
<?php
declare(strict_types=1);
/*
* This file is part of the package t3g/blog.
*
* For the full copyright and license information, please read the
* LICENSE file that was distributed with this source code.
*/
namespace Evoweb\EwBloggy\Controller;
use DateTimeImmutable;
use Psr\Http\Message\ResponseInterface;
use Evoweb\EwBloggy\Domain\Repository\PostRepository;
use Evoweb\EwBloggy\Pagination\BlogPagination;
use T3G\AgencyPack\Blog\Service\MetaTagService;
use T3G\AgencyPack\Blog\Utility\ArchiveUtility;
use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;
use TYPO3\CMS\Extbase\Pagination\QueryResultPaginator;
use TYPO3\CMS\Extbase\Persistence\QueryResultInterface;
use TYPO3\CMS\Extbase\Utility\LocalizationUtility;
class PostController extends ActionController
{
public function __construct(protected PostRepository $postRepository)
{
}
protected function initializeView(): void
{
foreach (($this->settings['flexform'] ?? []) as $key => $value) {
if (!empty($value)) {
$this->settings[$key] = $value;
}
}
if (isset($this->settings['flexform'])) {
unset($this->settings['flexform']);
}
$this->view->assign('settings', $this->settings);
$this->view->assign('data', $this->request->getAttribute('currentContentObject')?->data ?? null);
}
/**
* Show a list of recent posts.
*/
public function listRecentPostsAction(int $currentPage = 1): ResponseInterface
{
$maximumItems = (int) ($this->settings['lists']['posts']['maximumDisplayedItems'] ?? 0);
$posts = (0 === $maximumItems)
? $this->postRepository->findAll()
: $this->postRepository->findAllWithLimit($maximumItems);
$pagination = $this->getPagination($posts, $currentPage);
$this->view->assign('posts', $posts);
$this->view->assign('pagination', $pagination);
return $this->htmlResponse();
}
/**
* Show a number of latest posts.
*/
public function listLatestPostsAction(): ResponseInterface
{
$limit = (int) ($this->settings['limit'] ?? 3);
$posts = $this->postRepository->findAllWithLimit($limit);
$this->view->assign('posts', $posts);
return $this->htmlResponse();
}
/**
* Show posts beyond a certain date
*/
public function listPostsByDateAction(
?int $year = null,
?int $month = null,
int $currentPage = 1
): ResponseInterface {
if ($year === null) {
$posts = $this->postRepository->findMonthsAndYearsWithPosts();
$this->view->assign('archiveData', ArchiveUtility::extractDataFromPosts($posts));
} else {
$dateTime = new DateTimeImmutable(sprintf('%d-%d-1', $year, $month ?? 1));
$posts = $this->postRepository->findByMonthAndYear($year, $month);
$pagination = $this->getPagination($posts, $currentPage);
$this->view->assign('type', 'bydate');
$this->view->assign('month', $month);
$this->view->assign('year', $year);
$this->view->assign('timestamp', $dateTime->getTimestamp());
$this->view->assign('posts', $posts);
$this->view->assign('pagination', $pagination);
$title = str_replace(
[
'###MONTH###',
'###MONTH_NAME###',
'###YEAR###',
],
[
$month,
$dateTime->format('F'),
$year,
],
(string) LocalizationUtility::translate('meta.title.listPostsByDate', 'ew_bloggy')
);
MetaTagService::set(MetaTagService::META_TITLE, (string) $title);
MetaTagService::set(
MetaTagService::META_DESCRIPTION,
(string) LocalizationUtility::translate('meta.description.listPostsByDate', 'ew_bloggy')
);
}
return $this->htmlResponse();
}
protected function getPagination(QueryResultInterface $objects, int $currentPage = 1): ?BlogPagination
{
$maximumNumberOfLinks = (int) ($this->settings['lists']['pagination']['maximumNumberOfLinks'] ?? 0);
$itemsPerPage = 10;
if ($this->request->getFormat() === 'html') {
$itemsPerPage = (int) ($this->settings['lists']['pagination']['itemsPerPage'] ?? 10);
}
$paginator = new QueryResultPaginator($objects, $currentPage, $itemsPerPage);
return new BlogPagination($paginator, $maximumNumberOfLinks);
}
}

View File

@ -0,0 +1,61 @@
<?php
declare(strict_types=1);
/*
* This file is developed by evoWeb.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*/
namespace Evoweb\EwBloggy\DataProcessing;
use Evoweb\EwBloggy\Domain\Repository\PostRepository;
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
use TYPO3\CMS\Frontend\ContentObject\DataProcessorInterface;
/**
* Fetch blog records from the database, using the default .select syntax from TypoScript.
*
* This way, e.g. a FLUIDTEMPLATE cObject can iterate over the array of records.
*
* Example TypoScript configuration:
*
* 10 = Evoweb\EwBloggy\DataProcessing\BlogPostProcessor
* 10 {
* as = post
* }
*
* 10 = blog-post
*
* where "as" means the variable to be containing the result-set from the DB query.
*/
readonly class BlogPostProcessor implements DataProcessorInterface
{
public function __construct(
protected PostRepository $postRepository,
) {
}
public function process(
ContentObjectRenderer $cObj,
array $contentObjectConfiguration,
array $processorConfiguration,
array $processedData
): array {
if (!$cObj->checkIf($processorConfiguration['if.'] ?? [])) {
return $processedData;
}
$targetVariableName = $cObj->stdWrapValue('as', $processorConfiguration, 'post');
$processedData[$targetVariableName] = $this->postRepository->findByUid((int)$processedData['data']['uid']);
return $processedData;
}
}

View File

@ -0,0 +1,175 @@
<?php
declare(strict_types=1);
/*
* This file is developed by evoWeb.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*/
namespace Evoweb\EwBloggy\Domain\Model;
use TYPO3\CMS\Extbase\Annotation as Extbase;
use TYPO3\CMS\Extbase\Domain\Model\FileReference;
use TYPO3\CMS\Extbase\DomainObject\AbstractEntity;
use TYPO3\CMS\Extbase\Persistence\ObjectStorage;
class Author extends AbstractEntity
{
protected string $name = '';
protected string $title = '';
protected ?FileReference $image = null;
protected string $email = '';
protected string $website = '';
protected string $profile = '';
protected string $bio = '';
protected int $detailsPage = 0;
/**
* @var ObjectStorage<Post>
*/
#[Extbase\ORM\Lazy]
protected ObjectStorage $posts;
public function __construct()
{
$this->initializeObject();
}
public function initializeObject(): void
{
$this->posts = new ObjectStorage();
}
/**
* @return ObjectStorage<Post>
*/
public function getPosts(): ObjectStorage
{
return $this->posts;
}
/**
* @param ObjectStorage<Post> $posts
*/
public function setPosts(ObjectStorage $posts): void
{
$this->posts = $posts;
}
public function addPost(Post $post): void
{
$this->posts->attach($post);
}
public function removePost(Post $post): void
{
$this->posts->detach($post);
}
public function getName(): ?string
{
return $this->name;
}
public function setName(string $name): void
{
$this->name = $name;
}
public function getTitle(): string
{
return $this->title;
}
public function setTitle(string $title): void
{
$this->title = $title;
}
public function getImage(): ?FileReference
{
return $this->image;
}
public function setImage(FileReference $image): void
{
$this->image = $image;
}
public function getWebsite(): string
{
return $this->website;
}
public function setWebsite(string $website): void
{
$this->website = $website;
}
public function getEmail(): string
{
return $this->email;
}
public function setEmail(string $email): void
{
$this->email = $email;
}
public function getProfile(): string
{
return $this->profile;
}
public function setProfile(string $profile): void
{
$this->profile = $profile;
}
public function getBio(): string
{
return $this->bio;
}
public function setBio(string $bio): void
{
$this->bio = $bio;
}
public function getDetailsPage(): int
{
return $this->detailsPage;
}
public function setDetailsPage(int $page): void
{
$this->detailsPage = $page;
}
public function getAllTags(): array
{
$uniqueTags = [];
foreach ($this->getPosts() as $post) {
foreach ($post->getTags() as $tag) {
if (!array_key_exists((int) $tag->getUid(), $uniqueTags)) {
$uniqueTags[(int) $tag->getUid()] = $tag;
}
}
}
return $uniqueTags;
}
}

View File

@ -0,0 +1,102 @@
<?php
declare(strict_types=1);
/*
* This file is developed by evoWeb.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*/
namespace Evoweb\EwBloggy\Domain\Model;
use Evoweb\EwBloggy\Constants;
use TYPO3\CMS\Extbase\Annotation as Extbase;
use TYPO3\CMS\Extbase\DomainObject\AbstractEntity;
use TYPO3\CMS\Extbase\Persistence\ObjectStorage;
use TYPO3\CMS\Extbase\Persistence\Generic\LazyLoadingProxy;
class Category extends AbstractEntity
{
protected string $title = '';
protected string $description = '';
protected int $recordType = Constants::CATEGORY_TYPE_BLOG;
#[Extbase\ORM\Lazy]
protected Category|LazyLoadingProxy|null $parent;
/**
* @var ObjectStorage<Post>
*/
#[Extbase\ORM\Lazy]
protected ObjectStorage $posts;
public function __construct()
{
$this->initializeObject();
}
public function initializeObject(): void
{
$this->posts = new ObjectStorage();
}
/**
* @return ObjectStorage<Post>
*/
public function getPosts(): ObjectStorage
{
return $this->posts;
}
/**
* @param ObjectStorage<Post> $posts
*/
public function setPosts(ObjectStorage $posts): void
{
$this->posts = $posts;
}
public function getRecordType(): int
{
return $this->recordType;
}
public function getTitle(): string
{
return $this->title;
}
public function setTitle(string $title): void
{
$this->title = $title;
}
public function getDescription(): string
{
return $this->description;
}
public function setDescription(string $description): self
{
$this->description = $description;
return $this;
}
public function getParent(): ?self
{
return $this->parent;
}
public function setParent(self $parent): void
{
$this->parent = $parent;
}
}

View File

@ -0,0 +1,324 @@
<?php
declare(strict_types=1);
/*
* This file is developed by evoWeb.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*/
namespace Evoweb\EwBloggy\Domain\Model;
use Evoweb\EwBloggy\Constants;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Annotation as Extbase;
use TYPO3\CMS\Extbase\Domain\Model\FileReference;
use TYPO3\CMS\Extbase\DomainObject\AbstractEntity;
use TYPO3\CMS\Extbase\Mvc\Web\Routing\UriBuilder;
use TYPO3\CMS\Extbase\Persistence\ObjectStorage;
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
use TYPO3\CMS\Frontend\Typolink\LinkFactory;
class Post extends AbstractEntity
{
protected bool $hidden = false;
protected int $doktype = Constants::DOKTYPE_BLOG_POST;
protected string $title = '';
protected string $subtitle = '';
protected string $abstract = '';
protected string $description = '';
protected bool $commentsActive = true;
protected int $archiveDate = 0;
protected int $publishDate = 0;
protected \DateTime $crdate;
protected ?FileReference $featuredImage = null;
/**
* @var ObjectStorage<Category>
*/
#[Extbase\ORM\Lazy]
protected ObjectStorage $categories;
/**
* @var ObjectStorage<Tag>
*/
#[Extbase\ORM\Lazy]
protected ObjectStorage $tags;
/**
* @var ObjectStorage<Author>
*/
#[Extbase\ORM\Lazy]
protected ObjectStorage $authors;
/**
* @var ObjectStorage<FileReference>
*/
#[Extbase\ORM\Lazy]
protected ObjectStorage $media;
public function __construct()
{
$this->initializeObject();
}
public function initializeObject(): void
{
$this->categories = new ObjectStorage();
$this->tags = new ObjectStorage();
$this->authors = new ObjectStorage();
$this->media = new ObjectStorage();
}
public function addCategory(Category $category): void
{
$this->categories->attach($category);
}
public function removeCategory(Category $category): void
{
$this->categories->detach($category);
}
/**
* @return ObjectStorage<Category>
*/
public function getCategories(): ObjectStorage
{
return $this->categories;
}
/**
* @param ObjectStorage<Category> $categories
*/
public function setCategories(ObjectStorage $categories): void
{
$this->categories = $categories;
}
public function addTag(Tag $tag): void
{
$this->tags->attach($tag);
}
public function removeTag(Tag $tag): void
{
$this->tags->detach($tag);
}
/**
* @return ObjectStorage<Tag>
*/
public function getTags(): ObjectStorage
{
return $this->tags;
}
/**
* @param ObjectStorage<Tag> $tags
*/
public function setTags(ObjectStorage $tags): void
{
$this->tags = $tags;
}
public function addAuthor(Author $author): void
{
$this->authors->attach($author);
}
public function removeAuthor(Author $author): void
{
$this->authors->detach($author);
}
/**
* @return ObjectStorage<Author>
*/
public function getAuthors(): ObjectStorage
{
return $this->authors;
}
/**
* @param ObjectStorage<Author> $authors
*/
public function setAuthors(ObjectStorage $authors): void
{
$this->authors = $authors;
}
/**
* @return ObjectStorage<FileReference>
*/
public function getMedia(): ObjectStorage
{
return $this->media;
}
/**
* @param ObjectStorage<FileReference> $media
*/
public function setMedia(ObjectStorage $media): void
{
$this->media = $media;
}
public function getFeaturedImage(): ?FileReference
{
return $this->featuredImage;
}
public function setFeaturedImage(?FileReference $featuredImage): void
{
$this->featuredImage = $featuredImage;
}
public function getHidden(): bool
{
return $this->hidden;
}
public function isHidden(): bool
{
return $this->getHidden();
}
public function getDoktype(): int
{
return $this->doktype;
}
public function getTitle(): string
{
return $this->title;
}
public function setTitle(string $title): void
{
$this->title = $title;
}
public function getSubtitle(): string
{
return $this->subtitle;
}
public function setSubtitle(string $subtitle): void
{
$this->subtitle = $subtitle;
}
public function getAbstract(): string
{
return $this->abstract;
}
public function setAbstract(string $abstract): void
{
$this->abstract = $abstract;
}
public function getDescription(): string
{
return $this->description;
}
public function setDescription(string $description): void
{
$this->description = $description;
}
public function getCrdate(): \DateTime
{
return $this->crdate;
}
public function setCrdate(\DateTime $crdate): void
{
$this->crdate = $crdate;
}
public function getCommentsActive(): bool
{
return $this->commentsActive;
}
public function setCommentsActive(bool $commentsActive): void
{
$this->commentsActive = $commentsActive;
}
public function getArchiveDate(): int
{
return $this->archiveDate;
}
public function setArchiveDate(int $archiveDate): void
{
$this->archiveDate = $archiveDate;
}
public function getPublishDate(): int
{
return $this->publishDate;
}
public function setPublishDate(int $publishDate): void
{
$this->publishDate = $publishDate;
}
public function getUri(): string
{
if (class_exists(LinkFactory::class)) {
return (string) GeneralUtility::makeInstance(LinkFactory::class)->create(
'',
[
'parameter' => (string) $this->getUid(),
'forceAbsoluteUrl' => true
],
GeneralUtility::makeInstance(ContentObjectRenderer::class)
)->getUrl();
}
return GeneralUtility::makeInstance(UriBuilder::class)
->setCreateAbsoluteUri(true)
->setTargetPageUid((int) $this->getUid())
->build();
}
public function getAsArray(): array
{
return $this->__toArray();
}
public function __toArray(): array
{
return [
'uid' => $this->getUid(),
'hidden' => $this->getHidden(),
'doktype' => $this->getDoktype(),
'title' => $this->getTitle(),
'subtitle' => $this->getSubtitle(),
'abstract' => $this->getAbstract(),
'description' => $this->getDescription()
];
}
}

View File

@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
/*
* This file is developed by evoWeb.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*/
namespace Evoweb\EwBloggy\Domain\Model;
use TYPO3\CMS\Extbase\DomainObject\AbstractEntity;
class Tag extends AbstractEntity
{
protected string $title = '';
protected string $description = '';
public function getTitle(): string
{
return $this->title;
}
public function setTitle(string $title): void
{
$this->title = $title;
}
public function getDescription(): string
{
return $this->description;
}
public function setDescription(string $description): void
{
$this->description = $description;
}
}

View File

@ -0,0 +1,166 @@
<?php
declare(strict_types=1);
/*
* This file is developed by evoWeb.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*/
namespace Evoweb\EwBloggy\Domain\Repository;
use DateMalformedStringException;
use DateTimeImmutable;
use Evoweb\EwBloggy\Constants;
use Evoweb\EwBloggy\Domain\Model\Post;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Extbase\Persistence\Exception\InvalidQueryException;
use TYPO3\CMS\Extbase\Persistence\QueryInterface;
use TYPO3\CMS\Extbase\Persistence\QueryResultInterface;
use TYPO3\CMS\Extbase\Persistence\Repository;
/**
* A post repository
*
* @extends Repository<Post>
*/
class PostRepository extends Repository
{
protected array $defaultConstraints = [];
public function initializeObject(): void
{
$query = $this->createQuery();
$this->defaultConstraints[] = $query->equals('doktype', Constants::DOKTYPE_BLOG_POST);
if ($this->getRequest()->getAttribute('language')?->getLanguageId() === 0) {
$this->defaultConstraints[] = $query->logicalOr(
$query->equals('l18n_cfg', 0),
$query->equals('l18n_cfg', 2)
);
} else {
try {
$this->defaultConstraints[] = $query->lessThan('l18n_cfg', 2);
} catch (InvalidQueryException) {
}
}
$this->defaultOrderings = [
'publish_date' => QueryInterface::ORDER_DESCENDING,
];
}
/**
* @return QueryResultInterface<Post>
*/
public function findAll(): QueryResultInterface
{
return $this->getFindAllQuery()->execute();
}
/**
* @return QueryResultInterface<Post>
*/
public function findAllWithLimit(int $limit): QueryResultInterface
{
$query = $this->getFindAllQuery();
$query->setLimit($limit);
return $query->execute();
}
public function findMonthsAndYearsWithPosts(): array
{
$query = $this->createQuery();
$constraints = $this->defaultConstraints;
try {
$constraints[] = $query->greaterThan('crdateMonth', 0);
$constraints[] = $query->greaterThan('crdateYear', 0);
} catch (InvalidQueryException) {
}
$query->matching($query->logicalAnd(...$constraints));
$posts = $query->execute(true);
$result = [];
$currentIndex = -1;
$currentYear = null;
$currentMonth = null;
foreach ($posts as $post) {
$year = $post['crdate_year'];
$month = $post['crdate_month'];
if ($currentYear !== $year || $currentMonth !== $month) {
$currentIndex++;
$currentYear = $year;
$currentMonth = $month;
$result[$currentIndex] = [
'year' => $currentYear,
'month' => $currentMonth,
'count' => 1
];
} else {
$result[$currentIndex]['count']++;
}
}
return $result;
}
/**
* @return QueryResultInterface<Post>
*/
public function findByMonthAndYear(int $year, ?int $month = null): QueryResultInterface
{
$query = $this->createQuery();
$constraints = $this->defaultConstraints;
try {
if ($month !== null) {
$startDate = new DateTimeImmutable(sprintf('%d-%d-1 00:00:00', $year, $month));
$endDate = new DateTimeImmutable(
sprintf('%d-%d-%d 23:59:59', $year, $month, (int)$startDate->format('t'))
);
} else {
$startDate = new DateTimeImmutable(sprintf('%d-1-1 00:00:00', $year));
$endDate = new DateTimeImmutable(sprintf('%d-12-31 23:59:59', $year));
}
$constraints[] = $query->greaterThanOrEqual('publish_date', $startDate->getTimestamp());
$constraints[] = $query->lessThanOrEqual('publish_date', $endDate->getTimestamp());
} catch (InvalidQueryException | DateMalformedStringException) {
}
return $query->matching($query->logicalAnd(...$constraints))->execute();
}
/**
* @return QueryInterface<Post>
*/
protected function getFindAllQuery(): QueryInterface
{
$query = $this->createQuery();
$constraints = $this->defaultConstraints;
try {
$constraints[] = $query->logicalOr(
$query->equals('archiveDate', 0),
$query->greaterThanOrEqual('archiveDate', time())
);
} catch (InvalidQueryException) {
}
$query->matching($query->logicalAnd(...$constraints));
return $query;
}
protected function getRequest(): ?ServerRequestInterface
{
return $GLOBALS['TYPO3_REQUEST'];
}
}

View File

@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
/*
* This file is developed by evoWeb.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*/
namespace Evoweb\EwBloggy\EventListener;
use Evoweb\EwBloggy\Backend\View\PostHeaderRenderer;
use TYPO3\CMS\Backend\Controller\Event\ModifyPageLayoutContentEvent;
readonly class ModifyPageLayoutContent
{
public function __construct(
private PostHeaderRenderer $blogPostHeaderContentRenderer
) {
}
public function __invoke(ModifyPageLayoutContentEvent $event): void
{
$request = $event->getRequest();
$content = $this->blogPostHeaderContentRenderer->render($request);
$event->addHeaderContent($content);
}
}

View File

@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
/*
* This file is developed by evoWeb.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*/
namespace Evoweb\EwBloggy\EventListener;
use Evoweb\EwBloggy\Backend\View\PostHeaderRenderer;
use TYPO3\CMS\Backend\Controller\Event\RenderAdditionalContentToRecordListEvent;
readonly class RenderAdditionalContentToRecordList
{
public function __construct(
private PostHeaderRenderer $blogPostHeaderContentRenderer
) {
}
public function __invoke(RenderAdditionalContentToRecordListEvent $event): void
{
$request = $event->getRequest();
$content = $this->blogPostHeaderContentRenderer->render($request);
$event->addContentAbove($content);
}
}

View File

@ -0,0 +1,167 @@
<?php
declare(strict_types=1);
/*
* This file is developed by evoWeb.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*/
namespace Evoweb\EwBloggy\Pagination;
use TYPO3\CMS\Core\Pagination\PaginationInterface;
use TYPO3\CMS\Core\Pagination\PaginatorInterface;
final class BlogPagination implements PaginationInterface
{
protected int $maximumNumberOfLinks = 10;
protected int $displayRangeStart = 0;
protected int $displayRangeEnd = 0;
protected PaginatorInterface $paginator;
public function __construct(PaginatorInterface $paginator, int $maximumNumberOfLinks = 10)
{
$this->paginator = $paginator;
$this->maximumNumberOfLinks = $maximumNumberOfLinks > 0 ? $maximumNumberOfLinks : 1;
$this->displayRangeStart = $this->getFirstPageNumber();
$this->displayRangeEnd = $this->getLastPageNumber();
$this->generateDisplayRange();
}
protected function generateDisplayRange(): void
{
$currentPageNumber = $this->getCurrentPageNumber();
$maximumNumberOfLinks = $this->getMaximumNumberOfLinks();
$delta = (int) floor($maximumNumberOfLinks / 2);
if ($this->getNumberOfPages() > $maximumNumberOfLinks) {
$this->displayRangeStart = $currentPageNumber - $delta;
$this->displayRangeEnd = $this->displayRangeStart + $maximumNumberOfLinks - 1;
while ($this->displayRangeStart < $this->getFirstPageNumber()) {
$this->displayRangeStart++;
$this->displayRangeEnd++;
}
while ($this->displayRangeEnd > $this->getLastPageNumber()) {
$this->displayRangeStart--;
$this->displayRangeEnd--;
}
}
}
public function getPreviousPageNumber(): ?int
{
$previousPage = $this->paginator->getCurrentPageNumber() - 1;
if ($previousPage > $this->paginator->getNumberOfPages()) {
return null;
}
return $previousPage >= $this->getFirstPageNumber()
? $previousPage
: null;
}
public function getCurrentPageNumber(): int
{
return $this->paginator->getCurrentPageNumber();
}
public function getNextPageNumber(): ?int
{
$nextPage = $this->paginator->getCurrentPageNumber() + 1;
return $nextPage <= $this->paginator->getNumberOfPages()
? $nextPage
: null;
}
public function getFirstPageNumber(): int
{
return 1;
}
public function getLastPageNumber(): int
{
return $this->paginator->getNumberOfPages();
}
public function getStartRecordNumber(): int
{
if ($this->paginator->getCurrentPageNumber() > $this->paginator->getNumberOfPages()) {
return 0;
}
return $this->paginator->getKeyOfFirstPaginatedItem() + 1;
}
public function getEndRecordNumber(): int
{
if ($this->paginator->getCurrentPageNumber() > $this->paginator->getNumberOfPages()) {
return 0;
}
return $this->paginator->getKeyOfLastPaginatedItem() + 1;
}
public function getNumberOfPages(): int
{
return $this->paginator->getNumberOfPages();
}
/**
* @return int[]
*/
public function getAllPageNumbers(): array
{
return range($this->getFirstPageNumber(), $this->getLastPageNumber());
}
public function getDisplayRangeStart(): int
{
return $this->displayRangeStart;
}
public function getDisplayRangeEnd(): int
{
return $this->displayRangeEnd;
}
/**
* @return int[]
*/
public function getDisplayPageNumbers(): array
{
return range($this->getDisplayRangeStart(), $this->getDisplayRangeEnd());
}
public function hasLessPages(): bool
{
return $this->getDisplayRangeStart() > $this->getFirstPageNumber();
}
public function hasMorePages(): bool
{
return $this->getDisplayRangeEnd() < $this->getLastPageNumber();
}
public function getMaximumNumberOfLinks(): int
{
return $this->maximumNumberOfLinks;
}
public function getPaginator(): PaginatorInterface
{
return $this->paginator;
}
public function getPaginatedItems(): iterable
{
return $this->paginator->getPaginatedItems();
}
}

View File

@ -0,0 +1,76 @@
<?php
declare(strict_types=1);
/*
* This file is developed by evoWeb.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*/
namespace Evoweb\EwBloggy\ViewHelpers\Link;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Extbase\Mvc\Web\Routing\UriBuilder;
use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractTagBasedViewHelper;
class ArchiveViewHelper extends AbstractTagBasedViewHelper
{
protected $tagName = 'a';
public function __construct(protected UriBuilder $uriBuilder)
{
parent::__construct();
}
public function initializeArguments(): void
{
parent::initializeArguments();
$this->registerArgument('month', 'int', 'The month to link to');
$this->registerArgument('year', 'int', 'The year to link to', true);
}
public function render(): string
{
$request = $this->renderingContext->getAttribute(ServerRequestInterface::class);
$year = (int)$this->arguments['year'];
$month = (int)$this->arguments['month'];
// @todo migrate to site settings
$pageUid = (int)($request->getAttribute('frontend.typoscript')
->getSetupTree()
->getChildByName('plugin')
?->getChildByName('tx_ewbloggy')
?->getChildByName('settings')
?->getChildByName('archiveUid')
?->getValue() ?? 0);
$arguments = [
'year' => $year
];
if ($month > 0) {
$arguments['month'] = $month;
}
$this->uriBuilder->reset()
->setRequest($request)
->setTargetPageUid($pageUid);
$uri = $this->uriBuilder->uriFor('listPostsByDate', $arguments, 'Post', 'EwBloggy', 'Archive');
$linkText = $this->renderChildren() ?? implode('-', $arguments);
if ($uri !== '') {
$this->tag->addAttribute('href', $uri);
$this->tag->setContent((string)$linkText);
$result = $this->tag->render();
} else {
$result = $linkText;
}
return (string)$result;
}
}

View File

@ -0,0 +1,87 @@
<?php
declare(strict_types=1);
/*
* This file is developed by evoWeb.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*/
namespace Evoweb\EwBloggy\ViewHelpers\Link\Be;
use Psr\Http\Message\ServerRequestInterface;
use Evoweb\EwBloggy\Domain\Model\Post;
use TYPO3\CMS\Backend\Routing\UriBuilder;
use TYPO3\CMS\Extbase\Utility\LocalizationUtility;
use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractTagBasedViewHelper;
class PostViewHelper extends AbstractTagBasedViewHelper
{
/**
* @var string
*/
protected $tagName = 'a';
public function __construct(readonly private UriBuilder $uriBuilder)
{
parent::__construct();
}
public function initializeArguments(): void
{
parent::initializeArguments();
$this->registerArgument('post', Post::class, 'The post to link to', true);
$this->registerArgument('returnUri', 'bool', 'return only uri', false, false);
$this->registerArgument('action', 'string', 'action to link', false, null);
}
public function render(): string
{
$request = $this->renderingContext->getAttribute(ServerRequestInterface::class);
if (!$request instanceof ServerRequestInterface) {
throw new \RuntimeException(
'ViewHelper ew:link.be.post needs a request implementing ServerRequestInterface.',
1684305293
);
}
/** @var Post $post */
$post = $this->arguments['post'];
switch ($this->arguments['action']) {
case 'edit':
$uri = (string)$this->uriBuilder->buildUriFromRoute('record_edit', [
'edit' => ['pages' => [$post->getUid() => 'edit']],
'returnUrl' => $request->getAttribute('normalizedParams')->getRequestUri(),
]);
break;
default:
$uri = (string)$this->uriBuilder->buildUriFromRoute('web_layout', [
'id' => $post->getUid(),
'returnUrl' => $request->getAttribute('normalizedParams')->getRequestUri(),
]);
break;
}
if (isset($this->arguments['returnUri']) && $this->arguments['returnUri'] === true) {
return htmlspecialchars($uri, ENT_QUOTES | ENT_HTML5);
}
$linkText = $this->renderChildren() ?? (
$post->getTitle() !== ''
? $post->getTitle()
: LocalizationUtility::translate('backend.message.nopost', 'blog')
);
$this->tag->addAttribute('href', $uri);
$this->tag->setContent($linkText);
return $this->tag->render();
}
}

View File

@ -0,0 +1,18 @@
<?php
declare(strict_types = 1);
/*
* This file is part of the package t3g/blog.
*
* For the full copyright and license information, please read the
* LICENSE file that was distributed with this source code.
*/
return [
\Evoweb\EwBloggy\Domain\Model\Post::class => [
'tableName' => 'pages',
],
\Evoweb\EwBloggy\Domain\Model\Category::class => [
'tableName' => 'sys_category',
],
];

View File

@ -0,0 +1,28 @@
<T3DataStructure>
<sheets>
<sDEF>
<ROOT>
<sheetTitle>LLL:EXT:ew_bloggy/Resources/Private/Language/locallang_db.xlf:archive</sheetTitle>
<type>array</type>
<el>
<settings.flexform.limit>
<label>LLL:EXT:ew_bloggy/Resources/Private/Language/locallang_db.xlf:settings.flexform.limit</label>
<config>
<type>number</type>
</config>
</settings.flexform.limit>
<settings.flexform.postsUid>
<label>LLL:EXT:ew_bloggy/Resources/Private/Language/locallang_db.xlf:settings.flexform.postsUid</label>
<config>
<type>group</type>
<allowed>pages</allowed>
<size>1</size>
<maxitems>1</maxitems>
</config>
</settings.flexform.postsUid>
</el>
</ROOT>
</sDEF>
</sheets>
</T3DataStructure>

View File

@ -0,0 +1,38 @@
<T3DataStructure>
<sheets>
<sDEF>
<ROOT>
<sheetTitle>LLL:EXT:ew_bloggy/Resources/Private/Language/locallang_db.xlf:latestpost</sheetTitle>
<type>array</type>
<el>
<settings.flexform.limit>
<label>LLL:EXT:ew_bloggy/Resources/Private/Language/locallang_db.xlf:settings.flexform.limit</label>
<config>
<type>number</type>
</config>
</settings.flexform.limit>
<settings.flexform.postsUid>
<label>LLL:EXT:ew_bloggy/Resources/Private/Language/locallang_db.xlf:settings.flexform.postsUid</label>
<config>
<type>group</type>
<allowed>pages</allowed>
<size>1</size>
<maxitems>1</maxitems>
</config>
</settings.flexform.postsUid>
<settings.flexform.archiveUid>
<label>LLL:EXT:ew_bloggy/Resources/Private/Language/locallang_db.xlf:settings.flexform.postsUid</label>
<config>
<type>group</type>
<allowed>pages</allowed>
<size>1</size>
<maxitems>1</maxitems>
</config>
</settings.flexform.archiveUid>
</el>
</ROOT>
</sDEF>
</sheets>
</T3DataStructure>

View File

@ -0,0 +1,28 @@
<T3DataStructure>
<sheets>
<sDEF>
<ROOT>
<sheetTitle>LLL:EXT:ew_bloggy/Resources/Private/Language/locallang_db.xlf:posts</sheetTitle>
<type>array</type>
<el>
<settings.flexform.limit>
<label>LLL:EXT:ew_bloggy/Resources/Private/Language/locallang_db.xlf:settings.flexform.limit</label>
<config>
<type>number</type>
</config>
</settings.flexform.limit>
<settings.flexform.archiveUid>
<label>LLL:EXT:ew_bloggy/Resources/Private/Language/locallang_db.xlf:settings.flexform.postsUid</label>
<config>
<type>group</type>
<allowed>pages</allowed>
<size>1</size>
<maxitems>1</maxitems>
</config>
</settings.flexform.archiveUid>
</el>
</ROOT>
</sDEF>
</sheets>
</T3DataStructure>

12
Configuration/Icons.php Normal file
View File

@ -0,0 +1,12 @@
<?php
use TYPO3\CMS\Core\Imaging\IconProvider\SvgIconProvider;
return array_map(static fn (string $source) => ['provider' => SvgIconProvider::class, 'source' => $source], [
'record-blog-author' => 'EXT:ew_bloggy/Resources/Public/Icons/blog-author.svg',
'record-blog-category' => 'EXT:ew_bloggy/Resources/Public/Icons/blog-category.svg',
'record-blog-post' => 'EXT:ew_bloggy/Resources/Public/Icons/blog-post.svg',
'plugin-blog-archive' => 'EXT:ew_bloggy/Resources/Public/Icons/plugin-blog-archive.svg',
'plugin-blog-latestposts' => 'EXT:ew_bloggy/Resources/Public/Icons/plugin-blog-latestposts.svg',
'plugin-blog-posts' => 'EXT:ew_bloggy/Resources/Public/Icons/plugin-blog-posts.svg',
]);

View File

@ -0,0 +1,23 @@
services:
_defaults:
autowire: true
autoconfigure: true
public: false
Evoweb\EwBloggy\:
resource: '../Classes/*'
Evoweb\EwBloggy\DataProcessing\BlogPostProcessor:
public: true
tags:
- { name: 'data.processor', identifier: 'blog-post' }
Evoweb\EwBloggy\EventListener\ModifyPageLayoutContent:
tags:
- name: event.listener
identifier: 'evoweb/ew-bloggy/modify-page-module-content'
Evoweb\EwBloggy\EventListener\RenderAdditionalContentToRecordList:
tags:
- name: event.listener
identifier: 'evoweb/ew-bloggy/render-additional-content-to-record-list'

View File

@ -0,0 +1 @@
name: evoweb/ew-bloggy

View File

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
<file source-language="en" datatype="plaintext" original="EXT:ew_bloggy/Configuration/Sets/Plugin/labels.xlf" date="2025-01-19T16:00:00Z" product-name="ew_bloggy">
<header/>
<body>
<trans-unit id="label" resname="label">
<source>Simple and small blog</source>
</trans-unit>
<trans-unit id="categories.ew-bloggy" resname="categories.ew-bloggy">
<source>Simple and small blog</source>
</trans-unit>
<trans-unit id="settings.evoweb.tx-ewbloggy.limit" resname="settings.evoweb.tx-ewbloggy.limit">
<source>Limit of posts to display</source>
</trans-unit>
<trans-unit id="settings.evoweb.tx-ewbloggy.postsUid" resname="settings.evoweb.tx-ewbloggy.postsUid">
<source>Posts page uid</source>
</trans-unit>
<trans-unit id="settings.evoweb.tx-ewbloggy.archiveUid" resname="settings.evoweb.tx-ewbloggy.archiveUid">
<source>Archive page uid</source>
</trans-unit>
</body>
</file>
</xliff>

View File

@ -0,0 +1,16 @@
categories:
ew-bloggy: ~
settings:
evoweb.tx-ewbloggy.limit:
type: int
default: 0
category: ew-bloggy
evoweb.tx-ewbloggy.postsUid:
type: int
default: 0
category: ew-bloggy
evoweb.tx-ewbloggy.archiveUid:
type: int
default: 0
category: ew-bloggy

View File

@ -0,0 +1 @@
@import 'EXT:ew_bloggy/Configuration/TypoScript/setup.typoscript'

View File

@ -0,0 +1,206 @@
<?php
// Add new page types as possible select item:
use Evoweb\EwBloggy\Constants;
use TYPO3\CMS\Core\Domain\Repository\PageRepository;
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
ExtensionManagementUtility::addTcaSelectItem(
'pages',
'doktype',
[
'label' => 'LLL:EXT:ew_bloggy/Resources/Private/Language/locallang_tca.xlf:pages.doktype.blog-post',
'value' => (string) Constants::DOKTYPE_BLOG_POST,
'icon' => 'record-blog-post',
],
'1',
'after'
);
// Add icon for new page types:
$GLOBALS['TCA']['pages']['ctrl']['typeicon_classes'][(string) Constants::DOKTYPE_BLOG_POST] = 'record-blog-post';
$GLOBALS['TCA']['pages']['types'][(string) Constants::DOKTYPE_BLOG_POST]
= $GLOBALS['TCA']['pages']['types'][PageRepository::DOKTYPE_DEFAULT];
$languageFile = 'LLL:EXT:ew_bloggy/Resources/Private/Language/locallang_db.xlf:';
// Register fields
$GLOBALS['TCA']['pages']['columns'] = array_replace_recursive(
$GLOBALS['TCA']['pages']['columns'],
[
'crdate' => [
'label' => 'crdate',
'config' => [
'type' => 'passthrough',
],
],
'archive_date' => [
'label' => $languageFile . 'pages.archive_date',
'config' => [
'type' => 'datetime',
'size' => '13',
'default' => '0',
'behaviour' => [
'allowLanguageSynchronization' => true
]
],
],
'publish_date' => [
'label' => $languageFile . 'pages.publish_date',
'config' => [
'type' => 'datetime',
'size' => '13',
'default' => '0',
'behaviour' => [
'allowLanguageSynchronization' => true
]
],
],
'featured_image' => [
'label' => $languageFile . 'pages.featured_image',
'config' => [
'type' => 'file',
'minitems' => 0,
'maxitems' => 1,
'allowed' => 'common-image-types',
'behaviour' => [
'allowLanguageSynchronization' => true
]
],
],
'authors' => [
'label' => $languageFile . 'pages.authors',
'l10n_mode' => 'exclude',
'config' => [
'type' => 'select',
'renderType' => 'selectMultipleSideBySide',
'multiple' => false,
'foreign_table' => 'tx_ewbloggy_domain_model_author',
'foreign_table_where' => 'AND {#tx_ewbloggy_domain_model_author}.{#sys_language_uid} IN (0,-1)'
. ' AND {#tx_ewbloggy_domain_model_author}.{#pid} = ###PAGE_TSCONFIG_ID###'
. ' ORDER BY tx_ewbloggy_domain_model_author.name ASC',
'MM' => 'tx_ewbloggy_post_author_mm',
'minitems' => 0,
'maxitems' => 99999
],
],
]
);
ExtensionManagementUtility::addFieldsToPalette(
'pages',
'publish_date',
'publish_date, archive_date'
);
ExtensionManagementUtility::addToAllTCAtypes(
'pages',
'
--div--;' . $languageFile . 'pages.tabs.blog,
--palette--;' . $languageFile . 'pages.palettes.publish_date;publish_date,
featured_image,
tags,
authors,
comments_active,
comments
',
(string) Constants::DOKTYPE_BLOG_POST,
'after:subtitle'
);
$GLOBALS['TCA']['pages']['types'][Constants::DOKTYPE_BLOG_POST]['columnsOverrides'] = [
'categories' => [
'config' => [
'behaviour' => [
'allowLanguageSynchronization' => true
],
'foreign_table_where' => 'AND sys_category.sys_language_uid IN (0,-1)'
. ' AND sys_category.pid = ###PAGE_TSCONFIG_ID###',
]
],
'featured_image' => [
'config' => [
'maxitems' => 10,
'overrideChildTca' => [
'columns' => [
'description' => [
'type' => 'input',
],
'crop' => [
'config' => [
'cropVariants' => [
'default' => [
'disabled' => true,
],
'latest' => [
'title' => 'Blog Latest',
'allowedAspectRatios' => [
'910:1080' => [
'title' => '910:1080',
'value' => 910 / 1080,
],
],
'selectedRatio' => '910:1080',
'cropArea' => [
'x' => 0.0,
'y' => 0.0,
'width' => 1.0,
'height' => 1.0,
],
],
'list' => [
'title' => 'Blog Archive',
'allowedAspectRatios' => [
'1:1' => [
'title' => '1:1',
'value' => 1,
],
],
'selectedRatio' => '1:1',
'cropArea' => [
'x' => 0.0,
'y' => 0.0,
'width' => 1.0,
'height' => 1.0,
],
],
'details' => [
'title' => 'Blog Detail',
'allowedAspectRatios' => [
'1068:600' => [
'title' => '1068:600',
'value' => 1068 / 600,
],
],
'selectedRatio' => '1068:600',
'cropArea' => [
'x' => 0.0,
'y' => 0.0,
'width' => 1.0,
'height' => 1.0,
],
],
'opengraph' => [
'title' => 'Opengraph',
'allowedAspectRatios' => [
'1200:627' => [
'title' => '1200:627',
'value' => 1200 / 627,
],
],
'selectedRatio' => '1200:627',
'cropArea' => [
'x' => 0.0,
'y' => 0.0,
'width' => 1.0,
'height' => 1.0,
],
],
],
],
],
],
],
],
]
];

View File

@ -0,0 +1,103 @@
<?php
use Evoweb\EwBloggy\Constants;
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
$languageFile = 'LLL:EXT:blog/Resources/Private/Language/locallang_db.xlf:';
// Add category types
$GLOBALS['TCA']['sys_category']['ctrl']['type'] = 'record_type';
$GLOBALS['TCA']['sys_category']['ctrl']['typeicon_column'] = 'record_type';
$GLOBALS['TCA']['sys_category']['ctrl']['typeicon_classes'][(string) Constants::CATEGORY_TYPE_BLOG] = 'record-blog-category';
ExtensionManagementUtility::addToAllTCAtypes(
'sys_category',
'record_type',
'',
'before:title'
);
$GLOBALS['TCA']['sys_category']['types'][(string) Constants::CATEGORY_TYPE_BLOG] =
$GLOBALS['TCA']['sys_category']['types'][1];
// Limit parent categories to blog types
$GLOBALS['TCA']['sys_category']['types'][Constants::CATEGORY_TYPE_BLOG]['columnsOverrides'] = [
'parent' => [
'config' => [
'foreign_table_where' => ' AND sys_category.record_type = ' . Constants::CATEGORY_TYPE_BLOG . ' ' .
' AND sys_category.pid = ###CURRENT_PID### ' .
($GLOBALS['TCA']['pages']['columns']['categories']['config']['foreign_table_where'] ?? '')
]
]
];
// Register fields
$GLOBALS['TCA']['sys_category']['columns'] = array_replace_recursive(
$GLOBALS['TCA']['sys_category']['columns'],
[
'record_type' => [
'label' => $languageFile . 'sys_category.record_type',
'config' => [
'type' => 'select',
'renderType' => 'selectSingle',
'items' => [
[
'label' => 'LLL:EXT:blog/Resources/Private/Language/locallang_tca.xlf:sys_category.record_type.blog',
'value' => (string) Constants::CATEGORY_TYPE_BLOG,
'icon' => 'record-blog-category'
]
],
'default' => (string) Constants::CATEGORY_TYPE_BLOG,
]
],
'slug' => [
'label' => $languageFile . 'sys_category.slug',
'config' => [
'type' => 'slug',
'generatorOptions' => [
'fields' => ['title'],
'replacements' => [
'/' => ''
],
],
'fallbackCharacter' => '-',
'eval' => 'uniqueInSite',
'default' => '',
'max' => '2048',
]
],
'posts' => [
'label' => $languageFile . 'sys_category.posts',
'config' => [
'type' => 'group',
'size' => 5,
'allowed' => 'pages',
'foreign_table' => 'pages',
'MM' => 'sys_category_record_mm',
'MM_match_fields' => [
'fieldname' => 'categories',
'tablenames' => 'pages',
],
'maxitems' => 1000
],
],
]
);
// Add slug field to all types
ExtensionManagementUtility::addToAllTCAtypes(
'sys_category',
'slug',
'',
'after:title'
);
// Add blog specific fields to blog categories
ExtensionManagementUtility::addToAllTCAtypes(
'sys_category',
'
--div--;' . $languageFile . 'sys_category.tabs.blog,
posts
',
(string) Constants::CATEGORY_TYPE_BLOG
);

View File

@ -0,0 +1,10 @@
<?php
// Add static templates
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
ExtensionManagementUtility::addStaticFile(
'ew_bloggy',
'Configuration/TypoScript/',
'Simple and small blog'
);

View File

@ -0,0 +1,43 @@
<?php
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
$languageFile = 'LLL:EXT:ew_bloggy/Resources/Private/Language/locallang_db.xlf:';
ExtensionManagementUtility::addTcaSelectItemGroup(
'tt_content',
'CType',
'ew_bloggy',
$languageFile . 'CType.div.ew_bloggy'
);
foreach (['posts', 'latestposts', 'archive'] as $type) {
$contentTypeName = 'ewbloggy_' . $type;
ExtensionManagementUtility::addRecordType(
[
'label' => $languageFile . 'plugin.' . $contentTypeName . '.title',
'description' => $languageFile . 'plugin.' . $contentTypeName . '.description',
'value' => $contentTypeName,
'icon' => 'plugin-blog-' . $type,
'group' => 'ew_bloggy'
],
'
--div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:general,
--palette--;;general,
--palette--;;headers,
pi_flexform,
pages;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:pages.ALT.list_formlabel,
--div--;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:tabs.appearance,
--palette--;;frames,
--palette--;;appearanceLinks,
--div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:categories,
categories,
',
);
ExtensionManagementUtility::addPiFlexFormValue(
'*',
'FILE:EXT:ew_bloggy/Configuration/FlexForms/' . $type . '.xml',
$contentTypeName
);
}

View File

@ -0,0 +1,177 @@
<?php
$languageFile = 'LLL:EXT:ew_bloggy/Resources/Private/Language/locallang_db.xlf:';
return [
'ctrl' => [
'label' => 'name',
'tstamp' => 'tstamp',
'crdate' => 'crdate',
'title' => $languageFile . 'tx_ewbloggy_domain_model_author',
'default_sortby' => 'ORDER BY title',
'delete' => 'deleted',
'transOrigPointerField' => 'l18n_parent',
'transOrigDiffSourceField' => 'l18n_diffsource',
'languageField' => 'sys_language_uid',
'enablecolumns' => [
'disabled' => 'hidden',
],
'typeicon_classes' => [
'default' => 'record-blog-author'
],
'searchFields' => 'uid,name,title',
],
'columns' => [
'name' => [
'label' => $languageFile . 'tx_ewbloggy_domain_model_author.name',
'l10n_mode' => 'exclude',
'l10n_display' => 'defaultAsReadonly',
'config' => [
'type' => 'input',
'size' => 30,
'required' => true,
],
],
'title' => [
'label' => $languageFile . 'tx_ewbloggy_domain_model_author.title',
'config' => [
'type' => 'input',
'size' => 30,
],
],
'slug' => [
'label' => $languageFile . 'tx_ewbloggy_domain_model_author.slug',
'config' => [
'type' => 'slug',
'size' => 50,
'generatorOptions' => [
'fields' => ['name'],
'replacements' => [
'/' => ''
],
],
'fallbackCharacter' => '-',
'eval' => 'uniqueInSite',
'default' => ''
],
],
'image' => [
'label' => $languageFile . 'tx_ewbloggy_domain_model_author.image',
'l10n_mode' => 'exclude',
'config' => [
'type' => 'file',
'allowed' => 'common-image-types',
'appearance' => [
'createNewRelationLinkTitle' =>
'LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:images.addFileReference'
],
'overrideChildTca' => [
'types' => [
\TYPO3\CMS\Core\Resource\FileType::IMAGE->value => [
'showitem' => '
--palette--;;imageoverlayPalette,
--palette--;;filePalette',
],
],
],
'maxitems' => 1,
],
],
'email' => [
'label' => $languageFile . 'tx_ewbloggy_domain_model_author.email',
'l10n_mode' => 'exclude',
'config' => [
'type' => 'email',
'size' => 30,
],
],
'website' => [
'label' => $languageFile . 'tx_ewbloggy_domain_model_author.website',
'l10n_mode' => 'exclude',
'config' => [
'type' => 'input',
'size' => 30,
'eval' => 'domainname',
],
],
'profile' => [
'label' => $languageFile . 'tx_ewbloggy_domain_model_author.profile',
'l10n_mode' => 'exclude',
'config' => [
'type' => 'input',
'size' => 30,
'eval' => 'domainname',
],
],
'bio' => [
'label' => $languageFile . 'tx_ewbloggy_domain_model_author.bio',
'config' => [
'type' => 'text',
'eval' => 'trim',
],
],
'posts' => [
'label' => $languageFile . 'tx_ewbloggy_domain_model_author.posts',
'l10n_mode' => 'exclude',
'config' => [
'type' => 'select',
'renderType' => 'selectMultipleSideBySide',
'multiple' => 0,
'foreign_table' => 'pages',
'foreign_table_where' => 'AND {#pages}.{#doktype}=' . \Evoweb\EwBloggy\Constants::DOKTYPE_BLOG_POST
. ' AND {#pages}.{#sys_language_uid} IN (-1,0)',
'MM' => 'tx_ewbloggy_post_author_mm',
'MM_opposite_field' => 'authors',
'minitems' => 0,
'maxitems' => 99999,
],
],
'details_page' => [
'label' => $languageFile . 'tx_ewbloggy_domain_model_author.details_page',
'l10n_mode' => 'exclude',
'config' => [
'type' => 'group',
'allowed' => 'pages',
'size' => 1,
'maxitems' => 1,
'minitems' => 0,
'default' => 0
],
]
],
'types' => [
0 => [
'showitem' => '
--div--;' . $languageFile . 'tx_ewbloggy_domain_model_author.tab_profile,
--palette--;' . $languageFile . 'tx_ewbloggy_domain_model_author.palette_personal;personal,
slug,
image,
--palette--;' . $languageFile . 'tx_ewbloggy_domain_model_author.palette_contact;contact,
profile,
--div--;' . $languageFile . 'tx_ewbloggy_domain_model_author.tab_blog,
posts,
details_page,
--div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:access,
--palette--;;hidden
',
],
],
'palettes' => [
'hidden' => [
'showitem' => '
hidden;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:field.default.hidden
',
],
'language' => [
'showitem' => '
sys_language_uid,l18n_parent
',
],
'personal' => [
'showitem' => 'name, title'
],
'contact' => [
'showitem' => 'website, email'
],
],
];

View File

@ -0,0 +1,5 @@
plugin {
tx_ewbloggy {
limit = 3
}
}

View File

@ -0,0 +1,7 @@
plugin {
tx_ewbloggy {
settings {
limit = {$plugin.tx_ewbloggy.limit ?? $evoweb.tx-ewbloggy.limit}
}
}
}

8
README.md Normal file
View File

@ -0,0 +1,8 @@
# TYPO3 Extension ``ew_bloggy``
This extension is highly inspired by ``t3g/blog`` part of the code was copied
from it and composed to a blog that is limited to latest post, post list and
archive.
The extension tries to integrate every new feature of TYPO3 13 to make the
integration easier to me.

View File

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
<file source-language="en" datatype="plaintext" original="messages" date="2025-01-17T21:47:12Z" product-name="ew_bloggy">
<header/>
<body>
<trans-unit id="pagelayout.section.published" xml:space="preserve">
<source>Published</source>
</trans-unit>
<trans-unit id="pagelayout.section.authors" xml:space="preserve">
<source>Authors</source>
</trans-unit>
<trans-unit id="pagelayout.message.noauthor" xml:space="preserve">
<source>No author assigned.</source>
</trans-unit>
<trans-unit id="pagelayout.section.featuredimage" xml:space="preserve">
<source>Featured Image</source>
</trans-unit>
<trans-unit id="pagelayout.message.nofeaturedimage" xml:space="preserve">
<source>No featured image assigned.</source>
</trans-unit>
<trans-unit id="pagelayout.section.categories" xml:space="preserve">
<source>Categories</source>
</trans-unit>
<trans-unit id="pagelayout.button.post.edit" xml:space="preserve">
<source>Edit Post Meta-Data</source>
</trans-unit>
</body>
</file>
</xliff>

View File

@ -0,0 +1,136 @@
<?xml version="1.0" encoding="utf-8" standalone="yes" ?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
<file source-language="en" datatype="plaintext" original="messages" date="2025-01-17T14:18:34Z" product-name="ew_bloggy">
<header/>
<body>
<trans-unit id="CType.div.ew_bloggy" xml:space="preserve">
<source>Bloggy</source>
</trans-unit>
<trans-unit id="plugin.ewbloggy_posts.title" xml:space="preserve">
<source>Bloggy: List of posts</source>
</trans-unit>
<trans-unit id="plugin.ewbloggy_posts.description" xml:space="preserve">
<source>Displays a list of blog posts ordered by date. All non-hidden, non-deleted and non-archived posts are shown in the list.</source>
</trans-unit>
<trans-unit id="plugin.ewbloggy_latestposts.title" xml:space="preserve">
<source>Bloggy: Latest posts</source>
</trans-unit>
<trans-unit id="plugin.ewbloggy_latestposts.description" xml:space="preserve">
<source>Displays a number of latest posts. You can specify the amount of items yourself.</source>
</trans-unit>
<trans-unit id="latestpost">
<source>Latest posts settings</source>
</trans-unit>
<trans-unit id="plugin.ewbloggy_archive.title" xml:space="preserve">
<source>Bloggy: Archive</source>
</trans-unit>
<trans-unit id="plugin.ewbloggy_archive.description" xml:space="preserve">
<source>The archive plugin displays all posts categorized by year and month.</source>
</trans-unit>
<trans-unit id="plugin.ewbloggy_header.title" xml:space="preserve">
<source>Bloggy: Header</source>
</trans-unit>
<trans-unit id="plugin.ewbloggy_header.description" xml:space="preserve">
<source>Displays post header</source>
</trans-unit>
<trans-unit id="plugin.ewbloggy_footer.title" xml:space="preserve">
<source>Bloggy: Footer</source>
</trans-unit>
<trans-unit id="plugin.ewbloggy_footer.description" xml:space="preserve">
<source>Displays post footer</source>
</trans-unit>
<trans-unit id="pages.tabs.blog" xml:space="preserve">
<source>Blog</source>
</trans-unit>
<trans-unit id="pages.palettes.publish_date" xml:space="preserve">
<source>Publish date</source>
</trans-unit>
<trans-unit id="pages.publish_date" xml:space="preserve">
<source>Publish date</source>
</trans-unit>
<trans-unit id="pages.crdate_month" xml:space="preserve">
<source>Publish date month</source>
</trans-unit>
<trans-unit id="pages.crdate_year" xml:space="preserve">
<source>Publish date year</source>
</trans-unit>
<trans-unit id="pages.archive_date" xml:space="preserve">
<source>Archive date</source>
</trans-unit>
<trans-unit id="pages.featured_image" xml:space="preserve">
<source>Featured Image</source>
</trans-unit>
<trans-unit id="pages.authors" xml:space="preserve">
<source>Authors</source>
</trans-unit>
<trans-unit id="tx_ewbloggy_domain_model_author" xml:space="preserve">
<source>Blog: Author</source>
</trans-unit>
<trans-unit id="tx_ewbloggy_domain_model_author.name" xml:space="preserve">
<source>Name</source>
</trans-unit>
<trans-unit id="tx_ewbloggy_domain_model_author.slug" xml:space="preserve">
<source>Path Segment</source>
</trans-unit>
<trans-unit id="tx_ewbloggy_domain_model_author.image" xml:space="preserve">
<source>Image</source>
</trans-unit>
<trans-unit id="tx_ewbloggy_domain_model_author.title" xml:space="preserve">
<source>Title</source>
</trans-unit>
<trans-unit id="tx_ewbloggy_domain_model_author.website" xml:space="preserve">
<source>Website</source>
</trans-unit>
<trans-unit id="tx_ewbloggy_domain_model_author.email" xml:space="preserve">
<source>Email</source>
</trans-unit>
<trans-unit id="tx_ewbloggy_domain_model_author.profile" xml:space="preserve">
<source>Author profile URL (used for schema.org markup)</source>
</trans-unit>
<trans-unit id="tx_ewbloggy_domain_model_author.bio" xml:space="preserve">
<source>Bio</source>
</trans-unit>
<trans-unit id="tx_ewbloggy_domain_model_author.posts" xml:space="preserve">
<source>Blog posts</source>
</trans-unit>
<trans-unit id="tx_ewbloggy_domain_model_author.details_page" xml:space="preserve">
<source>Details page</source>
</trans-unit>
<trans-unit id="tx_ewbloggy_domain_model_author.palette_personal" xml:space="preserve">
<source>Personal data</source>
</trans-unit>
<trans-unit id="tx_ewbloggy_domain_model_author.palette_contact" xml:space="preserve">
<source>Contact data</source>
</trans-unit>
<trans-unit id="tx_ewbloggy_domain_model_author.tab_profile" xml:space="preserve">
<source>Profile</source>
</trans-unit>
<trans-unit id="tx_ewbloggy_domain_model_author.tab_blog" xml:space="preserve">
<source>Blog data</source>
</trans-unit>
<trans-unit id="sys_category.record_type" xml:space="preserve">
<source>Record Type</source>
</trans-unit>
<trans-unit id="sys_category.slug" xml:space="preserve">
<source>Path Segment</source>
</trans-unit>
<trans-unit id="sys_category.posts" xml:space="preserve">
<source>Posts</source>
</trans-unit>
<trans-unit id="settings.flexform.limit">
<source>Limit</source>
</trans-unit>
<trans-unit id="settings.flexform.postsUid">
<source>Posts page uid</source>
</trans-unit>
<trans-unit id="settings.flexform.archiveUid">
<source>Archive page uid</source>
</trans-unit>
</body>
</file>
</xliff>

View File

@ -0,0 +1,10 @@
<html xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers" data-namespace-typo3-fluid="true">
<div class="
relative
min-h-screen
">
<f:render section="Content" />
</div>
</html>

View File

@ -0,0 +1,104 @@
<html xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers"
xmlns:ew="http://typo3.org/ns/Evoweb/EwBloggy/ViewHelpers"
data-namespace-typo3-fluid="true">
<f:if condition="{post}">
<f:asset.css identifier="ew-bloggy-pagelayout" href="EXT:ew_bloggy/Resources/Public/Css/pagelayout.min.css"/>
<div class="
tw-relative
tw-bg-white dark:tw-bg-component-bg
tw-rounded-component
shadow-component
tw-m-component
tw-border-solid
tw-border-l-12 tw-border-r-0 tw-border-b-0 tw-border-t-0
tw-border-green-600
">
<div class="tw-absolute tw-top-5 tw-left-4">
<core:iconForRecord table="pages" row="{post.asArray}" size="large" />
</div>
<div class="tw-relative tw-p-5 tw-pl-19 tw-min-h-19">
<div class="[&_>_*:first-child]:tw-mt-0 [&_>_*:last-child]:tw-mb-0">
<p><f:if condition="{post.abstract} || {post.description}">
<f:then>{f:if(condition: post.abstract, then: post.abstract, else: post.description)}</f:then>
<f:else><f:translate key="pagelayout.message.notabstractordescription" extensionName="ew_bloggy" /></f:else>
</f:if></p>
</div>
<div class="tw-grid tw-gap-2.5 tw-grid-cols-component">
<div class="tw-mt-5">
<h3 class="tw-text-xs tw-font-bold tw-mt-0 tw-mb-1.5"><f:translate key="pagelayout.section.published" extensionName="ew_bloggy" /></h3>
<div class="[&_>_*:first-child]:tw-mt-0 [&_>_*:last-child]:tw-mb-0">
<time datetime="{f:format.date(format: '%Y-%m-%dT%H:%M:%S-%z', date: post.publishDate)}" itemprop="datepublished">{f:format.date(format: '%d.%m.%Y - %H:%M', date: post.publishDate)}</time>
</div>
</div>
<div class="tw-mt-5">
<h3 class="tw-text-xs tw-font-bold tw-mt-0 tw-mb-1.5"><f:translate key="pagelayout.section.authors" extensionName="ew_bloggy" /></h3>
<div class="[&_>_*:first-child]:tw-mt-0 [&_>_*:last-child]:tw-mb-0">
<f:if condition="{post.authors}">
<f:then>
<ul class="tw-list-none tw-p-0">
<f:for each="{post.authors}" as="author">
<li>{author.name}</li>
</f:for>
</ul>
</f:then>
<f:else>
<p><f:translate key="pagelayout.message.noauthor" extensionName="ew_bloggy" /></p>
</f:else>
</f:if>
</div>
</div>
<div class="tw-mt-5">
<h3 class="tw-text-xs tw-font-bold tw-mt-0 tw-mb-1.5"><f:translate key="pagelayout.section.featuredimage" extensionName="ew_bloggy" /></h3>
<div class="[&_>_*:first-child]:tw-mt-0 [&_>_*:last-child]:tw-mb-0">
<f:if condition="{post.featuredImage}">
<f:then>
<f:image loading="lazy" class="tw-h-auto tw-max-w-full" image="{post.featuredImage}" alt="{post.featuredImage.alternative}" title="{post.featuredImage.title}" height="100c" width="200" />
</f:then>
<f:else>
<p><f:translate key="pagelayout.message.nofeaturedimage" extensionName="ew_bloggy" /></p>
</f:else>
</f:if>
</div>
</div>
<f:if condition="{post.tags}">
<div class="tw-mt-5">
<h3 class="tw-text-xs tw-font-bold tw-mt-0 tw-mb-1.5"><f:translate key="pagelayout.section.tags" extensionName="ew_bloggy" /></h3>
<div class="[&_>_*:first-child]:tw-mt-0 [&_>_*:last-child]:tw-mb-0">
<ul class="tw-list-none tw-p-0">
<f:for each="{post.tags}" as="tag">
<li class="tw-inline"><span class="badge badge-info">{tag.title}</span></li>
</f:for>
</ul>
</div>
</div>
</f:if>
<f:if condition="{post.categories}">
<div class="tw-mt-5">
<h3 class="tw-text-xs tw-font-bold tw-mt-0 tw-mb-1.5"><f:translate key="pagelayout.section.categories" extensionName="ew_bloggy" /></h3>
<div class="[&_>_*:first-child]:tw-mt-0 [&_>_*:last-child]:tw-mb-0">
<ul class="tw-list-none tw-p-0">
<f:for each="{post.categories}" as="category">
<li class="tw-inline"><span class="badge badge-info">{category.title}</span></li>
</f:for>
</ul>
</div>
</div>
</f:if>
</div>
</div>
<div class="tw-p-5 tw-pt-0">
<ew:link.be.post post="{post}" action="edit" class="btn btn-default">
<core:icon identifier="record-blog-post" />
<f:translate key="pagelayout.button.post.edit" extensionName="ew_bloggy" />
</ew:link.be.post>
</div>
</div>
</f:if>
</html>

View File

@ -0,0 +1,133 @@
<html xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers" data-namespace-typo3-fluid="true">
<f:layout name="Default" />
<f:section name="Content">
<f:if condition="{posts.0} && {posts.0.featuredImage}">
<f:asset.css identifier="blog_latest_featured_image">
@media (max-width:1023px){ .bg-featured-image { --featured-image-url: url(<f:uri.image image="{posts.0.featuredImage}" cropVariant="latest" maxWidth="908" maxHeight="c320"/>); } }
@media (min-width:1024px){ .bg-featured-image { --featured-image-url: url(<f:uri.image image="{posts.0.featuredImage}" cropVariant="latest" maxWidth="910" maxHeight="1080"/>); } }
</f:asset.css>
</f:if>
<div class="grid min-h-screen lg:grid-cols-2 dlg:min-h-screen">
<div class="
lg:order-last
w-full
h-full
min-h-80
bg-cover bg-center bg-featured-image">
</div>
<div class="
group
max-w-117
mx-auto pt-12 pb-24 lg:py-24">
<f:for each="{posts}" as="post" iteration="iterator">
<f:if condition="{iterator.isFirst}">
<f:then><f:render section="firstPost" arguments="{_all}"/></f:then>
<f:else><f:render section="otherPosts" arguments="{_all}"/></f:else>
</f:if>
</f:for>
<f:if condition="{posts -> f:count()} >= {settings.limit}">
<div class="pt-24">
<a href="{f:uri.page(pageUid: settings.postsUid)}"
class="
font-montserrat text-green-dark font-bold
text-lg leading-9 uppercase
block
">Weitere Beiträge</a>
</div>
</f:if>
</div>
</div>
</f:section>
<f:section name="firstPost">
<article id="post-{post.uid}" class="
transition duration-200 ease-in-out
group-hover:blur-[2px] group-hover:hover:blur-none
pb-4 lg:pb-28
">
<div class="flex font-montserrat text-sm leading-9 font-bold uppercase">
<f:if condition="{post.categories.0}">
<ul>
<li class="
inline-block mr-1.5
after:content-[','] last:after:content-['']
">
<span class="text-green-dark">{post.categories.0.title}</span>
</li>
</ul>
</f:if>
<div class="
flex-auto
published
text-gray-800
before:content-['\2022']
before:inline-block
before:mr-1.5
">
<f:format.date format="%e. %B %Y">{post.publishDate}</f:format.date>
</div>
</div>
<h1 class="font-joschmi font-bold text-5xl leading-13 tracking-wider pt-0 pb-10 lg:pt-10">
<a href="{f:uri.page(pageUid: post.uid)}"
class="text-gray-1300 hover:text-green-dark">{post.title}</a>
</h1>
<div class="excerpt font-libre text-base leading-7 text-gray-800">
<f:format.crop maxCharacters="300" respectWordBoundaries="1"><f:if condition="{post.abstract}"><f:then>{post.abstract}</f:then><f:else>{post.description}</f:else></f:if></f:format.crop>
<a href="{f:uri.page(pageUid: post.uid)}"
title="{post.title}"
class="
inline-block
w-8 h-8
ml-4
align-text-top
text-green
">
<i class="fal fa-arrow-right fa-2x"></i>
</a>
</div>
</article>
</f:section>
<f:section name="otherPosts">
<article id="post-{post.uid}" class="
transition duration-200 ease-in-out
group-hover:blur-[2px] group-hover:hover:blur-none
">
<h2 class="font-montserrat font-bold text-3xl pt-8 pb-0">
<a href="{f:uri.page(pageUid: post.uid)}"
class="text-gray-1300 hover:text-green-dark">{post.title}</a>
</h2>
<div class="flex font-montserrat text-sm leading-9 font-bold uppercase">
<f:if condition="{post.categories.0}">
<ul class="cat-links flex-none">
<li class="
inline-block mr-1.5
after:content-[','] last:after:content-['']
">
<span class="text-green-dark">{post.categories.0.title}</span>
</li>
</ul>
</f:if>
<div class="
flex-auto
published
text-gray-800
before:content-['\2022']
before:inline-block
before:mr-1.5
">
<f:format.date format="%e. %B %Y">{post.publishDate}</f:format.date>
</div>
</div>
</article>
</f:section>
</html>

View File

@ -0,0 +1,54 @@
<html xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers"
xmlns:ew="http://typo3.org/ns/Evoweb/EwBloggy/ViewHelpers"
data-namespace-typo3-fluid="true">
<f:layout name="Default" />
<f:section name="Content">
<f:if condition="!{archiveData}">
<f:then>
<f:comment><!--If a year/+month is selected show posts matching.--></f:comment>
<header class="blogarchiveheader blogarchiveheader--archive">
<h1 class="blogarchiveheader__title">
<span class="blogarchiveheader__titletext">
<f:if condition="{month}">{f:format.date(format: '{settings.widgets.archive.monthDateFormat}', date: timestamp)}</f:if> {year}
</span>
<ew:link.archive class="blogarchiveheader__titlelink" rss="true" year="{year}" month="{month}">
<span class="blogicon"><f:render partial="General/SocialIcons" section="Rss" optional="true" /></span>
</ew:link.archive>
</h1>
</header>
<f:render partial="List" arguments="{_all}" />
<f:if condition="{settings.authorUid}">
<footer class="blogarchivefooter blogarchivefooter--archive">
<div class="blogarchivefooter__backlink">
<f:link.page pageUid="{settings.archiveUid}">
<f:translate key="list.backlink.archive" />
</f:link.page>
</div>
</footer>
</f:if>
</f:then>
<f:else>
<f:comment><!--If year is missing show an overview of the archive.--></f:comment>
<header class="blogarchiveheader blogarchiveheader--archive">
<h1 class="blogarchiveheader__title">
<span class="blogarchiveheader__titletext">
<f:translate key="headline.archive"/>
</span>
</h1>
</header>
<div class="bloglist bloglist--archive">
<f:for each="{archiveData}" as="months" key="year">
<f:render partial="List/Archive" arguments="{_all}" />
</f:for>
</div>
</f:else>
</f:if>
</f:section>
</html>

View File

@ -0,0 +1,53 @@
<html xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers" data-namespace-typo3-fluid="true">
<f:layout name="Default" />
<f:section name="Content">
<ol class="list-articles py-4">
<f:for each="{posts}" as="post">
<f:render section="post" arguments="{_all}"/>
</f:for>
</ol>
</f:section>
<f:section name="post">
<li class="
grid
grid-cols-[9rem_auto] grid-rows-[repeat(2,auto)]
gap-x-5
border-gray-1200 border-solid
border-b last:border-none
py-4
">
<f:if condition="{post.featuredImage}">
<f:then>
<f:link.page pageUid="{post.uid}" title="{post.title}" class="row-span-full">
<img src="{f:uri.image(image: post.featuredImage, width: 140, height: 140, cropVariant: 'list')}"
alt="{post.featuredImage.alternative}"
class="w-36 h-36"/>
</f:link.page>
</f:then>
<f:else>
<div class="block w-36"></div>
</f:else>
</f:if>
<header class="row-span-auto">
<f:link.page pageUid="{post.uid}">
<f:if condition="{post.categories.0}">
<h3 class="font-montserrat text-lg">{post.categories.0.title}</h3>
</f:if>
<h2 class="font-montserrat font-bold text-xl">{post.title}</h2>
</f:link.page>
</header>
<f:if condition="{post.abstract} || {post.description}">
<div class="row-span-auto">
<f:format.html><f:format.crop maxCharacters="300" respectWordBoundaries="1"><f:if condition="{post.abstract}">
<f:then>{post.abstract}</f:then>
<f:else>{post.description}</f:else>
</f:if></f:format.crop></f:format.html>
</div>
</f:if>
</li>
</f:section>
</html>

View File

@ -0,0 +1 @@
*,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }.tw-absolute{position:absolute}.tw-relative{position:relative}.tw-left-4{left:1rem}.tw-top-5{top:1.25rem}.tw-m-component{margin:var(--typo3-spacing,1em) auto}.tw-mb-1{margin-bottom:.25rem}.tw-mb-1\.5{margin-bottom:.375rem}.tw-mt-0{margin-top:0}.tw-mt-5{margin-top:1.25rem}.tw-inline{display:inline}.tw-grid{display:grid}.tw-h-auto{height:auto}.tw-min-h-19{min-height:4.75rem}.tw-max-w-full{max-width:100%}.tw-list-none{list-style-type:none}.tw-grid-cols-component{grid-template-columns:repeat(auto-fit,minmax(150px,1fr))}.tw-gap-2\.5{gap:.625rem}.tw-rounded-component{border-radius:var(--typo3-component-border-radius)}.tw-border-b-0{border-bottom-width:0}.tw-border-l-12{border-left-width:12px}.tw-border-r-0{border-right-width:0}.tw-border-t-0{border-top-width:0}.tw-border-solid{border-style:solid}.tw-border-green-600{--tw-border-opacity:1;border-color:rgb(22 163 74/var(--tw-border-opacity,1))}.tw-bg-white{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity,1))}.tw-p-0{padding:0}.tw-p-5{padding:1.25rem}.tw-pl-19{padding-left:4.75rem}.tw-pt-0{padding-top:0}.tw-text-xs{font-size:.75rem;line-height:1rem}.tw-font-bold{font-weight:700}.shadow-component{box-shadow:var(--typo3-component-box-shadow)}@media (prefers-color-scheme:dark){.dark\:tw-bg-component-bg{background-color:var(--typo3-component-bg)}}.\[\&_\>_\*\:first-child\]\:tw-mt-0>:first-child{margin-top:0}.\[\&_\>_\*\:last-child\]\:tw-mb-0>:last-child{margin-bottom:0}

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg id="svg2" xmlns="http://www.w3.org/2000/svg" height="9in" width="9in" version="1.1" viewBox="0 0 640 640">
<path id="Auswahl1" d="m4.73 377l-4.73-45c-1.17-100.04 31.51-196.72 110-262.56 42.12-35.33 96.7-58.01 151-65.57l36-3.87h49s38 4.58 38 4.58c35.73 5.54 72.58 17.87 104 35.85 53.09 30.37 91.35 72.89 118.24 127.57 23.12 47.01 33.76 98.83 33.76 151h-555l1 18c0.07 13.74 5.29 37.39 8.88 51l3.12 11-81 29c-5.4-12.32-10.25-37.27-12.27-51zm489.43-221s-8.16-8.91-8.16-8.91c-34.3-32.33-78.39-52.53-125-58.81-9.87-1.33-20.05-2.26-30-2.28l-11-0.66s-17 0.66-17 0.66l-19 1.84c-35.79 3.82-69.99 15.91-100 35.83-30.13 20-55.03 47.97-71.25 80.33-4.56 9.1-12.24 25.24-13.75 35h443c-10.63-31.88-25.41-57.89-47.84-83z" fill="#87b200"/>
<path id="Auswahl2" d="m143.72 481c12.28 14.12 25.74 25.64 41.28 36 3.08 2.05 12.54 8.89 16 8.28 2.62-0.46 6.12-4.4 8-6.28l15.09-16s56.91-57 56.91-57l29-29c2.36-2.36 7.37-8.57 11-7.66 2.27 0.56 6.26 4.92 8 6.66l18 18 64 64 21 21c2.17 2.17 5.77 7.05 9.17 6.3 1.5-0.33 7.18-3.9 8.83-4.9 8.04-4.85 15.75-10.51 23-16.48 30.57-25.21 51.43-56.38 66-92.92 4.35-10.9 8.34-24.51 10.79-36 0.59-2.78 1.84-12.51 3.8-13.96 2.4-1.78 13.18 0.83 16.41 1.37 0 0 47 8.18 47 8.18 3.47 0.58 16.03 1.63 17.36 4.72 1.15 2.68-2.21 17.15-3.01 20.69-4.17 18.44-9.52 36.44-16.55 54-27.05 67.63-74.22 122.45-139.8 155.25-10.16 5.07-28.35 13.19-39 16.4-7.8 2.36-9.68-1.34-15-6.65l-15-15s-63-64-63-64l-16-16c-1.63-1.6-4.58-4.96-7-4.96-2.73 0-7.1 5.06-9 6.96l-20 20s-16.01 17-16.01 17-37.99 38-37.99 38l-25 24.36c-4.38 2.57-10.64-0.84-15-2.36-10.8-3.75-27.07-10.98-37-16.58-39.58-22.32-67.36-44.68-94.87-81.42-6.36-8.48-21.15-29.57-24.13-39l49-28.42 25-13.58c8.24 15.58 17.18 27.74 28.72 41z" fill="#4f660b"/>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128" viewBox="0 0 32 32"><path fill="#87b200" d="M16 4a5 5 0 1 1-5 5a5 5 0 0 1 5-5m0-2a7 7 0 1 0 7 7a7 7 0 0 0-7-7m10 28h-2v-5a5 5 0 0 0-5-5h-6a5 5 0 0 0-5 5v5H6v-5a7 7 0 0 1 7-7h6a7 7 0 0 1 7 7z"/></svg>

After

Width:  |  Height:  |  Size: 265 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128" viewBox="0 0 32 32"><path fill="#87b200" d="M27 22.141V18a2 2 0 0 0-2-2h-8v-4h2a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2h-6a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2h2v4H7a2 2 0 0 0-2 2v4.142a4 4 0 1 0 2 0V18h8v4.142a4 4 0 1 0 2 0V18h8v4.141a4 4 0 1 0 2 0M13 4h6l.001 6H13ZM8 26a2 2 0 1 1-2-2a2 2 0 0 1 2 2m10 0a2 2 0 1 1-2-2a2.003 2.003 0 0 1 2 2m8 2a2 2 0 1 1 2-2a2 2 0 0 1-2 2"/></svg>

After

Width:  |  Height:  |  Size: 431 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512"><path fill="#87b200" d="M172.2 226.8c-14.6-2.9-28.2 8.9-28.2 23.8V301c0 10.2 7.1 18.4 16.7 22c18.2 6.8 31.3 24.4 31.3 45c0 26.5-21.5 48-48 48s-48-21.5-48-48V120c0-13.3-10.7-24-24-24H24c-13.3 0-24 10.7-24 24v248c0 89.5 82.1 160.2 175 140.7c54.4-11.4 98.3-55.4 109.7-109.7c17.4-82.9-37-157.2-112.5-172.2M209 0c-9.2-.5-17 6.8-17 16v31.6c0 8.5 6.6 15.5 15 15.9c129.4 7 233.4 112 240.9 241.5c.5 8.4 7.5 15 15.9 15h32.1c9.2 0 16.5-7.8 16-17C503.4 139.8 372.2 8.6 209 0m.3 96c-9.3-.7-17.3 6.7-17.3 16.1v32.1c0 8.4 6.5 15.3 14.8 15.9c76.8 6.3 138 68.2 144.9 145.2c.8 8.3 7.6 14.7 15.9 14.7h32.2c9.3 0 16.8-8 16.1-17.3c-8.4-110.1-96.5-198.2-206.6-206.7"/></svg>

After

Width:  |  Height:  |  Size: 740 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128" viewBox="0 0 32 32"><circle cx="7" cy="9" r="3" fill="currentColor"/><circle cx="7" cy="23" r="3" fill="currentColor"/><path fill="currentColor" d="M16 22h14v2H16zm0-14h14v2H16z"/></svg>

After

Width:  |  Height:  |  Size: 252 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128" viewBox="0 0 32 32"><path fill="currentColor" d="M16 22h14v2H16zm0-14h14v2H16zm-8 4V4H6v1H4v2h2v5H4v2h6v-2zm2 16H4v-4a2 2 0 0 1 2-2h2v-2H4v-2h4a2 2 0 0 1 2 2v2a2 2 0 0 1-2 2H6v2h4z"/></svg>

After

Width:  |  Height:  |  Size: 255 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128" viewBox="0 0 32 32"><path fill="currentColor" d="M16 22h14v2H16zm-2-2.6L12.6 18L6 24.6L3.4 22L2 23.4l4 4zM16 8h14v2H16zm-2-2.6L12.6 4L6 10.6L3.4 8L2 9.4l4 4z"/></svg>

After

Width:  |  Height:  |  Size: 232 B

19
composer.json Normal file
View File

@ -0,0 +1,19 @@
{
"name": "evoweb/ew-bloggy",
"type": "typo3-cms-extension",
"description": "Simple and small blog",
"homepage": "https://www.evoweb.de/",
"version": "0.0.1",
"require": {
},
"autoload": {
"psr-4": {
"Evoweb\\EwBloggy\\": "Classes/"
}
},
"extra": {
"typo3/cms": {
"extension-key": "ew_bloggy"
}
}
}

28
ext_localconf.php Normal file
View File

@ -0,0 +1,28 @@
<?php
use Evoweb\EwBloggy\Controller\PostController;
use TYPO3\CMS\Extbase\Utility\ExtensionUtility;
ExtensionUtility::configurePlugin(
'EwBloggy',
'Posts',
[ PostController::class => 'listRecentPosts' ],
[],
ExtensionUtility::PLUGIN_TYPE_CONTENT_ELEMENT
);
ExtensionUtility::configurePlugin(
'EwBloggy',
'LatestPosts',
[ PostController::class => 'listLatestPosts' ],
[],
ExtensionUtility::PLUGIN_TYPE_CONTENT_ELEMENT
);
ExtensionUtility::configurePlugin(
'EwBloggy',
'Archive',
[ PostController::class => 'listPostsByDate' ],
[],
ExtensionUtility::PLUGIN_TYPE_CONTENT_ELEMENT
);

14
ext_tables.sql Normal file
View File

@ -0,0 +1,14 @@
#
# Table structure for table 'pages'
#
CREATE TABLE pages (
KEY post_crdate (crdate_year, crdate_month)
);
#
# Table structure for table 'sys_category'
#
CREATE TABLE sys_category (
record_type int(11) unsigned DEFAULT '1' NOT NULL
);