Add Projects page with detail view, removed navbar entries that only led to home and made featured projects links to the new site
All checks were successful
publish.yml / publish (push) Successful in 1m9s
All checks were successful
publish.yml / publish (push) Successful in 1m9s
This commit is contained in:
@@ -117,6 +117,28 @@ h2 {
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.project-link {
|
||||
display: block;
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.project-card {
|
||||
height: 100%;
|
||||
transition: border-color 120ms ease, transform 120ms ease, box-shadow 120ms ease;
|
||||
}
|
||||
|
||||
.project-link:hover .project-card,
|
||||
.project-link:focus-visible .project-card {
|
||||
border-color: rgba(88, 166, 255, 0.8);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 14px 28px rgba(31, 111, 235, 0.2);
|
||||
}
|
||||
|
||||
.project-link:focus-visible {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.card h3 {
|
||||
margin: 0;
|
||||
font-size: 1.08rem;
|
||||
|
||||
@@ -10,6 +10,25 @@
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section class="section" id="projects">
|
||||
<div class="section-title-row">
|
||||
<h2>Featured Projects</h2>
|
||||
<p>Recent work across web platforms, APIs, and mobile apps.</p>
|
||||
</div>
|
||||
<div class="card-grid projects-grid">
|
||||
@for (project of projects; track project.slug) {
|
||||
<a class="project-link" [routerLink]="['/projects', project.slug]">
|
||||
<app-card cardClass="project-card">
|
||||
<h3>{{ project.title }}</h3>
|
||||
<p>{{ project.description }}</p>
|
||||
<p class="meta">{{ project.stack.join(' - ') }}</p>
|
||||
<p class="impact">{{ project.impact }}</p>
|
||||
</app-card>
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section" id="stack">
|
||||
<div class="section-title-row">
|
||||
<h2>Tech Stack</h2>
|
||||
@@ -17,27 +36,10 @@
|
||||
</div>
|
||||
<div class="card-grid">
|
||||
@for (skill of skills; track skill.name) {
|
||||
<app-card cardClass="skill-card">
|
||||
<h3>{{ skill.name }}</h3>
|
||||
<p class="meta">{{ skill.category }} - {{ skill.level }}</p>
|
||||
</app-card>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section" id="projects">
|
||||
<div class="section-title-row">
|
||||
<h2>Featured Projects</h2>
|
||||
<p>Recent work across web platforms, APIs, and mobile apps.</p>
|
||||
</div>
|
||||
<div class="card-grid projects-grid">
|
||||
@for (project of projects; track project.title) {
|
||||
<app-card cardClass="project-card">
|
||||
<h3>{{ project.title }}</h3>
|
||||
<p>{{ project.description }}</p>
|
||||
<p class="meta">{{ project.stack.join(' - ') }}</p>
|
||||
<p class="impact">{{ project.impact }}</p>
|
||||
</app-card>
|
||||
<app-card cardClass="skill-card">
|
||||
<h3>{{ skill.name }}</h3>
|
||||
<p class="meta">{{ skill.category }} - {{ skill.level }}</p>
|
||||
</app-card>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -30,8 +30,11 @@ describe('Home', () => {
|
||||
expect(compiled.querySelector('.hero-cta a[routerlink="/about"]')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render all project cards', () => {
|
||||
it('should render all project cards as links', () => {
|
||||
const compiled = fixture.nativeElement as HTMLElement;
|
||||
expect(compiled.querySelectorAll('.project-card').length).toBe(component.projects.length);
|
||||
const links = compiled.querySelectorAll('#projects .project-link');
|
||||
|
||||
expect(links.length).toBe(component.projects.length);
|
||||
expect(links[0]?.getAttribute('href')).toContain('/projects/tasktimer');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { RouterLink } from '@angular/router';
|
||||
import { CardComponent } from '../../components/card/card';
|
||||
import { PROJECTS, Project } from '../projects/project-data';
|
||||
|
||||
interface Skill {
|
||||
name: string;
|
||||
@@ -8,13 +9,6 @@ interface Skill {
|
||||
level: string;
|
||||
}
|
||||
|
||||
interface Project {
|
||||
title: string;
|
||||
description: string;
|
||||
stack: string[];
|
||||
impact: string;
|
||||
}
|
||||
|
||||
interface TimelineItem {
|
||||
role: string;
|
||||
company: string;
|
||||
@@ -51,30 +45,7 @@ export class Home {
|
||||
{ name: 'Flutter', category: 'Mobile', level: 'Advanced' },
|
||||
];
|
||||
|
||||
readonly projects: Project[] = [
|
||||
{
|
||||
title: 'Tasktimer',
|
||||
description:
|
||||
'Jira time-tracking app with custom reporting and connectivity, build for mobile and web.',
|
||||
stack: ['Ionic', 'React', 'Spring Boot', 'Oracle', 'Web', 'Android', 'iOS'],
|
||||
impact:
|
||||
'Led backend and architectural work: implemented Spring Boot backend, database access for Oracle with reporting capabilities, including reliable Jira connectivity.',
|
||||
},
|
||||
{
|
||||
title: 'Synopsis Platform Core',
|
||||
description: 'Main Synopsis Platform codebase (backend + frontend pieces).',
|
||||
stack: ['C#', 'TypeScript', 'HTML', 'Docker', 'Web'],
|
||||
impact:
|
||||
'Contributed via issues, code and investigations (plugin-loader, microfrontend research), influencing platform direction and stability.',
|
||||
},
|
||||
{
|
||||
title: 'Website SV Hofkirchen (Chess Club)',
|
||||
description: 'Club management web app for the SV Hofkirchen chess club.',
|
||||
stack: ['C#', '.NET', 'Blazor', 'SQLite', 'Web'],
|
||||
impact:
|
||||
'Designed the technical core across backend, database, and Blazor UI: implemented backend services, and partially built the Blazor frontend',
|
||||
},
|
||||
];
|
||||
readonly projects: Project[] = PROJECTS;
|
||||
|
||||
readonly timeline: TimelineItem[] = [
|
||||
{
|
||||
|
||||
63
src/pages/projects/project-data.ts
Normal file
63
src/pages/projects/project-data.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
export interface Project {
|
||||
slug: string;
|
||||
title: string;
|
||||
description: string;
|
||||
stack: string[];
|
||||
impact: string;
|
||||
overview: string;
|
||||
highlights: string[];
|
||||
}
|
||||
|
||||
export const PROJECTS: Project[] = [
|
||||
{
|
||||
slug: 'tasktimer',
|
||||
title: 'Tasktimer',
|
||||
description:
|
||||
'Jira time-tracking app with custom reporting and connectivity, built for mobile and web.',
|
||||
stack: ['Ionic', 'React', 'Spring Boot', 'Oracle', 'Web', 'Android', 'iOS'],
|
||||
impact:
|
||||
'Led backend and architecture work for reporting and Jira synchronization reliability.',
|
||||
overview:
|
||||
'Tasktimer helps teams capture and report time across Jira projects while keeping data synchronized and audit-friendly.',
|
||||
highlights: [
|
||||
'Implemented a Spring Boot API with Oracle-backed reporting endpoints.',
|
||||
'Designed resilient Jira connectivity to keep remote data in sync.',
|
||||
'Supported cross-platform delivery for web and mobile clients.',
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: 'synopsis-platform-core',
|
||||
title: 'Synopsis Platform Core',
|
||||
description: 'Main Synopsis Platform codebase spanning backend and frontend modules.',
|
||||
stack: ['C#', 'TypeScript', 'HTML', 'Docker', 'Web'],
|
||||
impact:
|
||||
'Contributed investigations and implementation work that improved platform stability and direction.',
|
||||
overview:
|
||||
'The platform core powers shared capabilities used by multiple product teams, with a focus on maintainable architecture.',
|
||||
highlights: [
|
||||
'Delivered issue-driven improvements in plugin loading behavior.',
|
||||
'Contributed to microfrontend architecture research and experiments.',
|
||||
'Supported production hardening with iterative bug-fixing and code cleanup.',
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: 'sv-hofkirchen-website',
|
||||
title: 'Website SV Hofkirchen (Chess Club)',
|
||||
description: 'Club management web app for the SV Hofkirchen chess club.',
|
||||
stack: ['C#', '.NET', 'Blazor', 'SQLite', 'Web'],
|
||||
impact:
|
||||
'Built core backend services and key parts of the Blazor UI to support club operations.',
|
||||
overview:
|
||||
'A practical web app for memberships, event coordination, and day-to-day administration for a local chess club.',
|
||||
highlights: [
|
||||
'Implemented backend services and database flows with SQLite.',
|
||||
'Designed and built major parts of the Blazor frontend.',
|
||||
'Shaped the project architecture to stay lightweight and maintainable.',
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export function getProjectBySlug(projectSlug: string): Project | undefined {
|
||||
return PROJECTS.find((project) => project.slug === projectSlug);
|
||||
}
|
||||
|
||||
102
src/pages/projects/project-detail/project-detail.css
Normal file
102
src/pages/projects/project-detail/project-detail.css
Normal file
@@ -0,0 +1,102 @@
|
||||
.page-wrap {
|
||||
margin: 0 auto;
|
||||
padding: 1rem 1.25rem 4rem;
|
||||
}
|
||||
|
||||
.section {
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
.hero {
|
||||
padding: 2.5rem;
|
||||
border: 1px solid rgba(110, 118, 129, 0.28);
|
||||
border-radius: 24px;
|
||||
background: radial-gradient(circle at top right, rgba(88, 166, 255, 0.2), rgba(13, 17, 23, 0.85));
|
||||
}
|
||||
|
||||
.eyebrow {
|
||||
width: fit-content;
|
||||
margin: 0 0 1rem;
|
||||
padding: 0.35rem 0.7rem;
|
||||
border: 1px solid rgba(88, 166, 255, 0.5);
|
||||
border-radius: 999px;
|
||||
color: #9ecbff;
|
||||
font-size: 0.82rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
p,
|
||||
ul {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: clamp(2rem, 5vw, 3.2rem);
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.lede {
|
||||
max-width: 70ch;
|
||||
margin-top: 1rem;
|
||||
color: #c9d1d9;
|
||||
}
|
||||
|
||||
.hero-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.75rem;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.btn {
|
||||
display: inline-block;
|
||||
padding: 0.72rem 1.1rem;
|
||||
border: 1px solid #3d444d;
|
||||
border-radius: 10px;
|
||||
color: #e6edf3;
|
||||
background: rgba(33, 38, 45, 0.55);
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
transition: transform 120ms ease, border-color 120ms ease;
|
||||
}
|
||||
|
||||
.btn:hover,
|
||||
.btn:focus-visible {
|
||||
transform: translateY(-2px);
|
||||
border-color: #58a6ff;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.detail-grid {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
|
||||
}
|
||||
|
||||
.meta {
|
||||
color: #8b949e;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
ul {
|
||||
padding-left: 1.2rem;
|
||||
}
|
||||
|
||||
li + li {
|
||||
margin-top: 0.45rem;
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.page-wrap {
|
||||
padding-top: 0.5rem;
|
||||
}
|
||||
|
||||
.hero {
|
||||
padding: 2rem 1.2rem;
|
||||
}
|
||||
}
|
||||
|
||||
45
src/pages/projects/project-detail/project-detail.html
Normal file
45
src/pages/projects/project-detail/project-detail.html
Normal file
@@ -0,0 +1,45 @@
|
||||
<div class="page-wrap">
|
||||
@if (project; as project) {
|
||||
<section class="hero section">
|
||||
<p class="eyebrow">Project</p>
|
||||
<h1>{{ project.title }}</h1>
|
||||
<p class="lede">{{ project.overview }}</p>
|
||||
<div class="hero-actions">
|
||||
<a class="btn btn-ghost" routerLink="/projects">Back to projects</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<div class="detail-grid">
|
||||
<app-card>
|
||||
<h2>Stack</h2>
|
||||
<p class="meta">{{ project.stack.join(' - ') }}</p>
|
||||
</app-card>
|
||||
|
||||
<app-card>
|
||||
<h2>Impact</h2>
|
||||
<p>{{ project.impact }}</p>
|
||||
</app-card>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<app-card>
|
||||
<h2>Highlights</h2>
|
||||
<ul>
|
||||
@for (highlight of project.highlights; track highlight) {
|
||||
<li>{{ highlight }}</li>
|
||||
}
|
||||
</ul>
|
||||
</app-card>
|
||||
</section>
|
||||
} @else {
|
||||
<section class="hero section">
|
||||
<p class="eyebrow">Project</p>
|
||||
<h1>Project not found</h1>
|
||||
<p class="lede">The requested project does not exist. Please pick one from the projects page.</p>
|
||||
<a class="btn btn-ghost" routerLink="/projects">Back to projects</a>
|
||||
</section>
|
||||
}
|
||||
</div>
|
||||
|
||||
47
src/pages/projects/project-detail/project-detail.spec.ts
Normal file
47
src/pages/projects/project-detail/project-detail.spec.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { ActivatedRoute, convertToParamMap, provideRouter } from '@angular/router';
|
||||
|
||||
import { ProjectDetail } from './project-detail';
|
||||
|
||||
describe('ProjectDetail', () => {
|
||||
async function createComponent(projectName: string): Promise<ComponentFixture<ProjectDetail>> {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [ProjectDetail],
|
||||
providers: [
|
||||
provideRouter([]),
|
||||
{
|
||||
provide: ActivatedRoute,
|
||||
useValue: {
|
||||
snapshot: {
|
||||
paramMap: convertToParamMap({ projectName }),
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compileComponents();
|
||||
|
||||
const fixture = TestBed.createComponent(ProjectDetail);
|
||||
await fixture.whenStable();
|
||||
fixture.detectChanges();
|
||||
return fixture;
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
TestBed.resetTestingModule();
|
||||
});
|
||||
|
||||
it('should render project details for known project', async () => {
|
||||
const fixture = await createComponent('tasktimer');
|
||||
const compiled = fixture.nativeElement as HTMLElement;
|
||||
|
||||
expect(compiled.querySelector('h1')?.textContent).toContain('Tasktimer');
|
||||
expect(compiled.querySelectorAll('li').length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should show not found state for unknown project', async () => {
|
||||
const fixture = await createComponent('unknown-project');
|
||||
const compiled = fixture.nativeElement as HTMLElement;
|
||||
|
||||
expect(compiled.querySelector('h1')?.textContent).toContain('Project not found');
|
||||
});
|
||||
});
|
||||
20
src/pages/projects/project-detail/project-detail.ts
Normal file
20
src/pages/projects/project-detail/project-detail.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { Component, inject } from '@angular/core';
|
||||
import { ActivatedRoute, RouterLink } from '@angular/router';
|
||||
import { CardComponent } from '../../../components/card/card';
|
||||
import { getProjectBySlug, Project } from '../project-data';
|
||||
|
||||
@Component({
|
||||
selector: 'app-project-detail',
|
||||
imports: [RouterLink, CardComponent],
|
||||
templateUrl: './project-detail.html',
|
||||
styleUrl: './project-detail.css',
|
||||
})
|
||||
export class ProjectDetail {
|
||||
private readonly route = inject(ActivatedRoute);
|
||||
|
||||
get project(): Project | undefined {
|
||||
const projectName = this.route.snapshot.paramMap.get('projectName') ?? '';
|
||||
return getProjectBySlug(projectName);
|
||||
}
|
||||
}
|
||||
|
||||
98
src/pages/projects/projects.css
Normal file
98
src/pages/projects/projects.css
Normal file
@@ -0,0 +1,98 @@
|
||||
.page-wrap {
|
||||
margin: 0 auto;
|
||||
padding: 1rem 1.25rem 4rem;
|
||||
}
|
||||
|
||||
.section {
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
.hero {
|
||||
padding: 2.5rem;
|
||||
border: 1px solid rgba(110, 118, 129, 0.28);
|
||||
border-radius: 24px;
|
||||
background: radial-gradient(circle at top right, rgba(88, 166, 255, 0.2), rgba(13, 17, 23, 0.85));
|
||||
}
|
||||
|
||||
.eyebrow {
|
||||
width: fit-content;
|
||||
margin: 0 0 1rem;
|
||||
padding: 0.35rem 0.7rem;
|
||||
border: 1px solid rgba(88, 166, 255, 0.5);
|
||||
border-radius: 999px;
|
||||
color: #9ecbff;
|
||||
font-size: 0.82rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: clamp(2rem, 5vw, 3.2rem);
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.lede {
|
||||
max-width: 65ch;
|
||||
margin-top: 1rem;
|
||||
color: #c9d1d9;
|
||||
}
|
||||
|
||||
.card-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.project-link {
|
||||
display: block;
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.project-card {
|
||||
height: 100%;
|
||||
transition: border-color 120ms ease, transform 120ms ease, box-shadow 120ms ease;
|
||||
}
|
||||
|
||||
.project-link:hover .project-card,
|
||||
.project-link:focus-visible .project-card {
|
||||
border-color: rgba(88, 166, 255, 0.8);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 14px 28px rgba(31, 111, 235, 0.2);
|
||||
}
|
||||
|
||||
.project-link:focus-visible {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.project-card p {
|
||||
margin-top: 0.75rem;
|
||||
}
|
||||
|
||||
.meta {
|
||||
color: #8b949e;
|
||||
font-size: 0.92rem;
|
||||
}
|
||||
|
||||
.impact {
|
||||
color: #9ecbff;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.page-wrap {
|
||||
padding-top: 0.5rem;
|
||||
}
|
||||
|
||||
.hero {
|
||||
padding: 2rem 1.2rem;
|
||||
}
|
||||
}
|
||||
|
||||
26
src/pages/projects/projects.html
Normal file
26
src/pages/projects/projects.html
Normal file
@@ -0,0 +1,26 @@
|
||||
<div class="page-wrap">
|
||||
<section class="hero section">
|
||||
<p class="eyebrow">Projects</p>
|
||||
<h1>Featured work across web, backend, and mobile.</h1>
|
||||
<p class="lede">
|
||||
A selection of projects that demonstrate my skills in building production-ready software
|
||||
solutions across various platforms and technologies.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<div class="card-grid projects-grid">
|
||||
@for (project of projects; track project.slug) {
|
||||
<a class="project-link" [routerLink]="['/projects', project.slug]">
|
||||
<app-card cardClass="project-card">
|
||||
<h2>{{ project.title }}</h2>
|
||||
<p>{{ project.description }}</p>
|
||||
<p class="meta">{{ project.stack.join(' - ') }}</p>
|
||||
<p class="impact">{{ project.impact }}</p>
|
||||
</app-card>
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
33
src/pages/projects/projects.spec.ts
Normal file
33
src/pages/projects/projects.spec.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { provideRouter } from '@angular/router';
|
||||
|
||||
import { Projects } from './projects';
|
||||
|
||||
describe('Projects', () => {
|
||||
let component: Projects;
|
||||
let fixture: ComponentFixture<Projects>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [Projects],
|
||||
providers: [provideRouter([])],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(Projects);
|
||||
component = fixture.componentInstance;
|
||||
await fixture.whenStable();
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render project cards and links', () => {
|
||||
const compiled = fixture.nativeElement as HTMLElement;
|
||||
expect(compiled.querySelector('h1')?.textContent).toContain('Featured work');
|
||||
expect(compiled.querySelectorAll('.project-card').length).toBe(component.projects.length);
|
||||
expect(compiled.querySelectorAll('.project-link').length).toBe(component.projects.length);
|
||||
});
|
||||
});
|
||||
|
||||
15
src/pages/projects/projects.ts
Normal file
15
src/pages/projects/projects.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { RouterLink } from '@angular/router';
|
||||
import { CardComponent } from '../../components/card/card';
|
||||
import { PROJECTS, Project } from './project-data';
|
||||
|
||||
@Component({
|
||||
selector: 'app-projects',
|
||||
imports: [RouterLink, CardComponent],
|
||||
templateUrl: './projects.html',
|
||||
styleUrl: './projects.css',
|
||||
})
|
||||
export class Projects {
|
||||
readonly projects: Project[] = PROJECTS;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user