feat: scaffold initial Cheatsheet Hub application
This commit establishes the foundational structure for the Cheatsheet Hub project. It includes the setup of a Vue 3, Vite, and TailwindCSS stack, along with core application features: - **Routing:** Configures `vue-router` for Home, Cheatsheet List, and Cheatsheet Detail pages. - **State Management:** Implements a `pinia` store for theme management (dark/light mode) and mock data. - **UI Components:** Adds reusable components like `NavBar`, `CheatsheetCard`, and `PrintButton`. - **Styling:** Integrates TailwindCSS with custom base styles, dark mode support, and print-friendly optimizations. - **Dependencies:** Adds key libraries including `marked` for content rendering.
This commit is contained in:
6432
package-lock.json
generated
Normal file
6432
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -16,9 +16,11 @@
|
||||
"format": "prettier --write src/"
|
||||
},
|
||||
"dependencies": {
|
||||
"marked": "^16.3.0",
|
||||
"pinia": "^3.0.3",
|
||||
"vue": "^3.5.18",
|
||||
"vue-router": "^4.5.1"
|
||||
"vue-router": "^4.5.1",
|
||||
"axios": "^1.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tsconfig/node22": "^22.0.2",
|
||||
@@ -35,6 +37,9 @@
|
||||
"typescript": "~5.8.0",
|
||||
"vite": "^7.0.6",
|
||||
"vite-plugin-vue-devtools": "^8.0.0",
|
||||
"vue-tsc": "^3.0.4"
|
||||
"vue-tsc": "^3.0.4",
|
||||
"tailwindcss": "^3.3.3",
|
||||
"autoprefixer": "^10.4.14",
|
||||
"postcss": "^8.4.24"
|
||||
}
|
||||
}
|
||||
|
||||
589
pnpm-lock.yaml
generated
589
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
6
postcss.config.js
Normal file
6
postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
67
prompt.md
Normal file
67
prompt.md
Normal file
@@ -0,0 +1,67 @@
|
||||
# Cheatsheet 网站项目 Scaffold
|
||||
|
||||
你是一名资深前端架构师,熟悉 **Vue 3 + Vite + TailwindCSS** 的现代前端开发栈。
|
||||
请帮我生成一个 **Cheatsheet 网站** 的基础工程结构,要求如下:
|
||||
|
||||
## 1. 技术栈
|
||||
|
||||
* **框架**:Vue 3 (Composition API)
|
||||
* **构建工具**:Vite
|
||||
* **样式**:TailwindCSS + 自定义 base.css / main.css
|
||||
* **组件库**:官方 Vue 生态优先(可选 shadcn-vue / headlessui / radix-vue)
|
||||
* **工具库**:
|
||||
|
||||
* `vue-router` (多视图切换:主页 / Cheatsheet 列表 / 单个 Cheatsheet 页面)
|
||||
* `pinia` (全局状态管理,用于存储 Cheatsheet 配置、主题偏好)
|
||||
* `axios` 或 `fetch`(数据获取,预留接口位)
|
||||
|
||||
## 2. 项目结构
|
||||
|
||||
请生成一个基础目录结构,包含:
|
||||
|
||||
```
|
||||
src/
|
||||
assets/ # 静态资源
|
||||
components/ # 通用组件(按钮、卡片、导航栏)
|
||||
views/ # 页面视图(HomeView, CheatsheetListView, CheatsheetDetailView)
|
||||
store/ # Pinia 状态管理
|
||||
router/ # vue-router 配置
|
||||
styles/
|
||||
base.css # Tailwind 基础覆盖样式
|
||||
main.css # 全局样式入口
|
||||
App.vue
|
||||
main.js
|
||||
```
|
||||
|
||||
## 3. 基础样式
|
||||
|
||||
* 在 `base.css` 中:
|
||||
|
||||
* 定义打印优化样式(例如:去掉多余边框、背景)
|
||||
* 定义默认字体、字号(便于打印的 A4 规格)
|
||||
* 在 `main.css` 中:
|
||||
|
||||
* 引入 Tailwind
|
||||
* 定义全局主题色(默认浅色 / 深色模式切换)
|
||||
|
||||
## 4. 默认页面
|
||||
|
||||
请提供基础模板代码:
|
||||
|
||||
* **HomeView**:展示官方 Cheatsheet(Docker, Vim)+ 推荐的自定义入口
|
||||
* **CheatsheetListView**:显示所有 Cheatsheet 的卡片列表
|
||||
* **CheatsheetDetailView**:单个 Cheatsheet 的内容,支持「打印模式」按钮
|
||||
|
||||
## 5. 通用组件
|
||||
|
||||
请生成:
|
||||
|
||||
* `NavBar.vue`(带首页 / Cheatsheet 列表 / 主题切换按钮)
|
||||
* `CheatsheetCard.vue`(显示标题、简介、打印按钮)
|
||||
* `PrintButton.vue`(触发浏览器打印)
|
||||
|
||||
## 6. 额外要求
|
||||
|
||||
* 所有组件保持简洁、易扩展,留出注释
|
||||
* 尽量用 Tailwind Utility Class,而不是额外 CSS
|
||||
* 确保项目可以直接运行(npm run dev)
|
||||
36
src/App.vue
36
src/App.vue
@@ -1,11 +1,31 @@
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<template>
|
||||
<h1>You did it!</h1>
|
||||
<p>
|
||||
Visit <a href="https://vuejs.org/" target="_blank" rel="noopener">vuejs.org</a> to read the
|
||||
documentation
|
||||
</p>
|
||||
<div :class="{ 'dark': isDark }">
|
||||
<div class="min-h-screen bg-gray-50 dark:bg-gray-900 text-gray-900 dark:text-gray-100">
|
||||
<NavBar />
|
||||
<main class="container mx-auto px-4 py-8">
|
||||
<router-view />
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
<script lang="ts">
|
||||
import { mapState, mapWritableState } from 'pinia'
|
||||
import { useAppStore } from './stores'
|
||||
import NavBar from './components/NavBar.vue'
|
||||
|
||||
export default {
|
||||
name: 'App',
|
||||
components: {
|
||||
NavBar
|
||||
},
|
||||
computed: {
|
||||
...mapState(useAppStore, ['isDark']),
|
||||
...mapWritableState(useAppStore, ['theme'])
|
||||
},
|
||||
mounted() {
|
||||
// 初始化主题
|
||||
document.documentElement.setAttribute('data-theme', this.theme)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
29
src/components/CheatsheetCard.vue
Normal file
29
src/components/CheatsheetCard.vue
Normal file
@@ -0,0 +1,29 @@
|
||||
<template>
|
||||
<div class="card hover:shadow-lg transition-shadow duration-200">
|
||||
<h3 class="text-xl font-semibold mb-2">{{ cheatsheet.title }}</h3>
|
||||
<p class="text-gray-600 dark:text-gray-400 mb-4">{{ cheatsheet.description }}</p>
|
||||
<div class="flex justify-between items-center">
|
||||
<router-link :to="`/cheatsheet/${cheatsheet.id}`" class="text-primary-500 hover:text-primary-600 font-medium">
|
||||
View Details
|
||||
</router-link>
|
||||
<PrintButton :cheatsheet="cheatsheet" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import PrintButton from './PrintButton.vue'
|
||||
|
||||
export default {
|
||||
name: 'CheatsheetCard',
|
||||
components: {
|
||||
PrintButton
|
||||
},
|
||||
props: {
|
||||
cheatsheet: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
46
src/components/NavBar.vue
Normal file
46
src/components/NavBar.vue
Normal file
@@ -0,0 +1,46 @@
|
||||
<template>
|
||||
<nav class="bg-white dark:bg-gray-800 shadow-sm no-print">
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="flex justify-between items-center py-4">
|
||||
<router-link to="/" class="text-xl font-bold text-primary-500">
|
||||
Cheatsheet Hub
|
||||
</router-link>
|
||||
|
||||
<div class="flex items-center space-x-4">
|
||||
<router-link to="/" class="text-gray-700 dark:text-gray-300 hover:text-primary-500 transition-colors"
|
||||
:class="{ 'text-primary-500 font-medium': $route.name === 'home' }">
|
||||
Home
|
||||
</router-link>
|
||||
|
||||
<router-link to="/cheatsheets"
|
||||
class="text-gray-700 dark:text-gray-300 hover:text-primary-500 transition-colors"
|
||||
:class="{ 'text-primary-500 font-medium': $route.name === 'cheatsheets' }">
|
||||
Cheatsheets
|
||||
</router-link>
|
||||
|
||||
<button @click="toggleTheme"
|
||||
class="p-2 rounded-full hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors"
|
||||
aria-label="Toggle theme">
|
||||
<span v-if="isDark">🌙</span>
|
||||
<span v-else>☀️</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { mapState, mapActions } from 'pinia'
|
||||
import { useAppStore } from '../stores'
|
||||
|
||||
export default {
|
||||
name: 'NavBar',
|
||||
computed: {
|
||||
...mapState(useAppStore, ['isDark'])
|
||||
},
|
||||
methods: {
|
||||
...mapActions(useAppStore, ['toggleTheme'])
|
||||
}
|
||||
}
|
||||
</script>
|
||||
27
src/components/PrintButton.vue
Normal file
27
src/components/PrintButton.vue
Normal file
@@ -0,0 +1,27 @@
|
||||
<template>
|
||||
<button @click="handlePrint"
|
||||
class="p-2 text-gray-500 hover:text-gray-700 dark:hover:text-gray-300 rounded-full hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors no-print"
|
||||
aria-label="Print cheatsheet">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M17 17h2a2 2 0 002-2v-4a2 2 0 00-2-2H5a2 2 0 00-2 2v4a2 2 0 002 2h2m4 4h6a2 2 0 002-2v-4a2 2 0 00-2-2h-6a2 2 0 00-2 2v4a2 2 0 002 2z" />
|
||||
</svg>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'PrintButton',
|
||||
props: {
|
||||
cheatsheet: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handlePrint() {
|
||||
window.print()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -1,3 +1,5 @@
|
||||
import './styles/main.css'
|
||||
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
|
||||
|
||||
@@ -1,8 +1,29 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import HomeView from '../views/HomeView.vue'
|
||||
import CheatsheetListView from '../views/CheatsheetListView.vue'
|
||||
import CheatsheetDetailView from '../views/CheatsheetDetailView.vue'
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: '/',
|
||||
name: 'home',
|
||||
component: HomeView,
|
||||
},
|
||||
{
|
||||
path: '/cheatsheets',
|
||||
name: 'cheatsheets',
|
||||
component: CheatsheetListView,
|
||||
},
|
||||
{
|
||||
path: '/cheatsheet/:id',
|
||||
name: 'cheatsheet',
|
||||
component: CheatsheetDetailView,
|
||||
},
|
||||
]
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
routes: [],
|
||||
history: createWebHistory(),
|
||||
routes,
|
||||
})
|
||||
|
||||
export default router
|
||||
|
||||
42
src/stores/index.ts
Normal file
42
src/stores/index.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
export const useAppStore = defineStore('app', {
|
||||
state: () => ({
|
||||
theme: 'light',
|
||||
cheatsheets: [
|
||||
{
|
||||
id: 'docker',
|
||||
title: 'Docker Cheatsheet',
|
||||
description: 'Essential Docker commands and tips',
|
||||
content:
|
||||
'# Docker Commands\n\n## Container Management\n- `docker run [image]` - Start a new container\n- `docker ps` - List running containers\n- `docker stop [container]` - Stop a container',
|
||||
},
|
||||
{
|
||||
id: 'vim',
|
||||
title: 'Vim Cheatsheet',
|
||||
description: 'Vim commands for efficient editing',
|
||||
content:
|
||||
'# Vim Commands\n\n## Navigation\n- `h,j,k,l` - Move left, down, up, right\n- `w` - Move to next word\n- `b` - Move to previous word',
|
||||
},
|
||||
{
|
||||
id: 'git',
|
||||
title: 'Git Cheatsheet',
|
||||
description: 'Common Git commands for version control',
|
||||
content:
|
||||
'# Git Commands\n\n## Basics\n- `git init` - Initialize a new repository\n- `git add [file]` - Stage changes\n- `git commit -m "message"` - Commit changes',
|
||||
},
|
||||
],
|
||||
}),
|
||||
getters: {
|
||||
isDark: (state) => state.theme === 'dark',
|
||||
},
|
||||
actions: {
|
||||
toggleTheme() {
|
||||
this.theme = this.theme === 'light' ? 'dark' : 'light'
|
||||
document.documentElement.setAttribute('data-theme', this.theme)
|
||||
},
|
||||
getCheatsheetById(id) {
|
||||
return this.cheatsheets.find((c) => c.id === id)
|
||||
},
|
||||
},
|
||||
})
|
||||
53
src/styles/base.css
Normal file
53
src/styles/base.css
Normal file
@@ -0,0 +1,53 @@
|
||||
/* 打印优化样式 */
|
||||
@media print {
|
||||
.no-print {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
* {
|
||||
background: transparent !important;
|
||||
color: black !important;
|
||||
box-shadow: none !important;
|
||||
text-shadow: none !important;
|
||||
}
|
||||
|
||||
body {
|
||||
font-size: 12pt;
|
||||
line-height: 1.5;
|
||||
font-family: 'Times New Roman', Times, serif;
|
||||
}
|
||||
|
||||
/* 确保链接在打印时显示URL */
|
||||
a[href]:after {
|
||||
content: ' (' attr(href) ')';
|
||||
}
|
||||
|
||||
/* 避免在内容中间分页 */
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
page-break-after: avoid;
|
||||
}
|
||||
|
||||
pre,
|
||||
blockquote {
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
}
|
||||
|
||||
/* 默认字体和排版 */
|
||||
body {
|
||||
font-family:
|
||||
-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
/* A4规格尺寸参考 */
|
||||
.a4-container {
|
||||
width: 210mm;
|
||||
min-height: 297mm;
|
||||
}
|
||||
32
src/styles/main.css
Normal file
32
src/styles/main.css
Normal file
@@ -0,0 +1,32 @@
|
||||
/* These Tailwind directives must be processed by Tailwind CSS build tools like PostCSS.
|
||||
If you see "Unknown at rule @tailwind", make sure your build pipeline includes Tailwind CSS.
|
||||
If you want to use plain CSS, remove these lines and use the generated output from Tailwind. */
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
/* 全局主题变量 */
|
||||
:root {
|
||||
--color-primary: #3b82f6;
|
||||
--color-primary-dark: #2563eb;
|
||||
--color-background: #ffffff;
|
||||
--color-text: #1f2937;
|
||||
}
|
||||
|
||||
[data-theme='dark'] {
|
||||
--color-primary: #60a5fa;
|
||||
--color-primary-dark: #3b82f6;
|
||||
--color-background: #111827;
|
||||
--color-text: #f3f4f6;
|
||||
}
|
||||
|
||||
/* 自定义组件样式 */
|
||||
@layer components {
|
||||
.btn-primary {
|
||||
@apply bg-primary-500 hover:bg-primary-600 text-white font-medium py-2 px-4 rounded transition-colors duration-200;
|
||||
}
|
||||
|
||||
.card {
|
||||
@apply bg-white dark:bg-gray-800 rounded-lg shadow-md p-6 border border-gray-200 dark:border-gray-700;
|
||||
}
|
||||
}
|
||||
54
src/views/CheatsheetDetailView.vue
Normal file
54
src/views/CheatsheetDetailView.vue
Normal file
@@ -0,0 +1,54 @@
|
||||
<template>
|
||||
<div v-if="cheatsheet" class="a4-container mx-auto">
|
||||
<div class="card mb-6 no-print">
|
||||
<div class="flex justify-between items-center">
|
||||
<h1 class="text-3xl font-bold">{{ cheatsheet.title }}</h1>
|
||||
<PrintButton :cheatsheet="cheatsheet" />
|
||||
</div>
|
||||
<p class="text-gray-600 dark:text-gray-400 mt-2">{{ cheatsheet.description }}</p>
|
||||
</div>
|
||||
|
||||
<div class="bg-white dark:bg-gray-800 p-8 rounded-lg shadow-md">
|
||||
<div v-html="renderedContent" class="prose dark:prose-invert max-w-none"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="text-center py-12">
|
||||
<p class="text-xl">Cheatsheet not found</p>
|
||||
<router-link to="/cheatsheets" class="text-primary-500 hover:underline mt-4 inline-block">
|
||||
Back to all cheatsheets
|
||||
</router-link>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { marked } from 'marked'
|
||||
import { mapActions } from 'pinia'
|
||||
import { useAppStore } from '../stores'
|
||||
import PrintButton from '../components/PrintButton.vue'
|
||||
|
||||
export default {
|
||||
name: 'CheatsheetDetailView',
|
||||
components: {
|
||||
PrintButton
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
cheatsheet: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
renderedContent() {
|
||||
if (!this.cheatsheet?.content) return ''
|
||||
return marked(this.cheatsheet.content)
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
const cheatsheetId = this.$route.params.id
|
||||
this.cheatsheet = this.getCheatsheetById(cheatsheetId)
|
||||
},
|
||||
methods: {
|
||||
...mapActions(useAppStore, ['getCheatsheetById'])
|
||||
}
|
||||
}
|
||||
</script>
|
||||
25
src/views/CheatsheetListView.vue
Normal file
25
src/views/CheatsheetListView.vue
Normal file
@@ -0,0 +1,25 @@
|
||||
<template>
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold mb-8">All Cheatsheets</h1>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
<CheatsheetCard v-for="cheatsheet in cheatsheets" :key="cheatsheet.id" :cheatsheet="cheatsheet" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { mapState } from 'pinia'
|
||||
import { useAppStore } from '../stores'
|
||||
import CheatsheetCard from '../components/CheatsheetCard.vue'
|
||||
|
||||
export default {
|
||||
name: 'CheatsheetListView',
|
||||
components: {
|
||||
CheatsheetCard
|
||||
},
|
||||
computed: {
|
||||
...mapState(useAppStore, ['cheatsheets'])
|
||||
}
|
||||
}
|
||||
</script>
|
||||
44
src/views/HomeView.vue
Normal file
44
src/views/HomeView.vue
Normal file
@@ -0,0 +1,44 @@
|
||||
<template>
|
||||
<div>
|
||||
<section class="mb-12">
|
||||
<h1 class="text-4xl font-bold text-center mb-6">Welcome to Cheatsheet Hub</h1>
|
||||
<p class="text-xl text-center text-gray-600 dark:text-gray-400 max-w-2xl mx-auto">
|
||||
Your one-stop destination for all programming cheatsheets. Find, create, and share useful references.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 class="text-2xl font-semibold mb-6">Popular Cheatsheets</h2>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
<CheatsheetCard v-for="cheatsheet in featuredCheatsheets" :key="cheatsheet.id" :cheatsheet="cheatsheet" />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="mt-12">
|
||||
<div class="card text-center">
|
||||
<h2 class="text-2xl font-semibold mb-4">Can't find what you're looking for?</h2>
|
||||
<p class="mb-6">Suggest a new cheatsheet or contribute to our growing collection.</p>
|
||||
<button class="btn-primary">Suggest a Cheatsheet</button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { mapState } from 'pinia'
|
||||
import { useAppStore } from '../stores'
|
||||
import CheatsheetCard from '../components/CheatsheetCard.vue'
|
||||
|
||||
export default {
|
||||
name: 'HomeView',
|
||||
components: {
|
||||
CheatsheetCard
|
||||
},
|
||||
computed: {
|
||||
...mapState(useAppStore, ['cheatsheets']),
|
||||
featuredCheatsheets() {
|
||||
return this.cheatsheets.slice(0, 3)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
24
tailwind.config.js
Normal file
24
tailwind.config.js
Normal file
@@ -0,0 +1,24 @@
|
||||
// tailwind.config.js
|
||||
export default {
|
||||
content: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
primary: {
|
||||
50: '#eff6ff',
|
||||
100: '#dbeafe',
|
||||
200: '#bfdbfe',
|
||||
300: '#93c5fd',
|
||||
400: '#60a5fa',
|
||||
500: '#3b82f6',
|
||||
600: '#2563eb', // This defines bg-primary-600
|
||||
700: '#1d4ed8',
|
||||
800: '#1e40af',
|
||||
900: '#1e3a8a',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
darkMode: 'class',
|
||||
}
|
||||
Reference in New Issue
Block a user