Skip to content

Commit dd75be0

Browse files
committed
Add demo on website
1 parent d7d6bb5 commit dd75be0

File tree

11 files changed

+323
-2
lines changed

11 files changed

+323
-2
lines changed

ux.symfony.com/assets/styles/components/_Button.scss

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99

1010
background: var(--bs-body-bg);
1111
color: var(--bs-body-color);
12+
13+
transition: background-color 0.2s, color 0.2s;
1214
}
1315

1416
.Button--dark {
@@ -17,3 +19,38 @@
1719
color: #dee2e6;
1820
border: 1px solid #a6a0a0;
1921
}
22+
23+
.Button--large {
24+
font-size: 1.25rem;
25+
padding: 1rem 2rem;
26+
}
27+
28+
.Button--blue {
29+
background: #007bff;
30+
color: #fff;
31+
&:hover {
32+
background: #0056b3;
33+
}
34+
}
35+
36+
.BigButton {
37+
display: grid;
38+
place-content: center;
39+
background: var(--bg-color);
40+
background-blend-mode: color-burn;
41+
color: var(--color);
42+
font-size: 1rem;
43+
text-transform: uppercase;
44+
padding: .5rem 1rem;
45+
border-radius: 1.5rem;
46+
font-weight: 300;
47+
font-stretch: semi-condensed;
48+
opacity: .75;
49+
transition: all 150ms;
50+
border: 2px solid rgba(0, 0, 0, .6);
51+
display: flex;
52+
flex-direction: row;
53+
flex-wrap: nowrap;
54+
align-items: center;
55+
justify-content: space-between;
56+
}

ux.symfony.com/assets/styles/components/_DemoCard.scss

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,3 +85,14 @@
8585
flex-wrap: wrap;
8686
gap: .5rem;
8787
}
88+
89+
.DemoCard__badge {
90+
position: absolute;
91+
top: .75rem;
92+
right: .75rem;
93+
94+
.Badge {
95+
background: var(--bs-secondary-bg);
96+
border: 1px solid var(--bs-secondary-bg);
97+
}
98+
}

ux.symfony.com/src/Controller/Demo/LiveDemoController.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ public function invoice(LiveDemoRepository $liveDemoRepository, ?Invoice $invoic
100100
#[Route('/infinite-scroll-2', name: 'app_demo_live_component_infinite_scroll_2')]
101101
#[Route('/product-form', name: 'app_demo_live_component_product_form')]
102102
#[Route('/upload', name: 'app_demo_live_component_upload')]
103+
#[Route('/download', name: 'app_demo_live_component_download')]
103104
public function demo(
104105
LiveDemoRepository $liveDemoRepository,
105106
string $demo,

ux.symfony.com/src/Model/LiveDemo.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,4 +44,9 @@ public function getLongDescription(): string
4444
{
4545
return $this->longDescription;
4646
}
47+
48+
public function isNew(): bool
49+
{
50+
return \DateTimeImmutable::createFromFormat('Y-m-d', $this->getPublishedAt()) > new \DateTimeImmutable('-30 days');
51+
}
4752
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace App\Service;
13+
14+
use App\Model\Document;
15+
use Symfony\Component\DependencyInjection\Attribute\Autowire;
16+
use Symfony\Component\Filesystem\Filesystem;
17+
use Symfony\Component\Filesystem\Path;
18+
19+
final class DocumentStorage
20+
{
21+
private readonly Filesystem $filesystem;
22+
23+
public function __construct(
24+
#[Autowire('%kernel.project_dir%/assets/documents')]
25+
private readonly string $storageDirectory,
26+
) {
27+
$this->filesystem = new Filesystem();
28+
29+
if (!$this->filesystem->exists($this->storageDirectory)) {
30+
$this->filesystem->mkdir($this->storageDirectory);
31+
}
32+
}
33+
34+
public function readFile(string $path): string
35+
{
36+
if (!$this->hasFile($path)) {
37+
throw new \InvalidArgumentException(sprintf('The file "%s" does not exist.', $path));
38+
}
39+
40+
return $this->filesystem->readFile($this->getAbsolutePath($path));
41+
}
42+
43+
public function hasFile(string $path): bool
44+
{
45+
return $this->filesystem->exists($this->getAbsolutePath($path));
46+
}
47+
48+
public function getFile(string $path): Document
49+
{
50+
if (!$this->hasFile($path)) {
51+
throw new \InvalidArgumentException(sprintf('The file "%s" does not exist.', $path));
52+
}
53+
54+
return new Document($this->getAbsolutePath($path));
55+
}
56+
57+
private function getAbsolutePath(string $path): string
58+
{
59+
try {
60+
$absolutePath = Path::makeAbsolute($path, $this->storageDirectory);
61+
} catch (\Throwable $e) {
62+
throw new \InvalidArgumentException(sprintf('The file "%s" is not valid.', $path), 0, $e);
63+
}
64+
65+
return $absolutePath;
66+
}
67+
68+
}

ux.symfony.com/src/Service/LiveDemoRepository.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,16 @@ class LiveDemoRepository
2121
public function findAll(): array
2222
{
2323
return [
24+
new LiveDemo(
25+
'download',
26+
name: 'Downloading files',
27+
description: 'Return file as downloadable attachment from your Live Component.',
28+
author: 'smnandre',
29+
publishedAt: '2025-01-01',
30+
tags: ['file', 'upload', 'LiveAction', 'download', 'button'],
31+
longDescription: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur tincidunt vulputate felis a ultricies.
32+
* Morbi at odio nec nulla imperdiet scelerisque a eget nibh. Donec convallis turpis ut nunc egest',
33+
),
2434
new LiveDemo(
2535
'infinite-scroll-2',
2636
name: 'Infinite Scroll - 2/2',
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace App\Twig\Components;
13+
14+
use App\Service\DocumentStorage;
15+
use DateTimeImmutable;
16+
use SplTempFileObject;
17+
use Symfony\Component\HttpFoundation\BinaryFileResponse;
18+
use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;
19+
use Symfony\UX\LiveComponent\Attribute\LiveAction;
20+
use Symfony\UX\LiveComponent\Attribute\LiveArg;
21+
use Symfony\UX\LiveComponent\Attribute\LiveProp;
22+
use Symfony\UX\LiveComponent\DefaultActionTrait;
23+
use Symfony\UX\LiveComponent\LiveDownloadResponse;
24+
25+
#[AsLiveComponent]
26+
// #[AsTaggedItem('controller.service_arguments')]
27+
final class DownloadFiles
28+
{
29+
use DefaultActionTrait;
30+
31+
#[LiveProp(writable: true)]
32+
public int $year = 2025;
33+
34+
public function __construct(
35+
private readonly DocumentStorage $documentStorage,
36+
) {
37+
}
38+
39+
#[LiveAction]
40+
public function download(): BinaryFileResponse
41+
{
42+
$file = $this->documentStorage->getFile('demos/empty.html');
43+
44+
return new LiveDownloadResponse($file);
45+
}
46+
47+
#[LiveAction]
48+
public function generate(#[LiveArg] string $format): BinaryFileResponse
49+
{
50+
$report = match($format) {
51+
'csv' => $this->generateCsvReport($this->year),
52+
'json' => $this->generateJsonReport($this->year),
53+
'md' => $this->generateMarkdownReport($this->year),
54+
default => throw new \InvalidArgumentException('Invalid format provided'),
55+
};
56+
57+
$file = new SplTempFileObject();
58+
$file->fwrite($report);
59+
60+
return new LiveDownloadResponse($file, 'report.'.$format);
61+
}
62+
63+
private function generateCsvReport(int $year): string
64+
{
65+
$file = new SplTempFileObject();
66+
// $file->fputcsv(['Month', 'Number', 'Name', 'Number of days']);
67+
foreach ($this->getReportData($year) as $row) {
68+
$file->fputcsv($row);
69+
}
70+
71+
return $file->fread($file->ftell());
72+
}
73+
74+
private function generateMarkdownReport(int $year): string
75+
{
76+
$rows = iterator_to_array($this->getReportData($year));
77+
78+
foreach ($rows as $key => $row) {
79+
$rows[$key] = '|'.implode('|', $row).'|';
80+
}
81+
82+
return implode("\n", $rows);
83+
}
84+
85+
private function generateJsonReport(int $year): string
86+
{
87+
$rows = iterator_to_array($this->getReportData($year));
88+
89+
return \json_encode($rows, JSON_FORCE_OBJECT | JSON_THROW_ON_ERROR);
90+
}
91+
92+
/**
93+
* @param int<2000,2025> $year The year to generate the report for (2000-2025)
94+
*
95+
* @return iterable<string, array{string, string}>
96+
*/
97+
private function getReportData(int $year): iterable
98+
{
99+
foreach (range(1, 12) as $month) {
100+
$startDate = DateTimeImmutable::createFromFormat('Y', $year)->setDate($year, $month, 1);
101+
$endDate = $startDate->modify('last day of this month');
102+
yield $month => [
103+
'name' => $startDate->format('F'),
104+
'month' => $startDate->format('F'),
105+
'number' => $startDate->format('Y-m'),
106+
'nb_days' => $endDate->diff($startDate)->days,
107+
];
108+
}
109+
}
110+
}

ux.symfony.com/templates/_header.html.twig

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,10 +50,12 @@
5050
<a href="{{ path('app_live_component') }}" class="AppNav_item">Live <span>Components</span></a>
5151
<a href="{{ path('app_icons') }}" class="AppNav_item">Icons</a>
5252
<a href="{{ path('app_packages') }}" class="AppNav_item">Packages</a>
53-
<a href="{{ path('app_demos') }}" class="AppNav_item">Demos</a>
53+
<a href="{{ path('app_demos') }}" class="AppNav_item">
54+
<span class="AppNav_badge" data-content="New"></span>
55+
Demos
56+
</a>
5457
<a href="{{ path('app_cookbook') }}" class="AppNav_item AppNav_item--cs">Cookbook</a>
5558
<a href="{{ path('app_support') }}" class="AppNav_item AppNav_item--cs" rel="help">
56-
<span class="AppNav_badge" data-content="New"></span>
5759
Support
5860
</a>
5961
</div>

ux.symfony.com/templates/components/Demo/DemoCard.html.twig

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,14 @@
88
loading="lazy"
99
>
1010
</div>
11+
12+
{% if demo.isNew() %}
13+
<div class="DemoCard__badge">
14+
<div class="Badge">
15+
NEW
16+
</div>
17+
</div>
18+
{% endif %}
1119

1220
<div class="DemoCard__content">
1321
<h4 class="DemoCard__title">
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
<div class="container p-lg-4" {{ attributes }}>
2+
3+
<div class="row row-cols-lg-2">
4+
<div class="col" style="display: flex; flex-direction: column; justify-content: space-between;">
5+
6+
<div style="display: grid; place-content: center;padding-block: 2rem;">
7+
<div style="max-width: 36rem;">
8+
<h4>Download an existing File</h4>
9+
<p>
10+
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur tincidunt vulputate felis a ultricies.
11+
Morbi at odio nec nulla imperdiet scelerisque a eget nibh. Donec convallis turpis ut nunc egestas rutrum.
12+
</p>
13+
</div>
14+
</div>
15+
16+
<div style="display: grid; place-content: center;padding-block: 2rem;">
17+
<button
18+
data-action="live#action"
19+
data-live-action-param="download"
20+
class="BigButton"
21+
>
22+
<twig:ux:icon name="lucide:download" />
23+
Download !
24+
</button>
25+
</div>
26+
27+
</div>
28+
29+
<div class="col" style="display: flex; flex-direction: column; justify-content: space-between;">
30+
31+
<h3>Or one generated on-demand</h3>
32+
33+
<p>Choose a format:</p>
34+
35+
<div class="grid" style="grid-template-columns: repeat(3, 1fr); gap: 1rem;">
36+
{% for format in ['csv', 'json', 'md'] %}
37+
<button type="button"
38+
class="BigButton"
39+
data-action="live#action"
40+
data-live-action-param="generate"
41+
data-live-format-param="{{ format }}"
42+
style="display: flex;"
43+
>
44+
<twig:ux:icon name="bi:filetype-{{ format }}" style="height: 1.5rem; width: 1.5rem;" />
45+
<span>
46+
<small>Download</small>
47+
<code>.{{ format|lower }}</code>
48+
</span>
49+
</button>
50+
{% endfor %}
51+
</div>
52+
53+
</div>
54+
</div>
55+
</div>
56+
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{% extends 'demos/live_demo.html.twig' %}
2+
3+
{% block code_block_left %}
4+
<twig:CodeBlock filename="src/Twig/Components/DownloadFiles.php" height="400px" />
5+
{% endblock %}
6+
7+
{% block code_block_right %}
8+
<twig:CodeBlock filename="templates/components/DownloadFiles.html.twig" height="400px" />
9+
{% endblock %}
10+
11+
{% block demo_content %}
12+
<twig:DownloadFiles />
13+
{% endblock %}

0 commit comments

Comments
 (0)