Published by
v14 Ticket Tool
v14 Discord ticket tool bot
- License MIT
- Publication Date 25/02/2024 - 22:54
- Version Discord.js V14
- Command Type Slash Commands
- Views 2
- Downloads 1
- Files 10
- Images 0
Click to see the description in this file!
{
"scripts": {
"start": "node index.js"
},
"dependencies": {
"@discordjs/rest": "^1.0.0",
"discord-html-transcripts": "^3.2.0",
"discord.js": "^14.14.1",
"ejs": "^3.1.9",
"express": "^4.18.1",
"express-session": "^1.18.0",
"fs": "^0.0.1-security",
"lodash.uniqwith": "^4.5.0",
"memorystore": "^1.6.7",
"moment": "^2.29.4",
"orio.db": "^3.1.2",
"passport": "^0.7.0",
"passport-discord": "^0.1.4"
}
}
Click to see the description in this file!
const {
Client,
Collection,
GatewayIntentBits,
Partials,
} = require("discord.js");
const db = require("orio.db");
const client = new Client({
intents: [
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildMembers,
GatewayIntentBits.GuildEmojisAndStickers,
GatewayIntentBits.GuildIntegrations,
GatewayIntentBits.GuildWebhooks,
GatewayIntentBits.GuildInvites,
GatewayIntentBits.GuildVoiceStates,
GatewayIntentBits.GuildPresences,
GatewayIntentBits.GuildMessages,
GatewayIntentBits.GuildMessageReactions,
GatewayIntentBits.GuildMessageTyping,
GatewayIntentBits.DirectMessages,
GatewayIntentBits.DirectMessageReactions,
GatewayIntentBits.DirectMessageTyping,
GatewayIntentBits.MessageContent,
],
shards: "auto",
partials: [
Partials.Message,
Partials.Channel,
Partials.GuildMember,
Partials.Reaction,
Partials.GuildScheduledEvent,
Partials.User,
Partials.ThreadMember,
],
});
const config = require("./src/config.js");
const { readdirSync } = require("fs");
const moment = require("moment");
const { REST } = require("@discordjs/rest");
const { Routes } = require("discord-api-types/v10");
let token = config.token;
client.commands = new Collection();
const rest = new REST({ version: "10" }).setToken(token);
const log = (l) => {
console.log(`[${moment().format("DD-MM-YYYY HH:mm:ss")}] ${l}`);
};
//command-handler
const commands = [];
readdirSync("./src/commands").forEach(async (file) => {
const command = require(`./src/commands/${file}`);
commands.push(command.data.toJSON());
client.commands.set(command.data.name, command);
});
client.on("ready", async () => {
try {
await rest.put(Routes.applicationCommands(client.user.id), {
body: commands,
});
} catch (error) {
console.error(error);
}
log(`${client.user.username} Aktif Edildi!`);
});
//event-handler
readdirSync("./src/events").forEach(async (file) => {
const event = require(`./src/events/${file}`);
if (event.once) {
client.once(event.name, (...args) => event.execute(...args));
} else {
client.on(event.name, (...args) => event.execute(...args));
}
});
//nodejs-events
process.on("unhandledRejection", (e) => {
console.log(e);
});
process.on("uncaughtException", (e) => {
console.log(e);
});
process.on("uncaughtExceptionMonitor", (e) => {
console.log(e);
});
//
client.login(token);
const express = require("express");
const app = express();
const viewengine = require("ejs");
app.set("view engine", "ejs");
const path = require("path");
app.set("views", path.join(__dirname, "src/transcripts/ejs"));
callbackUrl = `http://localhost:80/callback`;
const passport = require("passport");
const Strategy = require("passport-discord").Strategy;
const session = require("express-session");
const MemoryStore = require("memorystore")(session);
// Deserializing and serializing users without any additional logic.
passport.serializeUser((user, done) => done(null, user));
passport.deserializeUser((obj, done) => done(null, obj));
passport.use(
new Strategy(
{
clientID: "761182128049225748",
clientSecret: "Gr9nUtkZSrkZcIsaOoOAoN0UQbCseuBz",
callbackURL: "http://localhost:80/callback",
scope: ["identify", "guilds"],
},
(accessToken, refreshToken, profile, done) => {
// On login we pass in profile with no logic.
process.nextTick(() => done(null, profile));
}
)
);
// We initialize the memorystore middleware with our express app.
app.use(
session({
store: new MemoryStore({ checkPeriod: 86400000 }),
secret:
"#@%#&^$^$%@$^$&%#$%@#$%$^%&$%^#$%@#$%#E%#%@$FEErfgr3g#%GT%536c53cc6%5%tv%4y4hrgrggrgrgf4n",
resave: false,
saveUninitialized: false,
})
);
// We initialize passport middleware.
app.use(passport.initialize());
app.use(passport.session());
// We declare a checkAuth function middleware to check if an user is logged in or not, and if not redirect him.
const checkAuth = (req, res, next) => {
// If authenticated we forward the request further in the route.
if (req.isAuthenticated()) return next();
// If not authenticated, we set the url the user is redirected to into the memory.
req.session.backURL = req.url;
// We redirect user to login endpoint/route.
res.redirect("/login");
};
// Login endpoint.
app.get(
"/login",
(req, res, next) => {
if (req.headers.referer) {
const parsed = url.parse(req.headers.referer);
if (parsed.hostname === app.locals.domain) {
req.session.lastVisitedPage = parsed.path;
}
} else {
req.session.lastVisitedPage = "/";
}
next();
},
passport.authenticate("discord")
);
app.get(
"/callback",
passport.authenticate("discord", { failureRedirect: "/" }),
(req, res) => {
if (req.session.lastVisitedPage) {
const lastVisitedPage = req.session.lastVisitedPage;
res.redirect(lastVisitedPage);
} else {
res.redirect("/");
}
}
);
// Logout endpoint.
app.get("/logout", function (req, res) {
// We destroy the session.
req.session.destroy(() => {
// We logout the user.
req.logout();
// We redirect user to index.
res.redirect("/");
});
});
app.get("/:userID/:ticketfullname", checkAuth, (req, res) => {
let userID = req.params.userID;
let ticketfullname = req.params.ticketfullname;
const guild = client.guilds.cache.get(config.guildID);
const member = guild.members.cache.get(req.user.id);
if (
userID !== req.user.id &&
member.roles.cache.has(db.get(`ticket-setup_${guild.id}`).role) === false
)
return res.redirect("/");
req.session.lastVisitedPage = `/${userID}/${ticketfullname}`;
res.render(ticketfullname);
});
app.get("/", checkAuth, (req, res) => {
const guild = client.guilds.cache.get(config.guildID);
const member = guild.members.cache.get(req.user.id);
const fs = require("fs");
const klasorYolu = "./src/transcripts/ejs";
const hedefKarakter = req.user.id;
const tarihSaatRegex = /(\d{4}-\d{2}-\d{2})-(\d{2}_\d{2})/;
fs.readdir(klasorYolu, (err, dosyaListesi) => {
if (err) {
console.error("Klasör okunamadı:", err);
return;
}
const tarihler = [];
const saatler = [];
const allFiles = [];
const userIDArray = [];
const UserNameArray = [];
if (member.roles.cache.has(db.get(`ticket-setup_${guild.id}`).role)) {
dosyaListesi
.filter((x) => !x.includes("index"))
.forEach((dosyaAdi) => {
const eslesme = dosyaAdi.match(tarihSaatRegex);
const userID = dosyaAdi.substring(0, dosyaAdi.indexOf("-"));
allFiles.push(dosyaAdi);
if (eslesme) {
const tarih = eslesme[1];
const saat = eslesme[2].replace("_", ":");
const userid = userID;
const username =
client.guilds.cache.get(config.guildID).members.cache.get(userid)
.user.globalName || null;
UserNameArray.push(username);
userIDArray.push(userid);
tarihler.push(tarih);
saatler.push(saat);
}
});
res.render("staff-index", {
tarihler: tarihler,
saatler: saatler,
filtresiz: allFiles,
userID: userIDArray,
username: UserNameArray,
});
} else {
dosyaListesi
.filter((x) => x.includes(hedefKarakter))
.forEach((dosyaAdi) => {
const eslesme = dosyaAdi.match(tarihSaatRegex);
allFiles.push(dosyaAdi);
if (eslesme) {
const tarih = eslesme[1];
const saat = eslesme[2].replace("_", ":");
tarihler.push(tarih);
saatler.push(saat);
}
});
res.render("index", {
tarihler: tarihler,
saatler: saatler,
filtresiz: allFiles,
userID: req.user.id,
});
}
});
});
app.listen(80, () => {
console.log("Ticket bot website listening on port 80!");
});
Click to see the description in this file!
module.exports = {
prefix: "/",
owner: "538316319829917701",
token: "",
guildID: "",
url: "http://localhost/", // ex: https://ticketbot.com/
};
Click to see the description in this file!
const {
EmbedBuilder,
PermissionsBitField,
Colors,
ButtonBuilder,
ButtonStyle,
} = require("discord.js");
const { SlashCommandBuilder } = require("@discordjs/builders");
const db = require("orio.db");
module.exports = {
data: new SlashCommandBuilder()
.setName("add")
.setDescription("You add a member to the ticket channel!")
.addUserOption((option) =>
option
.setName("user")
.setDescription("The user to add to the ticket")
.setRequired(true)
),
run: async (client, interaction) => {
const guildDB = db.get(`ticket-setup_${interaction.guild.id}`);
const user = interaction.options.getUser("user");
const member = interaction.guild.members.cache.get(user.id);
if (!interaction.member.roles.cache.has(guildDB.role)) {
interaction.reply({
content:
"> ❌ **Unsuccessful!** You are not allowed to use this command!",
ephemeral: true,
});
return;
}
if (!member) {
interaction.reply({
content: "> ❌ **Unsuccessful!** The user is not in the server!",
ephemeral: true,
});
return;
}
if (!interaction.channel.name.startsWith("ticket-")) {
interaction.reply({
content:
"> ❌ **Unsuccessful!** You can only use this command in a ticket channel!",
ephemeral: true,
});
return;
}
interaction.channel.permissionOverwrites.edit(member, {
ViewChannel: true,
SendMessages: true,
});
interaction.reply({
content: `> ✅ **Successful!** ${member} has been added to the ticket!`,
ephemeral: true,
});
},
};
Click to see the description in this file!
const {
EmbedBuilder,
PermissionsBitField,
Colors,
ButtonBuilder,
ButtonStyle,
} = require("discord.js");
const { SlashCommandBuilder } = require("@discordjs/builders");
const db = require("orio.db");
module.exports = {
data: new SlashCommandBuilder()
.setName("remove")
.setDescription("You remove a member to the ticket channel!")
.addUserOption((option) =>
option
.setName("user")
.setDescription("The user to add to the ticket")
.setRequired(true)
),
run: async (client, interaction) => {
const guildDB = db.get(`ticket-setup_${interaction.guild.id}`);
const user = interaction.options.getUser("user");
const member = interaction.guild.members.cache.get(user.id);
if (!interaction.member.roles.cache.has(guildDB.role)) {
interaction.reply({
content:
"> ❌ **Unsuccessful!** You are not allowed to use this command!",
ephemeral: true,
});
return;
}
if (!member) {
interaction.reply({
content: "> ❌ **Unsuccessful!** The user is not in the server!",
ephemeral: true,
});
return;
}
if (!interaction.channel.name.startsWith("ticket-")) {
interaction.reply({
content:
"> ❌ **Unsuccessful!** You can only use this command in a ticket channel!",
ephemeral: true,
});
return;
}
interaction.channel.permissionOverwrites.edit(member, {
ViewChannel: null,
SendMessages: null,
});
interaction.reply({
content: `> ✅ **Successful!** ${member} has been removed to the ticket!`,
ephemeral: true,
});
},
};
Click to see the description in this file!
const {
EmbedBuilder,
PermissionsBitField,
Colors,
ButtonBuilder,
ButtonStyle,
ChannelType,
ActionRowBuilder,
} = require("discord.js");
const { SlashCommandBuilder } = require("@discordjs/builders");
module.exports = {
data: new SlashCommandBuilder()
.setName("ticket-setup")
.setDescription("Setup the ticket system for your server!")
.addChannelOption((option) =>
option
.setName("channel")
.setDescription("The channel to create the tickets in!")
.setRequired(true)
.addChannelTypes(ChannelType.GuildText)
)
.addStringOption((option) =>
option
.setName("message-channel")
.setDescription("Message appearing in the ticket creation channel!")
.setRequired(true)
)
.addChannelOption((option) =>
option
.setName("category")
.setDescription("The category to create the tickets in!")
.setRequired(true)
.addChannelTypes(ChannelType.GuildCategory)
)
.addStringOption((option) =>
option
.setName("message")
.setDescription("The message to send when a ticket is created!")
.setRequired(true)
)
.addRoleOption((option) =>
option
.setName("role")
.setDescription("The role to ping when a ticket is created!")
.setRequired(true)
)
.addStringOption((option) =>
option
.setName("emoji")
.setDescription("The emoji to react with to create a ticket!")
.setRequired(true)
)
.addStringOption((option) =>
option
.setName("ticket-name")
.setDescription("The name of the ticket channel!")
.setRequired(true)
.addChoices(
{
name: "Number",
value: "number",
},
{
name: "Username",
value: "username",
}
)
)
.addChannelOption((option) =>
option
.setName("transcript-channel")
.setDescription("Transcript channel!")
.addChannelTypes(ChannelType.GuildText)
.setRequired(true)
),
run: async (client, interaction) => {
const category = interaction.options.getChannel("category");
const message = interaction.options.getString("message");
const role = interaction.options.getRole("role");
const emoji = interaction.options.getString("emoji");
const ticketName = interaction.options.getString("ticket-name");
const db = require("orio.db");
if (db.get(`ticket-setup_${interaction.guild.id}`)) {
return interaction.reply({
content: "> ❌ **Unsuccessful!** Ticket System already setup!",
ephemeral: true,
});
} else {
db.set(`ticket-setup_${interaction.guild.id}`, {
channel: interaction.options.getChannel("channel").id,
messageChannel: interaction.options.getString("message-channel"),
category: category.id,
message: message,
role: role.id,
emoji: emoji,
ticketName: ticketName,
transcriptChannel: interaction.options.getChannel("transcript-channel"),
});
if (ticketName === "number") {
db.set(`ticket-number_${interaction.guild.id}`, 0);
}
interaction.options.getChannel("channel").send({
embeds: [
new EmbedBuilder()
.setTitle("Ticket System")
.setDescription(
`> Click the button to create a ticket!\n\n${interaction.options.getString(
"message-channel"
)}`
)
.setColor(Colors.Blurple)
.setFooter({
text: "Ticket System",
iconURL: client.user.displayAvatarURL({ dynamic: true }),
}),
],
components: [
new ActionRowBuilder().addComponents(
new ButtonBuilder()
.setCustomId("createTicket")
.setLabel("Create Ticket")
.setStyle(ButtonStyle.Secondary)
.setEmoji(emoji)
),
],
});
interaction.reply({
content: "> ✅ **Successful!** Ticket System setup!",
ephemeral: true,
});
}
},
};
Click to see the description in this file!
const {
EmbedBuilder,
InteractionType,
PermissionsBitField,
Colors,
ButtonBuilder,
ButtonStyle,
ChannelType,
ActionRowBuilder,
} = require("discord.js");
const { readdirSync } = require("fs");
const db = require("orio.db");
module.exports = {
name: "interactionCreate",
execute: async (interaction) => {
let client = interaction.client;
if (interaction.type == InteractionType.ApplicationCommand) {
if (interaction.user.bot) return;
readdirSync("./src/commands").forEach((file) => {
const command = require(`../../src/commands/${file}`);
if (
interaction.commandName.toLowerCase() ===
command.data.name.toLowerCase()
) {
command.run(client, interaction);
}
});
}
const closeButton = new ButtonBuilder({
style: ButtonStyle.Danger,
label: "Close Ticket",
customId: "closeTicket",
emoji: "🔒",
});
const transcriptButton = new ButtonBuilder({
style: ButtonStyle.Secondary,
label: "Transcript",
customId: "transcript",
emoji: "📜",
});
const deleteButton = new ButtonBuilder({
style: ButtonStyle.Danger,
label: "Delete Ticket",
customId: "deleteTicket",
emoji: "🗑️",
});
if (interaction.customId === "createTicket") {
const guildDB = db.get(`ticket-setup_${interaction.guild.id}`);
if (!guildDB) {
interaction.reply({
content: "> ❌ **Unsuccessful!** Ticket System not setup!",
ephemeral: true,
});
interaction.message.delete();
} else {
const member = interaction.member;
const ticketName = guildDB.ticketName;
const ticketRole = guildDB.role;
let ticketR;
if (ticketName === "number") {
ticketR = db.get(`ticket-number_${interaction.guild.id}`);
}
if (ticketName === "username") {
ticketR = member.user.username;
}
if (db.get(`ticket-check-user_${interaction.user.id}`)) {
interaction.reply({
content: "> ❌ **Unsuccessful!** You already have a ticket!",
ephemeral: true,
});
return;
}
const ticketChannel = await interaction.guild.channels
.create({
name: `ticket-${ticketR}`,
type: ChannelType.GuildText,
parent: guildDB.category,
permissionOverwrites: [
{
id: interaction.guild.id,
deny: PermissionsBitField.Flags.ViewChannel,
},
{
id: member.id,
allow: [
PermissionsBitField.Flags.ViewChannel,
PermissionsBitField.Flags.SendMessages,
],
},
{
id: guildDB.role,
allow: [
PermissionsBitField.Flags.ViewChannel,
PermissionsBitField.Flags.SendMessages,
],
},
],
})
.then((x) => {
if (guildDB.ticketName === "number") {
db.add(`ticket-number_${interaction.guild.id}`, 1);
}
db.set(
`ticket-check-user_${interaction.user.id}`,
interaction.channel.id
);
db.set(`ticket-check-channel_${x.id}`, interaction.user.id);
x.send({
content: `<@&${ticketRole}>`,
embeds: [
new EmbedBuilder()
.setTitle("Ticket")
.setDescription(
`> Ticket created by ${member}! Staff will be with you shortly!\n\n${guildDB.message}`
)
.setColor(Colors.Blurple)
.setFooter({
text: "Ticket System",
iconURL: client.user.displayAvatarURL({ dynamic: true }),
}),
],
components: [new ActionRowBuilder().addComponents([closeButton])],
}).then((x) => x.pin());
interaction.reply({
content: `> ✅ **Successful!** Your ticket has been created ${x}!`,
ephemeral: true,
});
});
}
} else if (interaction.customId === "closeTicket") {
const user = db.get(`ticket-check-channel_${interaction.channel.id}`);
await db.delete(`ticket-check-user_${user}`);
interaction.deferReply();
interaction.channel.send({
embeds: [
new EmbedBuilder()
.setTitle("Ticket")
.setDescription(
`> Ticket closed by ${interaction.user}! Staff control buttons are below.`
)
.setColor(Colors.Blurple)
.setFooter({
text: "Ticket System",
iconURL: client.user.displayAvatarURL({ dynamic: true }),
}),
],
components: [
new ActionRowBuilder().addComponents(deleteButton, transcriptButton),
],
});
interaction.channel.permissionOverwrites.edit(
db.get(`ticket-check-channel_${interaction.channel.id}`),
{ ViewChannel: false, SendMessages: null }
);
interaction.deleteReply();
} else if (interaction.customId === "deleteTicket") {
await db.delete(`ticket-check-channel_${interaction.channel.id}`);
interaction.deferReply();
interaction.channel.send({
embeds: [
new EmbedBuilder()
.setTitle("Ticket")
.setDescription(
`> Ticket deleted by ${interaction.user}! This ticket will be deleted in 5 seconds.`
)
.setColor(Colors.Blurple)
.setFooter({
text: "Ticket System",
iconURL: client.user.displayAvatarURL({ dynamic: true }),
}),
],
});
interaction.deleteReply();
setTimeout(() => {
interaction.channel.delete();
}, 5000);
} else if (interaction.customId === "transcript") {
interaction.deferReply();
interaction.channel.send({
embeds: [
new EmbedBuilder()
.setTitle("Ticket")
.setDescription(`> Transcript requested by ${interaction.user}!`)
.setColor(Colors.Blurple)
.setFooter({
text: "Ticket System",
iconURL: client.user.displayAvatarURL({ dynamic: true }),
}),
],
});
const userID = db.get(`ticket-check-channel_${interaction.channel.id}`);
interaction.deleteReply();
const discordTranscripts = require("discord-html-transcripts");
const fs = require("fs");
let dt = new Date();
let tarih = `${dt.getFullYear().toString().padStart(4, "0")}-${(
dt.getMonth() + 1
)
.toString()
.padStart(2, "0")}-${dt.getDate().toString().padStart(2, "0")}-${dt
.getHours()
.toString()
.padStart(2, "0")}_${dt.getMinutes().toString().padStart(2, "0")}`;
const attachment2 = await discordTranscripts.createTranscript(
interaction.channel,
{
favicon: interaction.guild.iconURL(),
limit: -1, // Max amount of messages to fetch. `-1` recursively fetches.
returnType: "string", // Valid options: 'buffer' | 'string' | 'attachment' Default: 'attachment' OR use the enum ExportReturnType
filename: userID + "-" + tarih + "_transcript.ejs", // Only valid with returnType is 'attachment'. Name of attachment.
saveImages: false, // Download all images and include the image data in the HTML (allows viewing the image even after it has been deleted) (! WILL INCREASE FILE SIZE !)
poweredBy: false,
}
);
const files2 = `./src/transcripts/ejs/${
userID + "-" + tarih + "_transcript.ejs"
}`;
const content2 = "\u200B";
fs.writeFileSync(files2, content2);
const cs2 = fs.readFileSync(
`./src/transcripts/ejs/${userID + "-" + tarih + "_transcript.ejs"}`,
"utf-8"
);
fs.writeFileSync(
`./src/transcripts/ejs/${userID + "-" + tarih + "_transcript.ejs"}`,
attachment2 + cs2
);
const config = require("../config");
const userEmbed = new EmbedBuilder()
.setTitle("Transcript")
.setDescription(
`> [Your ticket transcript](${config.url}${userID}/${
userID + "-" + tarih + "_transcript"
})\n\nNote: After logging in, click the link again.`
)
.setColor(Colors.Blurple)
.setFooter({
text: "Ticket System",
iconURL: client.user.displayAvatarURL({ dynamic: true }),
});
const user = client.users.cache.get(userID);
user.send({ embeds: [userEmbed] });
const guildEmbed = new EmbedBuilder()
.setTitle("Transcript")
.setDescription(
`Ticket Closed: <t:${Math.floor(
interaction.message.createdTimestamp / 1000
)}:R>\n\n> [${user.username} Ticket transcript](${
config.url
}${userID}/${
userID + "-" + tarih + "_transcript"
})\n\nNote: After logging in, click the link again.`
)
.setColor(Colors.Blurple)
.setFooter({
text: "Ticket System",
iconURL: client.user.displayAvatarURL({ dynamic: true }),
});
interaction.guild.channels.cache
.get(db.get(`ticket-setup_${interaction.guild.id}`).transcriptChannel)
.send({ embeds: [guildEmbed] });
}
},
};
Click to see the description in this file!
module.exports = {
name: 'messageCreate',
execute: async(message) => {
let client = message.client;
if (message.author.bot) return;
if (message.channel.type === 'dm') return;
}};
Click to see the description in this file!
const { ActivityType } = require("discord.js");
module.exports = {
name: "ready",
once: true,
execute(client) {
let activities = [
`Developed by rynixelchavo`,
`${client.user.username} Ticket`,
],
i = 0;
setInterval(
() =>
client.user.setActivity({
name: `${activities[i++ % activities.length]}`,
type: ActivityType.Custom,
}),
22000
);
},
};
Click to see the description in this file!
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Transcript List</title>
<style>
body {
font-family: Arial, sans-serif;
display: flex;
flex-direction: column;
align-items: center;
margin: 0;
padding: 0;
background-color: #121212; /* Karanlık arka plan rengi */
color: #fff; /* Beyaz metin rengi */
}
.container {
max-width: 600px;
margin-top: 50px;
background-color: #333; /* Hafif karanlık gri arka plan rengi */
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
h1 {
text-align: center;
margin-bottom: 20px;
}
.transcript-link {
display: block;
text-decoration: none;
color: #ccc; /* Açık gri bağlantı rengi */
padding: 10px;
border-bottom: 1px solid #555; /* Daha koyu gri border */
transition: background-color 0.3s ease;
}
.transcript-link:hover {
background-color: #444; /* Daha koyu gri hover arka plan rengi */
}
p {
text-align: center;
margin-top: 20px;
}
</style>
</head>
<body>
<div class="container">
<h1>Transcript List</h1>
<% if (filtresiz.length === 0) { %>
<p>No transcripts found.</p>
<% } else { %>
<% for (var i = 0; i < filtresiz.length; i++) { %>
<a class="transcript-link" href="<%= userID %>/<%= filtresiz[i].replace(".ejs", "") %>">Your transcript recorded on <%= tarihler[i] %> at <%= saatler[i] %></a>
<% } %>
<% } %>
</div>
</body>
</html>